2.2 單週期 RISC-V 處理器實作#
學習目標#
- 以可合成的 Verilog-2001 實作單週期 RV32I 處理器(PC、暫存器堆、ALU、ImmGen、控制單元)。
- 理解各模組之間的介面設計與層次化(top-down)連線方式。
- 學會處理分支、跳躍指令的 PC 更新邏輯。
- 建立「控制訊號表 → 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
兩個細節值得注意:
- x0 的處理:讀取端直接回 0,寫入端用
rd != 0擋掉,雙重保險。若只擋寫入端,add x0, x1, x2後讀 x0 雖然仍是 0,但組合讀取已讀到舊值也無妨——不過明確雙向處理可避免模擬時x傳播。 - 同週期讀寫衝突:本設計中,寫入在正緣生效,同週期讀到的是舊值。單週期 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。 - 所有記憶體讀取為組合邏輯,時脈週期時間由「取指到寫回」的總延遲決定。
實作題#
- 補齊
bltu、bgeu:在 ALU 增加lessu輸出,並修改branch_unit.v,寫 testbench 驗證bltu x1, x2, label在x1=0xFFFFFFFE, x2=0x00000001時會跳躍。 - 目前的 I-Mem/D-Mem 是黑箱。請實作
imem.v(512×32,用$readmemh初始化)與dmem.v(位元組可寫:支援sb/sh/sw),並以sw後接lw驗證資料往返。 - 加入簡單的程式終止機制:約定「PC 到達 0x100 時 testbench 停止」,修改
cpu_top.v或 testbench,說明這樣做的好處。 - 計算最長路徑:假設 I-Mem 讀取 2ns、RegFile 1.5ns、ALU 3ns、D-Mem 3ns、各 Mux 0.5ns、路徑間暫存器 0.2ns,估計此設計的最高時脈頻率,並指出瓶頸指令。
- 將 2.2.6 的控制單元改為 ROM 查表實作(以 opcode 為位址),比較兩種寫法的合成面積與可讀性。