前回の記事で、「コンピュータシステムの理論と実装」の5章で学んだ考え方について書きました。今回は具体的な実装を書いていきます。
CPUの実装
これまでに登場したHack機械語の仕様、およびHack命令セットの一覧を参照しながら実装しました。
特に面白かったのは、命令セットの各ビットをもとに、どのように命令を絞り込むかの部分です。特に comp を表す acccccc の7ビットの扱い方がぱっと浮かばなかったのですが、何度も仕様を見返した結果、
a == 0ならcompに使うのはDかAのみ、a == 1ならDかMのみccccccの6ビットは、ALUの6つの入力にそのまま対応している
ということに気づいて驚きました。ALUでHack命令セットの計算全てをカバーしているので冷静に考えれば当たり前ではありますが、今までの章で登場した仕様はこのためにあったのかと伏線回収されたような気分でした。
一方で、PCチップにおいてジャンプ条件を満たしているかを jjj の3ビットで判定する部分は、便利なチップはないため、地道に1ビットずつ判定していくしかありません。ALUからは zr (0であるか)と ng (負であるか)が出力されるため、正の数であるかの判定はこれらを組み合わせて行います。
最終的な実装は以下のようになりました。
CHIP CPU {
IN inM[16], // M value input (M = contents of RAM[A])
instruction[16], // Instruction for execution
reset; // Signals whether to re-start the current
// program (reset==1) or continue executing
// the current program (reset==0).
OUT outM[16], // M value output
writeM, // Write to M?
addressM[15], // Address in data memory (of M)
pc[15]; // address of next instruction
PARTS:
// Aレジスタ手前のMux16
Mux16(a=instruction, b=fromALU, sel=instruction[15], out=toA);
// Aレジスタ
Not(in=instruction[15], out=nonOpeCode);
Or(a= nonOpeCode, b= instruction[5], out= isLoadA);
ARegister(in= toA, load= isLoadA, out= fromA, out[0..14]= addressM);
// Dレジスタ
And(a= instruction[4], b= instruction[15], out= load);
DRegister(in= fromALU, load= load, out= fromD);
// ALU手前のMux16
Mux16(a= fromA, b= inM, sel= instruction[12], out= fromAOrM);
// ALU
ALU(x= fromD, y= fromAOrM, zx= instruction[11], nx= instruction[10], zy= instruction[9], ny= instruction[8], f= instruction[7], no= instruction[6], out= fromALU, out= outM, zr= zr, ng= ng);
// writeM
And(a= instruction[3], b= instruction[15], out= writeM);
// PC
And(a= instruction[2], b= ng, out= jlt);
And(a= instruction[1], b= zr, out= jeq);
Or(a= ng, b= zr, out= ngOrZr);
Not(in= ngOrZr, out= ngNorZr);
And(a= instruction[0], b= ngNorZr, out= jgt);
Or(a= jlt, b= jeq, out= tempJumpCond);
Or(a= tempJumpCond, b= jgt, out= jumpCond);
And(a= jumpCond, b= instruction[15], out= jump);
PC(in= fromA, load= jump, inc= true, reset= reset, out[0..14]= pc);
}
データメモリの実装
RAM16K, Screen, Keyboardのアドレス空間を連続したものとして扱う必要があります。どのアドレス空間が使われるかが、 address 入力の上位2ビットを見れば判別できるようになっているのがポイントです。これをセレクタビットとして利用できます。
| 上位2ビット | 対応するメモリ |
|---|---|
| 00 | RAM16K |
| 01 | RAM16K |
| 10 | Screen |
| 11 | Keyboard |
(範囲外のアドレスにアクセスされるケースは、仕様に定義されていないので考慮しなくてOKです)
ただし、誤った状態変化を起こさないように気をつける必要があります。自分は最初、素直に全部のメモリに address の下位ビットを渡してから Mux4Way16 で結果を選ぶ実装にしましたが、これだとデータを書き込む際に全メモリで書き込みが発生してしまいます。まず最初に上位2ビットを見て、対応するもの以外のメモリはloadフラグを常に0にしておく必要があります。
最終的な実装は以下のようになりました。
CHIP Memory {
IN in[16], load, address[15];
OUT out[16];
PARTS:
// アドレス空間に該当しないメモリは、Dmux4Wayによってloadフラグを0にしておく
DMux4Way(in= load, sel= address[13..14], a= loadData1, b= loadData2, c= loadScreen, d= loadKeyboard);
Or(a= loadData1, b= loadData2, out= loadData);
RAM16K(in= in, load= loadData, address= address[0..13], out= data);
Screen(in= in, load= loadScreen, address= address[0..12], out= screen);
Keyboard(out= keyboard);
Mux4Way16(a= data, b= data, c= screen, d= keyboard, sel= address[13..14], out= out);
}
コンピュータの実装
CPU・データメモリ・命令メモリを接続すればOKです。
CHIP Computer {
IN reset;
PARTS:
ROM32K(address= pc, out= instruction);
Memory(in= outM, load= writeM, address= addressM, out= inM);
CPU(inM= inM, instruction= instruction, reset= reset, outM= outM, writeM= writeM, addressM= addressM, pc= pc);
}
シミュレーションとはいえ、「コンピュータを作る」ということを実際に体験できて感慨深いです。Hackコンピュータで使われているノイマン型アーキテクチャについてはざっくりとは知っていましたが、こうして自分で作ってみると、いかに考え抜かれた設計であるかを強く感じられます。
6章以降も内容が盛りだくさんなので、引き続き取り組んでいくつもりです。




