2026-11-04

コンピューターシステムの理論と設計 ~第五章 コンピュータアーキテクチャ~

引き続き、『コンピュータシステムの理論と実装』(通称 nand2tetris)を読み進めています。 今回は第5章「コンピュータアーキテクチャ」で、これまで作ってきたゲートやALU、レジスタを組み合わせて、Hackコンピュータの Memory・CPU・Computer を作ってみて学んだことをまとめてみました。

Hackコンピュータの全体像

第5章で作るチップは次の3つです。

  • Memory: データ用のメモリ全体。RAM・画面・キーボードへの窓口
  • CPU: 命令を読んで、各部品に「何をするか」の指示を配る
  • Computer: ROM32K(プログラム)・CPU・Memory をつないだHackコンピュータ全体

順番に見ていきます。

Memory

Memory は、自分の中では 受付係 のようなイメージです。 渡されたアドレスを見て、RAM16K・SCREEN・KBD のどれに値を渡すかを振り分けています。

渡す先番地
RAM16K0 〜 16383
SCREEN16384 〜 24575
KBD24576

ここでいうRAMは、番号付きのロッカーがずらっと並んでいて値を覚えておける装置(メモリ)のことです。第3章で作った RAM16K がそのまま使われています。

CPU

CPU の仕事は、命令の16ビットを見て、ALU・Aレジスタ・Dレジスタ・PC・Mux16・And・Or・Notなどの部品たちにスイッチを配ること です。

命令の16ビット

C命令の16ビットは次のような構成になっています。

  • 15番目: A命令(0)か、C命令(1)か
  • 14, 13番目: 使わない
  • 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レジスタに書き込むのはA命令のときだけではありません。C命令でも A=M のように結果をAに書くケースがあるので、「A命令である」または「ddd の A(instruction[5])が1」 のときに書き込むよう Or でまとめています。 また、Mux16 でA命令なら命令そのもの、C命令ならALUの計算結果をAレジスタへの入力に選んでいます。

ここで面白いのが、aOut は 前のサイクルまでに覚えた値 になるという点です。レジスタは書き込みがあっても、その値が出力に出てくるのは次のサイクルからだからです。 1サイクルの中で起きていることを整理すると、次のようになります。

  1. サイクルの始まり: ARegisterは、しまっている前回の値を出力し続ける
  2. サイクルの途中(同時に起きる)
    • Or で loadA のフラグを立てる
    • Mux16 で instruction か aluOut のどちらを aIn にするか決める
  3. サイクルの終わり: loadA が立っていれば、ARegisterが aIn の値をしまう → 次のサイクルから aOut に出てくる

ちなみに、A命令のときもALUの計算自体は行われますが、その結果はどこにも使われません。回路は全部同時に動いているので「計算しない」のではなく「計算しても使わない」という形になっているんですね。

Dレジスタに書き込む条件

Dレジスタは ddd の D(instruction[4])が1なら書き込む、で良さそうに見えますが、それだけだと問題があります。 A命令のときは下位15ビットが「値」なので、instruction[4] が1になることがあるからです。たとえば @16 〜 @31 のような番地を指定した場合がそうです。

Hackのアセンブラでは変数が16番地から順に割り当てられるので、変数を使うたびにこのケースが起きてしまいます。 そのため、isC と And を取って C命令かつ D ビットが1のときだけ Dレジスタに保存するようにしています。

writeM(RAMに書き込むか)

これは 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

Computer はHackコンピュータ全体で、ROM32K・CPU・Memory が入っています。 ROM32K の中にはプログラムの命令が1行ずつ番号順に並んでいて、実行する行番号(pc)を渡すと、その行の命令を出力してくれます。

ROM32K が命令を出し、CPU がそれを解釈し、Memory と値をやり取りして、次の pc を ROM32K に返す、という輪になっています。

電源を入れた直後の値はゴミになりうる

この3つのチップは全部並行に動いています。そのため、電源を入れた直後の outM・memOut・pc・instruction・writeM などは、意味のないゴミの値になっている可能性があります。

これに対しては、ハードウェアとソフトウェアの両方で対策がされています。

  • ハードウェアの対策: 本物のコンピュータでは、電源を入れた直後に reset を1にする回路を入れることで、確実に決まった行から実行が始まるようにしている
  • ソフトウェアの対策: ゴミがRAMに書き込まれてしまうことはありえるが、プログラムは変数を使う前に必ず初期値を入れるのが鉄則なので、ゴミが書かれていても初期値で上書きされて問題にならない

「変数は使う前に初期化する」という普段何気なく守っているルールが、こんなハードウェアの事情ともつながっているのは面白いなと感じました。

まとめ

第5章では、これまで作ってきた部品を組み合わせて、ついにHackコンピュータを完成させました。 CPUは結局「命令の16ビットを見て、各部品にスイッチを配っているだけ」だと分かったことで、第4章で書いた機械語が実際にどう動いているのかがかなり具体的にイメージできるようになりました。 特に、レジスタの値が次のサイクルから反映されることや、A命令のときに D や M に誤って書き込まないよう isC と And を取る必要があることは、実際に手を動かしてみて初めて気づけたポイントだったと思います。

次の第6章ではアセンブラを作ることになるので、@sum のようなシンボルがどうやって番地に変換されているのかを見ていきたいと思います。

参考資料

  • nand2tetris Web IDE
  • nand2tetris 公式サイト

Thanks for the visit Nozo Blog