gooブログサービス終了のため、noteに引っ越しました。
4004で動くVTLインタプリタでStarTrekを走らせる話です。
去年作ったVTLインタプリタですが、このときの目標はとりあえずマンデルブロ集合を表示するプログラムASCIIART.BASを走らせることだったので、未実装の機能がいくつかありました。
しかし、メモリ空間をほぼギリギリまで使っていたため機能追加は難しい状況。4004で動く8080のエミュレータがあれば8080用の言語処理系が動かせるのではないかと思って作ったのがこちら↓です。これでPalo Alto Tiny BASICやGAME80を動かすことはできたのですが、ちょっと実用にならないレベルの遅さでした。
というわけで、VTLインタプリタの方をもう少しちゃんとしたものにすることにしました。メモリ空間はモニタプログラムの機能を削って確保しました。
今回実装した機能は下記の通り。
古典的なコンピュータゲームであるStarTrekが走るレベルの機能を目標にしました。
まずは乱数。
疑似乱数の生成アルゴリズムというのは結構奥が深い世界なのですが、そんなに真面目に実装する気は無いし、そもそも4004には計算パワーも論理演算命令も無いので、とりあえず線形合同法でいいやと思って実装してみました。下記のような単純なものです。
uint16_t rand(){
static uint16_t x=1;
x=x*16877; /* 16877=7^5 */
return(x>>4);}
x_nをそのまま使うと出てこない数があったので>>4して0~4095までの乱数にしました。試しにx_nをプロットしてみるとこんな感じ。
周期性がありますが、まあわりと乱数っぽいです。ゲームぐらいには使えるかなと思ったのですが、後述するStarTrekで星がいつでも斜めに並ぶという現象が発生。
(x_{2n}, x_{2n+1})をプロットしてみると・・・
これはダメです。ちょっと使い物になりません。
というわけで、もうちょっと真面目に実装することにしました。疑似乱数生成アルゴリズムをググっていたところ、16ビットマイコンボードの製作 というページにxorshiftというアルゴリズムが紹介されているのを見つけました。
16 bit xorshift rng (now with more period)
聞いたこと無いなあと思って見てみたところ、どうやら2003年に提案されたアルゴリズムのようです。とてもシンプルなアルゴリズムです。
uint16_t rnd_xorshift_32() {
static uint16_t x=1,y=1;
uint16_t t=(x^(x>>5));
x=y;
return y=(y^(y>>1))^(t^(t>>3));
}
これが21世紀になるまで発見されていなかったというのは逆に驚きでした。
y_nをプロットしてみると、
この範囲では周期性っぽい模様は見えません。(y_{2n}, y_{2n+1})を見ても、
ちゃんと乱数っぽくなっています。良さそうなのでこれを実装することにしました。
しかしここで問題が。4004にはxorを計算する命令がありません。8080エミュレータのときに書いたコードをベースにして作ってみました。長いですが他に方法も無いのでこれを使うことにしました。
4004で16bitのxorを計算するプログラムです。
乱数生成部のルーチンはこんな感じになりました。
データRAMを16bitのレジスタとして使うためのサブルーチン群が既にあるのでこの程度に収まってます。ビットシフトは乗除算に使っているサブルーチンを流用しています。
これにより、システム変数 ' で0~32767までの乱数が得られるようになりました。0~99の乱数が欲しいときは、最初、
R=' / 100 R=%
のようにして求めていたのですが、見た目が悪いので剰余を求める二項演算子 % を実装して、
R='%100
と書けるようにしました。'(100)という記法にするより実装しやすいのでこうなりました。あと、せっかく排他的論理和のルーチンも作ったので二項演算子 ^ も実装しました。
乱数が作れたことにより、最古の経営シミュレーションと思われる「イスカンダルのトーフ屋ゲーム」を走らせることができました。動画はこちら。
8080エミュレータでは激遅でとても遊べない速度でしたが、今回はちゃんとした速度で動きました。
次に配列です。とりあえず1次元配列を1つだけ実装。記法は、@(x)にしました。TK-80BSのLevel 1 BASICをリスペクトしています。最初これをPEEK、POKEの機能にしようかとも思ったのですが、4004の機械語を書いてもそこにジャンプして実行できるわけではなく、データ格納以外に使い道が無いので、変数と同じ16bitの整数として読み書きします。
これでStarTrek走るかな?と思ってエンサイクロペディアASCIIのStarTrek特集を眺めてみると、多段のGOSUBが使われています。サブルーチンが1段だけのVTL用に移植されたもの(マイクロTREK)もあったのですが、配列に返り番地を積むとか、あまり美しくない移植だったので、多段のサブルーチンを真面目に実装することにしました。
GOSUBは現在実行中のコンテクストを退避してGOTOでジャンプするだけ、RETURNはコンテクストを復帰させるだけなので、それほど難しくはありません。退避するコンテクストは、
としました。本質的なのは実行中のプログラム位置だけですが、今回の実装ではその前後もある方が作りやすかったのでコンテクストに含めています。
実行中の行の先頭アドレスは、後述の高速GOTO用のシステム変数 # のため、次の行の先頭アドレスはIF文不成立時に文末まで舐めずに次の行に飛ぶためのものです。
これでどうにかStarTrekを動かせるレベルの言語処理系が準備できました。
StarTrekのプログラムは結構長いので、どこかに転がってないかとググってみたところ、こちらのサイト(Bequest333のページ)で電大版StarTrekが入ったパッケージを入手。どうやら電大版Tiny BASICにはFOR文が無いようで、GOTOを#=に、PRを?=に、IFを;=にのように機械的に置き換えるだけでほぼそのままVTL-4004に移植できました。
さっそく実行してみましたが何かがおかしく、いろいろ調べてみたところどうやらエネルギーが0で何も出来無いという状態。安田寿明著「マイ・コンピュータをつかう」に載っているソースと比べてみたところ、なぜか330行目のGOSUB 1600が存在せず、エネルギーや光子魚雷の数が設定されなくなっていたのが原因でした。
そこを修正したところ無事ゲームで遊べるようになったのですが、速度がかなり遅いです。Short Range Sensorの表示に1分ぐらいかかっていました。
考えられる原因はGOTO文(#=)の処理です。今の実装では#=は飛び先の場所をプログラムの先頭から行番号を探してジャンプします。一応行番号の隣に次の行へのポインタを格納しているので、プログラムの文字列を全部舐めているわけではないのですが、それでもプログラムの後ろの方に
5000 I=I+1 ;=I<10 #=5000
みたいな行があると、#=5000でプログラムの先頭から順番に見ていって5000の行まで探すので大規模なプログラムだとかなりのオーバーヘッドになります。
ジャンプ先を行番号ではなくプログラムの場所のポインタで指定できれば即座にジャンプできます。というわけで、システム変数 # を、現在実行している行の行番号ではなく、現在実行中の行の先頭アドレスに変更することにしました。
さらに、実行するプログラムの位置を変更するための記法として、>= を実装しました。さきほどの例は、
5000 I=I+1 ;=I<10 @(I)=0 >=#
のように書き直します。16bitの値で普通に変数に格納できるので、ループを多重にしたい場合は A=#のように保存しておいて、>=Aでジャンプすることもできます。
ループで何度も実行されるGOTOをこれに書き直すことによってかなり高速化され、Short Range Sensorも30秒ほどで表示できるようになりました。
ゲームの様子です。まだ遅いと言えば遅いですが、ギリギリ耐えられる遅さにすることができたかなと思います。
インタプリタのソースコード等一式はGitHubに置きました。
1年前にeBayで買ったまま積んであった4040があったので、これを使った基板を作ってみることにしました。4040は4004と互換性が高く、ピンの配置を変換するアダプタでも作ればそのまま4004実験ボードに搭載してそのまま動かすことができるはずなのですが、せっかくなので拡張された機能も使えるようにします。
ハードウェアに関連する主な拡張機能は次の3つ。
割り込みに関しては、通信ポートをポーリングではなく割り込みにするということも考えましたが、8251や6850のような通信用のLSIがあるわけでもなく面倒なので割り込みボタンを1つつけるだけにしました。
ROMのバンクを切り替えるための信号線CMROM1により、プログラムメモリ領域が2倍になりました。4004のときには8080エミュレータにしてもVTLにしても4KBのプログラム領域を使い切って足りなくなっていたのでかなりうれしいです。
シングルステップ実行は、アドレスやデータをラッチして表示したりしないと実用的じゃないし、使い道がいまいちイメージできなかったので信号線だけ出して未実装です。
ソフトウェアに関連する拡張機能には次のようなものがあります。
4004ではサブルーチンのネストが3段しか出来ず、プログラムを書く際の大きな制約になっていたのですが、少しはマシになりそうです。とはいえ、7段では構文解析みたいながっつり再帰的なプログラムには足りそうもないので使い道は限定されますが。
4004では256byteバンク内のメモリ値しか読めなかったので、4004実験機ではあらかじめ全部のバンクの末尾にメモリ値を読むためのサブルーチンを書いておいてそこをコールして読むという曲芸的なことをやっていました。4040ではRPM命令によって4289配下のプログラム領域のメモリの値を読めるようになります。
論理演算も4004ではえらい長いプログラムでなんとか実装していましたが、4040では少しマシになりそうです。
基板を設計するにあたり考慮した点を順不同で書き連ねます。
こんな方針で回路図を描いて基板を作ってみました。
さっそく組み立てて電源を入れてみたのですが、動かず。デバッグ開始です。
とりあえずクロックを見てみます。SYNC信号と2相クロックが綺麗に出ています。
次はメモリ回り。OEやCSはちゃんと出ていることが確認できたので、新規設計部分の拡張ROM領域関連が怪しい気がしてきました。
とりあえずA0とA12を見てみると、A12の幅が短い気がします。
A12は拡張ROM領域用のアドレス信号で、CMROM0=0のメモリアクセスのときに0、CMROM1=0のメモリアクセスときに1です。とりあえずCMROM0をそのまま出しておけばいいやと思って作っていたのですが、データシートを確認したところ、CMROM0はCPUサイクルのM1でしかアサートされないのでそこでラッチする必要があったのでした。
運良くNANDゲートが2つ余っていたので、CMROM=0で0、CMROM1=0で1になるようなRS-FFを作って修正。空中配線で配線しました。
電源を入れてみるとモニタプログラムが起動。4004に作ったVTLもそのまま動きました。
次は拡張機能部分の動作確認です。段階的に確認していきましたが、最終的に、
というプログラムが動きました。動画はこちらです。VTLインタプリタでカウント表示ループを実行中にINTボタンで割り込みをかけて、拡張ROM領域に配置したプログラムで"*INT!*"というメッセージを表示しています。
回路図とガーバーデータはとりあえずGitHubに置いてあります。
最後に一言。
4040は確かに4004より強力で、プログラムも書き易そうではあるのですが、いろいろ作っていて感じたのは、4004のときほどのワクワク感は無いなあということでした。
というわけで、4040はこれでクローズして4004に戻ろうかと思います。
Z80(Z84C0020PEG)をオーバークロックして33MHzで動かすことに成功しましたので、とりあえず備忘録としてまとめておきます。
回路図やプログラム等一式はGitHubに置いてあります。
Tang Nano 20K用のGPIOを5Vトレラントにするアダプタを作ったのでその応用例としてZ80のメモリシステムを作ったというストーリーになっていますが、昔のCPU(Z80に限らず)のメモリシステムをTang Nanoに実装するために5Vインターフェースを作ったというのが正確なところです。
レベル変換用のICはSN74CB3T3245という長い名前のICを使用。レベル変換についていろいろググってたら見つかったこのページ(5V系・3.3V系信号レベル変換)で知りました。
データシートには「レベルシフタ搭載バススイッチ」とあります。5V→3.3Vはレベル変換しますが、基本的には「スイッチ」なので、3.3V→5Vは変換せずに3.3Vの信号がそのまま5V側に出力されます。5VTTLの閾値は1.5V,5VCMOSの閾値は2.5Vなので問題無く動作するということのようです。
レベル変換用ICにはSN74CB3T3245の他に、LBF0108、TXS0108、TXB0108というデバイス(いずれもTexas Instruments製)などもあり、TIのレベル変換関連の資料を見ると最近の資料にはSN74CB3T3245は載っておらず、LBF0108を勧めている感じもあったのですが、実物を入手していろいろ比較した結果、結局SN74CB3T3245が一番良さそうという結論に落ち着きました。
最初に作った変換基板にTangNano20Kを搭載したのがこれ。幅を間違えてしまっていました。
機能的には問題無いはずなのでとりあえずこれを使ってブレッドボードでZ80と接続。
なんとなく動いてるんだけど、メモリがちゃんと認識されてなかったりして不安定。
データシートを読み返していたらクロックはTTLレベルじゃだめでHレベルはVcc-0.6V必要ということを発見。
74AHCT04を使ってクロックを5Vまで引き上げたところわりと安定して動作するようになりました。
この雑な配線でも12MHzで動作。とはいえさすがにこの配線はダメだろうと思ってもう少し真面目に配線しなおしました。
そしたらブレッドボードで27MHzで動作してしまいました。ただちょっと不安定。ブレッドボードでは限界っぽいので基板の到着を待ちます。
基板が完成したので部品を載せ替え。
しかしおかしなことに27MHzでは動作してくれなくなってしまいました。20MHzでは動作するので回路に問題は無さそう。
メモリアクセスのタイミングの問題かなと思い、TangNano側のロジックを変更してみました。メモリはクロックに同期させる方が安定するだろうと思ってクロックエッジでMREQ_nやRD_n、WR_nを見て読み書きしていたのですが、(~MREQ_n & ~RD_n)や(~MREQ_n &~WR_n)のエッジで(クロックとは非同期で)読み書きするように変更したところ問題解決。変なことするよりメモリ本来の動作に忠実に実装する方が良かったようです。
安定して動くようになったのでいよいよオーバークロックの実験。Z84C0020PEGはデータシート上のスペックはDC~20MHzです。
まずは27MHz。電源はTangNano経由のUSB給電という適当な設定。これでもASCIIART.BASは無事完走しました。
データシートには、
とありますので、Vccの電圧は5.5V程度までなら安心して上げられます。
試してみたところ、クロック31.5MHzでVcc=5.55Vで途中で暴走。Vcc=5.6Vだとわりと安定して動作している感じでした。
31.5MHzというのはちょっとキリが悪いので、33MHzに挑戦してみることにしました。データシートを見ると、絶対最大定格は7.0Vです。
そういうことならVcc=6.0Vは大丈夫でしょう。試してみたところあっさり動作しました。
動画はこちら。
以上、Z84C0020PEGをVcc=6.0V, Clock=33MHzで動かしてASCIIART.BASを36秒で完走したお話でした。
秋月電子で売ってる40円のマイコン(CH32V003J4M6)がTwitterで話題になっていてちょっと気になっていたのですが、かんぱぱさん(@kanpapa)のブログにArduino IDEでの使い方がまとめられていて簡単に使えそうだったので試しに買ってみることにしました。
40円は税込価格で本体は37円。1個づつ個別包装です。袋とシリカゲルで数円分ぐらいかかってるのではないだろうか。書き込みにはWCH−LinkEエミュレーターなるものが必要とのことなので別途購入しましたが、これも750円と安価です。
スペックを見ると、
とあります。このくらいあればBASICインタプリタが動くのではと思い、試してみることにしました。
とりあえず端末と通信できないことにはどうにもならないので、まずは通信周りについて調査。WCH-LinkEエミュレータにTX、RXなる端子があるのでこれを使えばUART通信ができるのかな?と思ってググってみたところ、こちらのブログとGitHubに関連情報を見つけました。
これによると、TXはPD_5=Pin 8、RXはPD_6=Pin 1のようです。
WCH-LinkEのTXをCH32のRX(Pin 1)、WCH-LinkEのRXをCH32のTX(Pin 8)に継げます。(ややこしいです。いつも混乱します。)
Pin 8はプログラム用の信号入力SWDIOも継がっているので共用になります。そのせいでトラブル発生。プログラムを書き替えようとしても継がらなくなってしまいました。
そういえば秋月のFAQに何か書いてあったような気がします。
32ビットRISC-Vマイコン CH32V003J4M6の質問と回答
なるほど。でもこれだけのためにMounRiver Studioを使うのは大袈裟だなあと思っていたところ、WCH-LinkUtilityにもこの機能があることを発見。こちらを使って無事クリアすることができました。
しかしすぐには動いてくれず、CH32→PCは継がるのに、PC→CH32の通信が出来ないという状態。原因を見つけるのに結構苦労したのですが、Serial.available()が常に-1なのが原因でした。RXにデータが来てるかどうかのチェックを、
if(Serial.available() > 0){ c = Serial.read(); }
ではなく、
c = Serial.read(); if(c > 0){ ... }
とすることで通信できるようになりました。Arduino用の環境がCH32V003F4用に作られているせいなのかもしれませんがよくわかりません。
ともかくUARTで通信ができるようになったので、いよいよBASICインタプリタの実装に移ります。1から作るのは大変なので何か使えるものはないかと探してみたところ、電脳伝説さん(@vintagechips)の著書「タイニーBASICをCで書く」に掲載されている豊四季Tiny BASICがコンパクトで良さそうだったので試してみることにしました。ソースは本に記載されているサイトからダウンロードしたのですが、GitHubにほぼ同じものが公開されていました。
Arduino用に作られているのでそのままコンパイル可能ですが、プログラム領域256byteではASCIIART.BASが入らないので512に変更しました。
#define SIZE_LIST 512 //List buffer size
最適化オプションが"Smallest (-Os default)"だと2kBほどサイズオーバーになり、"smallest (-Os) with LTO"だと16kB以内に収まりました。しかし、with LTOの方だと通信周りがちゃんと動かないようで、最初の1文字"T"が出て止まってしまいました。通信周りを直すか、別の開発環境を使えばうまくいくような気もしますが、とりあえずASCIIART.BASを走らせることを優先しようと思い、インタプリタの機能を削って小さくすることにしました。
やったことは、
です。
"2."の方はかなり乱暴で、配列、INPUT、GOSUB、RETURN、STOP、REM、RND、ABSを削りました。
これでやっと、
Sketch uses 16184 bytes (98%) of program storage space. Maximum is 16384 bytes.
Global variables use 1512 bytes (73%) of dynamic memory, leaving 536 bytes for local variables. Maximum is 2048 bytes.
に収まって無事起動。Serial.read, Serial.available周りに修正が必要かと思っていたのですが何も変更せずに動作しました。 ASCIIART.BASの方は、以前にFairchild F8 Family (F3850)とZilog Z8671 BASIC/Debug用に作った整数型BASIC用のプログラムをくっつけて作りました。
10 F=50
20 FOR Y=-12 TO 12
30 FOR X=-39 TO 39
40 C=X*229/100
50 D=Y*416/100
60 A=C; B=D
70 I=0
90 Q=B/F; S=B-Q*F
100 T=(A*A-B*B)/F+C
110 B=2*(A*Q+A*S/F)+D
120 A=T
130 P=A/F; Q=B/F
140 IF (P*P+Q*Q)>4 GOTO 200
150 I=I+1;IF I<=15 GOTO 90
160 PRINT " ",
170 GOTO 300
200 IF I<10 PRINT I,; GOTO 300
210 IF I=10 PRINT "A",; GOTO 300
220 IF I=11 PRINT "B",; GOTO 300
230 IF I=12 PRINT "C",; GOTO 300
240 IF I=13 PRINT "D",; GOTO 300
250 IF I=14 PRINT "E",; GOTO 300
260 IF I=15 PRINT "F",
300 NEXT X
310 PRINT
320 NEXT Y
動作している様子です。8秒です。さすが21世紀のマイコンは速いですね。
今回は動作させることを最優先にしたので機能を削るという乱暴な方法で実装しましたが、おそらくライブラリが余計なメモリを食っているような気がするので、開発環境を精査すれば16KBもあればフルセットで実装できると思います。
※追記(2023/6/30)
Serial関連のライブラリがかなりメモリを食ってる感じなのでそこを自分で書き直すのが良さそうかも。
※追記(2023/7/1)
メーカー推奨の開発環境MounRiver Studioだとフルセットでもオブジェクトサイズ9KB以下に収まりました。
const byte rom[ROM_SIZE] __at(0x10000) = {...
のように、配列をキリの良い固定アドレスに配置する手法があったのでそれを真似しました。#define digitalWrite digitalWriteFast
digitalWrite(DOUT0, data & bit(0));
digitalWrite(DOUT1, data & bit(1));
digitalWrite(DOUT2, data & bit(2));
digitalWrite(DOUT3, data & bit(3));