ブログの練習

ブログを書く練習です。
最近はレトロな計算機(電卓、マイコン、パソコンなど)
に関することを書き始めました。

Intel 4004 (その13) 8080エミュレータを作ってみる

2023-03-22 11:22:20 | マイコン(4004)
電卓や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づつ調べているのでかなり長くなっています。

;;;---------------------------------------------------------------------------
;;; 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に置きました。
GitHub - ryomuk/emu8080on4004: Intel 8080 Emulator on 4004 Evaluation Board

GitHub - ryomuk/emu8080on4004: Intel 8080 Emulator on 4004 Evaluation Board

Intel 8080 Emulator on 4004 Evaluation Board. Contribute to ryomuk/emu8080on4004 development by creating an account on GitHub.

GitHub

 
コメント
  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

Intel 4004 (その12) VTLインタプリタを作ってみる

2023-03-12 11:36:51 | マイコン(4004)

4004用のVTLインタプリタを書いてみました。当初はMITS_Altair_680_Very_Tiny_Language_VTL-2_Manual.PDFに載っている6800版のオリジナルのソースか、白石孝次氏による8080への移植版(「マイクロBASICインタプリタの製作」, 月刊ASCII, 1978年2月(エンサイクロペディアASCII vol.1収録))を参考にして移植ベースで作るつもりだったのですが、どちらもサブルーチンを多段に呼びまくりなコードで、PCのスタックが4段しかない(サブルーチンを3段までしか呼べない)4004では同じ構造のまま移植するのは無理そうだったので、多少参考にする程度でゼロから書くことにしました。
とはいえ、数式を評価するルーチンが再帰的な呼出しになり、データRAMにスタック領域を用意してそこにレジスタやPCを積めるような仕組みを作ることになったので、最終的には多段呼び出しも可能になったのですが。
とりあえず動けばいいと思って書いたもので、かなり汚いプログラムなのですが、せっかくなのでGitHubで公開しました。内部レジスタ(P0~P7)の使い方など、直した方がいい点が多々ありますが、下手にリファクタリングして動かなくなるのもいやなのでこれ以上手を加えるのはやめます。これを直すくらいなら、もう一度ゼロから書き直す方が良さそう。

GitHub - ryomuk/VTL4004: VTL Interpreter for 4004 Evaluation Board

GitHub - ryomuk/VTL4004: VTL Interpreter for 4004 Evaluation Board

VTL Interpreter for 4004 Evaluation Board. Contribute to ryomuk/VTL4004 development by creating an account on GitHub.

GitHub

 


プログラムを書くにあたり、いくつか準備をしました。まず1つ目は、前のブログ(Intel 4004 (その11) メモリ周りを改築する)に書いたメモリの拡張に関することです。物理メモリは00H~0FDHの254byte x 16バンクという、256byteごとに2byteの読み出しルーチンが書かれていて使いにくい空間なので、12bitのアドレスBA98.7654.3210を、BANK=3210、ADD=7654.BA98に変換してアクセスするルーチンを用意して、000H~0FDFHの論理空間に見せるようにしました。vtl.asmの LD_P1_PM12REG16P0, LD_PM12REG16P0_P1 がそのルーチンです。メモリ1byte読み出すのに30命令近く、実行時間で約300μ秒かかるということになってしまいましたが、これでメモリがかなり使い易くなりました。このメモリにはVTLのプログラムテキストを格納します。
プログラム領域のメモリはアクセスが遅いので、演算用の変数はデータレジスタに格納します。4002は4bit x 16キャラクターのレジスタを4つ(計32 byte)持っており、RAM0~3で計128 byteあります。
RAM0とRAM1にA~Zとシステム変数、RAM2にシステム変数や作業用変数、RAM3をスタック領域として使用することにしました。
当初のハードウェアでは入手性やコストの観点から4002-1(1個$8)を4つ使って、CMRAM0とCMRAM1に2個づつ継げていたのですが、4002-1を2つと4002-2(1個$20)を2つを同じライン(CMRAM0)に接続する方がCMRAMのラインを0に固定でき、アクセス毎に発生するDCL命令を省略できるのでそのようにしました。
eBayで注文したところ、最初4002-2ではなく4002-1が送られてきたのですが、店に連絡したらすぐに4002-2を再送してくれて返品も不要という対応をしてもらえて事なきを得ました。ICもちゃんとした新品で動作も良好。HIFIICというストアは信頼できそうです。
RAM3のスタック領域に内部レジスタをPUSH/POPするためルーチンと、変数をPUSH/POPするルーチンを用意しました。8bit CPUでは1命令で実行できるPUSH/POPですが、これにも30命令以上かかります。しかもレジスタ毎に別のルーチンをマクロで展開して作ったため、メモリもかなり食ってしまいました。このあたりは改善の余地がありそうです。
スタックを利用して返り先をスタックに積むようにすれば3段しか使えないJMSの代わりに、JUN命令でサブルーチンが実現できます。返るときにはvtl.asmのRETURN_P2を呼びます。12bitのアドレスに間接ジャンプする命令は無いので、8bitのラベルを使って間接ジャンプで飛んだ先で12bitにジャンプするという2段ジャンプのリターンになっています。
以上が主な準備です。

