文章摘要
加载中...|
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结 投诉

集创赛CPU有关笔记

7月7日更新

CPU代码仓库:

https://github.com/Stomatra/RV32Final

CPU工程基本结构

myCPU的外部接口:

SystemVerilog
//IROM 取指接口
output logic [11:0]  irom_addr,//CPU 当前要取的指令
input  logic [31:0]  irom_data,//IROM 返回的 32 位指令

//perip 数据/外设访问接口
output logic [31:0]  perip_addr,//load/store 访问的数据地址
output logic         perip_wen,//是否写
output logic [1:0]   perip_mask,//写字节/半字/字
output logic [31:0]  perip_wdata,//写入数据
input  logic [31:0]  perip_rdata//读回数据

CPU的五个阶段(五级流水线):

IF :取指

ID :译码 + 读寄存器,只决定“这条指令要干什么”

EX :执行 ALU / 分支 / CSR / M扩展 / trap 为真正计算的阶段

MEM :访存

WB :写回寄存器,写回 x0~x31

IF 阶段

IF 阶段主要是取指阶段,主要是从 IROM 中取出指令,计算下一条指令的地址。

核心寄存器:

SystemVerilog
logic [31:0] pc_q;
logic [31:0] pc_next;

PC更新逻辑:

SystemVerilog
// IF 级 PC 更新优先级:redirect > stall/hold > 顺序 +4。
always_comb begin
	if (ex_pc_redirect) begin
		//如果 EX 阶段要求跳转,PC = 跳转目标
		//包括:branch_taken jal jalr ecall mret mul_helper
		pc_next = ex_pc_target;
	end else if (load_use_hazard || pc_ex_hazard || pc_mem_hazard || mem_load_stall || m_stall) begin
		//如果存在相关冒险或停顿,PC 保持不变
		pc_next = pc_q;
	end else begin
		//顺序执行,PC + 4
		pc_next = pc_q + 32'd4;
	end
end

// IF 级 PC 寄存器,在 reset 时初始化为 RESET_PC。
always_ff @(posedge cpu_clk or posedge cpu_rst) begin
	if (cpu_rst) begin
		pc_q <= RESET_PC;
	end else begin
		pc_q <= pc_next;
	end
end

IF/ID 寄存器负责把当前取到的指令送到 ID 阶段:

SystemVerilog
ifid_pc    <= pc_q;
ifid_instr <= irom_data;

但遇到 load hazard、mem stall、div stall 时会保持。

ID 阶段

ID 阶段主要是指令译码阶段,负责解析指令的操作类型、操作数以及目标寄存器。ID 阶段还会读取寄存器文件中的源操作数,并生成控制信号供后续阶段使用。

ID 阶段主要干三件事:

  1. 从指令中拆出 opcode / rd / rs1 / rs2 / funct3 / funct7
SystemVerilog
assign opcode = instr[6:0];
assign rd     = instr[11:7];
assign funct3 = instr[14:12];
assign rs1    = instr[19:15];
assign rs2    = instr[24:20];
assign funct7 = instr[31:25];
  1. 从寄存器堆读取 rs1 / rs2
  2. 根据 opcode 生成控制信号

ID 阶段的主控制器就是这个大 always_comb:

SystemVerilog
always_comb begin
    id_uses_rs1      = 1'b0;
    id_uses_rs2      = 1'b0;
    id_rf_we         = 1'b0;
    id_wb_sel        = WB_SRC_ALU;
    ...
    if (ifid_valid) begin
        case (id_opcode)
            ...
        endcase
    end
end

这段负责告诉后面的流水线:

  • 这条指令用不用 rs1
  • 用不用 rs2
  • 写不写 rd
  • 写回来源是什么
  • ALU 做什么
  • 是不是访存
  • 是不是 CSR
  • 是不是 M 扩展

ID/EX 流水寄存器

把指令信息送到 EX。ID 阶段译码后,不能直接执行,而是存入 ID/EX 寄存器。

