ちまたによく売られているWS2812BのシリアルLEDシート、nano pixel LEDとか製品名のほうが知られてたりします。前から気になってたので、買ってみました。フルカラーの高輝度LED。きれいです。テンションあがります。
この記事でわかること。
1)WS2812bの制御がわかる
2)FastLED libraryを使い方が分かる
3)ライブラリに頼らず、自力で書く方法がわかる
WS2812/WS2812b/WS2822Sとは?
ネットで調べるとシリアルLEDでも色々種類があって、自分の買ったのがどれなのか最初に困りました。というのもマルツの店頭販売で購入したため、製品名も何も書かれてなかったので。
でも、WS2812bの見分け方は簡単でした。WS2812bだけ端子が4本で他は6本でています。自分のは4本でしたので、WS2812bとすぐに分かりました。WS2812とWS2822Sは同じ6本端子ですが、写真を比べると内部のチップの大きさが違いますね。
写真は、左からWS2812b, WS2812, WS2822Sの順です。
最新はWS2822Sです。この手の制御方式は一個のLEDチップが故障すると、下流側のLEDが全て全滅するというドSな特徴を持っていたのですが、最新のは、この弱点が解決されているようです。
とはいえ、自分の手にしたのが偶然にもWS2812bだったので、記事ではWS2812bの使用方法に限定して、話しをしたいと思います。
特徴は?
LEDが複数個あって、それぞれ個別に色とか変えたいよといった場合、そのままだと一個ずつデジタル入出力のポートにつないで、個別に制御する必要がありますが、LEDを数十個つなげるみたいにスケールが大きくなってくると、マイコンのポート数が足りなくなります。
WS2812bは、LED制御用にそれぞれのLEDで筐体内にチップを内蔵していて、このチップにマイコン側から、データを送信することで、LEDを点灯させることができます。要は電気的な回路ではなくて、制御信号を各LEDに送ることになる訳で、別途電源ラインは必要ですが、複数のLEDを一本の信号線にいっぱいぶらさげても問題ないという理屈です。
つなぎ方は?
背面から5V, GND, DINの3極のコネクタがオスとメスの二つと5V, GNDの切り離しの線が2本でてます。このうち、メスコネクタをArduinoにつなげます。
ちなみにオスコネクタはLEDシートをもう一個買ったときにつなげる為の拡張用コネクタです。切り離しの線はシートが複数枚になったときに外部から供給するための電源線です。一枚なら、Arduinoの5V電源で事足りますので、いらないです。
下が接続イメージ。デジタル出力ピン1本で64個のLEDを制御。すげ~
制御の中身は?
制御方法の詳細は、ここに書かれています。
https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf
要約すると..
・一個のLEDに対してRGB値を指示する3バイトのデータが必要。
・例えば、10個のLEDがぶら下がっている場合には、マイコン側は3×10バイトのデータを送信する。
・先頭のLED(#1)が最初の3バイトを読んで、データを次のLED(#2)に渡して、#2は、次の先頭3バイト読んでいき、#3に渡す..って感じのバケツリレー方式でデータを順に渡す。
・各bitのHigh/Lowは下のようなパルス幅で出力する。
・データはGreen, Red, Blueのバイト順
一見、簡単そうですが、パルス幅が狭い(最小で400nsec)のがネックです。Arduino UNOでは、クロック周波数16MHzが低いため、この時間幅のパルスをdigitalWriteで制御するのは、時間がかかりすぎて、ムリです。
16MHzというと、1clockで62.5ns。6~7clock分しか処理に余裕がありません。一方でdigitalWriteは44clock必要なのが、その理由です。(調べてません。ネットに書いてました)
こういう場合、よりクロック周波数の高いCPUを使うか、digitalWriteを使わずに処理を高速化してやる必要があります。
ただ、そんな細かいことはおいといて、とりあえず光らせたいよという自分みたいな人は、便利なライブラリが色々あるので、それを使いましょう。ライブリがそこら辺の処理を代行してくれるので、あまり時間かけずに使えますよ..
FastLED ライブラリ
Fast Led Animation Library というサイトがあって、ws2812b用のArduinoのライブラリがダウンロードできます。
上のページでDownload the libraryというリンクからGitHubにとべるので、そこから最新のver3.3.3をダウンロードします。
ArduinoIDEでスケッチ->ライブラリをインクルード->.ZIP形式のライブラリをインストールを選択して、ダウンロードしたファイルを”FastLED-3.3.3”を読み込みます。
スケッチ例からFastLEDのサンプルコードが使えるようになります。
この中のBlinkを使ってみました。
LEDが点滅するコードみたいです。下がサンプルコードのコピーですが、行数節約のため、コメント文は削除してます。処理内容はコメントをみてください。
#include <FastLED.h> // これがライブラリ
#define NUM_LEDS 64 //ここにLEDの個数をいれる。初期値が1だったので64にした。
#define DATA_PIN 3 // LEDシートにつなぐデジタル出力のピン番号をいれる
#define CLOCK_PIN 13 //ここは使わないので、無視
// Define the array of leds
CRGB leds[NUM_LEDS]; // LEDの構造体の配列。LEDの個数分、RGB情報を保持。
void setup() {
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
}
void loop() {
// Turn the LED on, then pause
leds[0] = CRGB::Red; //[]に点滅させるLEDの番号を入れる、HTMLカラーコードに対応してる
// 色の設定はほかにも方法があって、
//leds[0].setRGB( 255, 68, 221)でもいいし
//それか、leds[0].r =255; leds[0].g=68; leds[0]=221;でもいい
FastLED.show(); //ここでぴかってなる
delay(500); // まつ
// Now turn the LED off, then pause
leds[0] = CRGB::Black; //黒=消す色にセット
FastLED.show(); // ここで消す
delay(500);
}
色の設定はコメントに書いたとおり、何通りかありますので、やりやすい方法で。
例えば、leds[32] = CRGB::Red; に変更して、Gold色で#32のLEDをピカ。
以上が基本でしょうか。ドラスティックなやつをやりたい場合は、これをベースに自分で作りこむか、他にサンプルが色々ありますので、試してみるとおもしろいですよ。
Inline Assemblyを使って、制御する
どうやって、400nsのパルスをつくるかというと、タイトルの通りですが、インラインアセンブリを利用します。
ここで前の記事が役に立ちました。インラインアセンブリを解説してます。
やることは超シンプルで、仕様書で書いてあるパルス幅になるようにnopを適当にいれ、調整する。これだけです。例えば下のコードは#13(PB5)のピンをLoopの中で、High/Lowを切りかえていますが、実際オシロで確認すると
asm
(
"0: \n"
"sbi %0, %1 \n" //Highにセット
"nop \n"
"cbi %0, %1 \n" //Lowにセット
"jmp 0b \n"
: : "I"(_SFR_IO_ADDR(PORTB)),"I"(PORTB5) // unoの#13ピンをデジタル出力で使う
);
下みたいになります。
High時の幅は190nsでした。400nsにくらべ、これでは短すぎです。これにnopを追加して、HighとLowの時間幅を調整したコードが下になります。
asm volatile
(
"0: \n"
"sbi %0, %1 \n"
"nop \n""nop \n""nop \n""nop \n" //4clock待つ
"cbi %0, %1 \n"
"nop \n""nop \n""nop \n""nop \n""nop \n" //9clock待つ
"nop \n""nop \n""nop \n""nop \n"
"jmp 0b \n"
: : "I"(_SFR_IO_ADDR(PORTB)),"I"(PORTB5)
);
こうするとhigh側が375ns, low側は875nsになり、0コードに要求される信号がほぼ実現できました。
上の例は0コードと1コードを決め打ちでしたが、実際は、sbrsというオペコードを利用して、bit が1のときと0のときで処理を分けながら、信号を出力します。
方針は下のイメージの通りです。ちなみに青が0コード、赤が1コードです。
基本的にポイントはこれだけですが、全体のながれはこんな感じです。
1)データを汎用レジスタに1バイトずつ読み込む
2)ビット毎にHigh/Lowを判定する
3)上の結果によって、0codeか1codeを実行する
4)64×3バイト分やる
下がサンプルコードになります。LEDdataというの各LEDのRGBデータになっていて、SetLED関数で、そのデータをUNOのデジタルピン#13に出力します。
#include <Arduino.h>
#define NLED 64 //LEDの個数
#define BITSIZE (NLED * 3) //データサイズ LED一個につき3バイト
uint8_t LEDdata [BITSIZE] ={0};
void setLED(uint8_t *);
void Clear_Color(void);
void Set_Color_All (uint8_t,uint8_t,uint8_t);
void Set_Color (uint8_t,uint8_t,uint8_t,uint8_t);
void Set_Color_Random();
void setup()
{
asm volatile(
"sbi %0, %1 \n"
: : "I"(_SFR_IO_ADDR(DDRB)),"I"(DDB5) //#13ピンをデジタル出力にする
);
Clear_Color();
setLED(LEDdata);
}
void loop()
{
Set_Color_All(255,255,255);
setLED(LEDdata);
delay(1000);
for(int i=0;i<10;i++)
{
Set_Color_Random();
setLED(LEDdata);
delay(100);
}
Clear_Color();
setLED(LEDdata);
delay(100);
Set_Color_All(0,0,255);
setLED(LEDdata);
for(int i=0;i<64;i++)
{
Set_Color(i,255,0,0);
setLED(LEDdata);
delay(10);
}
for(int i=63;i>-1;i--)
{
Set_Color(i,0,255,0);
setLED(LEDdata);
delay(10);
}
}
void Clear_Color()
{
for (int i=0;i<BITSIZE*8;i++) LEDdata[i]=0U;
}
void Set_Color_All(uint8_t R,uint8_t G, uint8_t B)
{
for (int i=0;i<NLED;i++)
{
LEDdata[i*3]=G;
LEDdata[i*3+1]=R;
LEDdata[i*3+2]=B;
}
}
void Set_Color(uint8_t num,uint8_t R,uint8_t G, uint8_t B)
{
LEDdata[num*3]=G;
LEDdata[num*3+1]=R;
LEDdata[num*3+2]=B;
}
void Set_Color_Random()
{
//full random
for (int i=0;i<BITSIZE*8;i++)
LEDdata[i]=random(0,30);
}
void setLED(uint8_t * data)
{
noInterrupts(); //割り込まれると破綻するので、割り込み禁止にする
asm volatile(
"ldi r17,%3 \n" //r17はトータルのバイトサイズ
//処理が終わったらディクリメントしていく
"0: \n"
//bit7
"sbi %0, %1 \n" //とりあえずhigh
"ld __tmp_reg__, X+ \n" //新しいバイトを読む Xレジスタを利用してポインタで読み込む
"nop \n"
"sbrs __tmp_reg__,7 \n" //bit7が0か1かで分岐
"cbi %0, %1 \n" //0だったらすぐにlowにする
"nop \n" "nop \n" "nop \n" "nop \n"
"cbi %0, %1 \n" //1だったら、ここでlowにする
"nop \n" "nop \n" "nop\n" "nop \n" "nop \n""nop \n"
//bit6
"sbi %0, %1 \n" //とりあえずset high
"nop \n""nop \n" "nop \n"
"sbrs __tmp_reg__,6 \n" //bit6が0か1かで分岐
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n""nop \n""nop \n"
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n" "nop \n"
//bit5
"sbi %0, %1 \n" //とりあえずset high
"nop \n""nop \n" "nop \n"
"sbrs __tmp_reg__,5 \n" //bit5が0か1かで分岐
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n" "nop \n""nop \n"
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n""nop \n"
//bit4
"sbi %0, %1 \n" //とりあえずset high
"nop \n""nop \n" "nop \n"
"sbrs __tmp_reg__,4 \n" //bit4が0か1かで分岐
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n" "nop \n""nop \n"
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n""nop \n"
//bit3
"sbi %0, %1 \n" //とりあえずset high
"nop \n""nop \n" "nop \n"
"sbrs __tmp_reg__,3 \n" //bit3が0か1かで分岐
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n" "nop \n""nop \n"
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n""nop \n"
//bit2
"sbi %0, %1 \n" //とりあえずset high
"nop \n""nop \n" "nop \n"
"sbrs __tmp_reg__,2 \n" //bit2が0か1かで分岐
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n""nop \n""nop \n"
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n" "nop \n"
//bit1
"sbi %0, %1 \n" //とりあえずset high
"nop \n""nop \n" "nop \n"
"sbrs __tmp_reg__,1 \n" //bit1が0か1かで分岐
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n""nop \n""nop \n"
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n" "nop \n"
//bit0
"sbi %0, %1 \n" //とりあえずset high
"nop \n""nop \n" "nop \n"
"sbrs __tmp_reg__,0 \n" //bit0が0か1かで分岐
"cbi %0, %1 \n"
"nop \n" "nop \n" "nop \n" "nop \n""nop \n"
"cbi %0, %1 \n"
"subi r17, 1 \n" //r17をディクリメント
"brcs end \n" //全部のデータ処理がおわったら、ループ抜ける
"jmp 0b \n"
"end: \n"
:: "I"(_SFR_IO_ADDR(PORTB)),"I"(PORTB5),"x"(data),"M"(BITSIZE-1)
);
interrupts();
}
上のプログラムを実行した結果が下のような感じです。普通に光ってますね。