【Verilog自作CPU】シリーズ
| Part | タイトル | 内容 |
|---|---|---|
| Part1 | レジスタファイルとALU | 基本部品 |
| Part2 | 命令デコーダ | 本記事 |
| Part3 | メモリとロード・ストア | LW/SW命令 |
| Part4 | 分岐とジャンプ | BEQ/JAL命令 |
| Part5 | CPU統合と動作テスト | 完成 |
はじめに
前回はレジスタファイルとALUを作りました。今回は命令デコーダを実装します。
命令デコーダは、32ビットの機械語命令を受け取って、「どのレジスタを使うか」「どの演算をするか」「即値はいくつか」などの制御信号を生成する重要な部品です。
RISC-V命令フォーマット
RISC-Vの命令は全て32ビット固定長で、6つのタイプがあります。
命令タイプ一覧
R-type: レジスタ間演算(ADD, SUB, AND, OR...)
┌───────┬─────┬─────┬─────┬────┬─────────┐
│funct7 │ rs2 │ rs1 │func3│ rd │ opcode │
│ 31-25 │24-20│19-15│14-12│11-7│ 6-0 │
└───────┴─────┴─────┴─────┴────┴─────────┘
I-type: 即値演算・ロード(ADDI, LW, JALR...)
┌────────────┬─────┬─────┬────┬─────────┐
│ imm[11:0] │ rs1 │func3│ rd │ opcode │
│ 31-20 │19-15│14-12│11-7│ 6-0 │
└────────────┴─────┴─────┴────┴─────────┘
S-type: ストア(SW, SH, SB)
┌────────┬─────┬─────┬─────┬────────┬─────────┐
│imm[11:5]│ rs2 │ rs1 │func3│imm[4:0]│ opcode │
│ 31-25 │24-20│19-15│14-12│ 11-7 │ 6-0 │
└────────┴─────┴─────┴─────┴────────┴─────────┘
B-type: 分岐(BEQ, BNE, BLT...)
┌───┬────────┬─────┬─────┬─────┬────────┬───┬─────────┐
│[12]│[10:5] │ rs2 │ rs1 │func3│ [4:1] │[11]│ opcode │
│ 31 │ 30-25 │24-20│19-15│14-12│ 11-8 │ 7 │ 6-0 │
└───┴────────┴─────┴─────┴─────┴────────┴───┴─────────┘
U-type: 上位即値(LUI, AUIPC)
┌──────────────────────┬────┬─────────┐
│ imm[31:12] │ rd │ opcode │
│ 31-12 │11-7│ 6-0 │
└──────────────────────┴────┴─────────┘
J-type: ジャンプ(JAL)
┌───┬──────────┬───┬────────┬────┬─────────┐
│[20]│ [10:1] │[11]│[19:12] │ rd │ opcode │
│ 31 │ 30-21 │ 20 │ 19-12 │11-7│ 6-0 │
└───┴──────────┴───┴────────┴────┴─────────┘
オペコード一覧
| opcode | 命令タイプ | 例 |
|---|---|---|
| 0110011 | R-type | ADD, SUB, AND, OR |
| 0010011 | I-type | ADDI, ANDI, ORI |
| 0000011 | Load | LW, LH, LB |
| 0100011 | S-type | SW, SH, SB |
| 1100011 | B-type | BEQ, BNE, BLT |
| 0110111 | U-type | LUI |
| 0010111 | U-type | AUIPC |
| 1101111 | J-type | JAL |
| 1100111 | I-type | JALR |
デコーダの実装
出力する制御信号
デコーダは以下の情報を出力します:
| 信号 | 説明 |
|---|---|
| rs1, rs2, rd | レジスタアドレス |
| imm | 即値(符号拡張済み) |
| reg_write | レジスタ書き込み有効 |
| mem_read | メモリ読み出し |
| mem_write | メモリ書き込み |
| branch | 分岐命令 |
| jump | ジャンプ命令 |
| alu_src | ALU第2オペランド選択(0:rs2, 1:imm) |
| alu_op | ALU演算コード |
| mem_to_reg | ライトバック選択(0:ALU結果, 1:メモリ) |
Verilog実装
src/decoder.v
// RISC-V 命令デコーダ
module decoder (
input wire [31:0] instr,
// レジスタアドレス
output wire [4:0] rs1,
output wire [4:0] rs2,
output wire [4:0] rd,
// 即値
output reg [31:0] imm,
// 制御信号
output reg reg_write,
output reg mem_read,
output reg mem_write,
output reg branch,
output reg jump,
output reg alu_src,
output reg [3:0] alu_op,
output reg mem_to_reg
);
// 命令フィールドの抽出
wire [6:0] opcode = instr[6:0];
wire [2:0] funct3 = instr[14:12];
wire [6:0] funct7 = instr[31:25];
assign rs1 = instr[19:15];
assign rs2 = instr[24:20];
assign rd = instr[11:7];
// オペコード定義
localparam OP_R = 7'b0110011; // R-type
localparam OP_I_ALU = 7'b0010011; // I-type ALU
localparam OP_LOAD = 7'b0000011; // Load
localparam OP_STORE = 7'b0100011; // Store
localparam OP_BRANCH = 7'b1100011; // Branch
localparam OP_JAL = 7'b1101111; // JAL
localparam OP_JALR = 7'b1100111; // JALR
localparam OP_LUI = 7'b0110111; // LUI
localparam OP_AUIPC = 7'b0010111; // AUIPC
// ALU操作コード
localparam ALU_ADD = 4'b0000;
localparam ALU_SUB = 4'b0001;
localparam ALU_AND = 4'b0010;
localparam ALU_OR = 4'b0011;
localparam ALU_XOR = 4'b0100;
localparam ALU_SLL = 4'b0101;
localparam ALU_SRL = 4'b0110;
localparam ALU_SRA = 4'b0111;
localparam ALU_SLT = 4'b1000;
localparam ALU_SLTU = 4'b1001;
// 即値生成
always @(*) begin
case (opcode)
OP_I_ALU, OP_LOAD, OP_JALR:
// I-type: 符号拡張
imm = {{20{instr[31]}}, instr[31:20]};
OP_STORE:
// S-type: 符号拡張
imm = {{20{instr[31]}}, instr[31:25], instr[11:7]};
OP_BRANCH:
// B-type: 符号拡張、LSBは0
imm = {{19{instr[31]}}, instr[31], instr[7],
instr[30:25], instr[11:8], 1'b0};
OP_LUI, OP_AUIPC:
// U-type: 上位20ビット
imm = {instr[31:12], 12'b0};
OP_JAL:
// J-type: 符号拡張、LSBは0
imm = {{11{instr[31]}}, instr[31], instr[19:12],
instr[20], instr[30:21], 1'b0};
default:
imm = 32'h0;
endcase
end
// ALU操作コード生成
always @(*) begin
case (opcode)
OP_R: begin
case (funct3)
3'b000: alu_op = (funct7[5]) ? ALU_SUB : ALU_ADD;
3'b001: alu_op = ALU_SLL;
3'b010: alu_op = ALU_SLT;
3'b011: alu_op = ALU_SLTU;
3'b100: alu_op = ALU_XOR;
3'b101: alu_op = (funct7[5]) ? ALU_SRA : ALU_SRL;
3'b110: alu_op = ALU_OR;
3'b111: alu_op = ALU_AND;
default: alu_op = ALU_ADD;
endcase
end
OP_I_ALU: begin
case (funct3)
3'b000: alu_op = ALU_ADD; // ADDI
3'b001: alu_op = ALU_SLL; // SLLI
3'b010: alu_op = ALU_SLT; // SLTI
3'b011: alu_op = ALU_SLTU; // SLTIU
3'b100: alu_op = ALU_XOR; // XORI
3'b101: alu_op = (funct7[5]) ? ALU_SRA : ALU_SRL;
3'b110: alu_op = ALU_OR; // ORI
3'b111: alu_op = ALU_AND; // ANDI
default: alu_op = ALU_ADD;
endcase
end
OP_BRANCH: begin
// 分岐は減算で比較
alu_op = ALU_SUB;
end
default: begin
// ロード/ストア/ジャンプはアドレス計算(ADD)
alu_op = ALU_ADD;
end
endcase
end
// 制御信号生成
always @(*) begin
// デフォルト値
reg_write = 1'b0;
mem_read = 1'b0;
mem_write = 1'b0;
branch = 1'b0;
jump = 1'b0;
alu_src = 1'b0;
mem_to_reg = 1'b0;
case (opcode)
OP_R: begin
reg_write = 1'b1;
alu_src = 1'b0; // rs2
end
OP_I_ALU: begin
reg_write = 1'b1;
alu_src = 1'b1; // 即値
end
OP_LOAD: begin
reg_write = 1'b1;
mem_read = 1'b1;
alu_src = 1'b1;
mem_to_reg = 1'b1;
end
OP_STORE: begin
mem_write = 1'b1;
alu_src = 1'b1;
end
OP_BRANCH: begin
branch = 1'b1;
alu_src = 1'b0;
end
OP_JAL: begin
reg_write = 1'b1;
jump = 1'b1;
end
OP_JALR: begin
reg_write = 1'b1;
jump = 1'b1;
alu_src = 1'b1;
end
OP_LUI: begin
reg_write = 1'b1;
alu_src = 1'b1;
end
OP_AUIPC: begin
reg_write = 1'b1;
alu_src = 1'b1;
end
endcase
end
endmodule
テストベンチ
test/decoder_tb.v
`timescale 1ns/1ps
module decoder_tb;
reg [31:0] instr;
wire [4:0] rs1, rs2, rd;
wire [31:0] imm;
wire reg_write, mem_read, mem_write;
wire branch, jump, alu_src, mem_to_reg;
wire [3:0] alu_op;
decoder dut (
.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)
);
initial begin
$display("=== Decoder Test ===\n");
// Test 1: ADDI x1, x0, 5
// 000000000101 00000 000 00001 0010011
instr = 32'h00500093;
#10;
$display("Test 1: ADDI x1, x0, 5");
$display(" rd=%d, rs1=%d, imm=%d", rd, rs1, imm);
$display(" reg_write=%d, alu_src=%d, alu_op=%b",
reg_write, alu_src, alu_op);
// Test 2: ADD x3, x1, x2
// 0000000 00010 00001 000 00011 0110011
instr = 32'h002080b3;
#10;
$display("\nTest 2: ADD x3, x1, x2");
$display(" rd=%d, rs1=%d, rs2=%d", rd, rs1, rs2);
$display(" reg_write=%d, alu_src=%d, alu_op=%b",
reg_write, alu_src, alu_op);
// Test 3: SUB x4, x2, x1
// 0100000 00001 00010 000 00100 0110011
instr = 32'h40110233;
#10;
$display("\nTest 3: SUB x4, x2, x1");
$display(" rd=%d, rs1=%d, rs2=%d", rd, rs1, rs2);
$display(" alu_op=%b (expected: 0001 for SUB)", alu_op);
// Test 4: LW x5, 4(x1)
// 000000000100 00001 010 00101 0000011
instr = 32'h0040a283;
#10;
$display("\nTest 4: LW x5, 4(x1)");
$display(" rd=%d, rs1=%d, imm=%d", rd, rs1, imm);
$display(" mem_read=%d, mem_to_reg=%d", mem_read, mem_to_reg);
// Test 5: SW x2, 8(x1)
// 0000000 00010 00001 010 01000 0100011
instr = 32'h0020a423;
#10;
$display("\nTest 5: SW x2, 8(x1)");
$display(" rs1=%d, rs2=%d, imm=%d", rs1, rs2, imm);
$display(" mem_write=%d", mem_write);
// Test 6: BEQ x1, x2, 16
// 0000000 00010 00001 000 1000 0 1100011
instr = 32'h00208863;
#10;
$display("\nTest 6: BEQ x1, x2, offset=16");
$display(" rs1=%d, rs2=%d, imm=%d", rs1, rs2, imm);
$display(" branch=%d", branch);
$display("\n*** ALL TESTS COMPLETED ***");
$finish;
end
endmodule
実行結果
$ iverilog -o decoder_test src/decoder.v test/decoder_tb.v
$ ./decoder_test
=== Decoder Test ===
Test 1: ADDI x1, x0, 5
rd= 1, rs1= 0, imm= 5
reg_write=1, alu_src=1, alu_op=0000
Test 2: ADD x3, x1, x2
rd= 3, rs1= 1, rs2= 2
reg_write=1, alu_src=0, alu_op=0000
Test 3: SUB x4, x2, x1
rd= 4, rs1= 2, rs2= 1
alu_op=0001 (expected: 0001 for SUB)
Test 4: LW x5, 4(x1)
rd= 5, rs1= 1, imm= 4
mem_read=1, mem_to_reg=1
Test 5: SW x2, 8(x1)
rs1= 1, rs2= 2, imm= 8
mem_write=1
Test 6: BEQ x1, x2, offset=16
rs1= 1, rs2= 2, imm= 16
branch=1
*** ALL TESTS COMPLETED ***
すべての命令が正しくデコードされています!
機械語の確認方法
「機械語ってどうやって作るの?」と思った方へ。
GCCを使う方法
# RISC-Vツールチェーンをインストール
sudo apt install gcc-riscv64-unknown-elf
# アセンブリから機械語を生成
echo "addi x1, x0, 5" | riscv64-unknown-elf-as -o test.o
riscv64-unknown-elf-objdump -d test.o
手動で計算する方法
例:ADDI x1, x0, 5 の場合
命令: I-type
opcode: 0010011 (I-type ALU)
rd: 00001 (x1)
funct3: 000 (ADDI)
rs1: 00000 (x0)
imm: 000000000101 (5)
結合: 000000000101_00000_000_00001_0010011
16進: 0x00500093
まとめ
Part2では、命令デコーダを実装しました:
- RISC-Vの6つの命令フォーマット(R, I, S, B, U, J)に対応
- 即値の符号拡張を正しく処理
- ALU操作コードと制御信号を生成
次回Part3では、メモリモジュールを実装し、ロード・ストア命令が使えるようにします。
Part1 [完了] レジスタファイル + ALU
Part2 [完了] 命令デコーダ
Part3 [次回] メモリアクセス
Part4 [予定] 分岐・ジャンプ
Part5 [予定] 統合テスト