跳轉到

2.2 單週期 RISC-V 處理器實作#

學習目標#

  1. 以可合成的 Verilog-2001 實作單週期 RV32I 處理器(PC、暫存器堆、ALU、ImmGen、控制單元)。
  2. 理解各模組之間的介面設計與層次化(top-down)連線方式。
  3. 學會處理分支、跳躍指令的 PC 更新邏輯。
  4. 建立「控制訊號表 → case 敘述」的系統化編碼方法。

2.2.1 模組層次#

本節的專案結構如下,每個小模組獨立檔案、獨立可測試,最後由 top module 連接:

cpu_top.v        頂層:連接所有模組
├── pc.v         程式計數器
├── imem.v       指令記憶體
├── dmem.v       資料記憶體
├── regfile.v    暫存器堆
├── alu.v        ALU
├── immgen.v     立即值產生器
├── control.v    主控制單元
├── aludec.v     ALU 解碼器
└── branch_unit.v 分支/比較單元

設計原則:記憶體用同步讀寫或組合讀取皆可,單週期設計中為了讓「PC 更新後同一週期讀到指令」,I-Mem 採組合讀取(非同步),暫存器堆讀取也採組合讀取,寫入在時脈正緣發生。

2.2.2 程式計數器(PC)#

PC 是整條資料路徑唯一的狀態暫存器(單週期不考慮 CSR 與例外)。每個時脈正緣載入下一個 PC:

// pc.v
module pc (
    input  wire        clk,
    input  wire        rst,
    input  wire [31:0] pc_next,
    output reg  [31:0] pc
);
    always @(posedge clk) begin
        if (rst)
            pc <= 32'h0000_0000;
        else
            pc <= pc_next;
    end
endmodule

注意我們讓 I-Mem 以字組位址定址(mem[pc[31:2]]),PC 本身仍保有位元組位址的語意,分支目標加法直接用 pc + imm 即可。

2.2.3 暫存器堆(RegFile)#

32 個 32 位元通用暫存器,兩讀一寫,x0 永遠為 0:

// regfile.v
module regfile (
    input  wire        clk,
    input  wire        we,        // RegWrite
    input  wire [4:0]  rs1, rs2,
    input  wire [4:0]  rd,
    input  wire [31:0] wd,        // write data
    output wire [31:0] rd1, rd2
);
    reg [31:0] regs [0:31];

    // 組合讀取
    assign rd1 = (rs1 == 5'd0) ? 32'b0 : regs[rs1];
    assign rd2 = (rs2 == 5'd0) ? 32'b0 : regs[rs2];

    always @(posedge clk) begin
        if (we && rd != 5'd0)
            regs[rd] <= wd;
    end
endmodule

兩個細節值得注意:

  1. x0 的處理:讀取端直接回 0,寫入端用 rd != 0 擋掉,雙重保險。若只擋寫入端,add x0, x1, x2 後讀 x0 雖然仍是 0,但組合讀取已讀到舊值也無妨——不過明確雙向處理可避免模擬時 x 傳播。
  2. 同週期讀寫衝突:本設計中,寫入在正緣生效,同週期讀到的是舊值。單週期 CPU 中每條指令只寫一個暫存器且不會在同一週期又讀同一個暫存器當運算元(rd 不等於 rs1/rs2 的語意由編譯器保證),因此無須前遞(forwarding)。

2.2.4 ALU#

// alu.v
module alu (
    input  wire [31:0] a, b,
    input  wire [3:0]  alu_ctl,
    output reg  [31:0] y,
    output wire        zero,   // a == b
    output wire        less    // a <  b (signed)
);
    localparam ADD = 4'd0, SUB = 4'd1, AND = 4'd2, OR  = 4'd3,
               XOR = 4'd4, SLL = 4'd5, SRL = 4'd6, SRA = 4'd7,
               SLT = 4'd8, SLTU= 4'd9;

    wire [31:0] b_inv = alu_ctl == SUB ? ~b : b;
    wire [32:0] sum   = a + b_inv + (alu_ctl == SUB);

    always @(*) begin
        case (alu_ctl)
            ADD:    y = sum[31:0];
            SUB:    y = sum[31:0];
            AND:    y = a & b;
            OR:     y = a | b;
            XOR:    y = a ^ b;
            SLL:    y = a << b[4:0];
            SRL:    y = a >> b[4:0];
            SRA:    y = $signed(a) >>> b[4:0];
            SLT:    y = {31'b0, $signed(a) < $signed(b)};
            SLTU:   y = {31'b0, a < b};
            default: y = 32'b0;
        endcase
    end

    assign zero  = (a == b);
    assign less  = $signed(a) < $signed(b);
endmodule

zero 與 less 獨立輸出,供分支單元使用:beq 看 zero、bne 看 ~zero、blt/bge 看 less、bltu/bgeu 則需要無號比較結果(可另加 lessu,練習題會要求你補上)。

2.2.5 立即值產生器(ImmGen)#

依照 2.1.5 節的位元拼接規則:

// immgen.v
module immgen (
    input  wire [31:0] instr,
    output reg  [31:0] imm
);
    wire [6:0] opcode = instr[6:0];

    always @(*) begin
        case (opcode)
            7'b0010011,                        // I 型運算
            7'b0000011,                        // lw
            7'b1100111:                        // jalr
                imm = {{20{instr[31]}}, instr[31:20]};
            7'b0100011:                        // S 型
                imm = {{20{instr[31]}}, instr[31:25], instr[11:7]};
            7'b1100011:                        // B 型
                imm = {{19{instr[31]}}, instr[31], instr[7],
                       instr[30:25], instr[11:8], 1'b0};
            7'b0110111,                        // lui
            7'b0010111:                        // auipc
                imm = {instr[31:12], 12'b0};
            7'b1101111:                        // J 型 jal
                imm = {{11{instr[31]}}, instr[31], instr[19:12],
                       instr[20], instr[30:21], 1'b0};
            default:
                imm = 32'b0;
        endcase
    end
endmodule

2.2.6 控制單元(Control)#

把 2.1.4 節的控制表直接翻成 case 敘述。MemToReg 採 2 位元編碼(0=ALU、1=Mem、2=PC+4、3=PC+Imm):

// control.v
module control (
    input  wire [6:0] opcode,
    output reg        alusrc,      // 0: rs2, 1: imm
    output reg        memread,
    output reg        memwrite,
    output reg        regwrite,
    output reg [1:0]  memtoreg,
    output reg        branch,
    output reg        jump,
    output reg [1:0]  aluop        // 10:R型 00:加 01:減 11:直通B
);
    always @(*) begin
        {alusrc, memread, memwrite, regwrite, memtoreg,
         branch, jump, aluop} = 10'b0;
        case (opcode)
            7'b0110011: begin // R 型
                {alusrc, memread, memwrite, regwrite, memtoreg,
                 branch, jump, aluop} = {1'b0, 1'b0, 1'b0, 1'b1,
                                          2'd0, 1'b0, 1'b0, 2'b10};
            end
            7'b0000011: begin // lw
                {alusrc, memread, memwrite, regwrite, memtoreg,
                 branch, jump, aluop} = {1'b1, 1'b1, 1'b0, 1'b1,
                                          2'd1, 1'b0, 1'b0, 2'b00};
            end
            7'b0100011: begin // sw
                {alusrc, memread, memwrite, regwrite, memtoreg,
                 branch, jump, aluop} = {1'b1, 1'b0, 1'b1, 1'b0,
                                          2'd0, 1'b0, 1'b0, 2'b00};
            end
            7'b1100011: begin // 分支
                {alusrc, memread, memwrite, regwrite, memtoreg,
                 branch, jump, aluop} = {1'b0, 1'b0, 1'b0, 1'b0,
                                          2'd0, 1'b1, 1'b0, 2'b01};
            end
            7'b0110111: begin // lui
                {alusrc, memread, memwrite, regwrite, memtoreg,
                 branch, jump, aluop} = {1'b1, 1'b0, 1'b0, 1'b1,
                                          2'd3, 1'b0, 1'b0, 2'b11};
            end
            7'b0010111: begin // auipc
                {alusrc, memread, memwrite, regwrite, memtoreg,
                 branch, jump, aluop} = {1'b1, 1'b0, 1'b0, 1'b1,
                                          2'd3, 1'b0, 1'b0, 2'b11};
            end
            7'b1101111: begin // jal
                {alusrc, memread, memwrite, regwrite, memtoreg,
                 branch, jump, aluop} = {1'b0, 1'b0, 1'b0, 1'b1,
                                          2'd2, 1'b0, 1'b1, 2'b11};
            end
            7'b1100111: begin // jalr
                {alusrc, memread, memwrite, regwrite, memtoreg,
                 branch, jump, aluop} = {1'b1, 1'b0, 1'b0, 1'b1,
                                          2'd2, 1'b0, 1'b1, 2'b00};
            end
            default: ;
        endcase
    end
endmodule

ALU 最終控制由 ALUDec 依 funct3/funct7 細分;lui/auipc/jal 的 ALU 結果不會被寫回(MemToReg 已指向其他來源),故 ALU 做什麼都無所謂。

2.2.7 頂層連線(cpu_top)#

// cpu_top.v
module cpu_top (
    input  wire clk,
    input  wire rst
);
    wire [31:0] pc, pc_next, pc_plus4, pc_target;
    wire [31:0] instr, imm, rd1, rd2, wd3, alu_y, dmem_rdata;

    // 控制訊號
    wire alusrc, memread, memwrite, regwrite, branch, jump;
    wire [1:0] memtoreg, aluop;
    wire [3:0] alu_ctl;
    wire zero, less;
    wire pcsrc;
    wire [31:0] alu_b;

    // 取指
    pc u_pc (.clk(clk), .rst(rst), .pc_next(pc_next), .pc(pc));
    imem u_imem (.addr(pc), .instr(instr));

    assign pc_plus4  = pc + 32'd4;
    assign pc_target = pc + imm;

    // 解碼
    control u_ctrl (.opcode(instr[6:0]), .alusrc(alusrc),
        .memread(memread), .memwrite(memwrite), .regwrite(regwrite),
        .memtoreg(memtoreg), .branch(branch), .jump(jump),
        .aluop(aluop));
    immgen u_imm (.instr(instr), .imm(imm));

    // 暫存器堆
    regfile u_rf (.clk(clk), .we(regwrite),
        .rs1(instr[19:15]), .rs2(instr[24:20]), .rd(instr[11:7]),
        .wd(wd3), .rd1(rd1), .rd2(rd2));

    // ALU
    assign alu_b = alusrc ? imm : rd2;
    alu u_alu (.a(rd1), .b(alu_b), .alu_ctl(alu_ctl),
               .y(alu_y), .zero(zero), .less(less));
    aludec u_aludec (.opcode(instr[6:0]), .funct3(instr[14:12]),
        .funct7(instr[31:25]), .aluop(aluop), .alu_ctl(alu_ctl));

    // 資料記憶體
    dmem u_dmem (.clk(clk), .addr(alu_y), .wd(rd2),
        .we(memwrite), .re(memread), .rd(dmem_rdata));

    // 寫回 Mux
    always @(*) begin
        case (memtoreg)
            2'd0:    wd3 = alu_y;
            2'd1:    wd3 = dmem_rdata;
            2'd2:    wd3 = pc_plus4;   // jal / jalr
            default: wd3 = pc_target;  // auipc(lui 時 aluop=11 直通 imm)
        endcase
    end

    // 下一個 PC
    branch_unit u_bu (.funct3(instr[14:12]), .zero(zero), .less(less),
        .branch(branch), .jump(jump), .pcsrc(pcsrc));
    assign pc_next = (pcsrc | jump) ? pc_target : pc_plus4;
endmodule

分支單元依 funct3 決定條件成立與否:

// branch_unit.v
module branch_unit (
    input  wire [2:0] funct3,
    input  wire       zero, less,
    input  wire       branch, jump,
    output wire       pcsrc
);
    wire taken;
    assign taken =
        (funct3 == 3'b000) ?  zero       : // beq
        (funct3 == 3'b001) ? ~zero       : // bne
        (funct3 == 3'b100) ?  less       : // blt
        (funct3 == 3'b101) ? ~less       : // bge
        1'b0;                               // 其餘(bltu/bgeu 見習題)
    assign pcsrc = branch & taken;
endmodule

lui 的實作技巧:aluop=11 時 ALU 直通第二運算元(imm),而 alusrc=1、MemToReg=3 選擇 pc_target——對 lui 而言 ALU 直通輸出的就是 imm,與寫回路徑獨立,因此正確。auipc 則讓 ALU 做加法(可由 ALUDec 在 aluop=11 且 opcode=0010111 時選 ADD),寫回選 pc + imm。


本章小結#

  • 單週期 RV32I CPU 由 PC、兩個記憶體、暫存器堆、ALU、ImmGen、控制單元與少數 Mux 組成,模組化後每個檔案都簡短易測。
  • 暫存器堆採組合讀、同步寫;x0 在讀寫兩端都要擋。
  • 控制訊號表可直接翻譯為控制單元的 case 敘述;MemToReg 需 2 位元以覆蓋 jal/jalr/auipc 的特殊寫回值。
  • 分支條件由 funct3 決定,pcsrc = branch & taken,與 jump 一起驅動 PC Mux。
  • 所有記憶體讀取為組合邏輯,時脈週期時間由「取指到寫回」的總延遲決定。

實作題#

  1. 補齊 bltu、bgeu:在 ALU 增加 lessu 輸出,並修改 branch_unit.v,寫 testbench 驗證 bltu x1, x2, label 在 x1=0xFFFFFFFE, x2=0x00000001 時會跳躍。
  2. 目前的 I-Mem/D-Mem 是黑箱。請實作 imem.v(512×32,用 $readmemh 初始化)與 dmem.v(位元組可寫:支援 sb/sh/sw),並以 sw 後接 lw 驗證資料往返。
  3. 加入簡單的程式終止機制:約定「PC 到達 0x100 時 testbench 停止」,修改 cpu_top.v 或 testbench,說明這樣做的好處。
  4. 計算最長路徑:假設 I-Mem 讀取 2ns、RegFile 1.5ns、ALU 3ns、D-Mem 3ns、各 Mux 0.5ns、路徑間暫存器 0.2ns,估計此設計的最高時脈頻率,並指出瓶頸指令。
  5. 將 2.2.6 的控制單元改為 ROM 查表實作(以 opcode 為位址),比較兩種寫法的合成面積與可讀性。