本文介绍单周期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 → 访存 → 写回),时钟频率因此受限;实际处理器多在此基础上加入流水线,把执行拆成多个流水级来提升吞吐率。极度建议自己在数据通路图中标注信号,逐条跑一下各类指令加深理解。