🧠 第3课:RV32I CPU 深入 — 流水线是怎么工作的
上节课我们看了整个芯片的架构,这节课我们钻进 CPU 核心,理解它怎么"执行"一条指令。
理解 CPU 的"3级流水线"是什么意思、RISC-V 指令长什么样、load-use冒险怎么处理、CSR寄存器是干嘛的。学完你能看懂 rv32i_core.v 这 415 行代码的逻辑。
一、CPU 怎么"吃"指令?—— 3级流水线比喻
想象一个洗车流水线:
- 第1站(取指/Fetch):从内存中"捞"出一条指令
- 第2站(译码/Decode):看懂这条指令是什么意思——"哦,你要把 x1 和 x2 加起来放到 x3"
- 第3站(执行/Execute):真正做加法运算,把结果写回寄存器
关键是:这三站同时在工作。当第3站在执行第1条指令时,第2站在译码第2条指令,第1站同时在取第3条指令。这就是流水线的威力——每个时钟周期都能完成一条指令(理想情况下)。
3级流水线 —— 时间轴视图
看!虽然每条指令要3个周期才能完成,但每1个周期就有一条指令完成(因为三级同时在工作)。这就是流水线的加速原理。
二、流水线冒险 —— 什么时候会"堵车"?
Load-Use 冒险(最重要的一种)
问题来了:如果你刚执行完一条"从内存加载数据到 x1"的指令(LW),下一条指令就要用 x1 来做加法——但加载的数据还在路上(要等 EX 阶段结束才写回),译码阶段读到的 x1 是旧值!
Load-Use 冒险 —— 流水线"卡住"
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 的作用
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
代码中声明了 csr_write 信号用于修改 CSR 的值,但在 always 块里始终赋值为 0——这意味着 CSRRW/CSRRS/CSRRC 这三条写 CSR 的指令看起来译码正确,但实际不会修改任何 CSR。中断处理所需的 MIE、MTVEC、MEPC 全部无法写入——这是个需要修复的问题。
五、寄存器文件 —— 32个"草稿纸"
rv_control 有 32 个 32 位通用寄存器(x0-x31)。其中 x0 永远是 0(硬件强制),所以实际可用 31 个。
一个聪明的设计:下降沿写回。寄存器在时钟下降沿写入,而读操作在上升沿——这样同周期的指令可以"看到"刚写入的值,避免了额外冒险。
寄存器文件的读写时序
六、关键代码片段:流水线控制
这个 always 块控制着流水线的暂停(stall)和冲刷(flush):
always @(*) begin
stall_pipe = load_use_hazard; // LW后跟依赖指令→暂停1周期
flush_pipe = branch_taken | take_irq; // 分支跳转或中断→冲刷流水线
end
stall_pipe:让流水线"停一步",等待数据准备好。
flush_pipe:把流水线里还没执行完的指令全部清空(因为要跳转到新地址了)。