Featured image of post RV32I单周期CPU

RV32I单周期CPU

基于 Verilog 实现的单周期 RV32I 处理器:数据通路、模块划分与源码解析。

本文介绍单周期RV32I CPU的实现方式

在RV32I指令详解中展示了完整的指令集。本文基于一套可运行的 Verilog 代码,实现一个单周期(Single-Cycle)RV32I 处理器:每一条指令在一个完整时钟周期内完成「取指 → 译码 → 执行 → 访存 → 写回」全过程,因此不存在流水线冒险,也没有数据/控制冒险的问题。

下图为单周期数据通路结构,本项目的模块划分与此一一对应。所有源码均位于此处:

单周期数据通路(可点击查看)

文件 模块 作用
rtl/core_single_period.v core_single_period 顶层模块,组合所有数据通路
rtl/pc.v pc 程序计数器(PC)
rtl/imem.v imem 指令存储器(ROM)
rtl/control.v control 控制单元(译码)
rtl/regfile.v regfile 寄存器堆 x0~x31
rtl/immgen.v immgen 立即数生成器
rtl/alu.v alu 算术逻辑单元
rtl/dmem.v dmem 数据存储器(RAM)
rtl/program.hex — 覆盖全部 37 条 RV32I 指令的测试程序

数据通路整体结构

顶层模块 core_single_period 把各子模块连接起来,形成如下数据流:

1
2
3
4
5
6
7
PC ──▶ IMEM ──▶ (指令字段提取) ──▶ Control / RegFile / ImmGen
                                        │
                                        ▼
                                     ALU ──▶ DMEM
                                        │        │
                                        ▼        ▼
                                    wb_data 选择  ──▶ RegFile (写回)

首先从指令中提取各字段,供后续模块使用:

1
2
3
4
5
6
wire [6:0] opcode = instr[6:0];
wire [2:0] funct3 = instr[14:12];
wire [6:0] funct7 = instr[31:25];
wire [4:0] rs1    = instr[19:15];
wire [4:0] rs2    = instr[24:20];
wire [4:0] rd     = instr[11:7];

其中 opcode、funct3、funct7 送给控制单元译码,rs1/rs2 作为寄存器堆读地址,rd 作为写回地址。

模块功能介绍

PC —— 程序计数器

pc.v:32 位寄存器,同步复位(低电平有效)。下一拍将 pc_next 写入 pc,pc 作为指令存储器的地址。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module pc (
    input  wire        clk,
    input  wire        rst_n,       // 低电平复位
    input  wire [31:0] pc_next,
    output reg  [31:0] pc
);
    always @(posedge clk) begin
        if (!rst_n) pc <= 32'h0000_0000;
        else     pc <= pc_next;
    end
endmodule

IMEM —— 指令存储器

imem.v:组合逻辑 ROM,按字节寻址,可存 256 条指令(1 KB)。字对齐取指:将字节地址右移 2 位得到字索引。仿真时通过 $readmemh("program.hex", mem) 载入指令。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
module imem #(parameter DEPTH = 256) (
    input  wire [31:0] addr,   // PC按字节寻址
    output wire [31:0] instr
);
    reg [31:0] mem [0:DEPTH-1];
    wire [7:0] word_idx = addr[9:2];

    initial $readmemh("program.hex", mem);
    assign instr = mem[word_idx];
endmodule

Control —— 控制单元

control.v:核心译码模块,根据 opcode(以及 funct3、funct7)生成所有控制信号。

信号 位宽 作用
reg_write 1 寄存器堆写使能
alu_src 1 ALU B 端选择:0=rs2,1=立即数
mem_read 1 数据存储器读使能
mem_write 1 数据存储器写使能
branch 1 是否是分支指令
jump 1 是否是跳转指令(JAL/JALR)
wb_sel 2 写回数据选择:00=ALU,01=MEM,10=PC+4,11=U型立即数
alu_a_sel 1 ALU A 端选择:0=rdata1,1=PC(AUIPC 用)
alu_op 4 选择 ALU 具体运算
imm_sel 3 立即数格式选择

各 opcode 译码结果如下:

