workshop/ArduinoBasis/LEDs

はじめに:チカチカとふわふわ

Arduinoのサンプルプログラムで最も基本的なものに,13ピンのLEDをブリンク(Blink:点灯消灯の繰り返し)を行うものがあります. まずはこの基本的なプログラムから学習してきます.Arduinoをつなぎ,下記Exampleを実行すると,ボード上のLEDが一秒おきに点滅をくりかえます.

Example-LED-1

int led = 13;
void setup() {                
  pinMode(led, OUTPUT);     // 13番ピンを出力モードに設定
}
void loop() {
  digitalWrite(led, HIGH);  // 13番ピンをHighにしてLEDを点灯
  delay(1000);              // 1秒ここで停止
  digitalWrite(led, LOW);   // 13番ピンをLowにしてLEDを消灯
  delay(1000);              // 1秒ここで停止
}

上記プログラムを利用して,13番ピンのLEDをチカチカ点滅からふわふわ点滅に変えてください.


高機能LED(WS2812B)を使う

近年LED制御でしばしば利用されるモジュールがあります.テープLEDやシリアルLEDなどと呼ばれるもので, マイコン側から電源,GND,制御PINの3つの電極だけで複数のLEDを制御可能なものです.一般的にフルカラーLEDを 制御する場合,PWMピンを3本使うのがマイコンでは一般的ですが,それではたくさんのLEDを制御する場合限界が 生じます.そこでこのような高機能LEDの扱いに慣れておきましょう.ここではWS2812Bと呼ばれるモジュールを複数 利用して実際にLEDを制御してみます.

ライブラリの追加

まずはhttps://github.com/adafruit/Adafruit_NeoPixelから ライブラリをダウンロードし,Arduinoで利用できるようにします.ダウンロードしたライブラリをどのようにするかはREADME.mdに書いてあります. 無事にライブラリの追加ができると,ArduinoのExampleにAdafruit_NeoPixelが追加されます.この中にあるsimpleを開いてみます. 下記にsimple Exampleを簡略化したものを記述しておきます.

Example-LED-2

#include <Adafruit_NeoPixel.h>
#include <avr/power.h>

#define PIN            7
#define NUMPIXELS      16

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
int delayval = 500; // delay for half a second

void setup() {
  pixels.begin(); // This initializes the NeoPixel library.
}

void loop() {
  for(int i=0;i <NUMPIXELS; i++){
    pixels.setPixelColor(i, pixels.Color(0,150,0)); // Moderately bright green color.
    pixels.show(); // This sends the updated pixel color to the hardware.
    delay(delayval); // Delay for a period of time (in milliseconds).
  }
}

上記プログラムを利用して下記動画のようなプログラムに変更してください.

下記動画のようにふわふわとLEDを変更させてください.

下記動画のようにLEDを変更させてください.ただしLEDの色は白色,移動スピードは500msです.

さて,練習を実装するにあたり,これまでのやり方では困難な点に気付いたでしょうか. ふわっとつけてふわっと消すことが出来てもそれを他のLEDも同時に動かさなければなりません. つまり,delay関数を利用すると他の処理ができない(シングルスレッド)の為,実装が困難なわけです. これに対して幾つかの解決法を紹介します.

割り込みTimerを利用する

一般的なマイコンには割り込みと呼ばれる機能があります.例えば,このボタンを押した時だけこの関数を呼び出してほしい, 1秒おきにこの関数を呼び出してほしい.といった場合です.この関数呼び出しが行われている間はメインであるloop()関数の 実行は一時停止になります. miso-engine(川鍋さんのブログ)を参照して, MsTimerライブラリをまず追加し,サンプルを動作させてみましょう.

サンプルを確認できたら,割り込みをつかって練習4を実現してみます.LEDの値設定を割り込みタイマーを利用して,500ms毎に呼び出します. loop()では値を常に減らし続けるコードを記述します.loop()のシングルスレッドだけでは困難と思われるこの処理は,割り込みを 利用することで比較的簡単に記述ができました.下記にそのサンプルコードを記述しておきます.

Example-LED-3

#include <Adafruit_NeoPixel.h>
#include <avr/power.h>
#include <MsTimer2.h>

#define PIN            7
#define NUMPIXELS      10

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
int r[NUMPIXELS],g[NUMPIXELS],b[NUMPIXELS];

