第4章:单周期数据通路设计:取指、译码、执行、访存、写回五阶段模型

各位同学,今天我们来聊聊单周期数据通路。说白了,就是一条指令在一个时钟周期内走完取指、译码、执行、访存、写回这五个阶段。听起来很简单对吧?但这里面的门道,我当年可是踩过不少坑的。

4.1 五阶段模型概览

先给大家画个轮廓。这五个阶段,每个都有自己明确的任务:

  • 取指(IF):从指令存储器中取出指令
  • 译码(ID):解析指令,读取寄存器
  • 执行(EX):ALU干活,算地址或做运算
  • 访存(MEM):读写数据存储器
  • 写回(WB):把结果写回寄存器堆

你想想看,单周期意味着什么?意味着所有阶段必须在同一个时钟边沿之前完成。我刚开始做RISC-V处理器时,就因为这个理解不到位,导致时序收敛出了问题。

核心要点:单周期数据通路中,组合逻辑路径长度决定了最大时钟频率。路径越短,频率越高。

4.2 取指阶段(IF)设计

取指阶段,说白了就是PC(程序计数器)干活。PC指向当前指令地址,然后从指令存储器里读出指令。

我个人习惯把PC设计成带使能的寄存器。为什么?因为遇到流水线停顿或异常时,我们需要冻结PC。来看个简单的Verilog代码:

module if_stage (
    input  clk,
    input  rst_n,
    input  pc_en,       // PC使能
    input  [31:0] pc_next,
    output [31:0] pc,
    output [31:0] instr
);

    // PC寄存器
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            pc <= 32'h0;
        else if (pc_en)
            pc <= pc_next;
    end

    // 指令存储器(简单ROM模型)
    instr_mem u_instr_mem (
        .addr(pc),
        .dout(instr)
    );

endmodule

嗯,这里要注意:指令存储器的地址必须是字对齐的。我记得有次调试,发现取出来的指令全是乱的,查了半天才发现是地址没对齐。

4.3 译码阶段(ID)设计

译码阶段要做两件事:解析指令操作码,读取寄存器堆。这里有个关键点——寄存器堆的读端口是组合逻辑还是时序逻辑?

我建议用组合逻辑读端口。为什么?因为单周期内要完成读操作,时序逻辑会多一个周期延迟。我曾经在一个项目里用了时序读端口,结果整个数据通路时序全乱了。

module id_stage (
    input  [31:0] instr,
    input  [31:0] wb_data,    // 写回数据
    input  [4:0]  wb_addr,    // 写回地址
    input         wb_en,      // 写回使能
    output [31:0] rs1_data,
    output [31:0] rs2_data,
    output [31:0] imm         // 立即数
);

    // 寄存器堆
    regfile u_regfile (
        .clk(clk),
        .raddr1(instr[19:15]),
        .raddr2(instr[24:20]),
        .rdata1(rs1_data),
        .rdata2(rs2_data),
        .waddr(wb_addr),
        .wdata(wb_data),
        .wen(wb_en)
    );

    // 立即数生成
    imm_gen u_imm_gen (
        .instr(instr),
        .imm(imm)
    );

endmodule

避坑指南:我曾经在译码阶段忘记处理x0寄存器。x0是硬连线的0,读它必须返回0,写它必须被忽略。这个坑,很多新手都会踩。

4.4 执行阶段(EX)设计

执行阶段是ALU的主场。加减乘除、与或非、移位,全在这里完成。但单周期里,ALU的运算时间直接决定了时钟周期。

举个例子,乘法器如果用组合逻辑实现,延迟会很大。我建议对于复杂运算,要么拆成多周期,要么用流水线乘法器。单周期里,老老实实用加法器和移位器就够了。

