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图展示了五级流水线的完整数据流。你可以看到指令从取指到写回,经过每一级流水线寄存器,最终完成执行。

五级流水线数据流图 五级流水线数据流图 取指 (IF) PC → 指令存储器 译码 (ID) 解析指令 → 读寄存器 执行 (EX) ALU运算 / 地址计算 访存 (MEM) load/store 数据存储器 写回 (WB) 结果写回寄存器堆 IF/ID ID/EX EX/MEM MEM/WB 说明 每个时钟周期,指令在阶段间前进一级。流水线寄存器在时钟上升沿锁存数据,保证各阶段并行工作。 控制信号(如reg_write、mem_read)随数据一起在流水线中传递,在对应阶段才被使用。

4.4 流水线冒险的初步认识

流水线虽然提高了吞吐量,但也带来了冒险(hazard)问题。简单说,就是下一条指令依赖上一条指令的结果,但结果还没算出来。

冒险主要有三种:

  • 结构冒险:硬件资源冲突。比如指令存储器和数据存储器共用同一个端口。
  • 数据冒险:指令间数据依赖。比如add指令的结果要被下一条sub指令使用。
  • 控制冒险:分支跳转导致流水线冲刷。