SystemVerilog
always_ff @(posedge cpu_clk or posedge cpu_rst) begin
		if (cpu_rst) begin
			idex_valid         <= 1'b0;
			idex_pc            <= 32'h0;
			idex_rs1           <= 5'h0;
			idex_rs2           <= 5'h0;
            ...
		end else if (mem_load_stall || m_stall) begin
			// hold IDEX - memory read stall
		end else if (ex_pc_redirect || load_use_hazard || pc_ex_hazard || pc_mem_hazard) begin
			idex_valid         <= 1'b0;
			idex_pc            <= 32'h0;
			idex_rs1           <= 5'h0;
			idex_rs2           <= 5'h0;
            ...
		end else if (id_mul_helper_hit) begin
			idex_valid         <= 1'b1;
			idex_pc            <= ifid_pc;
			idex_rs1           <= 5'h0;
			idex_rs2           <= 5'h0;
            ...
		end else begin
			idex_valid         <= ifid_valid;
			idex_pc            <= ifid_pc;
			idex_rs1           <= id_rs1;
			idex_rs2           <= id_rs2;
            ...
		end
	end

它的作用是:

ID 阶段:这条指令要干什么

ID/EX 保存:下一拍 EX 阶段照着这些信号执行

遇到 flush 时,这些信号会被清零。遇到 mem_load_stall || div_stall 时,ID/EX 会保持不动。

EX 阶段

EX 阶段主要是执行阶段,负责进行算术运算、逻辑运算、分支判断等操作。EX 阶段会根据 ID 阶段传递过来的控制信号和操作数,执行相应的操作,并将结果传递给 MEM 阶段。

目前EX阶段主要负责:

  • ALU 运算
  • branch 判断
  • jal/jalr 目标地址
  • CSR 读写计算
  • ecall/mret trap 跳转
  • RV32M 乘除法结果选择
  • store 写数据准备

ALU 输入选择

EX 阶段先通过前递逻辑得到 ex_rs1_valex_rs2_val

SystemVerilog
if (ex_fwd_rs1_from_exmem) begin
    ex_rs1_val = exmem_wb_data;
end else if (ex_fwd_rs1_from_memwb) begin
    ex_rs1_val = memwb_wdata;
end

ALU 运算

SystemVerilog
ALU #(32) u_alu (
    .A(ex_alu_a),
    .B(ex_alu_b),
    .ALUOp(idex_alu_op),
    .Result(ex_alu_y)
);

普通 RV32I 的 ADD、SUB、AND、OR、XOR、SLT、SLL、SRL、SRA 都走这里。

分支和跳转

分支比较在 EX 阶段做:

SystemVerilog
if (idex_pc_sel == PC_SRC_BRANCH) begin
    ex_cmp_eq = (ex_pc_rs1_val == ex_pc_rs2_val);
    ex_cmp_lt_signed = ($signed(ex_pc_rs1_val) < $signed(ex_pc_rs2_val));
    ex_cmp_lt_unsigned = (ex_pc_rs1_val < ex_pc_rs2_val);
end

然后根据 BEQ/BNE/BLT/BGE 等决定 ex_br_take

PC 跳转目标统一由 ex_pc_target 给出:

SystemVerilog
if (ex_trap_redirect) begin
    ex_pc_target = ex_trap_target;
end else if (idex_mul_helper) begin
    ex_pc_target = idex_mul_helper_ra;
end else begin
    case (idex_pc_sel)
        PC_SRC_BRANCH: ex_pc_target = ex_br_take ? ex_pc_plus_imm : ex_pc4;
        PC_SRC_JAL:    ex_pc_target = ex_pc_plus_imm;
        PC_SRC_JALR:   ex_pc_target = ex_jalr_target;
        default:       ex_pc_target = ex_pc4;
    endcase
end

真正决定要不要跳的是 ex_pc_redirect

SystemVerilog
if (ex_trap_enter || ex_trap_return) begin
    ex_pc_redirect = 1'b1;
end else if (idex_mul_helper) begin
    ex_pc_redirect = 1'b1;
end else begin
    case (idex_pc_sel)
        PC_SRC_BRANCH: if (ex_br_take) ex_pc_redirect = 1'b1;
        PC_SRC_JAL:    ex_pc_redirect = 1'b1;
        PC_SRC_JALR:   ex_pc_redirect = 1'b1;
    endcase
end

一句话:所有改变 PC 的东西,最后都归到 ex_pc_redirect + ex_pc_target

MEM 阶段

MEM 阶段主要是访存阶段,负责对数据存储器进行读写操作。MEM 阶段会根据 EX 阶段传递过来的控制信号和地址,进行相应的访存操作,并将结果传递给 WB 阶段。

