公開日:2020/05/30
title.png

11th Season


#11-2 円形配置RGB LED(WS2812B)を使う

 前回まで3回連続で使って来たTFTシールドはなかなか楽しくて、もっと遊んでいたいのだけど一旦休憩。今回は、TFTシールドと同じ時に通販で購入しておいたもう一つのデバイスを試してみたいと思います。

円形配置RGB LED

 そのデバイスとは、円形配置RGB LEDです(下図)。

a112_rgbring.jpg

 RGB LEDには、カスケード接続してRGB各色0〜255の256段階で明るさを指定可能なWS2812Bが搭載されています。

  WS2812Bのデータシートを検索する(別ウインドウが開きます)

 RGB各色256段階ですので、所謂1677万色フルカラー表示ってふれこみです。通信速度は800kHzですので、30fps(Frames Per Second)で更新しようとしても、1,000個程度をカスケード接続できる仕様です。
 WS2812Bは5V単一電源で、データ受信用のDI端子とカスケード接続用のデータ送信用DO端子を備えた4ピンパッケージとなっています。以下図の様に、複数のWS2812Bをカスケードに繋いで使用します。

a112_cascade.png
※WS2812Bデータシートから引用


 通信データフォーマットを下図にまとめてみました。

a112_commu.png

 通信線DIは通常LOWレベルになっており、通信ラインがリセットされた状態です。リセットと言っても、通信状態がリセットされるだけで、RGB LED自体は直前に設定された色で発光を続けています。リセットが解除されると同時に、データの受信を開始します。
 データはRGB各色を0〜255の8ビットで表現して、G→R→Bの順にMSBファーストで送信します。通信速度は1.25μs(TYP)と規定されており、ビット"0"と"1"ではデューティ(HIGH幅)が異なるフォーマットになっています。
 以下表に通信波形の規定をまとめておきます。

  TYP[ns] 許容[ns]   @16MHz
[clock]
T0H 400 ±150   6
T0L 850 ±150 14
T0cycle 1,250 ±600 20
T1H 800 ±150 13
T1L 450 ±150 7
T1cycle 1,250 ±600 20

 単位はナノ秒[ns]です。今回の課題は、何と言ってもナノ秒単位の高速なデータ通信をArduino UNOで実現することにあります。ATmega328 @16MHzのUNOでは、MCU 1クロックに要する時間が1/16MHz=62.5nsですので、上表に示した通り20クロック(TYP)で1波形を作り出さなくてはなりません。
 Arduino UNOで制御するには、中々シビアなタイミングです。

送信波形の生成

 ATmega328の命令は、ほぼ1クロック/1命令(1CPI;Clocks Per Instruction)なので、1ビット分の波形を20命令で送信する必要があります。一部、ブランチ命令やロード/ストア命令など、実行に2クロック以上を要する命令もあるので20クロックで実行可能な命令数はもっと少ない場合があります。
 波形のHIGH幅/LOW幅にしても、許容される偏差が±150nsで、クロック数にして±2.4クロックしかありません。
 Arduino UNOで20クロック周期の送信波形を生成するのは「かなり難しい」と言う事です。

 考えた中で唯一可能性のありそうな、タイマのPWM波形を使ってデータの送信波形を作成しようと思います。16ビットタイマ(TC1)でも構いませんが、今回は8ビットタイマ(TC2)を使いました。タイマ(TC2)を以下の様に設定します。