module ex_stage (
    input  [31:0] rs1_data,
    input  [31:0] rs2_data,
    input  [31:0] imm,
    input  [3:0]  alu_ctrl,   // ALU控制信号
    output [31:0] alu_result,
    output        zero_flag
);

    // ALU核心
    always_comb begin
        case (alu_ctrl)
            4'b0000: alu_result = rs1_data + rs2_data;       // ADD
            4'b0001: alu_result = rs1_data - rs2_data;       // SUB
            4'b0010: alu_result = rs1_data & rs2_data;       // AND
            4'b0011: alu_result = rs1_data | rs2_data;       // OR
            4'b0100: alu_result = rs1_data ^ rs2_data;       // XOR
            4'b0101: alu_result = rs1_data << rs2_data[4:0]; // SLL
            default: alu_result = 32'h0;
        endcase
    end

    assign zero_flag = (alu_result == 32'h0);

endmodule

你想想看,如果ALU里放一个64位乘法器,那这个周期得多长?所以,单周期设计里,ALU的运算类型要精挑细选。

4.5 访存阶段(MEM)设计

访存阶段只对load/store指令有意义。其他指令,比如加法、逻辑运算,访存阶段就是透明的——数据直接旁路过去。

数据存储器的设计,我建议用同步写、异步读。为什么?因为单周期内,写操作必须在时钟上升沿完成,而读操作可以随时进行。

module mem_stage (
    input         clk,
    input         mem_write,   // 写使能
    input         mem_read,    // 读使能
    input  [31:0] mem_addr,
    input  [31:0] mem_wdata,
    output [31:0] mem_rdata
);

    // 数据存储器
    data_mem u_data_mem (
        .clk(clk),
        .we(mem_write),
        .addr(mem_addr),
        .wdata(mem_wdata),
        .rdata(mem_rdata)
    );

endmodule

注意:数据存储器的地址必须对齐。我曾经遇到一个bug,因为地址没对齐,导致load指令读出来的数据全是错的。调试了整整两天才发现问题。

4.6 写回阶段(WB)设计

写回阶段,就是把结果写回寄存器堆。结果可能来自ALU,也可能来自数据存储器。所以需要一个多路选择器。

我个人习惯把写回逻辑放在译码阶段一起处理。为什么?因为寄存器堆的写端口在译码阶段,这样数据流更紧凑。但要注意,写回的数据必须在这个时钟周期内稳定。

module wb_stage (
    input  [31:0] alu_result,
    input  [31:0] mem_rdata,
    input         reg_wb_sel,   // 0: ALU结果, 1: 存储器数据
    output [31:0] wb_data
);

    // 写回数据选择
    assign wb_data = reg_wb_sel ? mem_rdata : alu_result;

endmodule

4.7 五阶段整合与关键路径分析

好了,五个阶段都讲完了。现在把它们串起来,看看关键路径在哪里。

单周期数据通路的关键路径,通常是从PC出发,经过指令存储器、寄存器堆读端口、ALU、数据存储器,最后到寄存器堆写端口。这条路径有多长?

阶段 典型延迟(ns) 说明
取指 2.0 指令存储器读延迟
译码 1.5 寄存器堆读延迟 + 立即数生成
执行 3.0 ALU运算延迟(含加法器)
访存 2.0 数据存储器读延迟
写回 0.5 多路选择器延迟
总计 9.0 加上寄存器建立时间约10ns

看到没?总延迟约10ns,意味着最大时钟频率只有100MHz。这就是单周期的瓶颈。我当年做的一个项目,目标频率是200MHz,单周期根本跑不到,最后只能改成流水线。

总结:单周期数据通路设计简单直观,但性能受限。适合教学演示或低频率应用。实际项目中,我们通常用流水线来提升频率。

4.8 课后思考

最后留几个问题,大家想想:

  1. 如果ALU里放一个32位乘法器,关键路径会变成多少?
  2. 单周期里,分支指令怎么处理?PC的更新逻辑要改吗?
  3. 为什么寄存器堆的写操作必须用时钟同步?

这些问题,我在下一章讲流水线时会用到。大家先琢磨琢磨。

个人经验:单周期设计虽然简单,但它是理解流水线的基础。我建议每个做数字前端设计的同学,都亲手搭一遍单周期数据通路。踩过坑,才能真正理解流水线的价值。