目次 [ Contents ]
懐かしの海外ドラマ「ナイトライダー」。車のフロントで放っていたメカチックなランプの動きをArduinoとLEDで再現してみたいと思います。
必要なもの
- LED x 6
- 抵抗 x 6
- タクトスイッチ
LEDに往復の動きをつける
配線図
LEDをあるだけピンにつなげて順番に点灯、消灯するスケッチを書けば終了です。と、これだけではあまりにも簡単な話で書く意味もないので、少しスケッチの仕方に工夫をしていきたいと思います。
For文の活用
ナイト2000は並んだLEDの光が左から右に流れ、端まで行くと戻ってくる反復運動を繰り返しています。単純に考えれば並んだ順番でdigitalWriteのHigh、Lowをしていけばできますが…
digitalWrite(3, HIGH); delay(100); digitalWrite(3, LOW); digitalWrite(4, HIGH); delay(100); digitalWrite(4, LOW....
この反復作業をFor文と配列でもう少し効率よく消化してみます。
まず、使うLEDの個数を定義し、それに接続されたピン番号を配列で並べます。
#define LED_SIZE 6 byte leds [LED_SIZE] = {3, 5, 6, 9, 10, 11};
これで、For文で繰り返せるようになります。
#define LED_SIZE 6 byte leds [LED_SIZE] = {3, 5, 6, 9, 10, 11}; void setup() { // set pin to OUTPUT & LED off for (byte i = 0 ; i < LED_SIZE ; i++) { pinMode(leds[i], OUTPUT); digitalWrite(leds[i], LOW); } } void loop() { // Left to right for (byte repeat = 0 ; repeat < (LED_SIZE - 1) ; repeat++) { digitalWrite(leds[repeat], HIGH); delay(100); digitalWrite(leds[repeat], LOW); } // Right to left for (byte repeat = (LED_SIZE - 1) ; repeat > 0 ; repeat--) { digitalWrite(leds[repeat], HIGH); delay(100); digitalWrite(leds[repeat], LOW); } }
繰り返しは一番端から始まり、逆端の手前で終わるようにLED_SIZE – 1にしています。
ただ、これだと片道づつの繰り返しが手間なので、もっとシンプルに。ひとつのFor文に組み込めるよう工夫します。
6個あるLEDを往復させるのに必要な繰り返し回数は10回です。折り返しで両端が重複しないようにするためです。つまりLED_SIZE – 1 を2回繰り返せばいいことになります。
Repeat回数 = (LED_SIZE-1)* 2
配列が0から始まるので混乱すると思いますが(自分がいつもそう)こんな循環になります。
そして折り返し地点であるLED_SIZEにrepeatが達したら、数が減るようif文で分岐を書いてやります。そのため新たにcount変数を用意して、その増減を操作します。
#define LED_SIZE 6 byte leds [LED_SIZE] = {3, 5, 6, 9, 10, 11}; void setup() { // set pin & LED off for (byte i = 0 ; i < LED_SIZE ; i++) { pinMode(leds[i], OUTPUT); digitalWrite(leds[i], LOW); } } void loop() { for (byte repeat = 0 ; repeat < ((LED_SIZE - 1) * 2) ; repeat++) { byte count = 0; if (repeat < LED_SIZE) { count = repeat ; } else { count = (LED_SIZE - 1) * 2 - repeat ; } digitalWrite(leds[count], HIGH); delay(100); digitalWrite(leds[count], LOW); } }
このようにrepeatがLED_SIZEと同じになったらトータル繰り返し回数からrepeatを引くようにすれば、countが減っていく形にできます。
ここでもうひとつ。if文で単純な代入をするなら省略できる書き方があります。
=(判定式)? 正しい場合の返し : 違う場合の返し;
正否の区切りがセミコロン;ではなく、コロン:なのに気をつけてください。これを利用して変数countの宣言と代入を1行にまとめられます。
#define LED_SIZE 6 byte leds [LED_SIZE] = {3, 5, 6, 9, 10, 11}; void setup() { // set pin & LED off for (byte i = 0 ; i < LED_SIZE ; i++) { pinMode(leds[i], OUTPUT); digitalWrite(leds[i], LOW); } } void loop() { for (byte repeat = 0 ; repeat < ((LED_SIZE - 1) * 2) ; repeat++) { byte count = (repeat < LED_SIZE) ? repeat : (LED_SIZE - 1) * 2 - repeat ; digitalWrite(leds[count], HIGH); delay(100); digitalWrite(leds[count], LOW); } }
Sketch:LEDの移動
ということで、ここまでのスケッチを一旦整理すると…。
#define LED_SIZE 6 #define LOOP_SIZE (LED_SIZE - 1) * 2 byte leds [LED_SIZE] = {3, 5, 6, 9, 10, 11}; void setup() { // set pin to OUTPUT & LED off for (byte i = 0 ; i < LED_SIZE ; i++) { pinMode(leds[i], OUTPUT); digitalWrite(leds[i], LOW); } } void loop() { for (byte repeat = 0 ; repeat < LOOP_SIZE ; repeat++) { byte count = (repeat < LED_SIZE) ? repeat : LOOP_SIZE - repeat ; digitalWrite(leds[count], HIGH); delay(100); digitalWrite(leds[count], LOW); } }
トータルの繰り返し回数も#define LOOP_SIZEでまとめることでスッキリします。
動きのスピードを変える
とりえあず動きをつけることはできましたが、動くスピードが違うと思うかもしれません。そのスピードを決めているのはdelay(100);で、中の値を変えればスピードも変えることができます。
でも、せっかくなので、可変抵抗器を用意して自由にスピードを変えられるようにします。
配線図
一般的な可変抵抗器をつなぐだけです。入力はA0です。
可変抵抗器を読む
ここでのアナログ入力に関する説明は省略します。詳しく知りたい方はこちらを読んでください。今回は抵抗読み取りを関数化してそのままdelayに反映します。
関数について
スケッチで必ず使うsetup、loopは関数です。そして頭についているvoidというのは無とか空っていう意味の「型」になります。型は変数の宣言でつけるbyteとかintなどの扱える数値のサイズです。つまりvoid setup()、void loop()は数値のない関数ということを示しています。
void setup () { } void loop() { }
もう一つ。digitalRead()も関数のひとつです。これを使うとピンの状態がLOWかHIGHを調べられますよね。digitalRead関数については普段見えないところに組み込まれていて、普段意識することなく使用できます。この関数の内容を実際調べたことはないですが、おおよそこうなっているはずです。
boolean digitalRead (byte pin_number) { 〜pin_numberピンの状態を見る命令〜 return ピンを調べた結果; }
関数はvoid以外の型がつくと、関数内で処理した結果を返すことができるようになります。こういうreturnで結果を返す値を「返り値」と呼んでいて、digitalRead()は調べた結果をLOWかHIGHの返り値(boolean)で渡す関数になります。
説明が長くなりましたが、つまり「よく行う処理は自分で関数化して簡略化できる」というのが言いたいわけです。
A0ピン読み取りの関数化
ということで、可変抵抗器の読み取りをして程よい秒数(20ms〜200ms)で返す処理を関数として作ります。とりあえずVOL_READという名前で関数化しますが、変数同様自分で好きに作れます(予約語は除く)。
int VOL_READ() { static int vol_val = 0; vol_val = vol_val * 0.9 + analogRead(A0) * 0.1; return map(vol_val, 0, 1013, 20, 200); }
staticは関数内で数値を保持するために使用します。つまり関数の外で変数宣言するのと同じ効果になります。これがないとvol_valは毎回0にセットされてしまいます。
そしてreturnで値をmap関数で変換した20〜200の返り値を渡すようにしています。
Sketch:可変抵抗でスピード調整
これをdelay()に直接組み込めば完了です。
#define LED_SIZE 6 #define LOOP_SIZE (LED_SIZE - 1) * 2 byte leds [LED_SIZE] = {3, 5, 6, 9, 10, 11}; void setup() { // set pin to OUTPUT & LED off for (byte i = 0 ; i < LED_SIZE ; i++) { pinMode(leds[i], OUTPUT); digitalWrite(leds[i], LOW); } pinMode(A0, INPUT); // set Variable resistor } int VOL_READ() { static int vol_val = 0; vol_val = vol_val * 0.9 + analogRead(A0) * 0.1; return map(vol_val, 0, 1013, 20, 200); } void loop() { for (byte repeat = 0 ; repeat < LOOP_SIZE ; repeat++) { byte count = (repeat < LED_SIZE) ? repeat : LOOP_SIZE - repeat ; digitalWrite(leds[count], HIGH); delay(VOL_READ()); digitalWrite(leds[count], LOW); } }
残像感を出す
LEDの移動で残像感を出します。実際のナイト2000は単純なON、OFFではなく、ハレーすい星のように残像の尻尾があります。それを再現します。
Sketch:残像付きの移動LED
ここまで出来れば、残像は難しくはありません。digitalWriteではon/offしかできませんが、analogWriteを使えばいいだけです。
#define VOL A0 #define LED_SIZE 6 #define LOOP_SIZE (LED_SIZE - 1) * 2 byte leds [LED_SIZE] = {3, 5, 6, 9, 10, 11}; byte led_luma [LED_SIZE]; void setup() { // set pin to OUTPUT & LED off for (byte i = 0 ; i < LED_SIZE ; i++) { pinMode(leds[i], OUTPUT); digitalWrite(leds[i], LOW); } pinMode(VOL, INPUT); // set variable resistor } int VOL_READ() { static int vol_val = 0; vol_val = vol_val * 0.9 + analogRead(VOL) * 0.1; return map(vol_val, 0, 1013, 20, 200); } void loop() { for (byte repeat = 0 ; repeat < LOOP_SIZE ; repeat++) { byte led_now = (repeat < LED_SIZE) ? repeat : LOOP_SIZE - repeat; // reduce led luma value for (byte i = 0 ; i < LED_SIZE ; i++) led_luma[i] /= 3; // set current led luma max led_luma[led_now] = 255; for (byte i = 0 ; i < LED_SIZE ; i++) analogWrite(leds[i], led_luma[i]); delay(VOL_READ()); } }
LEDの個数分、明るさを記憶できるように配列led_lumaを用意し、リピートの中でひたすら数値を割り続けます。そして先頭のLEDだけ最大値(255)にしてやれば、順当に数値が落ちていくので、残像に見えるという算段です。割り算の3を工夫すれば尻尾の長さも調整できるかと思います。
監視モードボタン
Huluでドラマを見直すと、このランプ部分はセンサーを表現していて監視モードで動くみたいです。それと、自分の車につけたいと思っても、日本の法律だと運転中にこういった類のイルミネーションはダメみたいですね。
ということで、実用性も考慮して単純なON/OFFボタンもつけてみます。
配線図
スイッチ判定の説明に関しても詳しく知りたければ、こちらを参照してください。
surveillance_mode変数を作り、ON/OFF状態を記憶します。また、先に作ったVOL_READ関数を修正し、スイッチの押し判定も組み込んだ状態を把握する関数に変更します。
#define SW 13 #define PUSH_SHORT 700 boolean surveillance_mode = 0; pinMode(SW, INPUT_PULLUP); int EXT_CONTROL() { long button = 0; while(!digitalRead(SW)) button++; if(button > PUSH_SHORT) surveillance_mode = !surveillance_mode; static int vol_val = 0; vol_val = vol_val * 0.9 + analogRead(VOL) * 0.1; return map(vol_val, 0, 1013, 20, 200); }
そしてsurveillance_modeの状態でそれぞれLEDの挙動を指定してやれば、スイッチでON/OFFできるようになります。
Sketch:Knight Rider LED
#define VOL A0 #define SW 13 #define PUSH_SHORT 700 #define LED_SIZE 6 #define LOOP_SIZE (LED_SIZE - 1) * 2 byte leds [LED_SIZE] = {3, 5, 6, 9, 10, 11}; byte led_luma [LED_SIZE]; boolean surveillance_mode = 0; void setup() { // set pin to OUTPUT & LED off for (byte i = 0 ; i < LED_SIZE ; i++) { pinMode(leds[i], OUTPUT); digitalWrite(leds[i], LOW); } pinMode(VOL, INPUT); // set variable resistor pinMode(SW, INPUT_PULLUP); // set surveillance switch } int EXT_CONTROL() { // surveillance button function long button = 0; while (!digitalRead(SW)) button++; if (button > PUSH_SHORT) surveillance_mode = !surveillance_mode; // speed read static int vol_val = 0; vol_val = vol_val * 0.9 + analogRead(VOL) * 0.1; return map(vol_val, 0, 1013, 20, 200); } void loop() { EXT_CONTROL(); for (byte repeat = 0 ; repeat < LOOP_SIZE ; repeat++) { if (surveillance_mode) { byte led_now = (repeat < LED_SIZE) ? repeat : LOOP_SIZE - repeat; // reduce led luma value for (byte i = 0 ; i < LED_SIZE ; i++) led_luma[i] /= 3; // set current led luma max led_luma[led_now] = 255; for (byte i = 0 ; i < LED_SIZE ; i++) analogWrite(leds[i], led_luma[i]); delay(EXT_CONTROL()); } else { // all led reset and off for (byte i = 0 ; i < LED_SIZE ; i++) { led_luma[i] = 0; analogWrite(leds[i], led_luma[i]); } } } }
repeat For文のなかでsurveillance_modeの状態で通常か、全て消灯にするかを分けるif文を作っています。また、For文の外でもEXT_CONTROL関数を呼びに行っているのは、OFFのときでもSW判定をさせるためです。こういう風に返り値を拾わなくても関数を呼びに行って処理を行わせることも出来ます。
最後に
ArduinoUnoではanalogWriteでPWM出力できるピンは6つしかありません。なので、この記事でも6つのLEDで説明しましたが、機種によっては(ArduinoMEGA、DUE、M0系など)PWMの扱える数が増やせたりします。その場合、以下の2箇所を書き換えればいいだけなので、変更拡張は簡単です。
例えばArduino Due互換機を使って8コのLEDを3-10ピンに指す場合(スペック的に勿体無いですが)、
#define LED_SIZE 8 byte leds [LED_SIZE] = {3, 4, 5, 6, 7, 8, 9, 10};
また、Arduino単体では20mAの電流しか流せないですが、トランジスタを併用すれば大きなLEDでも光らせることが出来ます。
興味がある方はお試しください。