レジスタ名 アドレス 設定値 説明
TCCR2A 0x00b0 0x03 Fast PWM (OCR2A) mode,OC2B=normal port
リセット出力中の設定値
0x23 Fast PWM (OCR2A) mode,OC2B=PWM output
データ送信中の設定値
TCCR2B 0x00b1 0x09 Fast PWM mode (OCR2A),fcpu/1
OCR2A 0x00b3 20−1 通信波形周期:20クロック
OCR2B 0x00b4 6−1 ビット"0"デューティ:6クロック
13−1 ビット"1"デューティ:13クロック
TIMSK2 0x0070 0x00 割り込み無効

 使用するモードはFast PWMモードです。OCR2Aで周期、OCR2Bでデューティを設定します。
 タイマのPWM出力は、デューティを設定して放置しておくと、ずっとそのデューティでPWMを出力し続けてしまいます。今回はこのようなPWMを出力することが目的ではなく、データの送信に使うので、オーバフロウフラグ(TOV2)をポーリングして毎周期デューティを変更して使います(↓)。

  // send
  //__asm__ __volatile__ ("wdr");
  for (idx=1; idx<rgb_MAX_PIX*rgb_SIZ_PIX*8; idx++)
  {
    // polling TOV2
    while (!(TIFR2&0x01))
      ;
    TIFR2=0x01;  // flag clear

    // set duty
    OCR2B=dbuf[idx];
  }
  //__asm__ __volatile__ ("wdr");


 Fast PWMモードではOCR2Bレジスタはダブルバッファになっており、値を書き換えてもその値は次のTOV2フラグが立つまで反映されません。よってTOV2フラグが立つ度に、次の送信データに対応するデューティ値を書き込みします。
 ここで、最重要事項が!上に示したデータの送信ループですが、1回のループ実行を20クロック以内に終了する必要があります。C言語のソースを眺めていてもらちが明かないので、Arduino IDEで「スケッチメニュー ⇒ コンパイルしたバイナリを出力」を実行して、ArduinoのFlashメモリに書き込むデータを出力します。
 この時、上のコード例ではコメントにしてある、wdr命令のインラインアセンブラ記述を活かした状態で実行します。「スケッチメニュー ⇒ スケッチのフォルダを表示」で開いたディレクトリ内に「<スケッチ名>.ino.standard.hex」と言うファイルができています。バイナリデータをインテルHEXと言う方式でテキスト出力したファイルです。

  インテルHEXについて検索する(別ウインドウが開きます)

 このファイルをテキストエディタで開いて、wdr命令のオペコードである「A895」を検索します。

    : (省略)
  :10040000B400A895EAEFF1E091E0B89BFECF97BB6E
  :1004100081918093B40084E0E933F807B1F7A8959F
    : (省略)


 wdr命令はウォッチドックタイマのカウンタをリセットするための命令で、通常、GCCが自動的に生成することはありません。上で見つかった「A895」のパタン2個は、インラインアセンブラで強制的に出力したwdr命令だと考えられます。そして、2個の「A895」の間に存在する命令がforループ内の実行命令です。

  AVR命令データシートを検索する(別ウインドウが開きます)

 ざっと30バイトそこそこの命令ですので、頑張ってハンド逆アセンブルしてください(1^−^;)。筆者が逆アセンブルした結果は以下の様になりました。

命令 実行クロック数 説明
loop: ループの先頭
sbis 0x17,0x0 1/2 TOV2=1までポーリング
rjmp loop 2
out 0x17,r25 1 TOV2クリア
ld r24,z+ 2 zレジスタ(dbuf)から1バイト読み込み、z++
sts 0x00b4,r24 2 読んだデータをOCR2Bに書き込み
ldi r24,0x04 1 ループの終了判定
cpi r30,0x39 1
cpc r31,r24 1
brne loop 1/2 ループ先頭へブランチ

※実行クロック数:スラッシュ(/)の左側は条件不成立時、右側は条件成立時です。

 このコードはTOV2ポーリングのタイミングによって若干実行クロック数が変化するのですが、ベスト〜ワーストケースを考えても、12〜14クロック程度で1回分のループ処理が終わっています。1回分のループが20クロック以内で終了する必要がありますので、余裕はありませんが何とか収まっているようです。

 ただ、GCCのバージョンが変わったり、前後のソースコードが変化したりすると、ループ内の命令が増加して20クロックをオーバする恐れがあります。本来であれば上のコード全体をインラインアセンブラで記述しておく方が安全だと思います。……20クロックに収まっているのに面倒なので、筆者はC言語のままにしておきますm(1^ー^)m。気になる方はご自身でどうぞ。

 万一、ループ内の処理が20クロック内に処理できなかった場合、1ビット分同じデータがダブって送信されてしまうことでそれ以降全てのデータが1ビットずれてしまい、誤った情報で発光してしまいます。全く意図しない発光パタンとなりますので、すぐに気が付くでしょう。

 レートが速すぎてSTM32のLogiscopeをもってしても波形のキャプチャは不可能なので、確かめられないのがちょっと不安ではありますが……。一応、この方法でデータの送信波形は生成できるはずです。

蛇足の考察

 フラグクリアコード「TIFR2=0x01;」は、あまり意識していなかったのですが、結果としてはGoodでした。以下、フラグクリアコードについて考察してみます。

 どのコードでも、フラグTOV2をクリアできて、アセンブラレベルで見ると全て2クロックで実行されています。しかし、今回選択した「TIFR2=0x01;」だけ、「ldi r25,0x01」の部分をループの外に押し出すことが出来るため、ループ内の処理としては1クロックで終了できました。
 たったこれだけのことで1クロック分の実行速度差が出てしまうんですね。今回の様に「20クロック以内」と言う厳しい制約がある場合には、じんわりと利いてきます。

 もう一歩踏み込んでおくと、この例では、一時レジスタr25が占有使用可能だったため、このような芸当ができたのです。もし、ループ前後のコードに処理を追加した場合で、何らかの変数にr25が割り当てられ一時レジスタが確保できなかった場合、GCCは「ldi r25,0x01」の部分をループ内に押し込んできます。
 現に、逆アセンブルしたループの終了判定コードでは、「ldi r24,0x04」がループ内で毎回実行されています。

 留意したいのは、「ループ内のコード」だけではなく、「ループ前後のコードにも、ループ内のアセンブラ(≒実行クロック数)は影響を受ける」ことです。