opcode 指令 reg_write alu_src mem_read mem_write branch jump wb_sel alu_a_sel imm_sel
0110011 R-type 1 0 0 0 0 0 00 0 —
0010011 I-type 1 1 0 0 0 0 00 0 000
0000011 LOAD 1 1 1 0 0 0 01 0 000
0100011 STORE 0 1 0 1 0 0 — 0 001
1100011 BRANCH 0 0 0 0 1 0 — 0 010
1101111 JAL 1 0 0 0 0 1 10 0 100
1100111 JALR 1 1 0 0 0 1 10 0 000
0110111 LUI 1 0 0 0 0 0 11 0 011
0010111 AUIPC 1 1 0 0 0 0 00 1 011

R 型、算术 I 型指令需要 ALU 完成不同算术逻辑运算,因此译码时结合funct7、funct3产生 ALU 控制信号,选择对应的运算功能;

B 类分支指令 ALU 固定执行rs1−rs2比较操作,ALU 运算功能无需更改,funct3仅用于判别比较结果、决定是否发生分支跳转。

R-type 指令根据 {funct7[5], funct3} 进一步区分运算:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
case({funct7[5],funct3})
    4'b0000: alu_op=4'b0000; // ADD
    4'b1000: alu_op=4'b0001; // SUB
    4'b0111: alu_op=4'b0010; // AND
    4'b0110: alu_op=4'b0011; // OR
    4'b0100: alu_op=4'b0100; // XOR
    4'b0010: alu_op=4'b0101; // SLT
    4'b0011: alu_op=4'b0110; // SLTU
    4'b0001: alu_op=4'b0111; // SLL
    4'b0101: alu_op=4'b1000; // SRL
    4'b1101: alu_op=4'b1001; // SRA
endcase

I-type指令根据 funct3 进一步区分运算:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case(funct3)
  3'h0: alu_op=4'b0000; // ADDI
  3'h7: alu_op=4'b0010; // ANDI
  3'h6: alu_op=4'b0011; // ORI
  3'h4: alu_op=4'b0100; // XORI
  3'h2: alu_op=4'b0101; // SLTI
  3'h3: alu_op=4'b0110; // SLTIU
  3'h1: alu_op=4'b0111; // SLLI
  3'h5: alu_op=(funct7[5])?4'b1001:4'b1000; // SRAI/SRLI
  default: alu_op=4'b0000;
endcase

B-type指令根据 funct3 进一步区分运算:

1
2
3
4
5
6
case(funct3)
    3'h0,3'h1: alu_op=4'b0001; // BEQ/BNE: SUB
    3'h4,3'h5: alu_op=4'b0101; // BLT/BGE: SLT
    3'h6,3'h7: alu_op=4'b0110; // BLTU/BGEU: SLTU
    default:   alu_op=4'b0001;
endcase end

RegFile —— 寄存器堆

