← ブログに戻るCPUアーキテクチャ

シミュレーターでゼロから CPU を作る

レジスター、ALU、共有バス、バイト幅 RAM、16 ビット命令、フラグ、マイクロシーケンス制御ユニットを備えた 8 ビット教育用 CPU を、一貫した仕様で検証可能に組み立てる手順。

DennyDenny
読了まで約16分
Chalkboard feature illustration for “シミュレーターでゼロから CPU を作る”

要約: このガイドでは、配線を描き始める前に完全な教育用アーキテクチャを定義します。8 ビットのデータパスとアドレス空間、バイト幅の統合 RAM、2 バイトでフェッチする 16 ビット命令、4 本の汎用レジスター、ALU とフラグ、1 本の共有バス、マイクロシーケンス制御器です。各サブシステムを構築・検証し、バスドライバーが一度に 1 個だけになるよう徹底してから、5 + 3 を計算してメモリーに 8 を格納する 5 命令のプログラムを実行します。

「CPU を作る」は、既製ブロックをつなぐことからトランジスターをレイアウトすることまで、さまざまな意味を持ちます。このチュートリアルはその両極の間に位置します。レジスター、マルチプレクサー、デコーダー、ALU、メモリー、制御ストアというデジタル構成ブロックからプロセッサーを組み立て、各ブロック内部のゲートは別途テストできる設計として扱います。

目的は現代の商用コアを模倣することではありません。各命令実装が満たすべき契約を明らかにすることです。

  1. 正しい命令バイトをフェッチする。
  2. デコード中もそれらを保持する。
  3. 意図したオペランドだけを移動する。
  4. 定義した演算を行う。
  5. 許可されたアーキテクチャ状態だけを更新する。
  6. 次の正しい命令アドレスを選ぶ。

1. 最初にアーキテクチャを固定する

メモリー幅、命令幅、PC の増分を構築途中で変えてしまうために、多くの CPU プロジェクトは失敗します。以下の仕様を一貫して使ってください。

属性教育用 CPU の定義
データ幅8 ビット
アドレス幅8 ビット
メモリーバイトアドレス指定の 256 位置。命令とデータを統合
命令幅連続する 2 バイトに格納する 16 ビット
命令バイト順序命令開始アドレスに上位バイトを先に置く
連続 PC 進行フェッチ中に 1 バイトずつ 2 回インクリメント
汎用レジスター8 ビットの 4 レジスター: R0–R3
オペランドラッチA と B。各 8 ビット
命令レジスター16 ビット
フラグZ(ゼロ)、N(負)、C(キャリー)、V(符号付きオーバーフロー)
相互接続1 本の 8 ビット教育用バス
制御マイクロステップカウンターと制御 ROM

8 ビットアドレスは 256 個の位置を選びます。このアーキテクチャでは各位置を明示的に 1 バイトとするため、メモリーは 256 バイトです。各命令は 2 バイトを占めるので、通常、命令の開始アドレスは偶数になります。

ブロック図

                         ┌──────────────────────┐
                         │   control ROM +      │
                         │ microstep sequencer  │
                         └──────────┬───────────┘
                                    │ control signals
                                    ▼
 ┌────┐   ┌─────┐   ┌────────────── 8-bit shared bus ───────────────┐
 │ PC │──▶│ MAR │   │       │       │       │       │       │      │
 └────┘   └──┬──┘  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼───┐
             │     │MDR│  │ R0│  │ R1│  │ R2│  │ R3│  │ A │  │ B │
             ▼     └─┬─┘  └───┘  └───┘  └───┘  └───┘  └┬──┘  └┬───┘
        ┌────────┐   │                                    └──┬───┘
        │256 × 8 │   │                                  ┌────▼────┐
        │  RAM   │   │                                  │ 8-bit   │
        └────────┘   │                                  │  ALU    │
                     │                                  └────┬────┘
                     └──────▶ 16-bit IR ◀────────────────────┘
                                      │
                                      ▼
                                opcode / fields

この図は概念図です。IR はバスから別々の 2 バイトをロードし、ALU の結果は専用に選択された出力経路を通ってバスへ届きます。

参照として Sequential Instruction Executor を開いてください。ただし、この演習ではここに書いたアーキテクチャを正とします。テンプレートが異なる内部規約を使う場合は、黙って混ぜずに違いを記録してください。

2. 命令形式を定義する

この独自 ISA は 2 種類の 16 ビット形式を使います。

レジスター形式

15            12 11             8 7              4 3              0
┌───────────────┬────────────────┬────────────────┬────────────────┐
│ opcode [3:0]  │ RA [3:0]       │ RB [3:0]       │ RD [3:0]       │
└───────────────┴────────────────┴────────────────┴────────────────┘

