17
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Verilog自作CPU】シリーズ

Part タイトル 内容
Part1 レジスタファイルとALU 本記事
Part2 命令デコーダ 命令解析と制御信号
Part3 メモリとロード・ストア LW/SW命令の実装
Part4 分岐とジャンプ BEQ/JAL命令の実装
Part5 CPU統合と動作テスト 完成・プログラム実行

はじめに

「CPUって何か難しそう...」

そう思っていませんか?実は、基本的なCPUはシンプルな部品の組み合わせで作れます。

このシリーズでは、RISC-Vという現代的でオープンな命令セットアーキテクチャ(ISA)を使って、ゼロから動くCPUを作ります。しかも、シミュレーションで実際に動作を確認しながら進めるので、「本当に動いてる!」という実感が得られます。

なぜRISC-V?

RISC-Vを選ぶ理由:

  1. オープン - 誰でも無料で使える
  2. シンプル - 基本命令セットが非常に整理されている
  3. 実用的 - SiFiveやEspressifなど、実際の製品で使われている
  4. 学習向け - 余計な歴史的経緯がない

開発環境

今回使う環境:

- 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で実装

src/regfile.v
// 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でガード)

テストベンチ

test/regfile_tb.v
`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で実装

src/alu.v
// 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は偽)

テストベンチ

test/alu_tb.v
`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つの部品を作りました:

  1. レジスタファイル - 32本の32ビットレジスタ、x0は常に0
  2. ALU - 10種類の演算をサポート

次回Part2では、命令デコーダを実装して、実際のRISC-V機械語命令を解析できるようにします。

Part1 [完了] レジスタファイル + ALU
Part2 [次回] 命令デコーダ
Part3 [予定] メモリアクセス
Part4 [予定] 分岐・ジャンプ
Part5 [予定] 統合テスト

参考資料


次回: 【Verilogで自作CPU】RISC-Vプロセッサを作る Part2 - 命令デコーダ

17
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?