アキュムレータ型という設計
もっとも単純な CPU の形のひとつである。計算の途中結果を置く場所が ACC(アキュムレータ)ひとつしかなく、 すべての演算が「ACC と、メモリから読んだ値」の間で行われる。レジスタがたくさんある現代の CPU と違い、 状態が ACC と PC(次に実行する命令の番地)の2つだけなので、動きを全部追いかけられる。
4つの命令だけの命令セット
命令は8ビット。上位2ビットが opcode(何をするか)、下位6ビットが address(どのデータを使うか)である。
| opcode | 命令 | 動作 | ACC はどうなる |
|---|---|---|---|
| 00 | LOAD | ACC ← dmem[addr] | メモリの値で置き換わる |
| 01 | STORE | dmem[addr] ← ACC | 変わらない(書き出すだけ) |
| 10 | ADD | ACC ← ACC + dmem[addr] | 足された値になる |
| 11 | HALT | 停止 | 変わらない |
動かすプログラム:5 + 3
dmem[0]=5, dmem[1]=3 を置いた状態で、次の4命令を順に実行すると dmem[2] に 8 が書き込まれる。
| 番地 | 機械語 | ニーモニック | 実行後の ACC |
|---|---|---|---|
| 0 | 00_000000 | LOAD 0 | 5 |
| 1 | 10_000001 | ADD 1 | 8 |
| 2 | 01_000010 | STORE 2 | 8(dmem[2] に 8 を書く) |
| 3 | 11_000000 | HALT | 8(停止) |
制御部を書いて、その場でチェック
CPU のうち、PC の更新やメモリの読み書きといった配線はすでにできている。残っているのは 制御部(デコーダ)——「いま読んだ opcode に対して、次の ACC は何になるか、メモリに書くか、止まるか」を決める部分である。 これは組み合わせ回路なので、ここでチェックできる。
- opcode
- いま実行中の命令の上位2ビット
- acc
- 現在の ACC の値
- mem_data
- dmem[addr] から読んだ値
- next_acc
- 次のクロックで ACC に入る値
- mem_we
- dmem に書き込むなら 1(STORE のときだけ)
- halt
- 停止するなら 1
module cpu_decode (
input [1:0] opcode,
input [7:0] acc,
input [7:0] mem_data,
output reg [7:0] next_acc,
output reg mem_we,
output reg halt
);
endmodule
答え合わせ(自分で試してから開く)
always @(*) begin // まず「何もしない」を既定値にしておく next_acc = acc; mem_we = 1'b0; halt = 1'b0; case (opcode) 2'b00: next_acc = mem_data; // LOAD 2'b01: mem_we = 1'b1; // STORE 2'b10: next_acc = acc + mem_data; // ADD 2'b11: halt = 1'b1; // HALT endcase end
最初に既定値を書いておく書き方は、CPU の制御部でよく使われる。 「その命令が変えないものは、そのまま」という意味になり、case の各行が短くなる。
CPU 全体のコード(参考)
上で書いた制御部を、PC・メモリ・クロックと組み合わせると CPU 全体になる。 クロックで状態が変わる部分(always @(posedge clk))は、ブラウザのチェッカーでは扱えないため、 動かしてみたい人は EDA Playground を使ってください。
module cpu_accum ( input clk, input rst, output [3:0] pc_out, output [7:0] acc_out, output halted ); reg [3:0] pc; reg [7:0] acc; reg [7:0] imem [0:15]; // 命令メモリ reg [7:0] dmem [0:63]; // データメモリ wire [7:0] instr = imem[pc]; wire [1:0] opcode = instr[7:6]; wire [5:0] addr = instr[5:0]; // ← ここが 04 で書いた制御部(next_acc / mem_we / halt) wire [7:0] next_acc; wire mem_we, halt; cpu_decode dec (.opcode(opcode), .acc(acc), .mem_data(dmem[addr]), .next_acc(next_acc), .mem_we(mem_we), .halt(halt)); // クロックで状態を進める部分(順序回路) always @(posedge clk) begin if (rst) begin pc <= 4'd0; acc <= 8'd0; end else if (!halt) begin acc <= next_acc; if (mem_we) dmem[addr] <= acc; pc <= pc + 4'd1; end end assign pc_out = pc; assign acc_out = acc; assign halted = halt; endmodule
発展課題
- SUB 命令に変える:opcode 10 を ADD ではなく SUB(ACC − dmem[addr])にしてみましょう。 演習Bで作った引き算がそのまま使える。チェッカーは ADD を期待しているので失敗するが、 「どのテストがどう失敗するか」を見れば、命令を1つ変えると機械の意味がどう変わるかが分かる。
- JMP 命令を考える:PC を addr にセットする命令を足すには、制御部にどんな出力を追加すればよいでしょうか。 いまの制御部は next_acc / mem_we / halt しか出していない。
- 命令の下位6ビットがアドレスなので、データメモリは何番地まで使えますか。これは命令フォーマットのどこで決まっていますか。
- この CPU と、演習3のミニ CPU シミュレーター(レジスタが複数ある型)を比べて、どちらがどんな場面で有利でしょうか。
発展:本物のツール(EDA Playground)で CPU 全体を動かす 任意
クロックを含む CPU 全体は、EDA Playground で動かせる(Gmail アカウントが必要)。 上の 05 のコードと、下のテストベンチを貼り付けてください。手順は 使い方ガイド へ。
module tb_cpu_accum; reg clk = 0, rst = 1; wire [3:0] pc_out; wire [7:0] acc_out; wire halted; cpu_accum uut (.clk(clk), .rst(rst), .pc_out(pc_out), .acc_out(acc_out), .halted(halted)); always #5 clk = ~clk; initial begin $dumpfile("dump.vcd"); $dumpvars(0, tb_cpu_accum); // プログラムとデータを載せる uut.imem[0] = 8'b00_000000; // LOAD 0 uut.imem[1] = 8'b10_000001; // ADD 1 uut.imem[2] = 8'b01_000010; // STORE 2 uut.imem[3] = 8'b11_000000; // HALT uut.dmem[0] = 8'd5; uut.dmem[1] = 8'd3; #12 rst = 0; wait (halted); #10; $display("ACC = %0d, dmem[2] = %0d", acc_out, uut.dmem[2]); $finish; end endmodule