※蛇足の蛇足ですが、コードサイズを最小化したい場合には、C言語で「TIFR2 &= 0x01;」とコーディングした時の「sbi TIFR2,0x01」が1命令、1ワードで最小です。

データの送信

 送信すべきデータは、1ピクセル当たりRGB各8ビットの計24ビット=3バイトです。今回の円形配置RGB LEDでは24ピクセルがカスケードされているので、3バイト×24ピクセル=72バイト分のデータが必要です。これをuint8型の配列pbuf[ ]に設定する事とします(↓)。

  const int rgb_MAX_PIX=24;  // rgb: count of pixel
  const int rgb_SIZ_PIX=3;  // rgb: bytes per pixel
  static uint8 pbuf[rgb_MAX_PIX*rgb_SIZ_PIX];  // rgb: pixel buffer


 pbufに設定するデータは、以下の通りです。

  pbuf[idx×3+0]=緑の発光強度0〜255
  pbuf[idx×3+1]=赤の発光強度0〜255
  pbuf[idx×3+2]=青の発光強度0〜255
  idx=0〜ピクセル数−1

※WS2812Bへの送信順になっているので、G→R→Bの順であることに注意。

 データをWS2812Bに送信する時は、pbuf[ ]のデータを読み出しながら、そのビットパタンを1ビットづつ判定して、OCR2Bに設定すべきデューティ値に変換しながら送信する必要があるのですが、前節で検討したように、そんな複雑な処理をしていては20クロック以内に送信ループ処理を終了することは不可能です。
 そこで、送信を開始する前にpbuf[ ]内のビットデータからOCR2Bのデューティデータdbuf[ ]に変換しておいて、dbuf[ ]を順次送信するようなプログラムにしています。

  static uint8 dbuf[rgb_MAX_PIX*rgb_SIZ_PIX*8];  // rgb: duty buffer

 dbuf[ ]は、1ビット当たり1バイトのデータ領域が必要なため、今回のケースでは24ビット×1バイト×24ピクセル=576バイトのデータ領域を必要とします。しかも、内容としては同じpbuf[ ]とdbuf[ ]を二重に持つ必要が有って無駄なのですが、利便性を確保した上での速度最優先なので止むを得ません。

 後は、前節で検討した送信ループで一気に送信するだけです。この時、送信中に割り込みが発生してしまうと、簡単に「20クロック」の制限時間をオーバしてしまうので、送信している間は割り込みを禁止しておきます。1ビット分の送信時間は1.25μsですので、送信にかかる時間は24ビット×24ピクセル×1.25μs=720μsと、少し長くなります。
 まだシステムタイマ(TC0、millis()やmicros()をカウントしているタイマ)の割り込み周期約2msよりは短く、システムタイマを破綻させるような時間ではありませんが、割り込み禁止期間が少し長めとなるので、何らかの割り込みを使うアプリケーションとの共存を考える場合には留意する必要があります。

電極の引き出し

 ArduinoとWS2812Bとの接続・通信に目途が立ったところで、肝心な円形配置RGB LEDの方を整備しておきます。
 入手した円形配置RGB LEDは裏面に下図(左端)の様な電極パタンが設置されており、WS2812Bの4本の端子と同じ、5V,GND,DI,DOの4端子を引き出すことが出来ます。

a112_pinh.jpg

 基板パタンだけでは配線を引き出すことが出来ないので、上図中央の様にピンヘッダを半田付けします。こうすることで、ピンヘッダにデュポンワイヤを接続して各電極を引き出すことが出来るようになります。
 半田付け後に表面から見たのが上図右端です。ピンヘッダのところに設置されているWS2812Bが第0番目のピクセルで、そこから時計回りに24個のWS2812Bがカスケード(数珠繋ぎ)につながっていました。
 送信したデータは、第0番目のピクセルから順に受け取って行き、最後に送ったデータが第23番目のピクセル用データとなります。

 基板パタンは、絶縁体の基板に薄い銅板を接着剤で張り付けただけのものです。そのパタンにピンヘッダを半田付けしているので、無理な力がかかると基板パタンがはがれて外れてしまいます。そうなったら修復不能ですので、注意して扱ってください。

実験です🐘

 実験に使うプログラムをリンクしておきます。

  実験プログラム:test_ws2812b.ino

 コンパイルしてUNOに転送すると、シリアルモニタから以下のコマンドを受け付けるようになっています。

  コマンド仕様:

 前節でピンヘッダを半田付けしてあればデュポンワイヤで接続するだけですので、以下の様に接続してください。