regfile.v:32 × 32-bit,2 个异步读端口、1 个同步写端口。x0 恒为 0,写入 x0 被忽略,读取 x0 也始终返回 0。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
module regfile (
    input  wire        clk,
    input  wire        we,
    input  wire [4:0]  waddr,
    input  wire [31:0] wdata,
    input  wire [4:0]  raddr1,
    output wire [31:0] rdata1,
    input  wire [4:0]  raddr2,
    output wire [31:0] rdata2
);
    reg [31:0] regs [0:31];
    integer i;
    initial for(i=0;i<32;i=i+1) regs[i]=32'b0;

    always @(posedge clk)
        if (we && waddr != 5'd0) regs[waddr] <= wdata;

    assign rdata1 = (raddr1 == 5'd0) ? 32'd0 : regs[raddr1];
    assign rdata2 = (raddr2 == 5'd0) ? 32'd0 : regs[raddr2];
endmodule

ImmGen —— 立即数生成器

immgen.v:按指令类型从指令中提取立即数并做符号扩展。imm_sel 的映射关系为 000=I, 001=S, 010=B, 011=U, 100=J。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
always @(*) begin
    case (imm_sel)
        3'b000: imm = {{20{instr[31]}}, instr[31:20]};              // I
        3'b001: imm = {{20{instr[31]}}, instr[31:25], instr[11:7]}; // S
        3'b010: imm = {{19{instr[31]}}, instr[31], instr[7],
                       instr[30:25], instr[11:8], 1'b0};            // B
        3'b011: imm = {instr[31:12], 12'b0};                        // U
        3'b100: imm = {{11{instr[31]}}, instr[31], instr[19:12],
                       instr[20], instr[30:21], 1'b0};              // J
        default: imm = 32'd0;
    endcase
end

各格式立即数的位序对照(均以 instr[31] 符号位扩展):

类型 立即数构成
I imm[11:0] = instr[31:20],符号扩展
S imm[11:5] = instr[31:25],imm[4:0] = instr[11:7],符号扩展
B imm[12] = instr[31],imm[11] = instr[7],imm[10:5] = instr[30:25],imm[4:1] = instr[11:8],imm[0]=0,符号扩展
U imm[31:12] = instr[31:12],低 12 位清零
J imm[20] = instr[31],imm[19:12] = instr[19:12],imm[11] = instr[20],imm[10:1] = instr[30:21],imm[0]=0,符号扩展

ALU —— 算术逻辑单元

alu.v:支持 10 种运算,输出 result 和 zero(结果为 0 时置 1,供分支指令判断)。移位量取自 B 端低 5 位 shamt = b[4:0]。

alu_op 运算 说明
0000 a + b ADD / ADDI
0001 a - b SUB
0010 a & b AND
0011 a | b OR
0100 a ^ b XOR
0101 $signed(a) < $signed(b) SLT(有符号)
0110 a < b SLTU(无符号)
0111 a << shamt SLL
1000 a >> shamt SRL
1001 $signed(a) >>> shamt SRA(算术右移)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module alu (
    input  wire [31:0] a,        // 操作数A (rs1)
    input  wire [31:0] b,        // 操作数B (rs2 or imm)
    input  wire [3:0]  alu_op,   // 选择操作
    output reg  [31:0] result,   // 计算结果
    output wire        zero      // 结果为0时置1(分支指令使用)
);
    wire [4:0] shamt = b[4:0];   // 移位的量为B的低5位

    always @(*) begin
        case (alu_op)
            4'b0000: result = a + b;                          // ADD
            4'b0001: result = a - b;                          // SUB
            4'b0010: result = a & b;                          // AND
            4'b0011: result = a | b;                          // OR
            4'b0100: result = a ^ b;                          // XOR
            4'b0101: result = ($signed(a) < $signed(b)) ? 32'd1 : 32'd0; // SLT
            4'b0110: result = (a < b)                 ? 32'd1 : 32'd0; // SLTU
            4'b0111: result = a << shamt;                     // SLL
            4'b1000: result = a >> shamt;                     // SRL
            4'b1001: result = $signed(a) >>> shamt;           // SRA
            default: result = 32'd0;
        endcase
    end

    assign zero = (result == 32'd0);
endmodule

DMEM —— 数据存储器

dmem.v:256 × 32-bit(1 KB),同步写、组合逻辑读,支持字节/半字/字三种读写宽度。

  • mem_read:读使能,来自控制单元(仅 load 指令为 1);为 0 时 rdata 输出 0;
  • mem_width:00=字节, 01=半字, 10=字,来自 funct3[1:0];
  • mem_sign:加载时是否符号扩展,来自 !funct3[2](funct3[2]=0 表示有符号);
  • 字节内偏移 byte_off = addr[1:0],用于确定字节/半字在字内的位置。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
always @(posedge clk) begin
    if (we) begin
        case (mem_width)
            2'b00: begin  // SB
                case (byte_off)
                    2'b00: mem[word_idx][7:0]   <= wdata[7:0];
                    2'b01: mem[word_idx][15:8]  <= wdata[7:0];
                    2'b10: mem[word_idx][23:16] <= wdata[7:0];
                    2'b11: mem[word_idx][31:24] <= wdata[7:0];
                endcase
            end
            2'b01: begin  // SH
                if (byte_off[1]) mem[word_idx][31:16] <= wdata[15:0];
                else             mem[word_idx][15:0]  <= wdata[15:0];
            end
            2'b10: mem[word_idx] <= wdata;  // SW
        endcase
    end
end

读取端根据 mem_read、mem_width 和 mem_sign 生成 rdata(mem_read=0 时输出 0,避免非 load 指令读到无意义的数据):

1
2
3
4
5
6
7
8
wire [31:0] rdata_b = mem_sign ? {{24{b[7]}}, b}        : {24'd0, b};    // LB / LBU
wire [31:0] rdata_h = mem_sign ? {{16{half[15]}}, half} : {16'd0, half}; // LH / LHU
wire [31:0] rdata_w = word;                                               // LW

// 读使能门控
assign rdata = mem_read ? ((mem_width == 2'b00) ? rdata_b :
                           (mem_width == 2'b01) ? rdata_h : rdata_w)
                        : 32'd0;

ALU 输入与写回数据选择

在顶层 core_single_period 中,ALU 的两个操作数由控制信号二选一:

1
2
wire [31:0] alu_a = alu_a_sel ? pc : rdata1;   // AUIPC 用 PC, 其余用 rs1
wire [31:0] alu_b = alu_src  ? imm : rdata2;   // I-type/load/store 用 imm, R-type 用 rs2

写回寄存器数据由 wb_sel 从四个来源中选择:

1
2
3
4
assign wb_data = (wb_sel == 2'b00) ? alu_result :
                 (wb_sel == 2'b01) ? mem_rdata  :
                 (wb_sel == 2'b10) ? (pc + 32'd4):
                                     imm;        // 2'b11: U-type immediate

分支 / 跳转 / 下一 PC

分支判断利用了 ALU 输出的 zero 和 result[0]。funct3[2:1] 区分相等比较与大小比较,funct3[0] 区分是否取反:

1
2
3
4
5
// funct3[2:1]: 00=BEQ/BNE(看zero), 10/11=BLT/BGE/BLTU/BGEU(看result[0])
// funct3[0]:   0=相等/小于时跳转, 1=不等/不小于时跳转
wire branch_cond = (funct3[2:1] == 2'b00) ? (funct3[0] ^ alu_zero)
                                          : (funct3[0] ^ alu_result[0]);
wire branch_taken = branch && branch_cond;

数据存储器地址与写使能:

1
2
wire [1:0] mem_width = funct3[1:0];   // 00=byte, 01=half, 10=word
wire       mem_sign  = !funct3[2];    // 0=unsigned, 1=signed

最后一拍根据 jump、alu_src、branch_taken 计算 pc_next:

1
2
3
4
5
6
7
8
wire [31:0] pc_plus_4 = pc + 32'd4;
wire [31:0] pc_branch = pc + imm;                 // B-type / J-type
wire [31:0] pc_jalr   = {alu_result[31:1], 1'b0}; // JALR 清最低位

// JAL: alu_src=0 → 用 PC+imm;  JALR: alu_src=1 → 用 ALU 结果
assign pc_next = jump      ? (alu_src ? pc_jalr : pc_branch) :
                 branch_taken ? pc_branch :
                 pc_plus_4;

测试程序

rtl/program.hex 是一条覆盖本处理器所支持的全部 37 条 RV32I 数据通路指令的测试程序(不含 FENCE/ECALL/EBREAK 等系统指令),包含 R-type ×10、I-type ALU ×9、LOAD ×5、STORE ×3、BRANCH ×6、JAL、JALR、LUI、AUIPC。由于是单周期实现,不存在冒险,可直接顺序验证。

部分测试片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 初始化 x5=10, x6=7, x7=-1
00A00293   // PC=0x00  ADDI x5,  x0, 10   | x5  = 10
00700313   // PC=0x04  ADDI x6,  x0, 7    | x6  = 7
FFF00393   // PC=0x08  ADDI x7,  x0, -1   | x7  = -1 = 0xFFFFFFFF

// R-type
00628433   // PC=0x10  ADD  x8,  x5, x6   | x8  = 17
406284B3   // PC=0x14  SUB  x9,  x5, x6   | x9  = 3

// STORE / LOAD
008A2023   // PC=0x70  SW   x8,  0(x20)   | mem[0x50] = 40
000A2E83   // PC=0x7C  LW   x29, 0(x20)   | x29 = 40

// 停机:BEQ x0,x0,0 进入无限循环
00000063   // PC=0xFC  BEQ  x0,  x0,  0

完整测试程序细节可查看 rtl/program.hex 文件中的注释。


小结

通过这套单周期 RV32I 实现,可以看到处理器的基本骨架:取指(PC+IMEM)→ 译码(Control)→ 执行(ALU)→ 访存(DMEM)→ 写回(RegFile)。单周期实现结构简单、无冒险,但每个时钟周期都要走完最长路径(寄存器堆读出 → ALU → 访存 → 写回),时钟频率因此受限;实际处理器多在此基础上加入流水线,把执行拆成多个流水级来提升吞吐率。极度建议自己在数据通路图中标注信号,逐条跑一下各类指令加深理解。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计