2026-11-04
引き続き、『コンピュータシステムの理論と実装』(通称 nand2tetris)を読み進めています。
今回は第5章「コンピュータアーキテクチャ」で、これまで作ってきたゲートやALU、レジスタを組み合わせて、Hackコンピュータの Memory・CPU・Computer を作ってみて学んだことをまとめてみました。
第5章で作るチップは次の3つです。
Memory: データ用のメモリ全体。RAM・画面・キーボードへの窓口CPU: 命令を読んで、各部品に「何をするか」の指示を配るComputer: ROM32K(プログラム)・CPU・Memory をつないだHackコンピュータ全体順番に見ていきます。
Memory は、自分の中では 受付係 のようなイメージです。
渡されたアドレスを見て、RAM16K・SCREEN・KBD のどれに値を渡すかを振り分けています。
| 渡す先 | 番地 |
|---|---|
RAM16K | 0 〜 16383 |
SCREEN | 16384 〜 24575 |
KBD | 24576 |
ここでいうRAMは、番号付きのロッカーがずらっと並んでいて値を覚えておける装置(メモリ)のことです。第3章で作った RAM16K がそのまま使われています。
CPU の仕事は、命令の16ビットを見て、ALU・Aレジスタ・Dレジスタ・PC・Mux16・And・Or・Notなどの部品たちにスイッチを配ること です。
C命令の16ビットは次のような構成になっています。
0)か、C命令(1)かa: ALUの y に A を入れるか M(inM)を入れるか(x には常に D)cccccc: ALUで何を計算するか(足す、引く、+1 など)ddd: 結果をどこに書くか(左から A・D・M)jjj: ジャンプするか(左から <0・=0・>0 ならジャンプ)第4章で書いた D=D+M や 0;JMP といった命令は、最終的にこの16ビットに変換されてCPUに渡されます。
自分が書いたCPUのHDLは次のとおりです。
ここからは、実装していて特に考えたポイントをいくつか取り上げます。
Aレジスタに書き込むのはA命令のときだけではありません。C命令でも A=M のように結果をAに書くケースがあるので、「A命令である」または「ddd の A(instruction[5])が1」 のときに書き込むよう Or でまとめています。
また、Mux16 でA命令なら命令そのもの、C命令ならALUの計算結果をAレジスタへの入力に選んでいます。
ここで面白いのが、aOut は 前のサイクルまでに覚えた値 になるという点です。レジスタは書き込みがあっても、その値が出力に出てくるのは次のサイクルからだからです。
1サイクルの中で起きていることを整理すると、次のようになります。
Or で loadA のフラグを立てるMux16 で instruction か aluOut のどちらを aIn にするか決めるloadA が立っていれば、ARegisterが aIn の値をしまう → 次のサイクルから aOut に出てくるちなみに、A命令のときもALUの計算自体は行われますが、その結果はどこにも使われません。回路は全部同時に動いているので「計算しない」のではなく「計算しても使わない」という形になっているんですね。
Dレジスタは ddd の D(instruction[4])が1なら書き込む、で良さそうに見えますが、それだけだと問題があります。
A命令のときは下位15ビットが「値」なので、instruction[4] が1になることがあるからです。たとえば @16 〜 @31 のような番地を指定した場合がそうです。
Hackのアセンブラでは変数が16番地から順に割り当てられるので、変数を使うたびにこのケースが起きてしまいます。
そのため、isC と And を取って C命令かつ D ビットが1のときだけ Dレジスタに保存するようにしています。
これは Memory のRAMに書き込むかどうかを決める信号です。Dレジスタと同じ理由で、C命令のときだけ有効にしています。
ここで気づいたのが、AレジスタとDレジスタは RAMとは別の、CPUの中にある記憶場所 だということです。A や D への書き込みはCPUの中で完結していて、M への書き込みだけが writeM を通じてCPUの外の Memory に届きます。
jjj の3ビットとALUの出力フラグ(zr: 結果が0、ng: 結果が負)を組み合わせて判定しています。
j1(instruction[2])が1 かつ 結果が負(ng) → ジャンプj2(instruction[1])が1 かつ 結果が0(zr) → ジャンプj3(instruction[0])が1 かつ 結果が正(0でも負でもない) → ジャンプこのどれかが成り立ち、かつC命令であれば jump が1になり、PC に aOut(ジャンプ先)が読み込まれます。ジャンプしないときは inc= true なので、PCは次の行に進みます。
第4章で「ジャンプ先は今の @ の値」と書きましたが、それはこの PC(in= aOut, ...) の部分のことだったんだなと腑に落ちました。
Computer はHackコンピュータ全体で、ROM32K・CPU・Memory が入っています。
ROM32K の中にはプログラムの命令が1行ずつ番号順に並んでいて、実行する行番号(pc)を渡すと、その行の命令を出力してくれます。
ROM32K が命令を出し、CPU がそれを解釈し、Memory と値をやり取りして、次の pc を ROM32K に返す、という輪になっています。
この3つのチップは全部並行に動いています。そのため、電源を入れた直後の outM・memOut・pc・instruction・writeM などは、意味のないゴミの値になっている可能性があります。
これに対しては、ハードウェアとソフトウェアの両方で対策がされています。
reset を1にする回路を入れることで、確実に決まった行から実行が始まるようにしている「変数は使う前に初期化する」という普段何気なく守っているルールが、こんなハードウェアの事情ともつながっているのは面白いなと感じました。
第5章では、これまで作ってきた部品を組み合わせて、ついにHackコンピュータを完成させました。
CPUは結局「命令の16ビットを見て、各部品にスイッチを配っているだけ」だと分かったことで、第4章で書いた機械語が実際にどう動いているのかがかなり具体的にイメージできるようになりました。
特に、レジスタの値が次のサイクルから反映されることや、A命令のときに D や M に誤って書き込まないよう isC と And を取る必要があることは、実際に手を動かしてみて初めて気づけたポイントだったと思います。
次の第6章ではアセンブラを作ることになるので、@sum のようなシンボルがどうやって番地に変換されているのかを見ていきたいと思います。