Arduino UNO 接続 円形配置RGB LED
5V 5V
GND GND
D3(OC2B) DI
  × DO

 タイマ(TC2)のOC2B端子を使ってPWM(送信波形)を出力するので、D3(OC1B)端子を接続します。

 以下、実験中の様子です。

a112_expr.jpg

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【pix,0,4,0,0】
  OK: pixel #0=(4, 0, 0)

  【pix,8,0,4,0】
  OK: pixel #8=(0, 4, 0)

  【pix,16,0,0,4】
  OK: pixel #16=(0, 0, 4)

 ピクセル0番から8ピクセル飛びに赤、緑、青で光らせました。明るさは全て4(/255=最大値)と、かなり抑えています。「さぞかし暗いのだろう」と思うかもしれませんが、多分、十分明るいはずです。注意して下さい、255で光らせると明るすぎます。直視すると目を傷めてしまうのではないか?と、思うほどです。
 WS2812Bが照明用途に設計・製造されたものなのか、あるいは、野外のイルミネイションとして使った時にも十分な光量を出力するために設計されたものなのかわかりませんが、部屋の中で数10センチの距離で光らせるには眩しすぎます。

※とは言っても、12個では部屋の照明にはちょっと足りません。机上のスタンド程度の使い方なら、何とかなるかな?と言うレベルです。帯に短し、襷に長し。

 上の実験サンプルの様に、明るさを抑えて使うことは十分可能ですので、用途に合わせてお使いください。

  【demo】
  OK: demo finished

  【clr】
  OK: clear all pixel

 万一、うまくデータの送・受信ができなかった場合には、328duino(第二形態)のクロック周波数を1MHzに落として(ヒューズ書き込みでCLKDIV8=0&内蔵RC発振器8MHzを選択)、プログラム(test_ws2812b.ino)を実行するとSTM32+Logiscopeで波形のキャプチャ可能なレートに落ちるので、それでデバッグしようかと考えていたのですが、デモの実行状況を見る限りそこまでする必要は無さそうです(∵想定通り動作している)。

 デモを実行した時の様子を動画にしておきました(↓)。



 発色は綺麗です。昔、「#4-6 LEDキャンドル」で使ったRGB LED(下図)は、R,G,Bの明るさのバランスがとれていなかったり、思ったような色合いを出すのは難しかったのですが、WS2812Bは出荷時点で調整されているらしく、ちゃんと設定した色が発色されます。

ar46_rgbled.jpg

 ただ、消費電流もそれなりに大きいみたいです。次節で電流消費量を計測してみます。

消費電流が気になる

 USB電流・電圧計(下図)を使って、UNO+円形配置RGB LEDモジュールの消費電流値を計測してみようと思います。

ar74_usbvam.jpg

 測定方法:

 計測結果は以下の通りとなりました。

a112_current.png

 縦軸に1ピクセル当たりの電流値[mA]、横軸にRGBの発光強度をプロットしています。黄色プロットが白色光の消費電流で、後はプロットの色がRGB各原色の消費電流値です。
 ほぼ、発光強度に対してリニアに電流値が増加しています。Excelに一次回帰直線の数式を表示させていますので、この系数を使って1ピクセル当たりの電流値を概算することが出来るようになっています。
 例えば、白色光で光らせた場合の電流値はy≒0.1419×xですので、x=255を設定して最高輝度で光らせた場合、消費電流は1ピクセル当たりy≒0.1419×255=36.2mAとなります。この明るさで24ピクセル全てを光らせてしまうと、36.2mA×24ピクセル≒870mA程にもなってしまいます。
 USBから給電可能な電流値は、500mA程度ですので、これを遥かに超えてしまいます。

 USB給電では、絶対に最高輝度で24ピクセル全てを光らせるようなことは止めてください。

 USB電源端子に500mAのヒューズでもついていればまだましですが、もしついていなかった場合、過電流でPCのUSB端子の方が壊れてしまう可能性もあります。
 照明代わりに使うなどの用途を想定して、どうしても最高輝度で光らせたいなら、十分な定格電流値を持ったACアダプタなどから給電する必要があります。

今回のお買い物

 今回のお買い物は、以下の通りです。

 TFTシールドと同じ時にamazonの「置き配」指定で購入しました。


 今回入手した円形配置RGB LEDは十分な電流供給能力を持った電源を使いさえすれば、非常に明るく発光させることが出来る上、色の再現性もなかなかのものなので電気屋さんで売っている、「昼光色」とか「電球色」、「昼白色」など、調光可能な照明としても使えるかもしれないし、中々興味深いです。気分を上げたい時は赤い照明、落ち着かせたい時は青い照明、リラックスしたい時は緑の照明、なんてのも良いですね。


btn_arduino.png

btn_prev.png btn_next.png

home