void flash() {
  static int count = 0;
  r[count] = 50;
  b[count] = 50;
  g[count] = 50;
  pixels.setPixelColor(count, pixels.Color(r[count],g[count],b[count]));
  count++;
  if( count > NUMPIXELS-1 ){
    count = 0;
  }
 
}

void setup() {
  pixels.begin(); // This initializes the NeoPixel library.
  MsTimer2::set(500, flash); // 500ms period
  MsTimer2::start();
  for( int i = 0; i < NUMPIXELS; i++ ){
    r[i] = g[i] = b[i] = 0;
  }
}

void loop() {
  for(int i=0;i < NUMPIXELS; i++){
    if( r[i] > 0 )r[i] = r[i] - 1;
    if( g[i] > 0 )g[i] = g[i] - 1;
    if( b[i] > 0 )b[i] = b[i] - 1;
    pixels.setPixelColor(i, pixels.Color(r[i],g[i],b[i]));
    pixels.show(); 
    delay(1);
  }
}

関数を利用する

上記の様に割り込み以外にも別の方法でも実現することは可能です.例えば500msに一度だけtrueを返す関数があったらどうでしょう. こんな関数があれば,割り込みを利用せずとも,500msに一度だけ任意のコードを実行させることができます. では関数名をmetro()として,実装してみます.

Example-LED-4

#include <Adafruit_NeoPixel.h>
#include <avr/power.h>
#include <MsTimer2.h>

#define PIN            7
#define NUMPIXELS      10

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
int r[NUMPIXELS],g[NUMPIXELS],b[NUMPIXELS];

boolean metro()
{
  static unsigned long int t = millis();
  if( (millis()-t) > 500 ){
    t = millis();
    return true;
  }
  return false;
}

void flash(){
  static int count = 0;

  r[count] = 50;
  b[count] = 50;
  g[count] = 50;
  pixels.setPixelColor(count, pixels.Color(r[count],g[count],b[count]));
  count++;
  if( count > NUMPIXELS-1 ){
    count = 0;
  }
}

void setup() {
  pixels.begin(); // This initializes the NeoPixel library.
  for( int i = 0; i < NUMPIXELS; i++ ){
    r[i] = g[i] = b[i] = 0;
  }
}

void loop() {
  if( metro() == true ){
    flash();
  }

  for(int i=0;i < NUMPIXELS; i++){
    if( r[i] > 0 )r[i] = r[i] - 1;
    if( g[i] > 0 )g[i] = g[i] - 1;
    if( b[i] > 0 )b[i] = b[i] - 1;
    pixels.setPixelColor(i, pixels.Color(r[i],g[i],b[i]));
    pixels.show(); 
    delay(1);
  }
}

2つのやり方で目的とする機能を実装しました.では一体どちらを利用するのが良いのか,, これは時と場合によります.まずはそれぞれの長所と短所を理解しておきましょう.

    metro()関数
  • 長所:何個でもタイマーを準備できる
  • 短所:loopの呼び出し周期に依存するので時間が精確でない
    割り込み処理
  • 長所:ハードウェア依存なので数個くらいしかせいぜいタイマーをつくれない
  • 短所:loop呼び出し周期に依存しないため,呼び出し時間が精確

2.5 LEDの減衰具合にロバスト性を持たせる

さて,ここまでのプログラムでテープLEDの発光アニメーションを実装してみました. このプログラムを少しいじると気づくかもしれませんが,それぞれのLED発光値が例えば 150の場合と15の場合では,LEDが消えるまでの時間が大きく変わります.現状のプログラム ではloopごとに-1を行っているため,150よりも15の方が10倍早く光が消えてしまいます. これではプログラムを作成する側から,「こんな感じで減衰してほしい」を的確に実現できて いるとは言いがたいです.ではどのような指定の仕方があると嬉しいでしょうか.

やはり,「このくらいの時間で減衰して光が消えてほしい」というのが使いやすいように感じないでしょうか? ここではそれをどのように実装すれば良いのか考えてみます. 先ほど学習した,時間感覚で一定に呼び出しをするような割り込みや関数を利用してみると うまく出来そうです.それではここれ練習問題です.

