3. 处理器微架构设计入门:经典5级流水线
好,咱们进入正题。今天聊的是RISC-V处理器的微架构设计,具体来说就是那个经典的5级流水线。很多教材一上来就画一堆花花绿绿的框图,说实话,我第一次看也是一头雾水。后来自己动手在FPGA上跑了一遍,才真正明白每一级在干什么。
我个人习惯,学微架构先别盯着细节看,先搞清楚数据是怎么“流”过去的。流水线说白了,就是把一条指令的执行过程拆成几个步骤,每个步骤由专门的硬件模块处理。这样,每个时钟周期都能有一条新指令进入流水线,吞吐量一下就上去了。
3.1 经典5级流水线结构概览
我们常说的5级流水线,指的是:取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回(WB)。这五个阶段,每个阶段完成一个特定的任务。
我刚开始做RISC-V项目时,犯过一个低级错误:以为流水线级数越多越好。其实不然。级数多了,虽然频率能跑高,但分支预测错误带来的惩罚也更大。对于FPGA原型验证来说,5级流水线是一个很好的起点——它足够简单,能让你看清数据通路的全貌,又足够复杂,能让你遇到真实的分支冒险和数据冒险问题。
下面这张图,是我自己画的一个简化版5级流水线结构。你仔细看看,数据是怎么从PC开始,一路走到寄存器堆的。
3.2 取指(IF)阶段设计
取指阶段的任务很简单:根据程序计数器(PC)的值,从指令存储器中读出指令,然后更新PC,为下一条指令做准备。
但简单归简单,里面有几个坑。我一个个说。
3.2.1 PC的更新逻辑
PC的更新,正常情况下是 PC = PC + 4(因为RISC-V指令长度固定为4字节)。但遇到分支或跳转指令时,PC需要被修改为目标地址。这就涉及到分支预测了——不过咱们这一章先不展开,先看最简单的顺序执行情况。
// 简化的PC更新逻辑(Verilog风格)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pc <= 32'h8000_0000; // 复位地址
else if (branch_taken)
pc <= branch_target; // 分支跳转
else
pc <= pc + 4; // 顺序执行
end
3.2.2 指令存储器的实现
在FPGA上,指令存储器通常用Block RAM(BRAM)来实现。BRAM是FPGA内部的专用存储资源,读写速度快,而且不占用逻辑单元。
我记得第一次用BRAM时,没注意它的读延迟。BRAM的读操作默认是有一个时钟周期的延迟的。也就是说,你给地址,下一个时钟周期才能拿到数据。这个延迟在流水线设计中必须考虑进去。
| 存储器类型 | 读延迟 | 资源消耗 | 适用场景 |
|---|---|---|---|
| 分布式RAM(LUT) | 0周期(组合逻辑) | 高(占用逻辑资源) | 小容量、高速缓存 |
| Block RAM(BRAM) | 1周期(同步读) | 低(专用存储块) | 大容量指令/数据存储器 |
| UltraRAM(URAM) | 2-3周期 | 极低(超大容量) | 大型SoC的共享存储器 |
说白了,如果你用BRAM做指令存储器,取指阶段实际上需要两个时钟周期:一个周期给地址,下一个周期才能拿到指令。这时候,流水线的IF阶段就要做相应的调整——要么把BRAM的读延迟算进流水线里,要么在IF阶段内部做一个“等待”状态。
3.3 译码(ID)阶段设计
译码阶段的任务,是把取回来的指令“翻译”成控制信号。RISC-V的指令格式很规整,译码逻辑相对简单。但简单不代表可以马虎。
3.3.1 指令格式解析
RISC-V有六种基本指令格式:R型、I型、S型、B型、U型、J型。每种格式的字段位置不同。译码器的核心工作,就是从32位指令中提取出opcode、funct3、funct7、rs1、rs2、rd、立即数等字段。
// R型指令格式示例
// [31:25] funct7 | [24:20] rs2 | [19:15] rs1 | [14:12] funct3 | [11:7] rd | [6:0] opcode
// 译码逻辑片段
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];
// 立即数扩展(I型)
assign imm_i = { {21{instr[31]}}, instr[30:20] };
3.3.2 寄存器堆的读写
ID阶段需要读取寄存器堆。RISC-V有32个通用寄存器(x0~x31),其中x0硬编码为0,写入x0无效。
寄存器堆在FPGA上可以用分布式RAM或BRAM实现。我个人建议用分布式RAM,因为寄存器堆的读写延迟要求很高——ID阶段需要在同一个时钟周期内完成读操作,否则流水线就要停顿。
这里有一个关键点:写回(WB)阶段和译码(ID)阶段可能会同时访问同一个寄存器。这就是数据冒险的一种。解决方法是“转发”(forwarding),但那是后面章节的内容了。在ID阶段,我们至少要做到:如果读地址等于写地址,并且写使能有效,那么读出的数据应该是写回的数据,而不是寄存器堆中旧的数据。
3.4 数据通路概览
好了,IF和ID阶段讲完了。咱们把这两个阶段连起来,看看数据是怎么流动的。
数据通路,说白了就是指令和数据在处理器内部走的“路”。从IF阶段取指令,到ID阶段译码并读取寄存器,再到EX阶段执行运算,MEM阶段访问数据存储器,最后WB阶段写回结果。每一步都有对应的硬件模块和流水线寄存器。
下面这个表格,总结了每个阶段的核心操作和涉及的硬件模块:
| 流水线阶段 | 核心操作 | 主要硬件模块 | 输出到下一级的数据 |
|---|---|---|---|
| IF | 取指令、更新PC | PC寄存器、指令存储器(BRAM) | 指令、PC+4 |
| ID | 译码、读寄存器堆 | 译码器、寄存器堆、立即数扩展器 | 控制信号、rs1数据、rs2数据、立即数、PC+4 |
| EX | 算术逻辑运算、地址计算 | ALU、加法器、多路选择器 | 运算结果、访存地址、写回数据 |
| MEM | 数据存储器读写 | 数据存储器(BRAM) | 读出的数据、写回数据 |
| WB | 写回寄存器堆 | 多路选择器、寄存器堆写端口 | (无,数据写入寄存器堆) |
你想想看,一条指令从IF到WB,需要5个时钟周期。但因为有流水线,每个时钟周期都有5条指令在不同阶段同时执行。这就是流水线的威力。
嗯,这一章的内容就到这里。IF和ID阶段是流水线的基础,后面的EX、MEM、WB阶段都是在此基础上扩展的。你先把这两个阶段吃透,在FPGA上把代码写出来、仿真通过,再往下走。别急,硬件设计急不得。