load/store 不直接访问 DRAM,而是通过 perip_bridge

CPU 对外的访存接口是:

SystemVerilog
assign perip_addr  = exmem_alu_y;
assign perip_wen   = exmem_valid && exmem_mem_req && exmem_mem_write;
assign perip_mask  = exmem_mem_mask;
assign perip_wdata = exmem_store_data;

`perip_bridge` 里面把地址分成:

```SystemVerilog
DRAM:0x8010_0000 ~ 0x8013_FFFF
SW0 :0x8020_0000
SW1 :0x8020_0004
KEY :0x8020_0010
SEG :0x8020_0020
LED :0x8020_0040
CNT :0x8020_0050

这些地址在 perip_bridge.sv 里定义。

外设桥里所有读源都统一打一拍,因此 CPU 这边每次 load 会插入 mem_load_stallperip_bridge 注释里明确写了 CPU 看到的 perip_rdata 是“地址给出后一拍有效”。

所以 CPU 里有:

SystemVerilog
assign mem_load_stall = exmem_valid && exmem_mem_req && !exmem_mem_write && !mem_stall_flag;

意思是:

每遇到 load,额外停一拍,等 perip_rdata 稳定

WB 阶段

WB 阶段主要负责将计算结果或访存结果写回寄存器文件。

MEM 阶段之后,结果进入 MEM/WB。

最终写回 RF 的是:

SystemVerilog
.wen   (memwb_rf_we && memwb_valid),
.waddr (memwb_rd),
.wdata (memwb_wdata)

mem_wb_data 的选择是:

SystemVerilog
assign mem_wb_data = (exmem_wb_sel == WB_SRC_MEM) ? mem_load_data : exmem_wb_data;

也就是说:

如果是 load,写回 perip_rdata 处理后的数据,否则,写回 EX 阶段已经算好的 exmem_wb_data

这个 CPU 的主线就是:PC 取指 → ID 译码生成控制信号 → ID/EX 保存 → EX 执行 ALU/CSR/trap/RV32M → EX/MEM 访存或传递结果 → MEM/WB 写回寄存器。

重要变量

rd

rd 是指令的目标寄存器编号,位于指令的 [11:7] 位。它表示当前指令执行后,结果将写入哪个寄存器。

rs1

rs1 是指令的第一个源寄存器编号,位于指令的 [19:15] 位。它表示当前指令需要读取的第一个操作数所在的寄存器。

rs2

rs2 是指令的第二个源寄存器编号,位于指令的 [24:20] 位。它表示当前指令需要读取的第二个操作数所在的寄存器。

funct3

funct3 是指令的功能码,位于指令的 [14:12] 位。它用于区分同一类指令的不同操作,例如在 ALU 指令中,funct3 可以表示加法、减法、与、或等操作。

funct7

funct7 是指令的功能扩展码,位于指令的 [31:25] 位。它用于进一步区分同一类指令的不同操作,通常与 funct3 结合使用,以确定具体的操作类型。

opcode

opcode 是指令的操作码,位于指令的 [6:0] 位。它用于确定指令的类型和操作类别,例如 ALU 指令、跳转指令、访存指令等。

冒险处理

普通ALU运算冒险

前递

前递(forwarding)是解决数据冒险的一种方法,通过将尚未写回寄存器文件的结果直接传递给需要使用该结果的指令,从而避免等待写回阶段。

假设有两条指令:

asm
add x3, x1, x2
sub x4, x3, x5

sub 指令到达 EX 阶段时,x3 的值还没有写回寄存器文件,但可以直接从 EX/MEM 阶段的 exmem_wb_data 前递过来,这样 sub 就可以立即使用 x3 的值进行计算,而不必等待写回阶段完成。

普通 ALU 运算冒险主要发生在 EX 阶段,当指令需要使用前一条指令的结果时,如果结果尚未写回寄存器文件,就会产生数据冒险。

普通数据冒险主要靠 forwarding。

代码里先定义了四组比较:

ex_match_rs1_exmemex_match_rs1_memwbex_match_rs2_exmemex_match_rs2_memwb

这些信号用于判断:

当前 EX 阶段要用的 rs1/rs2, 是不是等于后面 EX/MEM 或 MEM/WB 阶段即将写回的 rd。

相关信号定义在 EX 级 forwarding 区。

判断是否可以前递:

SystemVerilog
assign exmem_can_forward = exmem_rf_we && (exmem_rd != 5'h0) && (exmem_wb_sel != WB_SRC_MEM);
assign memwb_can_forward = memwb_rf_we && (memwb_rd != 5'h0);

这句话的意思是:

EX/MEM 可以前递:

  • 必须会写寄存器
  • rd 不能是 x0
  • 不能是 load,因为 load 数据此时还没回来

MEM/WB 可以前递:

  • 只要会写寄存器且 rd 不是 x0

然后根据命中结果决定 ex_rs1_val / ex_rs2_val:

SystemVerilog
ex_rs1_val = idex_rs1_val;
if (ex_fwd_rs1_from_exmem) begin
    ex_rs1_val = exmem_wb_data;
end else if (ex_fwd_rs1_from_memwb) begin
    ex_rs1_val = memwb_wdata;
end

rs2 同理。

sub 到 EX 阶段时,x3 可以直接从 exmem_wb_data 前递过来。

ID 阶段同拍旁路:MEM/WB → ID

除了 EX forwarding,ID 阶段还有一个小旁路:

SystemVerilog
if (id_rs1 == 5'd0) begin
    id_rs1_val = 32'h0;// x0 永远是 0
end else if (memwb_can_forward && (memwb_rd == id_rs1)) begin
    id_rs1_val = memwb_wdata;// MEM/WB → ID 旁路
end else begin
    id_rs1_val = rf_rs1_raw;// 正常读寄存器堆
end

rs2 也一样。

它解决的是这种情况:

asm
add x3, x1, x2
nop
nop
sub x4, x3, x5

当第一条指令正在 WB 写回时,ID 阶段直接拿 memwb_wdata,避免因为寄存器堆读写同拍顺序不确定导致读到旧值。

Load-use 冒险

Load-use 冒险主要发生在 EX 阶段,当一条指令需要使用前一条 load 指令的结果时,如果结果尚未从数据存储器中读取出来,就会产生数据冒险。

load 比普通 ALU 麻烦,因为 load 的数据要到 MEM/WB 附近才有效,EX/MEM 阶段还拿不到正确数据,所以不能像 ALU 一样直接前递。

典型例子:

asm
lw  x3, 0(x1)
add x4, x3, x5

add 下一拍就要用 x3,但 lw 的数据还没从 perip_rdata 回来,所以必须停。

代码里检测 load-use:

SystemVerilog
assign load_use_hazard = ifid_valid && idex_valid && idex_rf_we &&
                         (idex_wb_sel == WB_SRC_MEM) && (idex_rd != 5'h0) &&
                         ((id_uses_rs1 && (id_rs1 == idex_rd)) ||
                          (id_uses_rs2 && (id_rs2 == idex_rd)));

翻译一下:

  • ID/EX 里是一条 load;
  • 它会写 rd;
  • 当前 IF/ID 这条指令要用 rs1 或 rs2;
  • 并且 rs1/rs2 等于 load 的 rd;
  • 那么就产生 load_use_hazard。

处理方式是:

  • PC 保持
  • IF/ID 保持
  • ID/EX 插 bubble

PC 保持在这里:

SystemVerilog
else if (load_use_hazard || pc_ex_hazard || pc_mem_hazard || mem_load_stall || m_stall) begin
    pc_next = pc_q;
end

IF/ID 保持在这里:

SystemVerilog
else if (load_use_hazard || pc_ex_hazard || pc_mem_hazard || mem_load_stall || m_stall) begin
    ifid_pc    <= ifid_pc;
    ifid_instr <= ifid_instr;
end

else if (!load_use_hazard && !pc_ex_hazard && !pc_mem_hazard && !mem_load_stall && !m_stall) begin ifid_pc <= pc_q; ifid_instr <= irom_data; end

ID/EX 插 bubble 在这里:

SystemVerilog
else if (ex_pc_redirect || load_use_hazard || pc_ex_hazard || pc_mem_hazard) begin
    idex_valid <= 1'b0;
    ...
    idex_rf_we <= 1'b0;
    idex_mem_req <= 1'b0;
    idex_is_m_ext <= 1'b0;
end

这就是经典的:load-use stall 一拍 + 插入空泡

MEM load stall

这个工程里,DRAM/MMIO 读数据不是立刻回来,而是统一打一拍。所以只要 EX/MEM 阶段是 load,就要额外等一拍。

代码:

SystemVerilog
assign mem_load_stall = exmem_valid && exmem_mem_req && !exmem_mem_write && !mem_stall_flag;

意思是:

  • EX/MEM 是有效访存;
  • 它是 load,不是 store;
  • 而且这一拍还没 stall 过;
  • 那么 mem_load_stall = 1。

mem_stall_flag 用来保证每个 load 只额外停一拍,不会无限停。

处理方式:

  • PC hold
  • IF/ID hold
  • ID/EX hold
  • EX/MEM hold
  • MEM/WB hold

其中 ID/EX 在 mem_load_stall 时保持。 MEM/WB 在 mem_load_stall 时也保持旧值,避免错误写回。

这和 load-use 不一样:

load_use_hazard:为了下一条用 load 结果,插 bubble mem_load_stall:为了等 perip_rdata 稳定,整体 hold 一拍

branch / jalr 的 PC 操作数冒险:现在选择“等”,不前递

普通 ALU 可以从 EX/MEM 前递,但 branch/jalr 的 PC 选择链为了提频,不再吃 EXMEM 前递:

SystemVerilog
assign ex_pc_fwd_rs1_from_exmem = 1'b0;
assign ex_pc_fwd_rs1_from_memwb = 1'b0;
assign ex_pc_fwd_rs2_from_exmem = 1'b0;
assign ex_pc_fwd_rs2_from_memwb = 1'b0;

也就是说,分支和 JALR 不再用复杂前递链去抢最新数据,而是在 ID 阶段发现不安全时直接等。

两个 hazard 定义在这里:

SystemVerilog
assign pc_ex_hazard = ifid_valid && idex_valid && idex_rf_we &&
                     (idex_wb_sel != WB_SRC_MEM) && (idex_rd != 5'h0) &&
                     ((((id_pc_sel == PC_SRC_BRANCH) || (id_pc_sel == PC_SRC_JALR)) && (id_rs1 == idex_rd)) ||
                      ((id_pc_sel == PC_SRC_BRANCH) && (id_rs2 == idex_rd)));

assign pc_mem_hazard = ifid_valid && exmem_valid && exmem_rf_we &&
                      (exmem_wb_sel != WB_SRC_MEM) && (exmem_rd != 5'h0) &&
                      ((((id_pc_sel == PC_SRC_BRANCH) || (id_pc_sel == PC_SRC_JALR)) && (id_rs1 == exmem_rd)) ||
                       ((id_pc_sel == PC_SRC_BRANCH) && (id_rs2 == exmem_rd)));

翻译一下:

pc_ex_hazard

  • 当前 IF/ID 中的指令是 branch 或 jalr(id_pc_sel == PC_SRC_BRANCH/JALR
  • 且使用的操作数 rs1 或 rs2(分支用两个,jalr 用 rs1)恰好是 ID/EX 中正在执行的指令的目标寄存器(idex_rd
  • 且该寄存器是有效写回的(idex_rf_we && idex_wb_sel != WB_SRC_MEM
  • 问题:branch/jalr 需要在 EX 阶段就能拿到 rs1/rs2 的值来判断跳转,但这个值还在 ID/EX 中没有计算出来
  • 产生冒险的条件:branch/jalr 的 rs1/rs2 等于 ID/EX 的 rd,且 ID/EX 会写回寄存器,且不是 load。

pc_mem_hazard

  • 当前 IF/ID 中的指令是 branch 或 jalr
  • 使用的 rs1 或 rs2 恰好是 EX/MEM 中指令的目标寄存器(exmem_rd
  • 且该寄存器是有效写回的
  • 问题:值还在 EX/MEM 中,还没有进入 MEM 阶段完成,
  • 产生冒险的条件:branch/jalr 的 rs1/rs2 等于 EX/MEM 的 rd,且 EX/MEM 会写回寄存器,且不是 load。

它解决的是:

asm
add x1, x2, x3
beq x1, x0, label

或者:

asm
add x1, x2, x3
jalr x0, 0(x1)

这种指令如果不等,branch/jalr 在 EX 阶段可能拿旧 x1 算跳转结果。

现在处理方式是:

pc_ex_hazardpc_mem_hazard 出现:

  • PC hold
  • IF/ID hold
  • ID/EX 插 bubble

PC hold 在 PC 更新逻辑里。 IF/ID valid 也在 pc_ex_hazard || pc_mem_hazard 时保持。 ID/EX 在这两个 hazard 时清 bubble。

普通 ALU 数据冒险:前递 分支/jalr 的 PC 数据冒险:等待

这是为了缩短 PC redirect 关键路径。

M 扩展多周期冒险:m_stall

新版代码把 M 扩展做成了一个小状态机:

  • m_inflight:M 指令正在执行
  • m_result_ready:M 指令结果已经准备好
  • m_stall:当前 ID/EX 是 M 指令但结果还没好

相关信号在 EX 级定义区。

启动逻辑:

SystemVerilog
assign m_start = idex_valid && idex_is_m_ext && !m_inflight && !m_result_ready && !ex_pc_redirect;
assign mul_start = m_start && ex_m_is_mul;
assign div_start = m_inflight && m_is_div_reg && !m_div_started;
assign m_stall = idex_valid && idex_is_m_ext && !m_result_ready;

意思是:

M 指令进入 EX 后,先启动 M 状态机; 结果没好之前,m_stall=1; m_stall=1 时 PC/IFID/IDEX hold; EX/MEM 插 bubble; 结果好了后,放行一拍,把 ex_m_result 写入后级。

SystemVerilog
//PC hold:

else if (load_use_hazard || pc_ex_hazard || pc_mem_hazard || mem_load_stall || m_stall) begin
    pc_next = pc_q;
end

//IF/ID hold:

else if (!load_use_hazard && !pc_ex_hazard && !pc_mem_hazard && !mem_load_stall && !m_stall) begin
    ifid_pc    <= pc_q;
    ifid_instr <= irom_data;
end

//ID/EX hold:

else if (mem_load_stall || m_stall) begin
    // hold IDEX
end

//EX/MEM 在 m_stall 时清 bubble:

else if (m_stall) begin
    exmem_valid <= 1'b0;
    exmem_rf_we <= 1'b0;
    exmem_mem_req <= 1'b0;
    ...
end

M 指令在 EX 等结果时,后级不能反复收到同一条指令,所以 EX/MEM 要清空。

store 数据冒险

store 有两个源:

rs1:地址基址 rs2:要写入内存的数据

代码里 id_uses_rs2 = 1,所以 store 的 rs2 也会参与 forwarding/hazard 判断。

EX 阶段 store 数据来自:

SystemVerilog
assign ex_store_data = idex_mem_write ? ex_rs2_val : 32'h0;

ex_rs2_val 已经经过 EX forwarding,所以这种情况一般不用停。

asm
add x3, x1, x2
sw  x3, 0(x4)

sw 到 EX 阶段时,写入数据 x3 可以从 EX/MEM 或 MEM/WB 前递得到。

但如果是:

asm
lw x3, 0(x1)
sw x3, 0(x4)

这仍会被 load_use_hazard 检测到,因为 store 使用 rs2,且 load 的 rd 等于 store 的 rs2。

总结

冒险类型例子处理方法代码位置
普通 ALU 数据冒险add x3,... 后接 sub x4,x3,...EX/MEM 或 MEM/WB forwardingex_fwd_rs*_from_*ex_rs*_val
WB 同拍读冒险WB 写 x3,ID 同拍读 x3MEM/WB → ID 旁路id_rs1_val/id_rs2_val
load-use 冒险lw x3,... 后接 add ...,x3,...PC/IFID hold,IDEX bubbleload_use_hazard
load 读延迟任意 loadmem_load_stall 额外等一拍mem_load_stall/mem_stall_flag
branch/jalr 操作数冒险add x1,... 后接 beq x1,...不走 PC 前递,等待数据稳定pc_ex_hazard/pc_mem_hazard
控制冒险branch taken / jal / jalr / ecall / mretex_pc_redirect + flushex_pc_target/ex_pc_redirect
M 扩展多周期冒险mul/div/remm_stall hold 前级,EX/MEM bubblem_start/m_inflight/m_result_ready/m_stall
参考资料
赞赏博主