割り込みを1msにすることで,1,000msで減衰(発光→消灯)となるプログラムを割り込みで実装し, 1000msでLED点灯を行うプログラムをmetro()関数を利用して実装せよ.ただし割り込み関数の名前はfadeoutと すること.発光色は白であるが,random(51)として0-50までのランダム値を利用すること.

ちょっとこれは難しかったので回答も一緒に


#include <Adafruit_NeoPixel.h>
#include <avr/power.h>
#include <MsTimer2.h>

#define PIN            7
#define NUMPIXELS      10

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
float r[NUMPIXELS],g[NUMPIXELS],b[NUMPIXELS];

float dt[NUMPIXELS];
int count = 0;
void fadeout(){
 for(int i=0;i < NUMPIXELS; i++){
    if( r[i] > 0 )r[i] = r[i] - dt[i];
    if( g[i] > 0 )g[i] = g[i] - dt[i];
    if( b[i] > 0 )b[i] = b[i] - dt[i];
    pixels.setPixelColor(i, pixels.Color(r[i],g[i],b[i]));
  }
}

boolean metro()
{
  static unsigned long int t = millis();
  if( (millis()-t) > 1000 ){
    t = millis();
    return true;
  }
  return false;
}

void setup() {
  pixels.begin(); // This initializes the NeoPixel library.
  MsTimer2::set(1, fadeout); // 500ms period
  MsTimer2::start();
  for( int i = 0; i < NUMPIXELS; i++ ){
    r[i] = g[i] = b[i] = dt[i] = 0;
  }
}

void loop() {
  if( metro() == true ){
    r[count] = b[count] = g[count] = random(51);
    dt[count] = r[count]/1000.0;
    pixels.setPixelColor(count, pixels.Color(r[count],g[count],b[count]));
    count++;
    if( count > NUMPIXELS-1 ){
      count = 0;
    }
  }
  pixels.show(); 
}

さあ,ここまでやってみていかがでしょうか.段々とプログラムが複雑になってきました. さらにここからLED発光パターンの要求が複雑化する場合,例えば下記のようなものを作る場合, さらにプログラムが複雑になりそうです.

  • 右から左へ光が流れながら色が赤から青へ変化
  • 右から左へ緑色が光りながら進むなか,青色が途中から追い抜く
  • フルカラーで先ほどと同様に,時間指定での減衰効果を実装したい
  • このような問題に対して,頑張れば複雑なプログラムも対応できますが,できれば複雑になるのは 避けたいところです.そこで,これに対応するためクラスを導入します.

    クラスを用いてより複雑なLED表現をプログラムする

    ハードウェアをソフトウェアで制御する場合,簡単な動作であれば基本的な文法,構文で十分に対応できます. また,複雑なものでも頑張ればそれなりにプログラムも可能です.一方で,クラスで記述すると後々楽になることが たくさんあります.ここではオブジェクト(LEDモジュール)一つに対して一つのクラスで記述することを学習します. プログラム中級者くらいからはこのようなハードをオブジェクト指向で記述するように心掛けましょう.

    まずは,これまでコーディングしてきた,LEDがスーッと移動するプログラムをクラスを用いて 記述しなおしてみます.それでは最初にどうすればよいかですが,対象となる部品モジュールをよく みてください.みなさんの手元にはLEDが10個ついた部品が見えると思います.これがなにかというと, これを基にプログラムを記述します.つまり,この部品はLEDモジュールが10個あり,それが一つの モジュールになっている,というものです.

    第一段階として,LEDモジュール一つに対するクラスを記述し,最後にそれら10個をまとめるためのクラスを記述 します.言葉で説明してもなかなかピンと来ないと思うので実際にやっていきましょう.なお,ここではクラスの基礎を 書きませんので,各々参考書はウェブサイト等で学習しておいてください. http://www.asahi-net.or.jp/~wv7y-kmr/memo/old/cpp_cls.html

    クラス

    演習の中で,実際に設計をしながらコーディングを記述します.当初は簡単な各LEDの制御だけだったものが,アニメーションクラスまで 徐々に開発していく流れを一緒に体験します.出来上がったコードはこちら.

    https://www.dropbox.com/sh/r1ya81p48ft7iyn/AAANLCLfq0zsfk9d1Ju1FbGBa?dl=0

    Copyright (c) 2015 Tetsuaki BABA all rights reserved.