LEDシート(WS2812B制御)を購入してみた

ちまたによく売られている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のライブラリがダウンロードできます。

FastLED LED animation library for Arduino (formerly FastSPI_LED)

FastLED is a fast, efficient, easy-to-use Arduino library for programming addressable LED strips and pixels such as WS2810, WS2811, LPD8806, Neopixel and more. FastLED is used by thousands of developers, in countless art and hobby projects, and in numerous commercial products.

上のページで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();
}

上のプログラムを実行した結果が下のような感じです。普通に光ってますね。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です