IchigoJam と WS2812B マトリックスで「大石泉すき」

今日は12月3日。「いずみ」と読むことができ、大石泉の日。

# WS2812B

**WS2812B** は、フルカラーLEDである。
マイコンが内蔵されており、信号線1本だけで色の設定が可能である。
さらに、複数のWS2812Bを数珠つなぎにして、まとめて色を設定することが可能である。

WS2812B を用いた様々な製品が発売されているが、今回は以下の16×16のマトリックスを用いる。

[1~5Pcs WS2812B 8x8 16x16 RGB LED Digital Flexible Individually Addressable Panel Screen WS2812 8x32 Smart Pixel Module Matrix 5V - AliExpress](https://www.aliexpress.com/item/1005005250171855.html)

同じ「16×16のマトリックス」の製品でも複数の種類がある。
今回使用するのは、文字が普通の方向になるように置いたとき、以下の順番にLEDが並んでいる製品である。

* 最下段の一番左のLEDが最初である
* そこから、右に向かってLEDが配置されている
* LEDは段ごとにジグザグに配置されている。すなわち、下から奇数番目の段は左から右に、偶数番目の段は右から左にLEDが配置されている

# WS2812B と IchigoJam の連携

[IchigoJam](https://ichigojam.net/) (1.4系) には、WS2812B を操作する機能がある。

※IchigoJamはjig.jpの登録商標です。

## BASIC コマンド

```
WS.LED LEDデータ数
WS.LED LEDデータ数, リピート数
```

配列に格納されたデータを色データとして WS2812B に送信する。
「LEDデータ数」は、配列からLED何個分のデータを読み取るかを指定する。配列の3要素でLED 1個分のデータとなるので、参照する要素数はこの数の3倍である。データは配列の先頭 (`[0]`) から順に読み取られる。
「リピート数」は、「LEDデータ数」で指定した数のデータを1セットとして、何セット繰り返し送信するかを指定する。これにより、少ないデータで長い WS2812B の列を制御できる。「リピート数」は省略可能で、省略した場合は1となる。

IchigoJam の配列は102要素しか無いため、このコマンドで使用可能な最大のLEDデータ数は34である。

## マシン語API

```c
void ws_led (uint32_t countrepeat, const uint8_t* data, uint32_t gpiomask);
```

アドレス `#EE` から符号なしの2バイトを読み取り、これを関数のアドレスとして呼び出す。

`countrepeat` は、WS2812B に送信するデータ数を指定する。「LED数」を指定する BASIC コマンドとは異なり、送信する要素数をそのまま指定する。
`data` は、WS2812B に送信するデータが格納された先頭アドレスを指定する。IchigoJam の配列の要素は2バイトだが、ここで指定するデータは1バイトで1要素を表す。
`gpiomask` は、送信先のポートを表すデータを指定する。LED端子に出力する場合は、以下で定義された `GPIO_LED` を指定する。

```c
#define GPIO1 0x50010000
#define GPIO_LED	(GPIO1 + (1 << (5 + 2))) // LED
```

IchigoJam の VRAM は 32×24=768 バイトあるので、これを利用して LED 256個の色情報を個別に設定できる。(3×256=768)

# 「大石泉すき」を描くプログラム

16×16の WS2812B マトリックスに「大石泉すき」をスクロールさせて表示するプログラムを作成した。

## まずは画面に表示する

```
10 ' ｵｵｲｼｲｽﾞﾐ ｽｷ (ｶﾞﾒﾝ)
20 CLV:LET[0],0,64,64,64,64,8190,192,448,352,800,560,1552,3096,6156,4102,0,0,0,0,8190,384,192,96,48,8184,4140,4134,4128,4128,8160,0,0,0,256,128,8188,4100,8188,4100,8188
30 LET[40],128,3324,960,416,664,1156,6336,0,0,128,128,4092,128,128,240,144,144,240,128,192,96,48,16,0,0,32,96,3904,508,128,7296,2044,256,256,256,388,12,24,8176,0
40 CLT:T=0:FOR I=-1 TO 4:FOR O=0 TO 15:CLS
50 IF I<0 GOTO 90
60 FOR X=0 TO 15-O:FOR Y=0 TO 15
70 IF ([16*I+Y]>>(O+X))&1 LOCATE 8+X,4+Y:PRINT "*";
80 NEXT:NEXT
90 IF O=0 OR I=4 GOTO 130
100 FOR X=16-O TO 15:FOR Y=0 TO 15
110 IF ([16*I+16+Y]>>(X-(16-O)))&1 LOCATE 8+X,4+Y:PRINT "*";
120 NEXT:NEXT
130 T=T+10
140 X=T-TICK():LOCATE 0,0:PRINT X:IF X>0 WAIT X
150 NEXT:NEXT
160 GOTO 40
```

20～30行目で、「大石泉すき」のデータを定義する。
それぞれの文字を16×16ピクセルで表す。
1行が16ビットなので、ちょうど配列の1要素で表すことができる。
1文字は上の行から下の行に向かう順番で表し、1行はLSBを左端、MSBを右端として表す。

40行目で、経過時間管理の初期化と文字配置のループを行う。
変数 `I` で、どの文字を表示するかを決める。
変数 `O` で、文字をどの位置に表示するかを決める。

50～80行目で、左側の文字の描画を行う。
スクロールさせるため、基本的に文字は2個を表示する。
変数 `O` に従って表示する文字の範囲を決定し、表示する。

90～120行目で、右側の文字の描画を行う。
条件やデータの位置は異なるが、基本的に50～80行目と同じである。

130～140行目で、画面の更新間隔を制御する。
130行目で「このフレームの終了時、経過しているべき時間」を計算する。
140行目で、130行目で計算した時間になるようにウェイトを入れる。

150～160行目で、40行目のループを回す。

---

[OneFiveCrowdで実行](https://onefivecrowd.mikecat.work/#text=RUN%0A&prg=CgAUJyC1tbK8sr3e0CC9tyAott7S3SkAFACmQ0xWOkxFVFswXSwwLDY0LDY0LDY0LDY0LDgxOTAsMTkyLDQ0OCwzNTIsODAwLDU2MCwxNTUyLDMwOTYsNjE1Niw0MTAyLDAsMCwwLDAsODE5MCwzODQsMTkyLDk2LDQ4LDgxODQsNDE0MCw0MTM0LDQxMjgsNDEyOCw4MTYwLDAsMCwwLDI1NiwxMjgsODE4OCw0MTAwLDgxODgsNDEwMCw4MTg4AAAeAJ5MRVRbNDBdLDEyOCwzMzI0LDk2MCw0MTYsNjY0LDExNTYsNjMzNiwwLDAsMTI4LDEyOCw0MDkyLDEyOCwxMjgsMjQwLDE0NCwxNDQsMjQwLDEyOCwxOTIsOTYsNDgsMTYsMCwwLDMyLDk2LDM5MDQsNTA4LDEyOCw3Mjk2LDIwNDQsMjU2LDI1NiwyNTYsMzg4LDEyLDI0LDgxNzYsMAAoAChDTFQ6VD0wOkZPUiBJPS0xIFRPIDQ6Rk9SIE89MCBUTyAxNTpDTFMAADIADklGIEk8MCBHT1RPIDkwADwAHkZPUiBYPTAgVE8gMTUtTzpGT1IgWT0wIFRPIDE1AABGADBJRiAoWzE2KkkrWV0-PihPK1gpKSYxIExPQ0FURSA4K1gsNCtZOlBSSU5UICIqIjsAUAAKTkVYVDpORVhUAABaABZJRiBPPTAgT1IgST00IEdPVE8gMTMwAGQAHkZPUiBYPTE2LU8gVE8gMTU6Rk9SIFk9MCBUTyAxNQBuADhJRiAoWzE2KkkrMTYrWV0-PihYLSgxNi1PKSkpJjEgTE9DQVRFIDgrWCw0K1k6UFJJTlQgIioiOwB4AApORVhUOk5FWFQAAIIABlQ9VCsxMACMACxYPVQtVElDSygpOkxPQ0FURSAwLDA6UFJJTlQgWDpJRiBYPjAgV0FJVCBYAACWAApORVhUOk5FWFQAAKAACEdPVE8gNDA)すると、画面上に「大石泉すき」が流れていく。

## WS2812B マトリックスに表示する

```
10 ' ｵｵｲｼｲｽﾞﾐ ｽｷ (WS.LED)
20 CLV:LET[0],0,64,64,64,64,8190,192,448,352,800,560,1552,3096,6156,4102,0,0,0,0,8190,384,192,96,48,8184,4140,4134,4128,4128,8160,0,0,0,256,128,8188,4100,8188,4100,8188
30 LET[40],128,3324,960,416,664,1156,6336,0,0,128,128,4092,128,128,240,144,144,240,128,192,96,48,16,0,0,32,96,3904,508,128,7296,2044,256,256,256,388,12,24,8176,0
40 POKE#8A0,15,0,20,15,0,20,15,0,20,2,20,2,5,15,0:POKE#700,63,161,128,49,128,49,3,32,0,2,2,74,238,35,27,136,24,71,0,0,128,0,1,80
50 CLT:T=0:FOR I=-1 TO 4:C=#8A0+3*I:FOR O=0 TO 15:CLS
60 IF I<0 GOTO 100
70 FOR Y=0 TO 15:M=[16*I+Y]>>O:FOR X=0 TO 15-O:IF Y%2 THEN U=X ELSE U=15-X
80 IF M>>X&1 COPY#BD0-48*Y+3*U,C,3
90 NEXT:NEXT
100 IF O=0 OR I=4 GOTO 140
110 FOR Y=0 TO 15:M=[16*I+16+Y]:FOR X=16-O TO 15:IF Y%2 THEN U=X ELSE U=15-X
120 IF M>>(X+O-16)&1 COPY#BD0-48*Y+3*U,C+3,3
130 NEXT:NEXT
140 Z=USR(#700)
150 T=T+90
160 X=T-TICK():PRINT X:IF X>0 WAIT X
170 NEXT:NEXT
180 GOTO 50
```

<details><summary>マシン語のソースコード</summary>

```
R1 = @ARRAY
R1 += #80
R1 += #80
R0 = 3
R0 = R0 << 8
R2 = [@GPIO_LED]L
R3 = #EE
R3 = [R3]W
GOTO R3

ALIGN 4,0,0
@GPIO_LED
DATAL #50010080

ORGR #100
@ARRAY
```

</details>

「大石泉」を大石泉をイメージした青色で表示し、「すき」をニューウェーブの他の2人をイメージした赤色と黄色で表示するようにした。
画面に描画するプログラムと比べて、以下の点を変更している。

* 配列の文字データの後に、LEDに設定する色のデータを追加した。各文字についてそれぞれ3バイトの色情報が並んでいる。
* データを配置する位置を今回のLEDマトリックスの仕様に合わせて計算して配置するようにした。
* 高速化のため、同じ計算の繰り返しを避けて変数に格納するようにした。
* データを WS2812B に送信するためのマシン語コードを追加し、呼び出すようにした。

動くことは動くが、実機 (IchigoJam Q、1.4.3) でビデオ出力なし (`VIDEO 0`) のとき1フレームの描画に最大1.5秒弱かかる程度に低速である。

## データの配置もマシン語で行う

```
10 'ｵｵｲｼｲｽﾞﾐ ｽｷ(WS.LED ﾏｼﾝｺﾞ)
20 CLV:LET[0],0,64,64,64,64,8190,192,448,352,800,560,1552,3096,6156,4102,0,0,0,0,8190,384,192,96,48,8184,4140,4134,4128,4128,8160,0,0,0,256,128,8188,4100,8188,4100,8188
30 LET[40],128,3324,960,416,664,1156,6336,0,0,128,128,4092,128,128,240,144,144,240,128,192,96,48,16,0,0,32,96,3904,508,128,7296,2044,256,256,256,388,12,24,8176,0,15,3860,5120,15,276,276,3845
40 POKE#700,240,180,114,160,6,138,135,139,54,178,61,165,160,53,173,25,173,25,173,25,0,36,58,160,113,1,64,24,0,25,0,25,0,35,54,66,0,212,3,136,0,34,4,46,0,218,2,140,18,4,155,24,251,64,15,34,1,33,11,66
50 POKE#73C,28,208,16,70,12,66,1,208,64,66,15,48,48,33,97,67,73,66,9,24,9,24,9,24,3,32,0,2,208,48,9,24,40,160,9,24,40,70,186,66,0,218,192,28,148,70,2,120,10,112,66,120,74,112,130,120,138,112,98,70
60 POKE#778,91,8,82,30,220,218,100,28,15,44,200,221,30,161,128,49,128,49,3,32,0,2,2,74,238,35,27,136,240,188,24,71,128,0,1,80
70 CLT:T=0:FORI=-1TO4:FORO=0TO15:CLS:Z=USR(#700)
80 T=T+10
90 X=T-TICK():?X:IFX>0WAITX:NEXT:NEXT:GOTO70
```

<details><summary>マシン語のソースコード</summary>

```
' R7 : O
' R6 : I
' R5 : 色データのアドレス (#8A0 + 3*I)
PUSH {R4,R5,R6,R7}
R0 = @VARS
R6 = [R0 + 8]W
R7 = [R0 + 14]W
R6 = SXTH(R6)
R5 = @ARRAY
R5 += #A0
R5 = R5 + R6
R5 = R5 + R6
R5 = R5 + R6
' R4 : Y
' R3 : 画像データ (≒M)
R4 = 0
@Y_LOOP
' 行の画像データを取得する
R0 = @ARRAY
R1 = R6 << 5
R0 = R0 + R1
R0 = R0 + R4
R0 = R0 + R4
R3 = 0
R6 & R6
IF MI GOTO @NO_FIRST_PART
R3 = [R0 + 0]W
@NO_FIRST_PART
R2 = 0
R6 - 4
IF GE GOTO @NO_SECOND_PART
R2 = [R0 + 16]W
@NO_SECOND_PART
R2 = R2 << 16
R3 = R3 + R2
R3 >>= R7
' 行の画像データをVRAMに反映する
' R2 : 15-X
R2 = 15
@X_LOOP
R1 = 1
R3 & R1
IF EQ GOTO @NO_PUT_DOT
' R0 : U
R0 = R2
R4 & R1
IF EQ GOTO @NO_REVERSE_X
R0 = -R0
R0 += 15
@NO_REVERSE_X
' R1 : #BD0-48*Y+3*U
R1 = 48
R1 *= R4
R1 = -R1
R1 = R1 + R0
R1 = R1 + R0
R1 = R1 + R0
R0 = #03
R0 = R0 << 8
R0 += #D0
R1 = R1 + R0
R0 = @ARRAY
R1 = R1 + R0
' R0 : C / C+3
R0 = R5
R2 - R7
IF GE GOTO @NO_SHIFT_COLOR
R0 = R0 + 3
@NO_SHIFT_COLOR
' 色データをコピーする
R12 = R2
R2 = [R0 + 0]
[R1 + 0] = R2
R2 = [R0 + 1]
[R1 + 1] = R2
R2 = [R0 + 2]
[R1 + 2] = R2
R2 = R12
@NO_PUT_DOT
R3 = R3 >> 1
R2 = R2 - 1
IF GE GOTO @X_LOOP
R4 = R4 + 1
R4 - 15
IF LE GOTO @Y_LOOP
' VRAM上の画像データをLEDに転送する
R1 = @ARRAY
R1 += #80
R1 += #80
R0 = 3
R0 = R0 << 8
R2 = [@GPIO_LED]L
R3 = #EE
R3 = [R3]W
POP {R4,R5,R6,R7}
GOTO R3

ALIGN 4,0,0
@GPIO_LED
DATAL #50010080

ORGR #100
@ARRAY
ORGR #1CC
@VARS
```

</details>

データを配置する処理もマシン語で行うようにし、BASICでは描画位置とタイミングの管理のみを行うようにした。
マシン語では、レジスタが32ビットあることを活かし、2文字分のデータをまとめて取得して処理するようにした。
更新間隔を調整しやすいよう、タイミングを決定する80行目 `T=T+10` は独立した行にした。
実機 (IchigoJam Q、1.4.3) で1フレームの処理に1/60秒以下しかかからず、高速で処理できるようになった。

<iframe width="560" height="315" src="https://www.youtube.com/embed/8WXHhm0d7R8?si=X-UrcpWmGoj5Olw5" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

[OneFiveCrowdで実行する](https://onefivecrowd.mikecat.work/#text=WAIT1%3ARUN%0A&prg=CgAaJ7W1sryyvd7QIL23KFdTLkxFRCDPvN263ikAFACmQ0xWOkxFVFswXSwwLDY0LDY0LDY0LDY0LDgxOTAsMTkyLDQ0OCwzNTIsODAwLDU2MCwxNTUyLDMwOTYsNjE1Niw0MTAyLDAsMCwwLDAsODE5MCwzODQsMTkyLDk2LDQ4LDgxODQsNDE0MCw0MTM0LDQxMjgsNDEyOCw4MTYwLDAsMCwwLDI1NiwxMjgsODE4OCw0MTAwLDgxODgsNDEwMCw4MTg4AAAeALxMRVRbNDBdLDEyOCwzMzI0LDk2MCw0MTYsNjY0LDExNTYsNjMzNiwwLDAsMTI4LDEyOCw0MDkyLDEyOCwxMjgsMjQwLDE0NCwxNDQsMjQwLDEyOCwxOTIsOTYsNDgsMTYsMCwwLDMyLDk2LDM5MDQsNTA4LDEyOCw3Mjk2LDIwNDQsMjU2LDI1NiwyNTYsMzg4LDEyLDI0LDgxNzYsMCwxNSwzODYwLDUxMjAsMTUsMjc2LDI3NiwzODQ1AAAoAMRQT0tFIzcwMCwyNDAsMTgwLDExNCwxNjAsNiwxMzgsMTM1LDEzOSw1NCwxNzgsNjEsMTY1LDE2MCw1MywxNzMsMjUsMTczLDI1LDE3MywyNSwwLDM2LDU4LDE2MCwxMTMsMSw2NCwyNCwwLDI1LDAsMjUsMCwzNSw1NCw2NiwwLDIxMiwzLDEzNiwwLDM0LDQsNDYsMCwyMTgsMiwxNDAsMTgsNCwxNTUsMjQsMjUxLDY0LDE1LDM0LDEsMzMsMTEsNjYAADIAwlBPS0UjNzNDLDI4LDIwOCwxNiw3MCwxMiw2NiwxLDIwOCw2NCw2NiwxNSw0OCw0OCwzMyw5Nyw2Nyw3Myw2Niw5LDI0LDksMjQsOSwyNCwzLDMyLDAsMiwyMDgsNDgsOSwyNCw0MCwxNjAsOSwyNCw0MCw3MCwxODYsNjYsMCwyMTgsMTkyLDI4LDE0OCw3MCwyLDEyMCwxMCwxMTIsNjYsMTIwLDc0LDExMiwxMzAsMTIwLDEzOCwxMTIsOTgsNzAAADwAelBPS0UjNzc4LDkxLDgsODIsMzAsMjIwLDIxOCwxMDAsMjgsMTUsNDQsMjAwLDIyMSwzMCwxNjEsMTI4LDQ5LDEyOCw0OSwzLDMyLDAsMiwyLDc0LDIzOCwzNSwyNywxMzYsMjQwLDE4OCwyNCw3MSwxMjgsMCwxLDgwAEYALkNMVDpUPTA6Rk9SST0tMVRPNDpGT1JPPTBUTzE1OkNMUzpaPVVTUigjNzAwKQAAUAAGVD1UKzEwAFoAKlg9VC1USUNLKCk6P1g6SUZYPjBXQUlUWDpORVhUOk5FWFQ6R09UTzcw&virtualFullColorLED=connect%3D1%26type%3Dmatrix%26dataOrder%3Dgrb%26matrixWidth%3D16%26matrixHeight%3D16%26matrixStart%3Dleft_down%26matrixDir%3Dhorizontal%26matrixZigzag%3Don%26ringSize%3D12%26ringStart%3Dleft%26ringDir%3Dclockwise)

## ライセンス

今回のプログラムは、[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja) でライセンスする。
このプログラム (改造したものを含む) を公開の場で利用する際は、出典を示していただけると嬉しい。
これは、Qiitaの利用規約に基づくプログラムの利用を妨げるものではない。

# 結論

IchigoJam のマシン語APIを利用することで、BASICコマンドで制御できるより多くの WS2812B LED を制御し、マトリックスに「大石泉すき」を表示することができた。
大石泉すき。