【Verilog自作CPU】シリーズ
| Part | タイトル | 内容 |
|---|---|---|
| Part1 | レジスタファイルとALU | 本記事 |
| Part2 | 命令デコーダ | 命令解析と制御信号 |
| Part3 | メモリとロード・ストア | LW/SW命令の実装 |
| Part4 | 分岐とジャンプ | BEQ/JAL命令の実装 |
| Part5 | CPU統合と動作テスト | 完成・プログラム実行 |
はじめに
「CPUって何か難しそう...」
そう思っていませんか?実は、基本的なCPUはシンプルな部品の組み合わせで作れます。
このシリーズでは、RISC-Vという現代的でオープンな命令セットアーキテクチャ(ISA)を使って、ゼロから動くCPUを作ります。しかも、シミュレーションで実際に動作を確認しながら進めるので、「本当に動いてる!」という実感が得られます。
なぜRISC-V?
RISC-Vを選ぶ理由:
- オープン - 誰でも無料で使える
- シンプル - 基本命令セットが非常に整理されている
- 実用的 - SiFiveやEspressifなど、実際の製品で使われている
- 学習向け - 余計な歴史的経緯がない
開発環境
今回使う環境:
- WSL2 (Ubuntu 22.04)
- Icarus Verilog (iverilog) - シミュレータ
- GTKWave - 波形ビューア(オプション)
インストール
sudo apt update
sudo apt install iverilog gtkwave -y
これだけでOK!
$ iverilog -v
Icarus Verilog version 11.0 (stable)
プロジェクト構成
~/riscv-cpu/
├── src/ # Verilogソース
│ ├── regfile.v # レジスタファイル
│ ├── alu.v # ALU
│ ├── decoder.v # 命令デコーダ
│ ├── imem.v # 命令メモリ
│ ├── dmem.v # データメモリ
│ └── riscv_cpu.v # トップモジュール
└── test/ # テストベンチ
├── regfile_tb.v
├── alu_tb.v
└── riscv_cpu_tb.v
mkdir -p ~/riscv-cpu/{src,test}
cd ~/riscv-cpu
CPUの基本構造
まず、CPUの全体像を理解しましょう。
┌──────────────────────────────────────────────────────────┐
│ CPU │
│ ┌─────┐ ┌────────┐ ┌─────────┐ ┌─────┐ ┌─────┐ │
│ │ PC │──▶│ I-MEM │──▶│ DECODER │──▶│ REG │──▶│ ALU │ │
│ └──┬──┘ └────────┘ └────┬────┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │ │
│ │ │ ▼ ▼ │
│ │ │ ┌───────────────┐ │
│ │ └──────▶│ D-MEM │ │
│ ◀────────────────────────────────┴───────────────┘ │
└──────────────────────────────────────────────────────────┘
- PC(プログラムカウンタ): 現在実行中の命令アドレス
- I-MEM(命令メモリ): プログラムを格納
- DECODER: 命令を解析して制御信号を生成
- REG(レジスタファイル): データを一時保存(32本のレジスタ)
- ALU: 演算を行う
- D-MEM(データメモリ): データを読み書き
レジスタファイルを作る
RISC-Vのレジスタ
RISC-Vには32本の32ビットレジスタ(x0〜x31)があります。
特徴的なのはx0は常に0ということ。書き込んでも0のままです。これ、意外と便利なんです。
x0 = 0 (zero) 常に0
x1 = ra リターンアドレス
x2 = sp スタックポインタ
x3 = gp グローバルポインタ
...
x31 = t6 一時レジスタ
Verilogで実装
// RISC-V レジスタファイル
// 32本の32ビットレジスタ (x0は常に0)
module regfile (
input wire clk,
input wire we, // 書き込み有効
input wire [4:0] rs1, // ソースレジスタ1アドレス
input wire [4:0] rs2, // ソースレジスタ2アドレス
input wire [4:0] rd, // デスティネーションレジスタアドレス
input wire [31:0] wdata, // 書き込みデータ
output wire [31:0] rdata1, // 読み出しデータ1
output wire [31:0] rdata2 // 読み出しデータ2
);
// 32本のレジスタ
reg [31:0] registers [0:31];
// 初期化
integer i;
initial begin
for (i = 0; i < 32; i = i + 1)
registers[i] = 32'h0;
end
// 同期書き込み(x0には書き込まない!)
always @(posedge clk) begin
if (we && rd != 5'b0)
registers[rd] <= wdata;
end
// 非同期読み出し(x0は常に0)
assign rdata1 = (rs1 == 5'b0) ? 32'h0 : registers[rs1];
assign rdata2 = (rs2 == 5'b0) ? 32'h0 : registers[rs2];
endmodule
ポイント:
-
書き込みは同期(
always @(posedge clk)) -
読み出しは非同期(
assign) -
x0への書き込みは無視(
rd != 5'b0でガード)
テストベンチ
`timescale 1ns/1ps
module regfile_tb;
reg clk, we;
reg [4:0] rs1, rs2, rd;
reg [31:0] wdata;
wire [31:0] rdata1, rdata2;
regfile dut (
.clk(clk), .we(we),
.rs1(rs1), .rs2(rs2), .rd(rd),
.wdata(wdata),
.rdata1(rdata1), .rdata2(rdata2)
);
// クロック生成
initial clk = 0;
always #5 clk = ~clk;
initial begin
$display("=== Regfile Test ===\n");
we = 0; rs1 = 0; rs2 = 0; rd = 0; wdata = 0;
// Test 1: x0は常に0
#10;
rs1 = 0; #1;
$display("Test 1: x0 read = %d (expected: 0)", rdata1);
// Test 2: x1に書き込み
we = 1; rd = 1; wdata = 32'hDEADBEEF;
@(posedge clk); #1;
we = 0; rs1 = 1; #1;
$display("Test 2: x1 = 0x%h (expected: 0xDEADBEEF)", rdata1);
// Test 3: x0への書き込みは無視される
we = 1; rd = 0; wdata = 32'hFFFFFFFF;
@(posedge clk); #1;
we = 0; rs1 = 0; #1;
$display("Test 3: x0 after write = %d (expected: 0)", rdata1);
// Test 4: 複数レジスタ同時読み出し
we = 1; rd = 2; wdata = 32'h12345678;
@(posedge clk); #1;
we = 0; rs1 = 1; rs2 = 2; #1;
$display("Test 4: x1=0x%h, x2=0x%h", rdata1, rdata2);
if (rdata1 == 32'hDEADBEEF && rdata2 == 32'h12345678)
$display("\n*** ALL TESTS PASSED! ***");
else
$display("\n*** SOME TESTS FAILED ***");
#20; $finish;
end
endmodule
実行結果
$ cd ~/riscv-cpu
$ iverilog -o regfile_test src/regfile.v test/regfile_tb.v
$ ./regfile_test
=== Regfile Test ===
Test 1: x0 read = 0 (expected: 0)
Test 2: x1 = 0xdeadbeef (expected: 0xDEADBEEF)
Test 3: x0 after write = 0 (expected: 0)
Test 4: x1=0xdeadbeef, x2=0x12345678
*** ALL TESTS PASSED! ***
いいですね!x0が常に0なのも確認できました。
ALUを作る
ALU(Arithmetic Logic Unit)は、CPUの計算を担う中心部品です。
サポートする演算
RISC-Vの基本演算命令に対応した10種類の演算を実装します:
| 演算コード | 演算 | 説明 |
|---|---|---|
| 0000 | ADD | 加算 |
| 0001 | SUB | 減算 |
| 0010 | AND | 論理AND |
| 0011 | OR | 論理OR |
| 0100 | XOR | 排他的OR |
| 0101 | SLL | 左論理シフト |
| 0110 | SRL | 右論理シフト |
| 0111 | SRA | 右算術シフト |
| 1000 | SLT | 符号付き比較 |
| 1001 | SLTU | 符号なし比較 |
Verilogで実装
// RISC-V ALU (Arithmetic Logic Unit)
module alu (
input wire [31:0] a, // オペランド1
input wire [31:0] b, // オペランド2
input wire [3:0] alu_op, // ALU操作
output reg [31:0] result, // 結果
output wire zero // ゼロフラグ
);
// ALU操作コード
localparam ALU_ADD = 4'b0000; // 加算
localparam ALU_SUB = 4'b0001; // 減算
localparam ALU_AND = 4'b0010; // AND
localparam ALU_OR = 4'b0011; // OR
localparam ALU_XOR = 4'b0100; // XOR
localparam ALU_SLL = 4'b0101; // 左論理シフト
localparam ALU_SRL = 4'b0110; // 右論理シフト
localparam ALU_SRA = 4'b0111; // 右算術シフト
localparam ALU_SLT = 4'b1000; // Set Less Than (符号付き)
localparam ALU_SLTU = 4'b1001; // Set Less Than Unsigned
// ゼロフラグ
assign zero = (result == 32'h0);
// ALU演算
always @(*) begin
case (alu_op)
ALU_ADD: result = a + b;
ALU_SUB: result = a - b;
ALU_AND: result = a & b;
ALU_OR: result = a | b;
ALU_XOR: result = a ^ b;
ALU_SLL: result = a << b[4:0];
ALU_SRL: result = a >> b[4:0];
ALU_SRA: result = $signed(a) >>> b[4:0];
ALU_SLT: result = ($signed(a) < $signed(b)) ? 32'h1 : 32'h0;
ALU_SLTU: result = (a < b) ? 32'h1 : 32'h0;
default: result = 32'h0;
endcase
end
endmodule
ポイント:
-
シフト量は下位5ビットのみ使用(
b[4:0])- 32ビットなので0〜31 -
算術右シフトは
$signed()と>>>を使う(符号ビットで埋める) -
SLT/SLTUの違いは符号付き比較か否か(
-1 < 1は真、0xFFFFFFFF < 1は偽)
テストベンチ
`timescale 1ns/1ps
module alu_tb;
reg [31:0] a, b;
reg [3:0] alu_op;
wire [31:0] result;
wire zero;
alu dut (.a(a), .b(b), .alu_op(alu_op), .result(result), .zero(zero));
integer pass_count = 0;
integer test_count = 0;
task check;
input [31:0] expected;
input [255:0] name;
begin
test_count = test_count + 1;
if (result == expected) begin
$display("%s PASS", name);
pass_count = pass_count + 1;
end else begin
$display("%s FAIL: got %h, expected %h", name, result, expected);
end
end
endtask
initial begin
$display("=== ALU Test ===\n");
// ADD
a = 10; b = 20; alu_op = 4'b0000; #10;
check(30, "Test 1: ADD 10+20=30");
// SUB
a = 30; b = 10; alu_op = 4'b0001; #10;
check(20, "Test 2: SUB 30-10=20");
// AND
a = 32'hFF00FF00; b = 32'h0F0F0F0F; alu_op = 4'b0010; #10;
check(32'h0F000F00, "Test 3: AND");
// OR
a = 32'hFF00FF00; b = 32'h0F0F0F0F; alu_op = 4'b0011; #10;
check(32'hFF0FFF0F, "Test 4: OR");
// XOR
a = 32'hAAAAAAAA; b = 32'h55555555; alu_op = 4'b0100; #10;
check(32'hFFFFFFFF, "Test 5: XOR");
// SLL
a = 1; b = 4; alu_op = 4'b0101; #10;
check(16, "Test 6: SLL 1<<4=16");
// SRL
a = 32'h80000000; b = 4; alu_op = 4'b0110; #10;
check(32'h08000000, "Test 7: SRL (logical)");
// SRA
a = 32'h80000000; b = 4; alu_op = 4'b0111; #10;
check(32'hF8000000, "Test 8: SRA (arithmetic)");
// SLT signed: -1 < 1
a = 32'hFFFFFFFF; b = 1; alu_op = 4'b1000; #10;
check(1, "Test 9: SLT -1<1 = 1");
// SLT signed: 1 < -1
a = 1; b = 32'hFFFFFFFF; alu_op = 4'b1000; #10;
check(0, "Test 10: SLT 1<-1 = 0");
// SLTU: 0xFFFFFFFF < 1 (unsigned)
a = 32'hFFFFFFFF; b = 1; alu_op = 4'b1001; #10;
check(0, "Test 11: SLTU large<1 = 0");
// SLTU: 1 < 0xFFFFFFFF (unsigned)
a = 1; b = 32'hFFFFFFFF; alu_op = 4'b1001; #10;
check(1, "Test 12: SLTU 1<large = 1");
// Zero flag test
a = 5; b = 5; alu_op = 4'b0001; #10;
$display("Test 13: Zero flag = %d (expected: 1)", zero);
if (zero == 1) pass_count = pass_count + 1;
test_count = test_count + 1;
a = 5; b = 3; alu_op = 4'b0001; #10;
$display("Test 14: Zero flag = %d (expected: 0)", zero);
if (zero == 0) pass_count = pass_count + 1;
test_count = test_count + 1;
$display("\n=== %0d/%0d Tests Passed ===", pass_count, test_count);
$finish;
end
endmodule
実行結果
$ iverilog -o alu_test src/alu.v test/alu_tb.v
$ ./alu_test
=== ALU Test ===
Test 1: ADD 10+20=30 PASS
Test 2: SUB 30-10=20 PASS
Test 3: AND PASS
Test 4: OR PASS
Test 5: XOR PASS
Test 6: SLL 1<<4=16 PASS
Test 7: SRL (logical) PASS
Test 8: SRA (arithmetic) PASS
Test 9: SLT -1<1 = 1 PASS
Test 10: SLT 1<-1 = 0 PASS
Test 11: SLTU large<1 = 0 PASS
Test 12: SLTU 1<large = 1 PASS
Test 13: Zero flag = 1 (expected: 1)
Test 14: Zero flag = 0 (expected: 0)
=== 14/14 Tests Passed ===
全部通った!特にSRA(算術右シフト)とSLT/SLTUの符号処理がちゃんと動いてるのがわかります。
まとめ
Part1では、CPUの基本となる2つの部品を作りました:
- レジスタファイル - 32本の32ビットレジスタ、x0は常に0
- ALU - 10種類の演算をサポート
次回Part2では、命令デコーダを実装して、実際のRISC-V機械語命令を解析できるようにします。
Part1 [完了] レジスタファイル + ALU
Part2 [次回] 命令デコーダ
Part3 [予定] メモリアクセス
Part4 [予定] 分岐・ジャンプ
Part5 [予定] 統合テスト