4. CPU微架构基础
好,咱们今天来聊聊五级流水线的骨架。说白了,就是把一条指令的执行过程拆成五个独立的步骤,每个步骤由一个专门的硬件阶段来处理。这样做的好处很明显——吞吐量上去了。你想想看,如果每个时钟周期都能让一条指令进入流水线,那理想情况下,CPI(每指令周期数)就能接近1。
我个人习惯把五级流水线比作一条汽车装配线。每个工位只干一件事,干完就交给下一个工位。CPU里的指令也是这样,从取指到写回,一站一站往下传。
4.1 五级流水线各阶段详解
咱们先逐个看看每个阶段到底在干什么。我当年刚接触RISC-V时,总觉得这些阶段很抽象,后来画了几张时序图才彻底搞明白。
| 阶段 | 英文缩写 | 核心任务 | 关键硬件 |
|---|---|---|---|
| 取指 | IF | 从指令存储器中取出指令 | PC、指令存储器、加法器 |
| 译码 | ID | 解析指令、读取寄存器 | 译码器、寄存器堆 |
| 执行 | EX | 执行算术逻辑运算或计算地址 | ALU、旁路逻辑 |
| 访存 | MEM | 访问数据存储器(仅load/store) | 数据存储器 |
| 写回 | WB | 将结果写回寄存器堆 | 写回多路选择器 |
4.1.1 取指阶段 (IF)
取指阶段是整个流水线的起点。它的任务很简单:根据程序计数器(PC)的值,从指令存储器中取出对应的指令。同时,PC加4(假设是32位指令),为取下一条指令做准备。
嗯,这里要注意:如果遇到分支或跳转,PC的值会被后续阶段修改。所以取指阶段拿到的PC,不一定是顺序递增的那个。
关键信号:PC、PC+4、inst(取出的指令编码)
4.1.2 译码阶段 (ID)
译码阶段要干两件事:解析指令和读取寄存器。RISC-V的指令编码很规整,译码器能很快识别出指令类型(R型、I型、S型等),然后生成控制信号。
我记得第一次写译码器时,被opcode和funct3/funct7的组合搞得有点晕。后来发现,只要把指令格式的位域搞清楚,译码其实就是个查表的过程。
个人经验:译码阶段会同时读取rs1和rs2对应的寄存器值。即使这条指令不需要两个源操作数,读出来也没关系,后续阶段会用控制信号屏蔽掉无用数据。
4.1.3 执行阶段 (EX)
执行阶段是流水线的计算核心。ALU在这里完成加减乘除、逻辑运算、移位等操作。对于load/store指令,这里计算的是访存地址(基址+偏移量)。对于分支指令,这里计算目标地址并判断跳转条件。
说白了,这个阶段就是真正干活的地方。前面取指译码都是在做准备,后面访存写回都是在收尾。
4.1.4 访存阶段 (MEM)
访存阶段只对load和store指令有意义。对于其他指令(如算术运算),这个阶段只是把执行结果透传过去,不做任何操作。
我曾经在项目中遇到过一个问题:数据存储器的读使能没控制好,导致非load指令也去读存储器,白白浪费了功耗。后来加了一个mem_read控制信号,只在load指令时才拉高读使能。
4.1.5 写回阶段 (WB)
写回阶段是流水线的终点。它把计算结果(来自ALU或数据存储器)写回到寄存器堆的rd寄存器中。这里需要一个多路选择器,来决定写回的数据来源:是ALU结果,还是从存储器读出的数据。
注意:写回阶段只对需要写寄存器的指令有效(如R型、I型、load指令)。像分支、store这类指令,写回阶段什么都不做。
4.2 流水线寄存器设计
流水线寄存器是连接各个阶段的纽带。每个时钟周期,当前阶段的计算结果会被锁存到流水线寄存器中,下一个时钟周期再传给下一阶段。这样,各个阶段就能并行工作,互不干扰。
五级流水线需要4组流水线寄存器:
- IF/ID寄存器:锁存取指阶段的PC和指令
- ID/EX寄存器:锁存译码阶段的控制信号、操作数、立即数等
- EX/MEM寄存器:锁存执行阶段的ALU结果、分支信息等
- MEM/WB寄存器:锁存访存阶段的读出数据、写回地址等
你想想看,如果没有这些寄存器,每个阶段的输出会直接连到下一阶段的输入,那整个电路就变成了一个巨大的组合逻辑块,根本没法在时钟驱动下有序工作。
4.2.1 流水线寄存器的典型结构
每个流水线寄存器本质上就是一个D触发器阵列。时钟上升沿到来时,输入数据被锁存到输出端,直到下一个时钟沿才更新。
// 以ID/EX寄存器为例
module ID_EX_Reg (
input wire clk,
input wire rst_n,
// 来自译码阶段的输入
input wire [31:0] id_pc,
input wire [31:0] id_rs1_data,
input wire [31:0] id_rs2_data,
input wire [31:0] id_imm,
input wire [4:0] id_rd_addr,
input wire id_reg_write,
input wire id_mem_read,
input wire id_mem_write,
// 输出到执行阶段
output reg [31:0] ex_pc,
output reg [31:0] ex_rs1_data,
output reg [31:0] ex_rs2_data,
output reg [31:0] ex_imm,
output reg [4:0] ex_rd_addr,
output reg ex_reg_write,
output reg ex_mem_read,
output reg ex_mem_write
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
ex_pc <= 32'b0;
ex_rs1_data <= 32'b0;
ex_rs2_data <= 32'b0;
ex_imm <= 32'b0;
ex_rd_addr <= 5'b0;
ex_reg_write <= 1'b0;
ex_mem_read <= 1'b0;
ex_mem_write <= 1'b0;
end else begin
ex_pc <= id_pc;
ex_rs1_data <= id_rs1_data;
ex_rs2_data <= id_rs2_data;
ex_imm <= id_imm;
ex_rd_addr <= id_rd_addr;
ex_reg_write <= id_reg_write;
ex_mem_read <= id_mem_read;
ex_mem_write <= id_mem_write;
end
end
endmodule
避坑指南:我曾经在复位逻辑上吃过亏。如果复位时不清零控制信号(如reg_write、mem_read),流水线启动时可能会误写寄存器或误读存储器。所以,所有控制信号在复位时都必须清零。
4.2.2 控制信号的流水传递
控制信号(如reg_write、mem_read、mem_write)必须跟着数据一起在流水线中传递。你不能在译码阶段生成控制信号后就直接去控制执行阶段的ALU,因为时序上对不上。
举个例子:译码阶段在时钟周期1生成reg_write=1,这个信号必须锁存到ID/EX寄存器中,在时钟周期2才到达执行阶段。如果执行阶段需要这个信号来控制写回操作,那它还得继续传递到EX/MEM寄存器,再到MEM/WB寄存器,最终在写回阶段才被使用。
| 控制信号 | 生成阶段 | 使用阶段 | 传递路径 |
|---|---|---|---|
| reg_write | ID | WB | ID→EX→MEM→WB |
| mem_read | ID | MEM | ID→EX→MEM |
| mem_write | ID | MEM | ID→EX→MEM |
| alu_op | ID | EX | ID→EX |
4.3 五级流水线数据流图
下面这张SVG图展示了五级流水线的完整数据流。你可以看到指令从取指到写回,经过每一级流水线寄存器,最终完成执行。
4.4 流水线冒险的初步认识
流水线虽然提高了吞吐量,但也带来了冒险(hazard)问题。简单说,就是下一条指令依赖上一条指令的结果,但结果还没算出来。
冒险主要有三种:
- 结构冒险:硬件资源冲突。比如指令存储器和数据存储器共用同一个端口。
- 数据冒险:指令间数据依赖。比如add指令的结果要被下一条sub指令使用。
- 控制冒险:分支跳转导致流水线冲刷。