実装したインタプリタの仕様の表をここに書こうと思ったのですが、このブログで表を書く方法がわからなかったのでやめました。GitHubのREADME.mdをご覧下さい。表に従って順に解説していきます。
プログラムサイズはモニターと合わせて約3.5KBになりました。8bit CPU版に比べると3~4倍でしょうか。8bitの値を操作するのにも複数の命令が必要なので仕方ありません。
オリジナルのVTLの変数の値域は0~65535ですが、マンデルブロ集合を計算したいので2の補数で-32768~32767とすることにしました。加算と減算ルーチンは変更無しで使えます。除算は正負を判断して絶対値の除算を行い符号を補正するという計算になります。乗算も除算と同様のことをやる必要があるかと思ったのですが、下位16bitだけ使うのであれば何もしなくていいようで、正数の乗算ルーチンのまま変更無しで動きました。これに伴い、行番号が1~32766となっています。
16進の数値が扱える方が何かとうれしいので、0で始まる数値は16進数として扱えるようにしました。出力も??=で16進4桁、?$=で16進2桁で表示するようにしました。
通常のPRINT(?=e, ?=STR)、改行抑止(;)、入力(A=?)、文字入出力(A=$, $=e)、GOTO(#=)、GOSUB(!=)、LIST(0)等々はオリジナルのVTLと同じです。
オリジナルのVTLではIF文は無く、#=(A=B)*100のようにGOTO先の行番号を比較演算子の結果との乗算で代用するという手法が使われていたようなのですが、乗算は計算コストが大きいのでIF文(;=)を実装しました。;=0 の後は次の行にスキップするのでコメント文の代用に使えます。なのでコメント文の実装はサボりました。
演算子に優先順位は無く、左から右に評価しますが、括弧内を先に評価する機能はさすがに実装しました。上の方に書いたJUN命令とスタックを使ったサブルーチンコールで数式の値計算(EVAL_EXPRESSION_PMINDEX_REG16P1)と因子の値計算(GETFACTOR_PMINDEX_REG16P1)の再帰呼出しをしています。
行の編集機能(挿入、削除等)はサボりました。真面目に実装すると結構メモリを食いそうなので。最初から行番号昇順のプログラムが入力されることを前提にします。

あと、配列とPEEK、POKEあたりを実装したかったのですが、マンデルブロ集合の計算に使わないので後回しになっています。

こちらが動作の様子です。


約2週間ほどかかってどうにか動くものが完成し、目標にしていたマンデルブロ集合の計算が出来ました。時間は2時間54分13秒。今回は速度は頑張りどころではないので完走できたことで満足です。

プログラムはこんな感じ。基本的に以前にF8用に書いた整数BASIC用のプログラムと同じです。不等号">"がVTLでは">="の意味であるということらしいので、比較文の数値がちょっと違います。

10 F=50
20 Y=-12
30 X=-39
40 C=X*229/100
50 D=Y*416/100
60 A=C
70 B=D
80 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 ;=((P*P)+(Q*Q))>5 #=180
150 I=I+1 ;=I<16 #=90
160 ?=" ";
170 #=200
180 ;=I>10 I=I+7
190 $=48+I
200 ;=X<39 X=X+1 #=40
210 ?=""
220 ;=Y<12 Y=Y+1 #=30


消費電力はシステム全体(表示用VFDは除く)で270mA前後とそれほど多くありませんでした。

発熱も4002が50度ぐらいになっている程度、CPUはあまり発熱していませんね。


最後に、試しに手動でコンパイルして変数アクセスや演算ルーチンを直接叩くようにして実行した結果です。55分56秒。実行時間は約1/3ぐらいに短縮しました。1桁くらい速くなるかと思っていたのですが、思ったほどは速くなりませんでした。構文解析や制御よりも演算自体に時間がかかっているということですね。


コメント
  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする