4. 处理器微架构设计(一):单周期CPU数据通路设计、ALU设计、寄存器文件设计

各位同学,欢迎来到处理器微架构设计的第一站。

说实话,很多初学者一听到「微架构」三个字就头大。觉得那是大神才碰的东西。其实没那么玄乎。你想想看,处理器本质上就是个复杂的有限状态机,加上一堆数据通路。今天我们就从最简单的单周期CPU开始,把它的骨架搭起来。

4.1 单周期CPU:最朴素的实现方式

什么叫单周期?说白了,就是一条指令从取指、译码、执行、访存到写回,全部在一个时钟周期内完成。听起来很暴力对吧?

我在刚入行时,带我的老工程师跟我说:「单周期CPU虽然慢,但它是最清晰的教学模型。你把这个搞懂了,后面搞流水线就是水到渠成的事。」后来我发现,他说得真对。

核心要点:单周期CPU的CPI(每指令周期数)恒为1。但它的时钟频率受限于最慢的那条指令——通常是load指令,因为它要经历完整的五级操作。

单周期CPU的数据通路,我习惯把它拆成五个阶段来看:

  1. 取指(IF):从指令存储器中取出当前PC指向的指令
  2. 译码(ID):解析指令,生成控制信号,读取寄存器
  3. 执行(EX):ALU干活,计算地址或算术结果
  4. 访存(MEM):读写数据存储器(仅load/store需要)
  5. 写回(WB):将结果写回寄存器文件

嗯,这里要注意:单周期里这些阶段是串行完成的,所以组合逻辑路径特别长。这也是为什么现代处理器没人用纯单周期——太慢了。

4.2 数据通路整体结构

我们先画一张整体框架图,把各个模块之间的关系理清楚。

单周期CPU数据通路结构图 PC 指令存储器 寄存器文件 ALU 数据存储器 控制单元 地址 指令 rs1, rs2 结果 数据 PC + 4

这张图我故意画得比较简洁。实际项目中,数据通路上会有大量的多路选择器(MUX),用来在不同指令间切换数据来源。比如写回寄存器时,你要决定是写ALU结果还是写存储器读出的数据。

我的经验:刚开始设计数据通路时,别急着写代码。先在纸上把数据流画清楚。我见过太多人一上来就写Verilog,结果写到一半发现通路不通,回头改架构,浪费时间。

4.3 ALU设计:处理器的计算核心

ALU(算术逻辑单元)是处理器里最忙的模块。每条指令几乎都要经过它——要么做算术运算,要么算地址。

对于RISC-V,我们至少需要支持以下操作:

操作码 功能 说明
ADD 加法 rs1 + rs2
SUB 减法 rs1 - rs2
AND 按位与 rs1 & rs2
OR 按位或 rs1 | rs2
XOR 按位异或 rs1 ^ rs2
SLT 有符号小于 rs1 < rs2 ? 1 : 0
SLTU 无符号小于 同上,无符号比较
SLL 逻辑左移 rs1 << rs2[4:0]
SRL 逻辑右移 rs1 >> rs2[4:0]
SRA 算术右移 高位补符号位

下面是我常用的ALU Verilog代码。注意,我习惯用组合逻辑实现,因为单周期里ALU必须在同一个周期内出结果。

module alu (
    input  [31:0] a,        // 操作数1
    input  [31:0] b,        // 操作数2
    input  [3:0]  alu_ctrl, // 控制信号
    output reg [31:0] result,
    output reg          zero    // 结果为0时拉高
);

    always @(*) begin
        case (alu_ctrl)
            4'b0000: result = a + b;           // ADD
            4'b0001: result = a - b;           // SUB
            4'b0010: result = a & b;           // AND
            4'b0011: result = a | b;           // OR
            4'b0100: result = a ^ b;           // XOR
            4'b0101: result = ($signed(a) < $signed(b)) ? 32'd1 : 32'd0; // SLT
            4'b0110: result = (a < b) ? 32'd1 : 32'd0;                   // SLTU
            4'b0111: result = a << b[4:0];    // SLL
            4'b1000: result = a >> b[4:0];    // SRL
            4'b1001: result = $signed(a) >>> b[4:0]; // SRA
            default: result = 32'd0;
        endcase
        zero = (result == 32'd0);
    end

endmodule

避坑指南:我曾经在SRA(算术右移)上栽过跟头。Verilog的 >>> 操作符只有在操作数声明为signed时才会做符号扩展。如果你直接用 wire [31:0] a,那么 a >>> b 实际上还是逻辑右移。记得用 $signed() 转换,或者把a声明为 signed wire。

4.4 寄存器文件设计:数据的临时仓库

寄存器文件是处理器的本地存储。RISC-V架构有32个通用寄存器,每个32位宽(RV32)。

设计要点:

  • 两个读端口,一个写端口:这是RISC-V指令集的要求。大多数指令需要读两个源寄存器(rs1, rs2),写一个目标寄存器(rd)。
  • x0寄存器硬连线为0:这是RISC-V的一个巧妙设计。x0永远读出0,写入被忽略。省掉了一条「清零」指令。
  • 写优先 vs 读优先:如果在同一个周期内读写同一地址,写优先模式会先写入再读出新值;读优先模式则读出旧值。RISC-V规范要求采用写优先。

来看代码:

module regfile (
    input  clk,
    input  rst_n,
    input  [4:0] rs1_addr,   // 读端口1地址
    input  [4:0] rs2_addr,   // 读端口2地址
    input  [4:0] rd_addr,    // 写端口地址
    input  [31:0] rd_data,   // 写数据
    input  we,               // 写使能
    output [31:0] rs1_data,  // 读端口1数据
    output [31:0] rs2_data   // 读端口2数据
);

    reg [31:0] regs [0:31];  // 32个32位寄存器

    // 写操作(时序逻辑)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            integer i;
            for (i = 0; i < 32; i = i + 1)
                regs[i] <= 32'd0;
        end else if (we && (rd_addr != 5'd0)) begin
            regs[rd_addr] <= rd_data;
        end
    end

    // 读操作(组合逻辑)
    // 注意:x0永远返回0
    assign rs1_data = (rs1_addr == 5'd0) ? 32'd0 : regs[rs1_addr];
    assign rs2_data = (rs2_addr == 5'd0) ? 32'd0 : regs[rs2_addr];

endmodule

关键设计决策:读操作我用组合逻辑实现,这样在同一个周期内就能拿到数据。如果读操作也做成时序的(加一级寄存器),那单周期CPU的时序就绷不住了——你得多等一个周期才能读到寄存器值。

4.5 把它们连起来:数据通路的整合

有了ALU和寄存器文件,再加上PC、指令存储器、控制单元,我们就可以搭出一个能跑R型指令(如add, sub, and, or)的单周期CPU了。

控制单元负责根据指令的opcode和funct3/funct7字段,生成一系列控制信号:

  • RegWrite:是否写寄存器
  • ALUSrc:ALU的第二个操作数来自寄存器还是立即数
  • ALUCtrl:选择ALU执行哪种运算
  • MemWrite:是否写数据存储器
  • MemToReg:写回寄存器的数据来自ALU还是存储器

我习惯把这些控制信号做成一个查找表。不同的指令类型对应不同的控制信号组合。这样代码结构清晰,也方便后期扩展。

我的习惯:写控制单元时,我会先列一个Excel表格,把每条指令对应的控制信号填进去。然后对着表格写case语句。这样不容易漏掉某个指令,也方便review。

4.6 单周期CPU的局限性

讲到这里,我得泼点冷水。单周期CPU虽然好理解,但实际项目中几乎没人用。原因很简单:

  • 时钟频率上不去:最慢的指令(load)决定了整个系统的时钟周期。其他指令明明可以跑得更快,却被拖累了。
  • 资源利用率低:每个模块在每个周期只工作一小段时间,大部分时间在空转。
  • 不支持流水线:无法通过指令级并行提高吞吐量。

我记得有一次做FPGA原型验证,用单周期CPU跑一个简单的排序算法,结果时钟频率只能跑到25MHz。换成5级流水线后,直接飙到100MHz。差距就是这么大。

但话说回来,单周期CPU是理解处理器工作原理的最佳起点。你把它吃透了,后面学流水线、乱序执行、分支预测,都会轻松很多。


专注资料整理