【Verilog自作CPU】シリーズ 最終回!
| Part | タイトル | 内容 |
|---|---|---|
| Part1 | レジスタファイルとALU | 基本部品 |
| Part2 | 命令デコーダ | 命令解析 |
| Part3 | メモリとロード・ストア | LW/SW命令 |
| Part4 | 分岐とジャンプ | BEQ/JAL命令 |
| Part5 | CPU統合と動作テスト | 本記事(最終回) |
はじめに
ついに最終回です!これまで作ってきた部品を全て統合して、動くRISC-V CPUを完成させます。
今回やること
- CPUトップモジュールの実装
- 全コンポーネントの接続
- テストプログラムの実行
- 動作確認
CPUトップモジュール
全ての部品を接続したトップモジュール:
// RISC-V Single-Cycle CPU
module riscv_cpu (
input clk,
input reset,
output [31:0] pc_out,
output [31:0] instr_out
);
// PC
reg [31:0] pc;
wire [31:0] pc_next;
// 命令
wire [31:0] instr;
// デコード結果
wire [4:0] rs1, rs2, rd;
wire [31:0] imm;
wire reg_write;
wire mem_read, mem_write;
wire branch, jump;
wire alu_src;
wire [3:0] alu_op;
wire mem_to_reg;
// レジスタファイル
wire [31:0] rs1_data, rs2_data;
// ALU
wire [31:0] alu_a, alu_b, alu_result;
wire zero;
// データメモリ
wire [31:0] mem_data;
// ライトバック
wire [31:0] wb_data;
//=========================================
// 命令フェッチ
//=========================================
imem instruction_memory (
.addr(pc),
.data(instr)
);
//=========================================
// デコード
//=========================================
decoder instruction_decoder (
.instr(instr),
.rs1(rs1),
.rs2(rs2),
.rd(rd),
.imm(imm),
.reg_write(reg_write),
.mem_read(mem_read),
.mem_write(mem_write),
.branch(branch),
.jump(jump),
.alu_src(alu_src),
.alu_op(alu_op),
.mem_to_reg(mem_to_reg)
);
//=========================================
// レジスタファイル
//=========================================
regfile registers (
.clk(clk),
.we(reg_write),
.rs1(rs1),
.rs2(rs2),
.rd(rd),
.wdata(wb_data),
.rdata1(rs1_data),
.rdata2(rs2_data)
);
//=========================================
// ALU
//=========================================
// ALU入力選択
assign alu_a = rs1_data;
assign alu_b = alu_src ? imm : rs2_data;
alu main_alu (
.a(alu_a),
.b(alu_b),
.alu_op(alu_op),
.result(alu_result),
.zero(zero)
);
//=========================================
// データメモリ
//=========================================
dmem data_memory (
.clk(clk),
.addr(alu_result),
.write_data(rs2_data),
.write_enable(mem_write),
.read_data(mem_data)
);
//=========================================
// ライトバック
//=========================================
assign wb_data = mem_to_reg ? mem_data : alu_result;
//=========================================
// PC制御
//=========================================
wire branch_taken = branch & zero;
wire [31:0] branch_target = pc + imm;
wire [31:0] jump_target = pc + imm;
assign pc_next = jump ? jump_target :
branch_taken ? branch_target :
pc + 4;
// PC更新
always @(posedge clk or posedge reset) begin
if (reset)
pc <= 32'd0;
else
pc <= pc_next;
end
// デバッグ出力
assign pc_out = pc;
assign instr_out = instr;
endmodule
テストベンチ
実際にプログラムを実行するテストベンチ:
`timescale 1ns/1ps
module riscv_cpu_tb;
reg clk;
reg reset;
wire [31:0] pc, instr;
riscv_cpu dut (
.clk(clk),
.reset(reset),
.pc_out(pc),
.instr_out(instr)
);
// クロック生成
initial clk = 0;
always #5 clk = ~clk;
initial begin
$display("=== RISC-V CPU Test ===");
$display("");
$display("Testing simple program:");
$display(" ADDI x1, x0, 5 ; x1 = 5");
$display(" ADDI x2, x0, 10 ; x2 = 10");
$display(" ADD x3, x1, x2 ; x3 = 15");
$display(" SUB x4, x2, x1 ; x4 = 5");
$display(" AND x5, x1, x2 ; x5 = 0");
$display("");
// 命令メモリにプログラムをロード
// ADDI x1, x0, 5
dut.instruction_memory.memory[0] = 32'h00500093;
// ADDI x2, x0, 10
dut.instruction_memory.memory[1] = 32'h00a00113;
// ADD x3, x1, x2
dut.instruction_memory.memory[2] = 32'h002081b3;
// SUB x4, x2, x1
dut.instruction_memory.memory[3] = 32'h40110233;
// AND x5, x1, x2
dut.instruction_memory.memory[4] = 32'h0020f2b3;
// NOP
dut.instruction_memory.memory[5] = 32'h00000013;
dut.instruction_memory.memory[6] = 32'h00000013;
// リセット
reset = 1;
#20;
reset = 0;
$display("Executing instructions:");
$display("");
// 各命令を実行
repeat (6) begin
@(posedge clk);
#1;
$display("PC=0x%08x Instr=0x%08x", pc, instr);
$display(" x1=%d, x2=%d, x3=%d, x4=%d, x5=%d",
dut.registers.registers[1],
dut.registers.registers[2],
dut.registers.registers[3],
dut.registers.registers[4],
dut.registers.registers[5]);
end
$display("");
$display("=== Final Register State ===");
$display("x1 = %d (expected: 5)", dut.registers.registers[1]);
$display("x2 = %d (expected: 10)", dut.registers.registers[2]);
$display("x3 = %d (expected: 15)", dut.registers.registers[3]);
$display("x4 = %d (expected: 5)", dut.registers.registers[4]);
$display("x5 = %d (expected: 0)", dut.registers.registers[5]);
// 検証
if (dut.registers.registers[1] == 5 &&
dut.registers.registers[2] == 10 &&
dut.registers.registers[3] == 15 &&
dut.registers.registers[4] == 5 &&
dut.registers.registers[5] == 0) begin
$display("");
$display("*** ALL TESTS PASSED! ***");
end else begin
$display("");
$display("*** SOME TESTS FAILED ***");
end
#20;
$finish;
end
// 波形出力
initial begin
$dumpfile("riscv_cpu_tb.vcd");
$dumpvars(0, riscv_cpu_tb);
end
endmodule
実行結果
$ cd ~/riscv-cpu
$ iverilog -o cpu_test src/regfile.v src/alu.v src/decoder.v \
src/imem.v src/dmem.v src/riscv_cpu.v test/riscv_cpu_tb.v
$ ./cpu_test
=== RISC-V CPU Test ===
Testing simple program:
ADDI x1, x0, 5 ; x1 = 5
ADDI x2, x0, 10 ; x2 = 10
ADD x3, x1, x2 ; x3 = 15
SUB x4, x2, x1 ; x4 = 5
AND x5, x1, x2 ; x5 = 0
VCD info: dumpfile riscv_cpu_tb.vcd opened for output.
Executing instructions:
PC=0x00000004 Instr=0x00a00113
x1= 5, x2= 0, x3= 0, x4= 0, x5= 0
PC=0x00000008 Instr=0x002081b3
x1= 5, x2= 10, x3= 0, x4= 0, x5= 0
PC=0x0000000c Instr=0x40110233
x1= 5, x2= 10, x3= 15, x4= 0, x5= 0
PC=0x00000010 Instr=0x0020f2b3
x1= 5, x2= 10, x3= 15, x4= 5, x5= 0
PC=0x00000014 Instr=0x00000013
x1= 5, x2= 10, x3= 15, x4= 5, x5= 0
PC=0x00000018 Instr=0x00000013
x1= 5, x2= 10, x3= 15, x4= 5, x5= 0
=== Final Register State ===
x1 = 5 (expected: 5)
x2 = 10 (expected: 10)
x3 = 15 (expected: 15)
x4 = 5 (expected: 5)
x5 = 0 (expected: 0)
*** ALL TESTS PASSED! ***
🎉 CPU完成!全テスト合格!
動作の解説
シミュレーション結果を詳しく見てみましょう:
サイクル1: ADDI x1, x0, 5
PC=0x00000004 Instr=0x00a00113
x1=5, x2=0, x3=0, x4=0, x5=0
- PC=0 の命令
ADDI x1, x0, 5を実行 - x0(=0) + 5 = 5 を x1 に書き込み
- PC は次の命令 0x04 を指している
サイクル2: ADDI x2, x0, 10
PC=0x00000008 Instr=0x002081b3
x1=5, x2=10, x3=0, x4=0, x5=0
- x0 + 10 = 10 を x2 に書き込み
サイクル3: ADD x3, x1, x2
PC=0x0000000c Instr=0x40110233
x1=5, x2=10, x3=15, x4=0, x5=0
- x1(=5) + x2(=10) = 15 を x3 に書き込み
サイクル4: SUB x4, x2, x1
PC=0x00000010 Instr=0x0020f2b3
x1=5, x2=10, x3=15, x4=5, x5=0
- x2(=10) - x1(=5) = 5 を x4 に書き込み
サイクル5: AND x5, x1, x2
PC=0x00000014
x1=5, x2=10, x3=15, x4=5, x5=0
- x1(=5) & x2(=10) = 0b0101 & 0b1010 = 0
全ての演算が正しく実行されました!
波形ビューア
GTKWaveで波形を見ることもできます:
$ gtkwave riscv_cpu_tb.vcd
クロックに同期してPCが4ずつ増加し、レジスタの値が更新されていく様子が見えます。
完成したCPUの仕様
| 項目 | 仕様 |
|---|---|
| アーキテクチャ | RISC-V RV32I(サブセット) |
| データ幅 | 32ビット |
| レジスタ | 32本(x0は常に0) |
| 命令メモリ | 1KB (256命令) |
| データメモリ | 1KB |
| パイプライン | なし(シングルサイクル) |
サポート命令
| カテゴリ | 命令 |
|---|---|
| 算術演算 | ADD, SUB, ADDI |
| 論理演算 | AND, OR, XOR, ANDI, ORI, XORI |
| シフト | SLL, SRL, SRA, SLLI, SRLI, SRAI |
| 比較 | SLT, SLTU, SLTI, SLTIU |
| メモリ | LW, SW |
| 分岐 | BEQ (※他の分岐命令も対応可能) |
| ジャンプ | JAL, JALR |
今後の拡張
このCPUをベースに、以下の拡張が可能です:
パイプライン化
IF → ID → EX → MEM → WB
5段パイプラインで性能向上。ただしハザード処理が必要。
キャッシュの追加
CPU ←→ L1 Cache ←→ Main Memory
メモリアクセスの高速化。
割り込み対応
CSR(Control and Status Registers)
例外処理機構
OS実装に必須。
FPGA実装
実際のFPGA(Xilinx, Intel/Alteraなど)で動かす!
まとめ
5回にわたるシリーズで、ゼロからRISC-V CPUを作りました!
Part1: レジスタファイル + ALU
Part2: 命令デコーダ
Part3: メモリアクセス
Part4: 分岐・ジャンプ
Part5: CPU統合・完成!
学んだこと
- CPUの基本構造 - PC, レジスタ, ALU, メモリ
- RISC-V命令セット - 6つの命令フォーマット
- Verilog記述 - モジュール設計、テストベンチ
- シミュレーション - iverilogによる動作確認
ファイル一覧
~/riscv-cpu/
├── src/
│ ├── regfile.v # レジスタファイル
│ ├── alu.v # ALU
│ ├── decoder.v # 命令デコーダ
│ ├── imem.v # 命令メモリ
│ ├── dmem.v # データメモリ
│ └── riscv_cpu.v # CPUトップモジュール
└── test/
├── regfile_tb.v # テストベンチ
├── alu_tb.v
├── decoder_tb.v
└── riscv_cpu_tb.v
おわりに
「CPUって難しそう」と思っていた方も、一つずつ部品を作って組み合わせていけば、ちゃんと動くものが作れることがわかったと思います。
このシリーズのコードは自由に使ってください。FPGAに載せたり、パイプライン化したり、自分なりの拡張を楽しんでください!
ここまで読んでいただきありがとうございました!🎉
