Lチカ発展編とは
電子工作を始めた方の殆どが、電子工作の"Hello World"ことLチカを経験されているかと思います。
本記事では、Lチカをすこし捻って、「一定時間ボタン入力のタイミングを記録して、その通りに点灯する」というプログラムと回路を作成致しましたので、ご紹介したいと思います。
#本記事の対象読者
- 「LEDを点灯する」「ボタンを押したらLEDが光る」といった入門工作を果たしたけど何だか物足りない方
- C言語を学習中の方(配列、関数まで学習された方)
今回はプログラム(Arduino開発ではスケッチと呼びます)の解説に重点を置いた記事と致しますので、
ArduinoIDEの導入や電子回路の細かい解説は割愛します。
実現したいこと
- 10秒間ボタン入力を受け付けてタイミングを記録
- 記録したタイミング通りにLEDを点灯
- 記録モード、点灯モードをモード切替用ボタンで任意に切り替える
- せっかくなのでディスプレイに現在のモードを表示する
要は10秒間、ボタンを「ポチポチポチポチ・・・ポーチポチ」
と押したら
LEDが10秒間「ピカピカピカピカ・・・ピーカピカ」
と光る、という工作です。
本記事の再現に必要なもの
- Arduino本体(今回はnanoのコンパチを使用していますが、UNOでもOKです)
- LED1本、タクトスイッチ2個、抵抗3個(LED用とスイッチのプルアップ)
- OLEDディスプレイ(SSD1306チップ搭載のもの)
- ジャンパワイヤ
とりあえず配線
なんかごちゃごちゃしていますが、ざっくり解説します。
- LEDとタクトスイッチをデジタル端子につなぐ
- タクトスイッチはプルアップ
- ディスプレイをシリアル端子につなぐ
- 電源は5Vに
回路図また作ります。ざっくりですみません。
成果物の見た目がイケてる(当社比)というだけでArduino nanoを使用しています。本体はUNOでもなんでもOKです。
写真だと複雑っぽいですが「ボタンを押したらLEDが光る」回路になっていればOKで、その回路にスイッチを一つ追加しているだけの、「いつものLチカ」回路です。
追加したスイッチを「モード切替スイッチ」として利用します。
これをプログラムで「発展型Lチカ」に仕上げます。
とりあえずスケッチの全貌
本題のスケッチです。Arduino開発ではソースコードをスケッチと呼ぶようです。
Arduinoやった事無い方はC++のような何かだと思ってください。
以下、スケッチ全文
#include <Arduino.h>
#include <U8x8lib.h>
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#define LED 3
#define BTN1 4
#define BTN2 5
U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(/* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL, /* data=*/ SDA);
void setup() {
pinMode(LED,OUTPUT);
pinMode(BTN1,INPUT);
pinMode(BTN2,INPUT);
Serial.begin(9600);
u8x8.begin();
u8x8.setFont(u8x8_font_px437wyse700a_2x2_r);
u8x8.drawString(0, 0, "WELCOME!");
u8x8.drawString(0, 2, "PUSH1BTN");
u8x8.refreshDisplay();
}
int btn_push[120];
int mode = 0;
int i;
void ready(void){
for(i = 0; i < 2; i++){
digitalWrite(LED, HIGH);
delay(500);
digitalWrite(LED, LOW);
delay(500);
}
}
int btn_read(int x[]){
unsigned long now_time = millis()/1000;
i = 0;
while(millis()/1000 - now_time < 10){
x[i] = digitalRead(BTN2);
i++;
delay(100);
}
u8x8.refreshDisplay();
}
int led_on(int x[]){
for (i = 0; i < (sizeof(btn_push)/sizeof(btn_push[0])); i++){
delay(100);
if(btn_push[i] == 1){
digitalWrite(LED, HIGH);
}else{
digitalWrite(LED, LOW);
}
}
u8x8.refreshDisplay();
}
void loop() {
if(digitalRead(BTN1) == 1){
delay(100);
mode++;
}
switch(mode){
case 1:
u8x8.drawString(0, 0, "STANDBY!");
u8x8.drawString(0, 2, "PUSH1BTN");
ready();
mode++;
break;
case 2:
if(digitalRead(BTN1) == 1){
u8x8.refreshDisplay();
mode += 1;
}
break;
case 3:
u8x8.drawString(0, 0, "RECMODE!");
u8x8.drawString(0, 2, "PUSH2BTN");
btn_read(btn_push);
u8x8.drawString(0, 0, "STOP!!!!");
u8x8.drawString(0, 2, "PUSH1BTN");
mode++;
break;
case 4:
if(digitalRead(BTN1) == 1){
u8x8.refreshDisplay();
mode++;
}
break;
case 5:
u8x8.drawString(0, 0, "PLAYBACK");
u8x8.drawString(0, 2, "LEDFLASH");
u8x8.refreshDisplay();
led_on(btn_push);
u8x8.drawString(0, 0, "END.....");
u8x8.drawString(0, 2, "PUSH1BTN");
for (i = 0; i < (sizeof(btn_push)/sizeof(btn_push[0])); i++){
btn_push[i] = 0;
}
mode = 0;
break;
}
delay(500);
}
要点解説
セットアップ
//必要なヘッダファイルを読み込み
#include <Arduino.h>
#include <U8x8lib.h>
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
//LEDとボタンを接続した端子を定義
#define LED 3
#define BTN1 4
#define BTN2 5
//OLEDディスプレイのライブラリ設定
U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(/* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL, /* data=*/ SDA);
この辺はC言語と一緒で、ヘッダファイルの読み込みや記号定数を定義します。
#include <U8x8lib.h>
をはじめとしたU8X8に関する記述はディスプレイ用のライブラリに関するものですので現時点では説明を割愛します。
void setup() {
//各端子を入力、出力に設定
pinMode(LED,OUTPUT);
pinMode(BTN1,INPUT);
pinMode(BTN2,INPUT);
//OLEDディスプレイ起動
u8x8.begin();
u8x8.setFont(u8x8_font_px437wyse700a_2x2_r);
u8x8.drawString(0, 0, "WELCOME!");
u8x8.drawString(0, 2, "PUSH1BTN");
u8x8.refreshDisplay();
}
void setup()
関数はプログラム実行時1度だけ実行されるArduino独自の関数です。
pinMode()
の第一引数に冒頭で定義した記号定数を、第二引数にINPUTかOUTPUTを指定します。
LEDやボタンを接続した端子が、入力なのか出力なのか設定しています。
u8x8.drawString(0, 0, "WELCOME!");
このあたりの記述は
C言語でいうところのprintf()
的なものととらえておいてください。
ディスプレイに文字を出力しています。(ライブラリ頼みなので細かい説明は割愛します。)
Readyモード
//必要な変数を宣言
int btn_push[120]; //ボタン入力をタイミング記録する所。0か1しか入らないのでメモリ節約のためにchar型でもよかったかも。
int mode = 0; //モードチェンジ時に使います
int i; //for文回すのに使います
void ready(void){
for(i = 0; i < 2; i++){
digitalWrite(LED, HIGH);
delay(500);
digitalWrite(LED, LOW);
delay(500);
}
}
ready(void)
関数では、普通のLチカをします。
for文でチカチカ回数を2回に制限しています。(回数に特に意味は無いです。)
プログラム開始→いきなり記録モードに入ってボタン押しまくる・・・
の流れではちょっとせわしないので、プログラム起動後、待機状態を兼ねたこちらのモードが起動します。
「待機状態」については後程説明します。
記録モード
int btn_read(int x[]){
unsigned long now_time = millis()/1000;
i = 0;
while(millis()/1000 - now_time < 10){
x[i] = digitalRead(BTN2);
i++;
delay(100);
}
u8x8.refreshDisplay();
}
btn_read
関数では「10秒間ボタンの入力を受け付けて、ボタンのタイミングを記録する」処理を行います。
ボタンの入力はdigitalRead(BTN2)
で読み取り、「押されたら1」「押されてなければ0」が帰ってきます。
それを10秒間配列に格納し続ければOKです。
変数iをインクリメントして、x[i]に順番にボタンから帰ってきた値を格納していきます。
delay(100)
は処理を100ms止める記述です。これにより実質ボタンの読み取り回数を制限し、処理の負荷を抑えます。
関数の仮引数int x[]
には先ほど宣言したbtn_push[120]
の配列をぶちこみます。
上記の通り100ms毎の処理を10秒間繰り替えずので配列の要素数は100でいいと思いますが、なんとなく余白作って120としています。
millis()
関数はArduino独自の時間計測関数です。
プログラムがスタートしてからの経過時間を返してきます。1000で割ってるのは桁合わせです。
まず10秒ループのwhile文に入る前に一度「現在の経過時間」を格納します。
(millis()/1000)
今現在リアルタイムの経過時間から、先ほど作ったnow_time
変数を引いた結果が10より大きくなるまで、という条件式で10秒の計測を表現しています。(スタート時の引き算結果は0、millis()/1000側が時間が経つほど値が大きくなるから減算の結果が1秒ごとに1増える)
この度コメントを頂き、より精度の高い計測方法をご教示頂きました。
修正コードも含め、ご興味のございます方は下記コメント欄をご覧ください。
点灯モード
int led_on(int x[]){
for (i = 0; i < (sizeof(btn_push)/sizeof(btn_push[0])); i++){
delay(100);
if(btn_push[i] == 1){
digitalWrite(LED, HIGH);
}else{
digitalWrite(LED, LOW);
}
}
u8x8.refreshDisplay();
}
led_on
関数では先ほど大量の0と1の値が書き込まれたbtn_push[100]
の配列をぶち込みます。
for (i = 0; i < (sizeof(btn_push)/sizeof(btn_push[0])); i++)
の条件式は、
「配列の要素数の分だけ繰り返す」という意味です。
あとはif文の条件式を「出てきた値が1ならLED点灯、0なら消灯」として、100ms毎に配列の中身を読みだせばOKです。
今回の工作は「ボタンをポチポチ押したタイミングを記録して、そのとおりに点灯する」事が目的なので、
先ほどのbtn_read
関数で使用したdelay(100)
を記述することで同じタイミングでLEDが光ったり消えたりします。
配列の要素数を知るsizeof(btn_push)/sizeof(btn_push[0]
については「C言語 配列 要素数」とかでググって頂ければわかりやすい解説が多々出てきますので気になる方はそちらをご参照下さい。
ここまでちょいちょい出てきた
u8x8.refreshDisplay();
に関しては
一旦スルーでお願いします。
長くなってきたので・・・
なんだか思ったより長くなってしまいました・・・
続きを次回の記事に持ち越させて頂きます。
次回はC言語で言うところのメイン関数についての解説に入ります。