即値/アドレス形式

15            12 11             8 7                               0
┌───────────────┬────────────────┬─────────────────────────────────┐
│ opcode [3:0]  │ RD [3:0]       │ immediate / address [7:0]       │
└───────────────┴────────────────┴─────────────────────────────────┘

4 レジスター機で合法なのはレジスターコード 0–3 だけです。デコーダーは、値 4–15 が誤って制御線を選ばないよう、拒否するか安全にトラップしなければなりません。

オペコードニーモニック形式定義済みの効果
0NOP—すでに進んだ PC を除き、アーキテクチャ状態は変化しない
1LDI即値RD ← imm8
2LD即値RD ← RAM[addr8]
3ST即値RAM[addr8] ← RD
4ADDレジスターRD ← RA + RB。フラグを更新
5SUBレジスターRD ← RA − RB。フラグを更新
6JMP即値PC ← addr8
7JZ即値Z = 1 なら PC ← addr8
8HLT—停止した制御器状態へ入る

それ以外のオペコードはすべて不正です。明示的な illegal_instruction 経路を加えるか、デバッグ中は安全なトラップ/停止へ割り当ててください。

3. クロックをゲートせずにレジスターを作る

8 ビットレジスターは、1 本のクロックと 1 つの論理ロードイネーブルを共有する、エッジトリガー D フリップフロップ 8 個です。

各ビットについて:

Dff={Dbus,if load=1Q,if load=0D_{\text{ff}} = \begin{cases} D_{\text{bus}}, & \text{if load}=1 \\ Q, & \text{if load}=0 \end{cases}

この式は D 入力の手前に 2:1 マルチプレクサーを置いて実装するか、デバイス専用のクロックイネーブル機能を使って実装します。すべての状態要素を同じグローバルクロックに接続してください。

グローバルクロックと load を AND してローカルクロックを作ってはいけません。制御されないクロックゲーティングは、ハードウェアにスキューやグリッチを持ち込むことがあります。集積クロックゲーティングセルや FPGA のクロック制御リソースは、これとは別のタイミングを意識した仕組みです。

バスにつながる各レジスターには、次のものが必要です。

  • バスからの 8 ビット入力。
  • 保存された 8 ビット出力。
  • load_x 制御。
  • 教育用バスを駆動することを許可された oe_x 経路。

PC にはさらにインクリメント経路と明示的なロード経路が必要です。IR には上位バイトと下位バイトを独立にロードする機能が必要です。MAR はメモリーアドレスを保持し、MDR はメモリーデータ 1 バイトを取り込むか供給します。

複製する前に 1 レジスターを検証する

次のケースをテストします。

  1. load = 0: 複数クロック後も Q は変化しない。
  2. load = 1: アクティブエッジで Q がバスを取り込む。
  3. エッジから離れた時点でデータが変わる場合: Q は次の有効エッジでのみ変わる。
  4. リセットを備える場合: Q が文書化された状態へ到達する。
  5. 出力イネーブルをオフにした場合: レジスターはバスを駆動しない。

デジタルシミュレーターは、このモデルの論理的な順序を検証できます。ただしアナログのメタステビリティを再現したり、物理的なセットアップ/ホールドタイミングを証明したりはしません。

4. ALU を作り、すべてのフラグを定義する

A と B を安定したオペランドラッチとして使います。ALU は組み合わせ回路であり、隠れたアーキテクチャ状態を含んではなりません。

alu_op演算結果
000ADDA + B
001SUBA − B
010ANDA & B
011OR`A
100XORA ^ B
101NOT~A
110SHLA << 1、ゼロフィル
111SHR論理 A >> 1、ゼロフィル

ALU ガイドでは、演算レーンと出力マルチプレクサーを詳しく説明しています。算術部の基本設計は次のとおりです。

  • 8 段の 全加算器 チェーン。
  • A + (~B) + 1 で実装する減算。
  • 並列のビット単位ゲート。
  • 固定の 1 ビットシフター配線。
  • alu_op で選択する出力マルチプレクサー。

フラグの定義

論理命令とシフト命令が C と V に何をするかも含め、フラグを ISA の一部として定義してください。

  • Z: 結果の全ビットがゼロなら 1。
  • N: 結果のビット 7。符号付き演算においてのみ符号ビットとして解釈する。
  • ADD 時の C: ビット 7 からのキャリー出力。
  • SUB 時の C: 「ボローなし」か「ボロー」のどちらかを選んで文書化する。このガイドでは加算器のキャリー出力規約に合わせてボローなしを使う。
  • ADD 時の V: オペランドの符号が同じで、結果の符号が反対。
  • SUB 時の V: オペランドの符号が異なり、結果の符号が A と異なる。

減算レーンを B の反転オペランドとキャリー入力で実装する場合、キャリー入力とキャリー出力の XOR による等価式は、2 の補数の加算と減算に適用されます。具体例は CPU フラグ を参照してください。

この教育用 ISA は ADD と SUB で Z、N、C、V を更新します。論理演算とシフト演算では、マイクロコードを書く前に、C/V を保持するか新しい意味を定義するかという文書化済みの方針を選んでください。「たまたま配線に出ている値」は有効な方針ではありません。

ALU 検証表

最低限、次をテストします。

ケース確認の焦点
0x00 + 0x00Z
0xFF + 0x01符号なしキャリーとゼロ
0x7F + 0x01符号付きオーバーフローと N
0x80 + 0x80キャリーと符号付きオーバーフロー
0x00 − 0x01ボロー規約と N
0x80 − 0x01符号付きオーバーフロー
AND/OR/XOR の恒等式ビット単位レーン
0x81 のシフト方向、フィルビット、シフトアウトビットの方針

5. 共有バスを作る

教育用バスは複数の供給元を持つ 8 本の線です。中心となる不変条件は次のとおりです。

∑enabled bus drivers≤1\sum \text{enabled bus drivers} \le 1

可能なドライバーには PC、MDR、R0–R3、A、B、IR の 1 バイト、ALU の結果があります。正確な一覧は「レジスターの数」より長くなるため、制御仕様ですべての供給元を列挙してください。

DigiSim では、トライステートバッファーで共有バスをモデル化できます。

  • 転送中に有効にする供給元は必ず 1 個だけにする。
  • 次のアクティブエッジに向けて、1 個以上の宛先ロード信号をアサートする。
  • どの供給元もバスを駆動していないときは、決してバスを取り込まない。

競合は設計エラーです。物理的な出力競合は誤った電圧や過大な電流を生むおそれがあります。その結果は技術と継続時間に依存するため、「チップは必ず燃える」という言い方は役に立ちません。

現代の FPGA ファブリックでは、内部共有バスは一般に真の内部トライステート配線ではなくマルチプレクサーとして実装されます。ASIC のスタイルもさまざまです。トライステートバスは、所有権と転送を理解するための明快な教育モデルであり続けます。

バス調停テスト

R0 と R1 に既知の値を置きます。

  • R0 = 0xAA
  • R1 = 0x55

R0 単独、R1 単独、ドライバーなし、両ドライバーを検証します。後ろの 2 つは無効な制御器状態として扱うべきです。次にアサーションまたは可視インジケーターを追加し、以後のマイクロコードエラーを直ちに検出できるようにします。

6. バイト幅メモリーを接続する

シミュレーターの RAM コンポーネントはアーキテクチャ上のメモリーモデルとして使います。物理 RAM が文字どおり D ラッチの配列だと主張してはいけません。SRAM、DRAM、ブロック RAM、合成されたレジスター配列は、異なる記憶セルとタイミングを使います。

この CPU のインターフェースは次のとおりです。

  • MAR が駆動する ADDR[7:0]。
  • MDR または制御されたバスインターフェースを通じて転送する DATA[7:0]。
  • mem_read。
  • mem_write。
  • モデルが可変レイテンシーをサポートする場合は任意の ready。

この手順では、選んだ RAM の読み出しは専用の 1 マイクロステップで要求・取り込みできると仮定します。選んだメモリーが同期式、または目に見えるレイテンシーを持つ場合は、待機状態を挿入し、データが有効なときだけ取り込んでください。

命令メモリーとデータは同じ 256 バイトを共有します。メモリーを分けるハーバード設計も有効ですが、それは異なるアーキテクチャとフェッチスケジュールになります。

書き込み可能な記憶と不揮発/定数記憶という抽象化の違いについては、RAM と ROM を参照してください。

7. マイクロシーケンス制御器を作る

制御器は、オペコード、マイクロステップ、フラグ、マシン状態を次のものへ変換します。

  • バス出力イネーブル。
  • レジスターのロードイネーブル。
  • ALU 演算。
  • メモリー読み書き制御。
  • PC のインクリメント/ロード制御。
  • フラグロード。
  • IR の上位/下位バイトロード。
  • 次のマイクロステップまたは命令終了の制御。

実装の一つは制御 ROM を使います。

control address = {opcode, microstep}
control word    = {oe signals, load signals, alu_op, memory controls, next control}

フラグは選択された動作を修飾できます。たとえば JZ なら load_pc = microcode_load_pc AND Z とします。または制御ストアのアドレスに参加させることもできます。マイクロステップカウンターが常に増えると仮定するより、明示的な次状態ロジックの方が分かりやすいことが多いです。

これは教育目的のマイクロコード制御器です。実際のプロセッサーでは、組み合わせデコード、プログラム可能な制御ストア、状態機械、シーケンサーをさまざまに組み合わせます。RISC/CISC というラベルを実装の近道として使わないでください。

安全なデフォルト

未使用の制御 ROM エントリーはすべて、次を行うべきです。

  • すべてのバスドライバーを無効にする。
  • メモリー書き込みを無効にする。
  • アーキテクチャ上のレジスター書き込みを無効にする。
  • 定義済みの不正命令または停止経路へ入る。

初期化されていない制御ワードが「任意のメモリーに任意のデータを書き込む」状態になってはなりません。

8. 2 バイトの命令を一貫してフェッチする

RAM とデータパスは 8 ビット幅である一方、各命令は 16 ビットなので、フェッチには 2 回のバイト読み出しが必要です。定義により、上位バイトを先に格納します。

選択した RAM の読み出しに 1 マイクロステップで足りると仮定します。

ステップレジスター転送重要な制御
F0MAR ← PCoe_pc, load_mar
F1MDR ← RAM[MAR]; PC ← PC + 1mem_read, load_mdr, inc_pc
F2IR[15:8] ← MDRoe_mdr, load_ir_hi
F3MAR ← PCoe_pc, load_mar
F4MDR ← RAM[MAR]; PC ← PC + 1mem_read, load_mdr, inc_pc
F5IR[7:0] ← MDRoe_mdr, load_ir_lo

F5 の後、IR には完全な命令が 1 個入り、PC はすでに次の命令を指しています。デコードで実行シーケンスを選びます。

RAM がアドレスからデータまでの待機を必要とする場合、F1 と F4 を要求、待機、取り込みの各ステップへ分割してください。16 ビットフェッチを 1 回のバイト幅転送の中に隠してはいけません。

9. 実行マイクロ操作を定義する

ADD RD, RA, RB

ステップ転送
E0A ← R[RA]
E1B ← R[RB]
E2R[RD] ← A + B; FLAGS ← ALU flags
E3命令終了。F0 へ戻る

RA、RB、RD はデコードしたイネーブルにより R0–R3 のいずれかを選びます。不正なレジスターコードは不正命令経路へ入らなければなりません。

LDI RD, imm8

ステップ転送
E0R[RD] ← IR[7:0]
E1命令終了

LD RD, addr8

ステップ転送
E0MAR ← IR[7:0]
E1MDR ← RAM[MAR]
E2R[RD] ← MDR
E3命令終了

ST RD, addr8

ステップ転送
E0MAR ← IR[7:0]
E1MDR ← R[RD]
E2RAM[MAR] ← MDR
E3命令終了

JMP と JZ

  • JMP: PC ← IR[7:0] を行ってから終了。
  • JZ: Z が 1 のときだけ PC をロードし、いずれの場合も終了。

HLT

書き込みを一切発行しない安定した制御器状態へ入ります。手動駆動のシミュレータークロックを止めるのは便利ですが、クロックエッジが続いてもアーキテクチャ状態は明確に定義されたままでなければなりません。

10. テストプログラムを手でアセンブルしてロードする

このプログラムは 5 と 3 をロードし、R2 に加算して、結果をアドレス 0x10 に格納して停止します。

開始アドレス命令16 ビット語メモリー内のバイト
0x00LDI R0, #50x10050x10 0x05
0x02LDI R1, #30x11030x11 0x03
0x04ADD R2, R0, R10x40120x40 0x12
0x06ST R2, 0x100x32100x32 0x10
0x08HLT0x80000x80 0x00

この 10 バイトをアドレス 0x00 から 0x09 へロードしてから、リセットします。

  • PC = 0x00
  • 制御器マイクロステップ = F0
  • halted = 0
  • 制御ワードが選ぶときを除き、すべてのバスドライバーを無効にする

最初のフェッチをシングルステップ実行し、次を確認します。

  1. F2 の後、IR 上位バイトは 0x10。
  2. F5 の後、IR は 0x1005。
  3. PC は 0x02。
  4. LDI が R0 に 0x05 を置く。
  5. ほかのレジスターやメモリー位置は一切変化しない。

停止時:

  • R0 = 0x05
  • R1 = 0x03
  • R2 = 0x08
  • RAM[0x10] = 0x08
  • PC = 0x0A

この最終値は必要条件ですが十分条件ではありません。壊れた CPU でも、別の位置を破壊しつつこの値を作れてしまうことがあります。

11. 最終結果だけでなく不変条件を検証する

次のチェックを加えます。

  • 任意の時点でバス出力イネーブルが 2 個以上にならないこと。
  • ST の書き込みステップ中だけメモリー書き込みがアサートされること。
  • IR バイトが F2 と F5 でのみロードされること。
  • 通常のフェッチごとに PC が正確に 2 回インクリメントされること。
  • 更新すると定義された命令だけがフラグを書き込むこと。
  • 不正なオペコードまたはレジスターコードが意図しない書き込みを起こさないこと。
  • 停止状態がアーキテクチャ上の副作用を生まないこと。
  • リセットで既知の制御器状態に到達すること。

命令ごとに、次のトレースを記録します。

clock, microstep, PC, IR, bus owner, bus value,
register loads, memory read/write, ALU op, flags

テストに失敗したら、期待トレースから最初にずれたサイクルを見つけます。最終的な誤値からデバッグを始めると、通常は数ステップ遅すぎます。

オシロスコープデバッグガイドでは、モデル化された波形を物理的なタイミングサインオフと混同せずに調べる方法を説明しています。

12. よくある故障モード

症状起こりやすい原因最初の確認
IR のバイトが入れ替わっているローダーとフェッチで命令バイト順序が異なるアドレス PC と PC+1
PC が 4 進むバイトごとに 2 回増分した、または幅の仮定を混在させたF0–F5 トレース
バスが競合する2 つの出力イネーブルが 1 つの制御ビットを共有するデコードされた制御ワード
取り込み時にバスが未駆動供給元イネーブルの欠落バス所有者アサーション
ADD が誤ったレジスターへ書くRD フィールドデコードまたはレジスターコード順序IR[3:0] とロード線
ストアが命令メモリーを破壊するアドレス/即値または mem_write のタイミングMAR と ST E2
分岐が古い Z を使うフラグ未書き込み、または JZ が誤状態をサンプルするADD/SUB E2 と JZ
制御器がフェッチへ戻らない終了/次状態エントリーの欠落マイクロステップ遷移
不正オペコードが演算を行う未使用の制御 ROM が安全に初期化されていないデフォルト制御ワード

13. この CPU が教えること、教えないこと

この設計は、レジスター転送、命令エンコード、バス所有権、ALU フラグ、メモリーシーケンス、制御器状態を教えます。

次のものはモデル化しません。

  • トランジスターレベルの RAM や論理。
  • キャッシュ階層や仮想メモリー。
  • 投機的実行やアウトオブオーダー実行。
  • アナログクロックの挙動やメタステビリティ。
  • 物理的な伝搬、配線、電力、熱の限界。
  • 製品用のセキュリティまたは例外アーキテクチャ。

この単一共有バスは意図的に観察しやすくしたものであり、現代 CPU の内部トポロジーを主張するものではありません。

安全な順序での拡張

  1. 自動化された命令テストを加える。 機能を足す前に、各オペコードと不正エンコードを検証する。
  2. AND、OR、XOR、シフトを加える。 先にフラグ動作を定義し、その後に制御シーケンスを加える。
  3. 直接比較を使う条件分岐を加える。 これによりフラグベース ISA と比較・分岐 ISA の違いが分かる。
  4. スタックポインターと call/return を加える。 スタックの成長方向、バイト順序、オーバーフロー動作を指定する。
  5. 割り込み状態機械を加える。 ベクターをロードする前に復帰アドレスとフラグを保存し、優先順位とマスキングを定義する。
  6. 命令メモリーとデータメモリーを分ける。 新しいフェッチスケジュールを統合メモリー設計と比較する。
  7. マルチサイクルの基準実装が通ってからパイプライン化する。 その後、ステージ有効性、ストール、フォワーディング、フラッシュ、例外を定義する。

パイプライン拡張を試す前に、フェッチ–デコード–実行の詳細解説 と 命令レジスター/デコードガイド を使ってください。

試してみる

方向付けには Sequential Instruction Executor、算術を別に検証するには 8-bit ALU system、状態動作をテストするには CPU flags register を使います。回路図のそばに、書面のアーキテクチャとマイクロ操作表を置いてください。

重要な到達点は、8 が一度表示されることではありません。各状態変更がなぜ許可されたかを説明でき、トレースで再現でき、ほかの状態が変化しなかったと証明できることです。

アイデアを実際に動く回路にしましょう。