简介:CPU设计与体系结构实验指导手册(教师版)为高校计算机专业学生和教师提供了一套完整的CPU设计与体系结构实验教学方案,覆盖数字逻辑、计算机组成原理与计算机体系结构相关课程的实验环节。手册以配套实验箱与Vivado软件平台为实践基础,按照递进式结构展开:从数据运算实验中的定点加法、定点乘法入手,逐步深入到寄存器堆、算术逻辑单元、存储器实现,继而完成单周期CPU、多周期CPU以及静态5级流水线CPU的完整设计,课程设计拓展部分还涉及处理器系统优化,附录中给出了MIPS指令集与寄存器实现参考;每个实验均包含实验目的、设备、任务与设计步骤,便于教师按不同层次组织实验教学。从基本数据通路的搭建到流水线冲突处理,手册提供了完整讲解与设计参考。整份资源为PDF格式,共1个文件,压缩包大小6.09MB,目录清晰、可按章节直接查阅。目前已有680人浏览学习,既能作为教师的实验指导依据,也适合学生按实验顺序自学处理器内部结构、数据通路与流水线优化思路。
1. 这不是课程讲义,是一份能照着实机跑通的 MIPS CPU 实验地图
翻开这份《CPU 设计与体系结构实验指导手册》V1.0 教师版,封面标注 LS-CPU-EXB-003,内文对应的是 LS-CPU-EXB-002 教学实验系统。它不是标准教材的复读,而是一套从定点加法一路做到静态 5 级流水线的实验方案,共 7 个实验加 2 个课程设计,附录还给了 MIPS 指令集和 CP0 寄存器说明。适合两类人:一类是刚拿到实验箱、不知道第一步该点哪个按钮的学生;另一类是准备把实验课改成“真上板、真运行”的教师。它的核心价值在于把 LCD 触摸屏这类与 CPU 设计无关的板级驱动封装成黑盒,让你把精力全放在数据通路和控制逻辑上。这份资料值得下载,照着做一遍,等于完成了一轮完整的 CPU 设计训练。
2. 实验平台与第一个实验:Vivado 工程里的定点加法全流程
2.1 实验箱的硬件组成与软件工具链
先别急着写代码。LS-CPU-EXB-002 实验箱的硬件组成,手册在第一部分列得很清楚:一块 FPGA 实验板、一条下载线、一条串口线(含 USB 转串口适配器)、一根电源线。板载的 LCD 触摸屏已经由厂商做了硬件驱动,屏蔽了时序细节,你只需要在顶层模块里调用封装好的模块。手册里把它做成 dcp 网表文件,当作黑盒使用。这一点非常重要:如果你一上来就钻到 LCD 驱动的初始化时序里,半天就过去了,而它与 CPU 设计本身没有关系。
软件侧用的是 Vivado(手册里提到 Vivado 2012.3 版本),工程流程是新建工程、添加 RTL 源文件、添加约束文件、功能仿真、综合实现、下载上板。对新手,我建议把 Vivado 的工程目录固定在一个路径,不要放中文目录;本手册所有代码和工程名都是英文,这一点想省事就照做。
下面把硬件资源和对应的用途整理成一张表,做实验前对照着清点一次:
| 资源 | 用途 | 注意点 |
|---|---|---|
| FPGA 实验板 | 运行综合后的网表,承载 CPU 设计 | 上电前检查下载线连接 |
| 下载线 | 把 bitstream 写入 FPGA | 接触不良是上板失败的常见原因 |
| 串口线(USB 转串口) | 调试与数据交互备用 | 确认操作系统能识别虚拟串口 |
| LCD 触摸屏 | 输入操作数、显示结果 | 由 lcd_module 黑盒封装,顶层按固定端口对接 |
| Vivado 软件 | 编写、仿真、综合、下载 | 工程路径不要带空格和中文 |
实验一到实验七全部围绕“模块设计 → 仿真 → 上板演示”展开,后面每一个实验都建议严格走这条链路。
2.2 新建工程、添加源文件与约束文件
在 Vivado 里新建工程时,选择 RTL Project,并勾选 “Do not specify sources at this time”,这样可以先建空工程再手动添加源文件。器件型号在 Artix-7 系列下选 xc7a200tfbg676-2,这是手册配套实验板使用的型号,不要选错,否则综合时引脚约束可能对不上。
源文件组织上,定点加法实验只用一个主体模块 adder.v,加上一个外围展示模块 adder_display.v。adder.v 的参考实现如下:
`timescale 1ns / 1ps // 加法器:直接使用 "+",综合工具会自动映射到库里的加法器 module adder( input [31:0] operand1, // 加数1 input [31:0] operand2, // 加数2 input cin, // 低位进位 output [31:0] result, // 和 output cout // 向高位的进位 ); assign {cout, result} = operand1 + operand2 + cin; endmodule这段代码的逻辑说明:assign 右侧把 operand1、operand2 和 cin 相加,结果拼接成 33 位,低 32 位赋给 result,最高位赋给 cout。这种写法比手动搭全加器更优,因为综合工具会从库里选择面积和时序更合适的加法器实现。参数说明:输入输出全部是 32 位;cin 在 MIPS 加法指令中通常固定为 0,但保留这个端口方便后续扩展带进位加法指令。
外围展示模块 adder_display.v 的职责是:把加法器的输入输出接到 lcd_module 提供的接口上,用户通过触摸屏输入两个 32 位加数,触摸屏显示运算结果。lcd_module 以 dcp 网表形式添加。在 Vivado 中,通过 “Add or create design sources” 添加 .v 文件和 .dcp 文件。添加 dcp 时要注意:如果用 Vivado 2012.3,需要先解压再添加;新版本一般可以直接识别。添加完成后,工程管理区里 lcd_module 前面的问号会消失,表示模块已关联。
约束文件需要把顶层模块端口绑定到 FPGA 引脚。手册给了完整的 adder.xdc 示例,开头是时钟、复位、拨码开关和进位输入的绑定,后面是一大段 LCD 引脚绑定。大多数情况下,LCD 引脚约束是固定的,不要改;需要改的是你自己的输入输出端口,比如把 input_sel 绑到某个按键、把结果 led 绑到某个 LED。约束文件语法示例:
set_property PACKAGE_PIN AC19 [get_ports clk] set_property PACKAGE_PIN A3 [get_ports led_cout] set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports led_cout]这段约束文件的逻辑说明:PACKAGE_PIN 指定 FPGA 物理引脚,IOSTANDARD 指定该引脚使用的电平标准。参数说明:每个端口都要同时给出引脚和电平标准,只写引脚不写电平常见于新手工程,会导致实现阶段报错;LVCMOS33 是实验板上常见的电平标准,如果你额外接了其他电平域的外设,需要按原理图确认。
2.3 编写 testbench 与仿真检查
功能仿真阶段需要建立 testbench。手册给出的是最简形式:只产生输入激励,不自动比对结果,靠波形人工判断。下面这段 testbench 是可用的:
`timescale 1ns / 1ps module testbench; reg [31:0] operand1; reg [31:0] operand2; reg cin; wire [31:0] result; wire cout; adder uut ( .operand1(operand1), .operand2(operand2), .cin(cin), .result(result), .cout(cout) ); initial begin operand1 = 0; operand2 = 0; cin = 0; #100; end always #10 operand1 = $random; always #10 operand2 = $random; always #10 cin = {$random} % 2; endmodule逻辑说明:initial 块先把输入清零并等待 100ns,之后每隔 10ns 用 $random 刷新加数和进位。$random 是 Verilog 系统任务,每次调用生成一个 32 位随机数;cin 那句用拼接符 {$random} 取非负值,再对 2 取余得到 0 或 1。
参数说明:`timescale 1ns/1ps 意味着仿真时间单位是 1ns,精度是 1ps;always #10 中 10 表示 10ns。如果你希望模拟 100MHz 时钟,可以把 #10 改成 #5,但这里只是给加法器灌激励,没有时钟概念,不要把 10ns 理解成 CPU 时钟周期。
在 Vivado 中,把 testbench.v 添加到 simulation sources 后,要确保它被 Set as Top,否则仿真器可能把顶层模块当作 testbench。运行 Behavioral Simulation 后,32 位数据用二进制显示很难读,建议在波形窗口选中信号,右键 Radix 改为 Hexadecimal。手册里给了一组手工计算的核对数据:b897be71 + 42f24186 + 1 = fb89fff7,可以在波形里直接找到对应点验证。
仿真通过后,进入综合、实现、生成 bitstream,下载到 FPGA。上板前,至少用 report_timing_summary 检查有没有时序违例。教学实验对频率不敏感,但如果有大规模组合逻辑路径,比如 32 位加法器级联进位链,时序收敛失败时要考虑用流水寄存器切分。
3. 数据通路三大件:寄存器堆、ALU、存储器的实现要点
3.1 寄存器堆:读异步、写同步是教学 CPU 的默认约定
实验三寄存器堆实现,在 MIPS 教学 CPU 里的规格是 32 个 32 位寄存器,其中寄存器 0 恒为 0。寄存器堆通常有两个读端口和一个写端口,对应两条源寄存器读和一条目的寄存器写。教学 CPU 里默认读是组合逻辑,写是时钟沿触发,这是最稳妥的约定:读端口不依赖时钟,译码阶段可以直接拿到操作数;写端口在时钟上升沿触发,避免组合环路。
参考实现如下:
module regfile( input clk, input we, input [4:0] raddr1, raddr2, waddr, input [31:0] wdata, output reg [31:0] rdata1, rdata2 ); reg [31:0] regs [0:31]; always @(posedge clk) begin if (we) regs[waddr] <= wdata; end always @(*) begin rdata1 = (raddr1 == 5'b0) ? 32'b0 : regs[raddr1]; rdata2 = (raddr2 == 5'b0) ? 32'b0 : regs[raddr2]; end endmodule逻辑说明:写操作使用非阻塞赋值 <=,在时钟上升沿把 wdata 写入 waddr。读端口用 always @(*) 组合逻辑,并且对寄存器 0 做特殊处理,无论怎么读都返回 0。参数说明:waddr 和 raddr 都是 5 位,因为 MIPS 有 32 个寄存器;we 为写使能;clk 是全局时钟。输出 reg 类型只是为了能在 always 里赋值,综合出来仍是组合逻辑。
常见的坑是“写后读”行为。如果你在同一个时钟沿写入寄存器,又在同一时刻组合读,读到的可能是旧值也可能是新值,这取决于仿真器对事件队列的处理。教学 CPU 里通常让寄存器堆读操作发生在时钟低电平期间,写操作发生在时钟上升沿,这需要你在状态机里安排好读阶段和写阶段,不能想当然。
3.2 ALU 模块:用功能码扩展,别为每个指令单独写 always
实验四的 ALU 模块实现,是所有算术逻辑指令的汇聚点。MIPS 的 ALU 只需要支持 add、sub、and、or、slt 等少量运算,再用 zero 输出给 beq 分支判断用。因此接口设计成两个 32 位操作数和一个 4 位功能选择信号,输出 32 位结果和 1 位 zero。
建议写法是把 ALU 功能做成可枚举的 localparam,用一条 case 语句完成:
module alu( input [31:0] a, b, input [3:0] aluop, output reg [31:0] result, output zero ); localparam ALU_ADD = 4'd0, ALU_SUB = 4'd1, ALU_AND = 4'd2, ALU_OR = 4'd3, ALU_SLT = 4'd7; always @(*) begin case (aluop) ALU_ADD: result = a + b; ALU_SUB: result = a - b; ALU_AND: result = a & b; ALU_OR: result = a | b; ALU_SLT: result = ($signed(a) < $signed(b)) ? 32'd1 : 32'd0; default: result = 32'b0; endcase end assign zero = (result == 32'b0); endmodule逻辑说明:这个 ALU 是纯组合逻辑,aluop 决定具体运算。zero 信号由 result 是否为 0 得出,用于 beq 指令。参数说明:aluop 位宽取 4 位,是为后续扩展移位、异或等指令留空间;SLT 使用 $signed 比较,否则无符号数比较会把负数当成大正数,这是新手最容易翻车的地方。
有些参考设计会把 ALU 的控制信号直接复用指令的 funct 字段,但这会让控制逻辑耦合严重。我一般会在控制单元里把指令译码成统一的 aluop,再送给 ALU,这样新增指令时只需要改控制单元,不需要动 ALU 的 case 结构。
3.3 存储器:读异步、写同步,以及初始化
实验五存储器实现,对应 MIPS 里的数据存储器。教学 CPU 中数据存储器和指令存储器是两个独立模块,通常都做成读异步、写同步。读异步意味着读地址一变,读数据马上更新,不需要等时钟;写同步则是只有时钟沿到来且写使能有效时,数据才写入。这和 SRAM 的行为一致,也方便在单周期 CPU 里让 lw/sw 在一个周期内完成。
参考实现:
module data_mem( input clk, input we, input [31:0] addr, wdata, output reg [31:0] rdata ); reg [31:0] mem [0:1023]; initial begin $readmemh("mem.hex", mem); end always @(posedge clk) begin if (we) mem[addr[31:2]] <= wdata; end always @(*) begin rdata = mem[addr[31:2]]; end endmodule逻辑说明:地址线按字节寻址,但每个存储单元是 32 位,所以用 addr[31:2] 做索引,相当于忽略低 2 位,按字对齐。$readmemh 在仿真时可以加载初始化文件,上板时会被综合成 initial 值。参数说明:这里 mem 有 1024 个字,地址从 0 到 0x3FF;如果实验要求数据存储器更大,可以把数组上界改大,并同步调整地址译码逻辑。
特别提醒:$readmemh 只在仿真和部分综合流程里生效,用 Vivado 综合成 bitstream 时,initial 块里的 $readmemh 会被当成 ROM 初始化,但文件路径必须是综合时能访问到的相对路径。如果找不到文件,仿真时会拉 X 态。解决办法是在工程设置里把 hex 文件加入仿真库路径,或者改用相对路径从工程根目录算起。
4. 从单周期到多周期:控制信号与状态机的设计路径
4.1 单周期 CPU:用一份指令真值表生成控制信号
实验六单周期 CPU 实现,是数据通路第一次完整闭合。单周期的设计思想很简单:所有指令在一个时钟周期内完成取指、译码、执行、访存、写回。代价是时钟频率必须迁就最慢的那条指令,常见是 lw,因为要经过 PC → 指令存储器 → 寄存器堆 → ALU → 数据存储器 → 寄存器堆写回。
单周期 CPU 的重点不在数据通路,而在控制信号。你需要为每条指令画一张真值表,列出 RegDst、ALUSrc、MemtoReg、RegWrite、MemRead、MemWrite、Branch、Jump 等控制信号。手册附录 A1 列出的单周期指令是 MIPS 的子集,通常包括:
| 指令类别 | 代表指令 | 控制信号要点 |
|---|---|---|
| R 型 | add, sub, and, or, slt | RegDst=1, RegWrite=1, MemWrite=0 |
| I 型算术 | addi, andi, ori | ALUSrc=1, RegWrite=1 |
| 访存 | lw, sw | lw: MemtoReg=1, RegWrite=1;sw: MemWrite=1 |
| 分支 | beq | Branch=1, RegWrite=0 |
| 跳转 | j | Jump=1, RegWrite=0 |
控制信号生成用一条 case 语句就能写完,避免多层 if 嵌套:
always @(*) begin {regwrite, memwrite, branch, jump, memtoreg, alusrc, regdst} = 7'b0; case (opcode) 6'b000000: begin // R型 regwrite = 1'b1; regdst = 1'b1; alusrc = 1'b0; end 6'b100011: begin // lw regwrite = 1'b1; memtoreg = 1'b1; alusrc = 1'b1; end 6'b101011: begin // sw memwrite = 1'b1; alusrc = 1'b1; end 6'b000100: branch = 1'b1; // beq 6'b000010: jump = 1'b1; // j endcase end逻辑说明:case 语句根据 opcode 置位对应的控制信号。这里先默认全部为 0,再按指令逐项打开,保证未列出的指令不会意外写入寄存器或存储器。参数说明:opcode 是 6 位,来自指令 [31:26];R 型指令还需要根据 funct 字段进一步区分 add/sub,这部分由 ALU 控制单元完成。
常见做法是分两层译码:主译码器给 ALUSrc、RegWrite 等,ALU 译码器根据 funct 和 opcode 给出 aluop。如果你在实验课上做到这里,我建议先用表格列出每条指令的控制信号,再写代码,否则 debug 时面对波形完全不知道哪里错。
4.2 多周期 CPU:把指令拆成取指、译码、执行、访存、写回五步
实验七多周期 CPU 实现,本质上是把一条指令拆成多个时钟周期执行,每个周期只做一件事。MIPS 多周期的经典划分是:
- 取指(IF):PC 送到指令存储器,指令写入 IR,PC+4
- 译码(ID):读寄存器堆,结果存入 A、B;立即数扩展存入 Imm
- 执行(EX):ALU 计算地址或运算结果,存入 ALUOut
- 访存(MEM):lw/sw 访问数据存储器
- 写回(WB):把结果写回寄存器堆
多周期 CPU 的控制不再是一张真值表,而是一个有限状态机。状态机设计用 Moore 型比 Mealy 型容易理解,每个状态输出一组控制信号,状态转移由指令类型决定。
下面给出一个简化框架:
localparam S_IF = 3'd0, S_ID = 3'd1, S_EX = 3'd2, S_MEM = 3'd3, S_WB = 3'd4; reg [2:0] state, next_state; always @(posedge clk) begin if (!resetn) state <= S_IF; else state <= next_state; end always @(*) begin next_state = S_IF; case (state) S_IF: next_state = S_ID; S_ID: begin case (opcode) 6'b000000: next_state = S_EX; // R型 6'b100011, 6'b101011: next_state = S_EX; // lw/sw default: next_state = S_IF; // 分支或跳转 endcase end S_EX: begin if (opcode == 6'b100011 || opcode == 6'b101011) next_state = S_MEM; else next_state = S_WB; end S_MEM: next_state = S_WB; S_WB: next_state = S_IF; endcase end逻辑说明:这个状态机把 R 型、访存型指令分别导向不同的后续状态。注意 S_EX 之后,lw/sw 要进 S_MEM,其他运算指令直接进 S_WB。分支指令 beq 在 ID 阶段就能决定是否跳转,所以 ID 之后直接回到 S_IF。参数说明:状态编码用 3 位,这里只给了一个简化的 5 状态版本;实际多周期 CPU 还需要把 lw 的 S_MEM 和 sw 的 S_MEM 区分开,因为 sw 不需要写回寄存器。每条指令的控制信号随状态变化,而不是始终有效,这是多周期和单周期最本质的区别。
多周期 CPU 的价值在于降低长路径延迟:单周期里 lw 要在一个周期内完成全链路,多周期可以把这条链路切到多个周期里,让周期变短、频率提高。代价是每条指令需要的周期数不同,CPI 不再是 1,控制逻辑也更复杂。手册把实验七放在课程设计之前,就是让你先理解状态机,再去碰流水线的并行。
从实际调试角度看,多周期 CPU 比单周期难排错。我一般会在状态机中加入单步模式:把 clk 信号用一个按键脉冲代替,每按一次走一个状态,再通过 LCD 显示当前状态和 PC 值。这个技巧在手册里没有写,但实验箱的按键资源足够实现,强烈建议做实验时加上。
5. 避坑与排查:上板验证中的四个典型问题
很多同学的实验报告写到“仿真正确,上板失败”,其实问题都出在工程使用、约束或时序处理上。这里把我在指导这个实验箱使用时遇到的高频问题整理成四条,每一条都按照现象、原因、解决三个维度来说。
5.1 lcd_module 添加后仍然显示问号,综合报错找不到模块
现象:在 Vivado 工程管理区里,adder_display 下面的 lcd_module 前面一直带问号,启动综合时报错,提示某个实例找不到对应模块。
原因:lcd_module 是以 dcp 网表文件形式提供的,dcp 文件没有正确添加到工程里,或者添加的 dcp 与当前 Vivado 版本不兼容。手册特别说明,Vivado 2012.3 下需要先解压 dcp 文件再添加;新版本可能直接支持,但工程里如果同时存在旧版本缓存,也会导致识别失败。
解决:先把 dcp 文件从只读目录复制到工程目录下,再通过 Add Sources 添加为 design source。添加后检查 Sources 面板,确认 lcd_module 实例的名称和 dcp 顶层模块名完全一致。如果仍然失败,试试把工程清理缓存后重开。不要尝试打开 dcp 内部代码,它是黑盒。
5.2 testbench 设置为顶层了,但仿真波形窗口里没有内部信号
现象:运行行为仿真后,波形窗口只有几个端口,想看的中间信号添加后,点击 Relaunch 也没有反应,甚至仿真报错。
原因:仿真时顶层没有正确指定。testbench.v 必须作为 simulation top;如果工程里原本有 RTL 顶层,仿真器默认选择了综合顶层,就不会主动执行 testbench 里的 initial 块。另外,在波形窗口添加信号后必须重新运行仿真才能看到数据,手册里明确提示需要点击 Relaunch。
解决:在 Sources 面板里右键 testbench.v,选择 Set as Top,然后重新点击 Run Simulation。添加信号后,点击 Restart 并从 0 时刻重新运行,或者直接 Relaunch。仿真工具栏里第一个按钮就是“从 0 时刻开始仿真”,不要只在当前时刻点运行,否则可能错过信号更新的边沿。
5.3 $random 产生的随机数出现负值,加法结果带 X 态
现象:仿真时 result 波形中段出现红色的 X 或 Z,而加法器明明是纯组合逻辑,不应该出现不定态。
原因:$random 在 Verilog 中返回的是有符号的 32 位整数,可能为负数。负数的二进制表示在高位是 1,如果直接赋给 reg 型信号,会在后续比较或拼接时产生意想不到的扩展。更常见的原因是 testbench 里没有给所有输入初始化,或者 0 时刻采样竞争导致仿真器读到未初始化寄存器的值。
解决:对随机数做无符号处理。手册里那句cin = {$random} % 2就是标准做法:用拼接符 {$random} 把它转成非负数,再取余。对 32 位操作数,建议用operand1 = {$random};而不是operand1 = $random;。同时,initial 块里先给所有输入赋 0,等 100ns 再开始随机激励,避免 0 时刻采样。
5.4 上板后 LCD 无显示或触摸无反应,但仿真正确
现象:bitstream 下载成功,led 指示灯正常,但 LCD 屏不亮或触摸按键没有反应。用串口调试也得不到预期数据。
原因:这类问题九成出在约束文件。LCD 引脚的 PACKAGE_PIN 或 IOSTANDARD 写错,或顶层模块的端口名和 xdc 里 get_ports 的名字不一致。另一个常见原因是时钟引脚没有用全局时钟资源,或者 resetn 信号的有效电平和板卡假设相反。
解决:先核对 xdc 中 lcd 相关引脚和产品原理图是否一致,重点检查 lcd_rst、lcd_cs、lcd_rs、lcd_wr、lcd_rd、lcd_bl_ctr 这些控制信号。如果端口名和约束文件不匹配,Vivado 会在实现阶段报错;不报错但上板无反应,多半是复位电平写反或背光控制逻辑不对。最直接的检查方法:先用手册提供的 demo bitstream 下载,如果 demo 能亮,说明硬件正常,问题在自己代码或约束;如果 demo 也不亮,先检查下载线和电源。
5.5 综合通过但实现报引脚约束冲突,或者时序不收敛
现象:综合阶段没有错误,进入 Implementation 后报错,提示 IO placement failed 或时序路径不满足。有时候是多个信号被绑定到了同一个引脚。
原因:xdc 文件里可能存在重复的 set_property,或者你新增的调试信号借用了 LCD 已占用的引脚。另一个原因是设计中用了板载外设但没有添加对应的时钟约束,导致 Vivado 无法估算路径延迟。
解决:在工程中只保留一份 xdc,并检查是否有重复的 PACKAGE_PIN。使用 report_io 命令可以列出所有 IO 的分配情况,确认没有冲突。对时序不收敛,先给时钟引脚加 create_clock 约束,再把时钟频率设置为实验手册建议的安全值,比如 50MHz;如果还收敛不了,检查是否有组合逻辑环路,尤其注意 ALU 的 zero 信号和分支判断之间容易出环路。
6. 进阶技巧:静态 5 级流水线 CPU 的落地顺序与验证方法
手册的课程设计部分,把静态 5 级流水线 CPU 放在多周期之后,意图很明确:先掌握状态机,再做流水线。所谓静态流水线,是指指令按固定顺序流过取指、译码、执行、访存、写回五级,不实现动态调度。这个设计的难点不在五级本身,而在于冒险处理。
建议的落地顺序是:第一,先把多周期 CPU 的五个阶段映射成流水寄存器。IF/ID、ID/EX、EX/MEM、MEM/WB 四个流水寄存器是必须的,每个寄存器的位宽要仔细核对。我一般会把 PC、指令、操作数、立即数、控制信号全部打包放进流水寄存器,而不是散落成多个独立寄存器,这样综合后更清晰,也不容易漏信号。
第二,解决数据冒险。静态流水线里,最常见的冲突是相邻指令的寄存器写后读。处理策略分两级:
| 冒险类型 | 处理策略 | 代价 |
|---|---|---|
| ALU 结果写后读 | 在 EX/MEM、MEM/WB 阶段向 ID/EX 阶段转发 | 增加组合逻辑比较 |
| load-use 冒险 | 插入一个气泡,加载指令后暂停一拍 | CPI 增加 1 |
| 分支冒险 | 采用冻结或冲刷,取指暂停到分支结果出来 | 分支延迟 1~2 周期 |
第三,寄存器堆建议改成写前读策略:在一个时钟周期内,先执行写操作,再执行读操作。这样能消除同一周期内写后读的冲突。实现上就是把寄存器堆的写沿安排在时钟下降沿,或者使用写优先的读逻辑。手册附录 B 里提的 CP0 寄存器,可以顺带做成异常处理的状态保存,为流水线预留中断入口。
验证方法上,我会写一段固定的测试程序,包含 add、sub、lw、sw、beq、j 这几类指令,让数据冒险和 load-use 冒险各出现一次。上板后用 LCD 显示器逐条显示寄存器值,或者通过串口回传。如果第一条 lw 后面紧跟 add 的用例能跑出正确结果,基本说明转发逻辑正确;如果不正确,先把转发路径全部禁用,改成只在 load-use 时插入气泡,再逐步打开转发。那一次我排查了两天,发现是 MEM/WB 转发到 ID/EX 时,目标寄存器号用了写回阶段的 rd,没有提前一拍送到译码级。从那以后,我每次接手这类教学 CPU 工程,都强制先验证“单周期 → 多周期 → 流水线加转发”的完整链路,再做性能优化。希望帮到你。
本文还有配套的精品资源,点击获取