ORANGE pico で 1bit CPU をエミュレーションする

先日、[1bit CPU 組み立てキット](https://www.switch-science.com/products/9376)が発売されたが、すぐに売り切れてしまい、買いそこねてしまった。
そこで、公開されている写真と回路図に基づいて [ORANGE pico](http://www.picosoft.co.jp/orange/) でエミュレーションしてみた。

# プログラム

```
10 cls:line 0,0,0,0,1:is_tft=!point(0,0):cls
20 text_color=rgb(255,255,255)
30 background_color=rgb(0,64,0):power_color=rgb(0,255,0)
40 rega_on_color=rgb(255,0,0):rega_off_color=rgb(32,0,0)
50 pc_on_color=rgb(255,255,0):pc_off_color=rgb(32,32,0)
60 clock_on_color=rgb(0,0,255):clock_off_color=rgb(0,0,32)
70 rom_back_color=rgb(48,48,255):rom_switch_back_color=rgb(64,64,64):rom_switch_color=rgb(255,255,255)
80 reset_color=rgb(192,64,64):reset_back_color=rgb(128,128,128)
90 board_x=10:board_y=10:board_w=150:board_h=130:rega_x=70:rega_y=55:reg_dy=16:power_x=90:power_y=35:rom_x=90:rom_y=65:reset_x=80:reset_y=110
100 dim regtexts(6,2)=[rega_x-48,rega_y-3,&H52,&H65,&H67,&H20,&H41,rega_x-48,rega_y-3+reg_dy,&H20,&H20,&H20,&H50,&H43,rega_x-48,rega_y-3+reg_dy*2,&H43,&H6C,&H6F,&H63,&H6B]
110 dim othertexts(6,2)=[power_x-20,power_y-14,&H50,&H6F,&H77,&H65,&H72,rom_x+10,rom_y-20,&H20,&H52,&H4F,&H4D,&H20,reset_x-20,reset_y+10,&H52,&H65,&H73,&H65,&H74]
120 goto 280
130 rectc=pop():recth=pop():rectw=pop():recty=pop():rectx=pop()
140 line rectx,recty,rectx+rectw-1,recty,rectc:line rectx,recty,rectx,recty+recth-1,rectc
150 line rectx,recty+recth-1,rectx+rectw-1,recty+recth-1,rectc:line rectx+rectw-1,recty,rectx+rectw-1,recty+recth-1,rectc
160 return
170 rectc=pop():recth=pop():rectw=pop():recty=pop():rectx=pop()
180 for recti=recty to recty+recth-1
190 line rectx,recti,rectx+rectw-1,recti,rectc
200 next
210 return
220 circlec=pop():circler=pop():circley=pop():circlex=pop()
230 for circlerr=0 to circler
240 circle circlex,circley,circlerr,circlec
250 next
260 circlehack=circler*100/141:gosub 170,circlex-circlehack,circley-circlehack,circlehack*2+1,circlehack*2+1,circlec
270 return
280 if is_tft gosub 170,board_x,board_y,board_w,board_h,background_color else gosub 130,board_x,board_y,board_w,board_h,1
290 for i=0 to 2
300 for j=0 to 4
310 cpeek regtexts(j+2,i):mmove 8000-8,8000,8:cpeek othertexts(j+2,i)
320 for y=0 to 7
330 regp=mpeek(8000-8+y):otherp=mpeek(8000+y)
340 for x=0 to 7
350 if (regp >> (7-x)) & 1 then pset regtexts(0,i)+8*j+x,regtexts(1,i)+y,text_color
360 if (otherp >> (7-x)) & 1 then pset othertexts(0,i)+8*j+x,othertexts(1,i)+y,text_color
370 next
380 next
390 next
400 next
410 for i=0 to 3
420 cpeek &H30+i
430 for y=0 to 7
440 p=mpeek(8000+y)
450 for x=0 to 7
460 if (p >> (7-x)) & 1 then pset rom_x+4+12*(i>0)+20*(i>1)+12*(i>2)+x,rom_y-10+y,text_color
470 next
480 next
490 next
500 gosub 220,power_x,power_y,4,power_color
510 if is_tft gosub 170,rom_x,rom_y,28,32,rom_back_color else gosub 130,rom_x,rom_y,28,32,1
520 if is_tft gosub 170,rom_x+32,rom_y,28,32,rom_back_color else gosub 130,rom_x+32,rom_y,28,32,1
530 if is_tft then gosub 170,reset_x-6,reset_y-5,13,11,reset_back_color else gosub 130,reset_x-6,reset_y-5,13,11,1
540 gosub 220,reset_x,reset_y,3,reset_color
550 clt:u2a_q=0:u2b_q=0:u4_y1=0:u4_y2=0:rega=0:pc=0:clock=0:rom=0:rega_drawn=-1:pc_drawn=-1:clock_drawn=-1:rom_drawn=-1
560 if rega==rega_drawn then goto 600
570 gosub 220,rega_x,rega_y,4,rega*rega_on_color+!rega*rega_off_color*is_tft
580 if !is_tft then circle rega_x,rega_y,4,1
590 rega_drawn=rega
600 if pc==pc_drawn then goto 640
610 gosub 220,rega_x,rega_y+reg_dy,4,pc*pc_on_color+!pc*pc_off_color*is_tft
620 if !is_tft then circle rega_x,rega_y+reg_dy,4,1
630 pc_drawn=pc
640 if clock==clock_drawn then goto 680
650 gosub 220,rega_x,rega_y+reg_dy*2,4,clock*clock_on_color+!clock*clock_off_color*is_tft
660 if !is_tft then circle rega_x,rega_y+reg_dy*2,4,1
670 clock_drawn=clock
680 if rom==rom_drawn then goto 760
690 for i=0 to 3
700 x=rom_x+4+12*(i>0)+20*(i>1)+12*(i>2)
710 gosub 170,x,rom_y+8,8,16,rom_switch_back_color*is_tft
720 if !is_tft then gosub 130,x,rom_y+8,8,16,1
730 gosub 170,x+1,rom_y+17-8*((rom>>i)&1),6,6,rom_switch_color
740 next
750 rom_drawn=rom
760 key=inkey()
770 sw_push = !(key==&H20 || (kbstatus() & 16))
780 if key==&H30 then rom = rom ^ 1
790 if key==&H31 then rom = rom ^ 2
800 if key==&H32 then rom = rom ^ 4
810 if key==&H33 then rom = rom ^ 8
820 if !sw_push then u2a_q = 0 : u2b_q = 0
830 clock = tick() % 60 >= 30
840 if sw_push && clock && clock_drawn < 1 then u2a_q = u4_y1 : u2b_q = u4_y2
850 rega = u2a_q
860 pc = u2b_q
870 u1a = !u2b_q
880 u1b = !(!(!u2b_q && (rom & 1)) && !(!u1a && (rom & 4)))
890 u1c = !(!(!u2b_q && (rom & 2)) && !(!u1a && (rom & 8)))
900 u3a = !(u2a_q && u1c)
910 u3b = !(u2a_q && u3a)
920 u3c = !(u3a && u1c)
930 u3d = !(u3b && u3c)
940 if u1b then u4_y1 = u2a_q : u4_y2 = u1c else u4_y1 = u3d : u4_y2 = !u2b_q
950 goto 560
```

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

なお、参照した回路図は [MIT License](https://github.com/naoto64/1bit-CPU/blob/main/LICENSE) となっている。

> Copyright (c) 2021 naoto64

# 操作方法

* スペースキー：リセット
* 0～3の数字キー：ROMの対応するスイッチの切替

# 実行結果例

[1bit CPU 組み立てキット | 1bit-CPU](https://naoto64.github.io/1bit-CPU/)
に掲載されている「Aレジスタに1を加算し続けるプログラム（Lチカ）」を実行してみた。

![実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/0c77fde0-d3f5-82ba-f222-a0242cf3610d.gif)

また、TFT液晶によるカラー表示にも対応している。

![実行結果 (TFT液晶)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/ef5fb9da-0af4-e03b-385b-ddceb5c7ddc7.jpeg)


# 解説

## 10行目：環境の判別

画面上の点の色を取得する `point` 関数が、ビデオ出力モードでは `line` による描画を反映するが、TFT液晶モードでは反映しないことを利用し、TFT液晶モードかどうかを判別する。

## 20～120行目：パラメータ設定

描画用の色、座標、テキストを設定している。

`regtexts` および `othertexts` は、「x座標、y座標、文字列 (5文字)」を繰り返している。

## 130～270行目：描画用サブルーチン

* 130～160行目：左上の座標、幅、高さ、色を指定し、辺が各軸に平行な長方形を線で描画する。
* 170～210行目：左上の座標、幅、高さ、色を指定し、辺が各軸に平行な長方形を塗りつぶしで描画する。
* 220～270行目：中心の座標、半径、色を指定し、円を塗りつぶしで描画する。

## 280～540行目：画面の変化しない部分の描画

画面の以下の部分を描画する。

* 280行目：基板の枠
* 290～490行目：各種文字
* 500行目：電源LED
* 510～520行目：ROMスイッチの枠
* 530～540行目：リセットボタン

## 550行目：初期化

クロック・レジスタ・描画の状態を初期化する。

## 560～750行目：画面の変化する部分の描画

画面の以下の部分を描画する。
ただし、前回の描画時から対応する状態が変化していない場合は描画しない。

* 560～590行目：Reg A の LED
* 600～630行目：PC の LED
* 640～670行目：Clock の LED
* 680～750行目：ROMスイッチ

## 760～810行目：操作受付

キーの状態を読み込み、状態に反映させる。

## 820～950行目：論理回路エミュレーション

各素子の入力の値から、出力の値を計算していく。

# 今回新たに発見した罠

## 長めの変数名を使うと Syntax error になる

長めの変数名を使おうとすると、Syntax error が出てしまうようである。
試してみたところ、21文字の変数名を用いた

```
some_long_identifiers = 1
```

は実行できたが、22文字の変数名を用いた

```
the_long_variable_name = 1
```

は Syntax error になってしまった。
今のところドキュメントで変数名の長さの制限に関する記述は見つけられておらず、文法は間違っていなそうであるにもかかわらず Syntax error が出るため、初見では戸惑う可能性がある。

![長めの変数名で Syntax error](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/702b4001-0b55-ec68-9356-f52a53d24d5a.png)

## gosub のパラメータの文法に間違いがあるとき、その場ではエラーにならず無視される

たとえば、以下のプログラムを実行してみる。
このプログラムには、10行目の `2+3` の後に余計な `)` があるという文法エラーがある。
よって、ここで Syntax error が出ることを期待する。

```
10 gosub 30,1,2+3),4+5
20 end
30 print pop()
40 print pop()
50 print pop()
60 return
```

しかし、実際に実行してみると、「Stack underflow in 50」というエラーが出た。
すなわち、文法エラーがあるにもかかわらず**一部を無視する形で強引にサブルーチンを実行**した結果、引数が足りないとしてエラーになったのである。
実際に間違いがある行とエラーメッセージの行が異なってしまうため、原因特定の難易度が上がってしまう。

![gosubで文法エラーがあっても無視して実行される](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/bd15a190-22f1-c4ef-ade7-a9b98162fa1e.png)

## TFT液晶にもテキストが出力される

公式の [ORANGE pico type S のピン配置の資料](http://www.picosoft.co.jp/orange/download/typeS/ORANGEpicoSpin.pdf)には、以下のように書かれている。

> TFT液晶にはグラフィック出力のみが有効です。
> TFT液晶ではテキスト出力がないため、別途シリアルコンソールを接続しておく必要があります。

このため、TFT液晶モードでは、ビデオ出力モードではテキスト画面に出力するようなコマンドは画面への出力を行わず、UARTを使わなければ出力を確認できない、という印象を受けた。
言い換えれば、TFT液晶モードではグラフィック画面とテキスト画面が重ならず、分けて表示を確認できる、という印象である。

しかし、実際にTFT液晶モードを使用してみると、テキストも出力された。
起動時に表示されるテキスト、キーボードやUARTから入力した内容、`print` コマンドで出力した内容、すべて出力された。
テキストはグラフィックを上書きしており、全てグラフィック出力といえるという点では「テキスト出力がない」というのは嘘ではないかもしれないが、「別途シリアルコンソールを接続しておく必要があります」というのは**明確に嘘**であろう。

実機で試さず、ドキュメントから得られた解釈を鵜呑みにして画面出力が分離できる前提のプログラムを書いていると、意図しない挙動となってしまうかもしれない。

![TFT液晶にもテキストが出力される](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/66259e56-b70a-5916-54de-d5999489f7f5.jpeg)

## TFT液晶モードではシリアル送信時ウェイトを入れないと取りこぼしが発生しうる

TFT液晶モードにおいて、今回のプログラムをUART (115200bps、送信遅延0ミリ秒) で流し込み、`list` コマンドで認識されたプログラムを確認すると、以下のようになった。

```
10 cls:line 0,0,0,0,1:is_tft=!point(0,0):cls
20 text_color=rgb(255,255,255)
30 background_color=rgb(0,64,0):power_color=rgb(0,255,0)
40 rega_on_color=rgb(255,0,0):rega_off_color=rgb(32,0,0)
50 pc_on_color=rgb(255,255,0):pc_off_color=rgb(32,32,0)
60 clock_on_color=rgb(0,0,255):clock_off_color=rgb(0,0,32)
70 rom_back_color=rgb(48,48,255):rom_switch_back_color=rgb(64,64,64):rom_switch_color=rgb(255,255,255)
80 reset_color=rgb(192,64,64):reset_back_color=rgb(128,128,128)
90 board_x=10:board_y=10:board_w=150:board_h=130:rega_x=70:rega_y=55:reg_dy=16:power_x=90:power_y=35:rom_x=90:rom_y=65:reset_x=80:reset_y=110
100 dim regtexts(6,2)=[rega_x-48,rega_y-3,&h52,&h65,&h67,&h20,&h41,rega_x-48,rega_y-3+reg_dy,&h20,&h20,&h20,&h50,&h43,rega_x-48,rega_y-3+reg_dy*2,&h43,&h6c,&h6f,&h63,&h6b]
110 dim othertexts(6,2)=[power_x-20,power_y-14,&h50,&h6f,&h77,&h65,&h72,rom_x+10,rom_y-20,&h20,&h52,&h4f,&h4d,&h20,reset_x-20,reset_y+10,&h52,&h65,&h73,&h65,&h74]
120 goto 280
130 rectc=pop():recth=pop():rectw=pop():recty=pop():rectx=pop()
140 line rectx,recty,rectx+rectw-1,recty,rectc:line rectx,recty,rectx,recty+recth-1,rectc
150 line rectx,recty+recth-1,rectx+rectw-1,recty+recth-1,rectc:line rectx+rectw-1,recty,rectx+rectw-1,recty+recth-1,rectc
160 return
170 rectc=pop():recth=pop():rectw=pop():recty=pop():rectx=pop()
180 for recti=recty to recty+recth-1
190 line rectx,recti,rectx+rectw-1,recti,rectc
200 next
210 return
220 circlec=pop():circler=pop():circley=pop():circlex=pop()
230 for circlerr=0 to circler
240 circle circlex,circley,circlerr,circlec
250 next
260 circlehack=circler*100/141:gosub 170,circlex-circlehack,circley-circlehack,circlehack*2+1,circlehack*2+1,circlec
270 return
280 if is_tft gosub 170,board_x,board_y,board_w,board_h,background_color else gosub 130,board_x,board_y,board_w,board_h,1
290 for i=0 to 2
300 for j=0 to 4
310 cpeek regtexts(j+2,i):mmove 8000-8,8000,8:cpeek othertexts(j+2,i)
320 for y=0 to 7
330 regp=mpeek(8000-8+y):otherp=mpeek(8000+y)
340 for x=0 to 7
350 if (regp >> (7-x)) & 1 then pset regtexts(0,i)+8*j+x,regtexts(1,i)+y,text_color
360 if (otherp >> (7-x)) & 1 then pset othertexts(0,i)+8*j+x,othertexts(1,i)+y,text_color
370 next
380 next
390 next
400 next
410 for i=0 to 3
420 cpeek &h30+i
430 for y=0 to 7
440 p=mpeek(8000+y)
450 for x=0 to 7
460 if (p >> (7-x)) & 1 then pset rom_x+4+12*(i>0)+20*(i>1)+12*(i>2)+x,rom_y-10+y,text_color
470 next
480 next
490 next
500 gosub 220,power_x,power_y,4,power_color
510 if is_tft gosub 170,rom_x,rom_y,28,32,rom_back_color else gosub 130,rom_x,rom_y,28,32,1
520 if is_tft gosub 170,rom_x+32,rom_y,28,32,rom_back_color else gosub 130,rom_x+32,rom_y,28,32,1
530 if is_tft then gosub 170,reset_x-6,reset_y-5,13,11,reset_back_color else gosub 130,reset_x-6,r580 if !is_tft then rl g_,g_41590 rega_drawn=rega
600 if pc=pc_drawn then goto 640
610 gosub 220,rega_x,rega_y+reg_dy,4,pc*pc_on_color+!pc*pc_off_color*is_tft
620 if !is_tft then circle rega_x,rega_y+reg_dy,4,1
630 pc_drawn=pc
690 for i=0 to 3
700 x=rom_x+4+12*(i>0)+20*(i>1)+12*(i>2)
710 gosub 170,x,rom_y+8,8,16,rom_switch_back_color*is_tft
720 if !is_tft then gosub 130,x,rom_y+8,8,16,1
730 gosub 170,x+1,rom_y+17-8*(770 sw_push = !(key=&h20sas)&6)780 if key=&h30 then rom = rom ^ 1
790 if key=&h31 then rom = rom ^ 2
800 if key=&h32 then rom = rom ^ 4
810 if key=&h33 then rom = rom ^ 8
820 if !sw_push then u2a_q = 0 : u2b_q = 0
880 u1b = !(!(!u2b_q and (rom & 1)) and !(!u1a and (rom & 4)))
890 u1c = !(!(!u2b_q and (rom & 2)) and !(!u1a and (rom & 8)))
900 u3a = !(u2a_q and u1c)
910 u3b = !(u2a_q and u3a)
920 u3c = !(u3a and u1c)
930 u3d = !(u3b and u3
6680 if rom=rom_drawn then goto 760
```

530行目の途中からテキストが抜け始め、その後もところどころ抜けが発生している。
Tera Term の送信遅延を1ミリ秒/字、0ミリ秒/行に設定したところ、プログラムの流し込みは正常に行うことができたが、今回のプログラムの流し込みに1分強もかかってしまった。
ビデオ出力モードであれば、送信遅延ゼロで正常にプログラムを流し込むことができ、2秒ほどで完了した。

TFT液晶モードとビデオ出力モードの切り替えにはリセットが必要であるため、ビデオ出力モードで流し込んでおいてからTFT液晶モードに切り替えるのは有効ではない。
TFT液晶モードでの効率の良い開発には、EEPROMを活用する、書き換えた部分のみプログラムを転送する、などの工夫が求められそうである。

## TFT液晶モードでは gput が背景を黒く塗りつぶす

以下のプログラムを実行してみる。

```
10 white=rgb(255,255,255)
20 line 50,150,100,150,white
30 gprint 55,146,"12345",white
40 line 50,180,100,180,white
50 for i=0 to 4
60 cpeek &H31+i
70 gput 55+8*i,176,8,8000,white
80 next
```

ビデオ出力モードでは、`gprint` コマンドでは背景が黒く塗りつぶされてしまうが、`gput` コマンドを使うと背景の塗りつぶしを回避し、既に描かれている図形に重ねて文字を描画することができる。

![ビデオ出力モードでの gprint と gput](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/30d03d49-6a01-9fcb-ae94-fe83a55c0fd6.png)

しかし、TFT液晶モードでは、`gprint` コマンドだけでなく `gput` コマンドでも背景が黒く塗りつぶされてしまった。

![TFT液晶モードでの gprint と gput](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/e03870fc-9e8a-f623-acae-49d9478a71ad.jpeg)

ビデオ出力モードであれば問題になる場面は少ない可能性があるが、TFT液晶モードではカラー表示が可能であり、黒以外の背景に図形を重ねたい場面も出てくることが考えられるため、問題になりやすいだろう。
今回のプログラムでは、`gput` のかわりに `pset` を用いて1ドットずつ描画することで、この罠を回避した。