電卓やVTLインタプリタを書いてみて、ずいぶん4004のアセンブラに慣れてきました。とはいえ、いろいろなものを4004のアセンブラで一から作るのは面倒です。8080のエミュレータを作ってしまえばモニターなり言語処理系なり、8080の膨大なソフトウェアを走らせることができるのではないかと思い、エミュレータを作ってみることにしました。CPUの動作なんて命令コードで分岐してレジスタの値を書き替えるだけなんだから1日か2日で作れるんじゃないかと高を括っていたのですが、意外に大変で、4004のモニターもメモリ読み書き周りを手直ししたり、8080のプログラム入力用にIntel HEXを読み込めるようにしたりという改造もあったので結局1週間ほどかかりました。
実験機の4004のプログラムに使えるROM領域は3.75KBあります、モニター周りの機能追加や、レジスタ操作、PUSH/POP、I/O、論理メモリ空間アクセス、等々のルーチンで2KB使ってしまうので、エミュレータ用に使えるメモリは1.75KBしかありません。
当初のもくろみでは、4004の間接ジャンプ命令で命令コード毎に分岐して分岐先に各命令の処理を書けば簡単だと思っていたのですが、さすがにそれでは無駄が多くてメモリが足りなくなるので、もう少し真面目にやることにしました。
8080の命令コード表を見ると、40H~BFHの128命令はかなり規則的な作りになっています。その外側もある程度は規則的なのですが、かなり雑多です。
01H~3FHとC0H~FFHをJIN命令(間接ジャンプ)で分岐テーブルを使って分岐させて命令毎の処理、40H~7FHはMOV命令なのでSRCレジスタとDSTレジスタをデコードしてMOVの共通ルーチン、80H~BFHは、SRCレジスタをデコードして、演算内容を分岐テーブルで分岐させて演算毎の共通ルーチンで処理、というような作りにしました。
MOV命令は01000000+DDD000+SSSという構造になっているので、SRCとDSTをデコードしてあとは共通ルーチンに任せるということです。
ちなみにHLT(=76H)のコードはMOV M, Mに相当するのですが、MOVのルーチンで判別してHLTの処理に飛ばしました。
いくつか実装をサボったものもあります。
まず、演算結果のbitの偶奇を示すPフラグです。ハードウェアで実装したら簡単そうですが、ソフトウェアで実装するのは結構コストがかかります。また、フラグの使用の有無にかかわらず演算ごとに計算するのはかなりの無駄です。このフラグを使ったプログラムは滅多に無いと思われるので実装するのをやめました。
次にDAA、10進数の補正命令です。4bit目のキャリーであるACフラグ(Auxiliary Carry, NECのマニュアルだとハーフキャリーやCY4と表記されていたりします。)を用意しておく必要があるのですが、現在の実装では面倒なのでサボりました。あと、割り込み関連のDI、EIも割り込み自体が無いので省略。(※2023/4/3追記, DAA命令に関する記述について修正しました。)
IN、OUTはシリアルポートへの入出力になっています。INでの入力は4004のソフトウェアUARTのGETCHARルーチンに飛ぶので、入力があるまで止まってしまうのですが、これを回避するにはハードウェア的な機能追加が必要なので今のところはあきらめています。(BASICで停止のためのCtrl-Cのチェックとかができないのでなんとかしたいです。)
意外に面倒だったのが論理演算です。4004にはAND, OR, XORなどの論理演算用の命令がありません。MCS-4 Assembly Language Programming Manualには4bitのANDルーチンの例(下記)が載っているのですが、ちょっと難解で理解できなかったので自前で書きました。
自前で書いたのがこちら。ループを使わずに1bitづつ調べているのでかなり長くなっています。
これは4bitのレジスタの論理積なので、8bitのレジスタの演算には次のように2回呼ぶ必要があります。
8080で1命令(ANA)で済む1バイトの論理積の計算にこれだけのコードが必要になります。ORとXORも同様です。メモリが本当に厳しくなったら工夫して短くするところですが、今回はなんとか足りたのでこの実装で済ませました。
一通り書けたところで、動作確認です。全ての命令について、ステップ実行させながらレジスタ、フラグ、PC、SP、メモリの内容が仕様通りに動いているかを確認します。自動のテストプログラムを書くという方法もありますが、そうするとテストプログラム自体のデバッグも必要になるという無間地獄に陥るのでテストプログラム自体は単純な命令の羅列にして、結果を目視で確認するという手段をとりました。テストはかなり効果的で山ほどバグが見つかりました。
全ての命令で一応期待通りの動きが確認できたので、次は大規模なプログラムを走らせてみます。
ソースが入手可能で改変も可能なPalo Alto Tiny BASIC を試してみることにしました。ソースはhttps://www.autometer.de/unix4fun/z80pack/ftp/altair/から入手。
Pフラグ、DAA命令が使われていないことを確認。端末への入出力は、IN 1、OUT 1でデータ、IN 0がデータ有無を示す制御フラグのようだったので、エミュレータ側のIN、OUT命令をそれ用に設定。
アセンブラの"SHR"を">>"に、"AND"を"&"に修正、スタックポインタとメモリ領域のアドレスを修正したらすんなりアセンブルできてHEXファイルが生成できました。
実機にロードして実行してみたところ、"OK"のプロンプトが出てくれました。PRINT 123+234も正常。
しかしPRINT文やLIST出力の1行ごとに入力待ちになって停止してしまいます。
Tiny BASICでは、Ctrl-Cで止めるために入力のチェックがあり、入力が無ければ通過するのですが、4004実験機はGETCHARは入力があるまでそこで止まってしまうのでした。このあたりはハードウェアの改修も必要になるので、とりあえずの対処として、Tiny BASIC側でコマンド入力時以外のIN数カ所をコメントアウトして済ませることにしました。無限ループ時に止める手段が無くなるのですが仕方ありません。
以上の対処で動くようになった動画がこちら。ちゃんと動いているのは感動ですが、速度は思っていた以上に遅く、マンデルブロ集合の計算はあきらめました。
ソースコードと実験機の回路図はGitHubに置きました。
実験機の4004のプログラムに使えるROM領域は3.75KBあります、モニター周りの機能追加や、レジスタ操作、PUSH/POP、I/O、論理メモリ空間アクセス、等々のルーチンで2KB使ってしまうので、エミュレータ用に使えるメモリは1.75KBしかありません。
当初のもくろみでは、4004の間接ジャンプ命令で命令コード毎に分岐して分岐先に各命令の処理を書けば簡単だと思っていたのですが、さすがにそれでは無駄が多くてメモリが足りなくなるので、もう少し真面目にやることにしました。
8080の命令コード表を見ると、40H~BFHの128命令はかなり規則的な作りになっています。その外側もある程度は規則的なのですが、かなり雑多です。
01H~3FHとC0H~FFHをJIN命令(間接ジャンプ)で分岐テーブルを使って分岐させて命令毎の処理、40H~7FHはMOV命令なのでSRCレジスタとDSTレジスタをデコードしてMOVの共通ルーチン、80H~BFHは、SRCレジスタをデコードして、演算内容を分岐テーブルで分岐させて演算毎の共通ルーチンで処理、というような作りにしました。
MOV命令は01000000+DDD000+SSSという構造になっているので、SRCとDSTをデコードしてあとは共通ルーチンに任せるということです。
ちなみにHLT(=76H)のコードはMOV M, Mに相当するのですが、MOVのルーチンで判別してHLTの処理に飛ばしました。
いくつか実装をサボったものもあります。
まず、演算結果のbitの偶奇を示すPフラグです。ハードウェアで実装したら簡単そうですが、ソフトウェアで実装するのは結構コストがかかります。また、フラグの使用の有無にかかわらず演算ごとに計算するのはかなりの無駄です。このフラグを使ったプログラムは滅多に無いと思われるので実装するのをやめました。
次にDAA、10進数の補正命令です。4bit目のキャリーであるACフラグ(Auxiliary Carry, NECのマニュアルだとハーフキャリーやCY4と表記されていたりします。)を用意しておく必要があるのですが、現在の実装では面倒なのでサボりました。あと、割り込み関連のDI、EIも割り込み自体が無いので省略。(※2023/4/3追記, DAA命令に関する記述について修正しました。)
IN、OUTはシリアルポートへの入出力になっています。INでの入力は4004のソフトウェアUARTのGETCHARルーチンに飛ぶので、入力があるまで止まってしまうのですが、これを回避するにはハードウェア的な機能追加が必要なので今のところはあきらめています。(BASICで停止のためのCtrl-Cのチェックとかができないのでなんとかしたいです。)
意外に面倒だったのが論理演算です。4004にはAND, OR, XORなどの論理演算用の命令がありません。MCS-4 Assembly Language Programming Manualには4bitのANDルーチンの例(下記)が載っているのですが、ちょっと難解で理解できなかったので自前で書きました。
自前で書いたのがこちら。ループを使わずに1bitづつ調べているのでかなり長くなっています。
;;;---------------------------------------------------------------------------
;;; AND_R6_R7
;;; R6 = R6 & R7
;;;---------------------------------------------------------------------------
AND_R6_R7:
CLB
LD R7
RAR
JCN C, AND67_L1 ; jump if R7.bit0==1
LD R6
RAR
CLC
RAL
XCH R6 ; R6.bit0=0
AND67_L1: LD R7
RAR
RAR
JCN C, AND67_L2 ; jump if R7.bit1==1
LD R6
RAR
RAR
CLC
RAL
RAL
XCH R6 ; R6.bit1=0
AND67_L2:
LD R7
RAL
RAL
JCN C, AND67_L3 ; jump if R7.bit2==1
LD R6
RAL
RAL
CLC
RAR
RAR
XCH R6 ; R6.bit2=0
AND67_L3:
LD R7
RAL
JCN C, AND67_L4 ; jump if R7.bit3==1
LD R6
RAL
CLC
RAR
XCH R6 ; R6.bit3=0
AND67_L4:
BBL 0
これは4bitのレジスタの論理積なので、8bitのレジスタの演算には次のように2回呼ぶ必要があります。
;;;---------------------------------------------------------------------------
;;; AND_P1_P2
;;; P1 = P1 & P2
;;;---------------------------------------------------------------------------
AND_P1_P2:
LD P1_LO
XCH R6
LD P2_LO
XCH R7
JMS AND_R6_R7
LD R6
XCH P1_LO
LD P1_HI
XCH R6
LD P2_HI
XCH R7
JMS AND_R6_R7
LD R6
XCH P1_HI
BBL 0
8080で1命令(ANA)で済む1バイトの論理積の計算にこれだけのコードが必要になります。ORとXORも同様です。メモリが本当に厳しくなったら工夫して短くするところですが、今回はなんとか足りたのでこの実装で済ませました。
一通り書けたところで、動作確認です。全ての命令について、ステップ実行させながらレジスタ、フラグ、PC、SP、メモリの内容が仕様通りに動いているかを確認します。自動のテストプログラムを書くという方法もありますが、そうするとテストプログラム自体のデバッグも必要になるという無間地獄に陥るのでテストプログラム自体は単純な命令の羅列にして、結果を目視で確認するという手段をとりました。テストはかなり効果的で山ほどバグが見つかりました。
全ての命令で一応期待通りの動きが確認できたので、次は大規模なプログラムを走らせてみます。
ソースが入手可能で改変も可能なPalo Alto Tiny BASIC を試してみることにしました。ソースはhttps://www.autometer.de/unix4fun/z80pack/ftp/altair/から入手。
Pフラグ、DAA命令が使われていないことを確認。端末への入出力は、IN 1、OUT 1でデータ、IN 0がデータ有無を示す制御フラグのようだったので、エミュレータ側のIN、OUT命令をそれ用に設定。
アセンブラの"SHR"を">>"に、"AND"を"&"に修正、スタックポインタとメモリ領域のアドレスを修正したらすんなりアセンブルできてHEXファイルが生成できました。
実機にロードして実行してみたところ、"OK"のプロンプトが出てくれました。PRINT 123+234も正常。
しかしPRINT文やLIST出力の1行ごとに入力待ちになって停止してしまいます。
Tiny BASICでは、Ctrl-Cで止めるために入力のチェックがあり、入力が無ければ通過するのですが、4004実験機はGETCHARは入力があるまでそこで止まってしまうのでした。このあたりはハードウェアの改修も必要になるので、とりあえずの対処として、Tiny BASIC側でコマンド入力時以外のIN数カ所をコメントアウトして済ませることにしました。無限ループ時に止める手段が無くなるのですが仕方ありません。
以上の対処で動くようになった動画がこちら。ちゃんと動いているのは感動ですが、速度は思っていた以上に遅く、マンデルブロ集合の計算はあきらめました。
ソースコードと実験機の回路図はGitHubに置きました。
※コメント投稿者のブログIDはブログ作成者のみに通知されます