🧠 第3课:RV32I CPU 深入 — 流水线是怎么工作的

上节课我们看了整个芯片的架构,这节课我们钻进 CPU 核心,理解它怎么"执行"一条指令。

🎯 本课目标

理解 CPU 的"3级流水线"是什么意思、RISC-V 指令长什么样、load-use冒险怎么处理、CSR寄存器是干嘛的。学完你能看懂 rv32i_core.v 这 415 行代码的逻辑。

一、CPU 怎么"吃"指令?—— 3级流水线比喻

想象一个洗车流水线:

关键是:这三站同时在工作。当第3站在执行第1条指令时,第2站在译码第2条指令,第1站同时在取第3条指令。这就是流水线的威力——每个时钟周期都能完成一条指令(理想情况下)。

3级流水线 —— 时间轴视图

gantt title CPU 流水线执行过程(每条指令3个时钟周期) dateFormat X axisFormat %s section 指令1 (ADDI) 取指(IF) :0, 1 译码(ID) :1, 2 执行(EX) :2, 3 section 指令2 (ADDI) 取指(IF) :1, 2 译码(ID) :2, 3 执行(EX) :3, 4 section 指令3 (ADD) 取指(IF) :2, 3 译码(ID) :3, 4 执行(EX) :4, 5 section 指令4 (ECALL) 取指(IF) :3, 4 译码(ID) :4, 5 执行(EX) :5, 6

看!虽然每条指令要3个周期才能完成,但每1个周期就有一条指令完成(因为三级同时在工作)。这就是流水线的加速原理。

二、流水线冒险 —— 什么时候会"堵车"?

Load-Use 冒险(最重要的一种)

问题来了:如果你刚执行完一条"从内存加载数据到 x1"的指令(LW),下一条指令就要用 x1 来做加法——但加载的数据还在路上(要等 EX 阶段结束才写回),译码阶段读到的 x1 是旧值

Load-Use 冒险 —— 流水线"卡住"

sequenceDiagram participant IF as 取指(IF) participant ID as 译码(ID) participant EX as 执行(EX) Note over IF,EX: 指令1: LW x1, 0(x2) —— 从内存加载到x1 IF->>ID: 周期1: 取LW指令 ID->>EX: 周期2: 译码LW EX->>EX: 周期3: 执行LW,读内存 Note over EX: 数据要等到周期3结束时才写入x1! Note over IF,EX: 指令2: ADD x3, x1, x4 —— 需要用x1! IF->>ID: 周期2: 取ADD指令 ID->>ID: 周期3: 译码ADD……等等!
x1还是旧值!必须暂停! Note over IF,ID: ⚠️ 插入1个"气泡"(stall) ID->>EX: 周期4: 现在x1更新了
安全,继续执行ADD

CPU 怎么检测这种情况?很简单——看当前 EX 阶段的指令是不是 LW,且目标寄存器(x1)是不是下一阶段要用到的。如果是,插入一个"气泡"(暂停1周期)

这个检测逻辑在 rv32i_core.v 里就一行:

wire load_use_hazard = (ex_mem_read) && ((ex_rd == rs1_addr) || (ex_rd == rs2_addr));

分支预测失败

CPU 遇到分支指令(比如 BEQ:如果 x1==x2 就跳转),在译码阶段还不知道该不该跳转(要等到 EX 阶段算出结果才知道)。rv_control 的策略简单粗暴:默认不跳,如果猜错了就冲刷流水线,代价 2 个周期。

三、RISC-V 指令怎么编码?

一条 RISC-V 指令就是 32 个 bit(4 字节)。以 ADD x3, x1, x2 为例:

0000000 00010 00001 000 00011 0110011
funct7  rs2   rs1   f3  rd    opcode
说明: "把 x1(rs1) 和 x2(rs2) 相加,结果放 x3(rd)"

CPU 的译码阶段就是做这个"拆解"工作——从 32 个 bit 中提取出:这是什么操作(opcode)、操作数在哪(rs1/rs2)、结果放哪(rd)。

四、CSR 寄存器 —— CPU 的"控制面板"

CSR(Control and Status Register)是 RISC-V 中专门用于控制和状态的特殊寄存器。普通寄存器(x0-x31)存数据,CSR 存"系统配置"。

CSR 的作用

flowchart TB subgraph CSR["CSR 寄存器组(7个)"] MSTATUS["MSTATUS: 机器状态
MIE bit: 是否允许中断"] MIE["MIE: 中断使能
哪类中断可以触发"] MTVEC["MTVEC: 陷阱向量
中断发生后跳转到哪"] MEPC["MEPC: 异常PC
中断发生时正在执行哪条指令"] MCAUSE["MCAUSE: 异常原因
为什么进了中断(24=总线错误)"] end INT["⚡ 中断发生!"] -->|保存PC| MEPC INT -->|记录原因| MCAUSE INT -->|跳转到| MTVEC MSTATUS -->|"MIE=1才响应"| INT style CSR fill:#3987e5,color:#fff
⚠️ 已知Bug:CSR写入路径不完整

代码中声明了 csr_write 信号用于修改 CSR 的值,但在 always 块里始终赋值为 0——这意味着 CSRRW/CSRRS/CSRRC 这三条写 CSR 的指令看起来译码正确,但实际不会修改任何 CSR。中断处理所需的 MIE、MTVEC、MEPC 全部无法写入——这是个需要修复的问题。

五、寄存器文件 —— 32个"草稿纸"

rv_control 有 32 个 32 位通用寄存器(x0-x31)。其中 x0 永远是 0(硬件强制),所以实际可用 31 个。

一个聪明的设计:下降沿写回。寄存器在时钟下降沿写入,而读操作在上升沿——这样同周期的指令可以"看到"刚写入的值,避免了额外冒险。

寄存器文件的读写时序

sequenceDiagram participant CLK as 时钟 participant READ as 读端口(上升沿) participant REG as 寄存器文件 participant WRITE as 写端口(下降沿) CLK->>READ: ↑ 上升沿: 读 x1, x2 的值 READ->>REG: rs1=5, rs2=6 → val1, val2 CLK->>WRITE: ↓ 下降沿: 把 ALU 结果写回 x3 WRITE->>REG: rd=3, data=ALU_result Note over REG: 下一个上升沿就读到新值了!

六、关键代码片段:流水线控制

这个 always 块控制着流水线的暂停(stall)和冲刷(flush):

always @(*) begin
    stall_pipe = load_use_hazard;      // LW后跟依赖指令→暂停1周期
    flush_pipe = branch_taken | take_irq;  // 分支跳转或中断→冲刷流水线
end

stall_pipe:让流水线"停一步",等待数据准备好。
flush_pipe:把流水线里还没执行完的指令全部清空(因为要跳转到新地址了)。