简介:基于MIPS指令集的动态流水线CPU设计实现,源于同济大学计算机系统结构课程项目,完整覆盖取指、译码、执行、访存、写回五个阶段,并以Verilog HDL作为主要开发语言。资源面向计算机系统结构课程学习者、数字逻辑课程设计学生及CPU流水线技术爱好者,能够帮助梳理数据冒险、控制冒险、转发与暂停机制以及动态调度策略等关键知识点。压缩包共154个文件,核心为34个Verilog源文件,另有VHDL/VHD工程文件、ModelSim/Questa的do仿真脚本、coe与mif存储器初始化数据、xdc约束文件以及sh自动化脚本,另含说明文档、工程配置等辅助材料;整体仅756KB,轻量且按模块分区组织,便于对照理解。当前已有65人学习下载。解压后即可借助Xilinx Vivado/ISE或ModelSim进行仿真验证,也可参考工程组织方式将模块移植到Nexys等FPGA开发板上,适合作为课程设计实战项目参考、期末答辩或自学进阶使用。
1. 一个看得见信号冲突的MIPS动态流水线CPU:课程设计为什么值得自己写一遍
先抛个反直觉结论:五级流水线里最难的不是“取指、译码、执行、访存、写回”这五个阶段本身,而是“让冲突成为常态,然后有条不紊地收拾冲突”。这份某高校计算机系统结构课程项目的MIPS动态流水线CPU资源,恰好属于后者——它不是那种把五条指令依次走完就交差的静态流水线,而是在经典五级结构上加入了动态分支预测、数据转发与停顿控制,把信号竞争、冒险处理、流水线刷新这些真正的硬件难点暴露在你面前。适合的人群很明确:正在准备计算机系统结构课程设计、需要交付仿真波形与上板结果的学生,以及想把五级流水线从“纸上框图”变成“能跑sin、addi、beq、lw/sw完整指令序列”的Verilog作品集的开发者。这份资源能解决的核心问题,就是让你在动手前先看清楚流水线寄存器的每一个字段、每一条转发路径、每一拍停顿到底在干什么,而不是拿别人的顶层电路瞎改信号名。
2. 指令集与数据通路:先把地基做窄,流水线才能做深
2.1 固定的32位宽度:R型、I型与J型指令的位域切法
MIPS之所以适合做课程设计,是因为它的指令宽度固定为32位,且编码格式高度规整。资源里的处理器主基调就是MIPS子集,不在指令数量上堆功能,而是把每条指令的编码位域切得清清楚楚。常规做法是支持三类格式:R型指令负责寄存器到寄存器的运算,I型指令覆盖立即数运算、加载、存储和有条件跳转,J型指令只保留一条无条件的j跳转。这样做的意义在于,译码逻辑可以直接从固定的位域抽取信号,不需要像x86那样做变长解码,五级流水线的每一级都能在单个时钟周期内完成职责。
| 字段 | R型(add/sub/and/or/slt) | I型(addi/lw/sw/beq/bne) | J型(j) |
|---|---|---|---|
| opcode | 000000 | 各指令独立取值(如addi为001000) | 000010 |
| rs | bit[25:21] | bit[25:21] | 不关心 |
| rt | bit[20:16](R型为目的寄存器之一) | bit[20:16](rt作为源或目的) | 不关心 |
| rd | bit[15:11](R型目的寄存器) | 不关心 | 不关心 |
| shamt | bit[10:6] | 不关心 | 不关心 |
| funct | bit[5:0](区分add/sub/and/or/slt) | 不关心 | 不关心 |
| immediate/address | 不关心 | bit[15:0](立即数或地址偏移) | bit[25:0](26位字地址) |
拿到资源后第一步,就是去看它的instruction_decoder模块,确认各个位域切片是否与上表一致。我见过不少翻车案例是rs与rt写反,导致add和addi行为错乱,排错时看波形全是“合理但错误”的值,非常折磨。资源里每条指令的定义如果带注释,直接把注释逐行核对到你的数据通路图上,比先看代码再看图高效得多。
一个容易忽略的细节是符号扩展。I型指令的立即数在addi、lw、sw里是有符号扩展的,而在逻辑指令场景下可能需要零扩展,如果只接一个统一的扩展器,后续ALU运算和地址计算的符号就会出偏差。设计里的extend选择信号,建议在ID级就分开两条路径:一条给算术运算,另一条给地址计算,后续写beq判断时也要基于符号扩展后的值。
2.2 数据通路模块边界:取指、译码、执行、访存、写回分别管什么
把数据通路按模块边界切开,是这份资源能落地的第二个关键。很多初学者喜欢把整个CPU堆在同一个module里,信号一多就变成一团乱麻,仿真好几天找不出问题。这里的设计思路是每个阶段各司其职,阶段之间只用流水线寄存器传数据,模块内部的wire名字统一带阶段前缀,比如IF_IR、ID_RS、EX_ALUOUT、MEM_LD_DATA、WB_REG_WDATA这种风格。
取指级只管PC、PC+4、指令存储器读使能。ID级负责读寄存器堆、做立即数扩展、生成控制信号,并把源操作数传递到下一级。EX级是ALU计算和分支判断所在处,也是数据转发输入汇聚点。MEM级管数据存储器读写,以及分支预测错误时的写回抑制。WB级把结果写回寄存器堆,是整个流水线的终点,也是数据冒险依赖链上最远的一环。
我在拆这个资源时比较认可的一个点在模块边界,防止在MEM级直接访问寄存器堆。课程设计里很多人图省事,在访存级就写寄存器,导致WB级形同虚设,写回端口出现多条路径。一旦遇到连续两条指令一条写寄存器、一条读同一寄存器,时序就会变得混乱。资源里如果看到MEM级寄存器堆写使能为零,那说明设计意图是统一从WB级写回,应该保持这个约束,不要自己加“捷径”。
2.3 信号的命名与宽度:控制器输出与流水线寄存器怎么对接
控制信号是流水线的毛细血管。以经典五级流水的纵横式控制来看,控制器在ID级产出一组水平控制信号,比如RegWrite、MemWrite、MemRead、ALUSrc、RegDst、Branch、Jump、MemToReg等。它们已经通过ID/EX寄存器进入流水线,后面每一级都携带各自需要的控制位,严禁在EX级以后重新生成控制信号。如果debug时发现某个信号直接从控制器拉到MEM级,那等于把流水线结构架空了。
资源里比较讲究的做法是把控制信号按“阶段生命周期”分组。参考常见的课程设计实现,我会按如下宽度组织流水线寄存器:
- IF/ID寄存器:32位指令字、32位PC+4,关键是把分支目标地址提前计算还是延迟到ID级计算。
- ID/EX寄存器:源操作数rs_data、rt_data各32位,扩展立即数32位,PC+4 32位,控制信号若干位,以及rt/rd的寄存器号各5位。
- EX/MEM寄存器:ALU结果32位、写数据32位、分支目标地址32位,控制信号若干位。
- MEM/WB寄存器:访存读出数据32位、ALU结果32位,以及写寄存器号5位和写使能控制。
注意发射时rt、rd都带着,写回级根据RegDst选择哪个寄存器号生效。这个设计允许RegDst在WB级才做最终选定,为转发逻辑留下了明确的目标地址。控制信号的位宽越大越容易出错,所以resource通常会给ID/EX控制信号合并成一个受限的控制向量,再在每级用assign拆分出来。这样的好处是,stall时只需要冻结合并向量,而不是逐信号写enable逻辑。
3. 五级流水线的实现:把每一级拆成独立的寄存器与计算单元
3.1 从IF到WB:每一级职责和流水线寄存器字段
五级流水线的物理本质,是四个流水线寄存器把五个组合逻辑块串联起来。资源里会看到IF/ID、ID/EX、EX/MEM、MEM/WB这四组寄存器定义,每一组的关键字段都值得你先单独核对一遍。下面按最常见的课程设计参数给出一份参考位宽表:
| 流水线寄存器 | 关键字段示例 | 位宽 | 作用 |
|---|---|---|---|
| IF/ID | PC_plus4、Instruction | 64位左右 | 锁定取指结果,供ID级解码 |
| ID/EX | rs_data、rt_data、imm_ext、rd/rt编号、控制向量 | 130位以上 | 锁定译码产物,供EX级运算 |
| EX/MEM | ALU_result、write_data、branch_target、控制向量 | 100位以上 | 锁定执行结果,供MEM级访存 |
| MEM/WB | mem_read_data、alu_result、reg_dst、RegWrite | 80位以上 | 锁定访存结果,供WB级写回 |
写Verilog时我习惯把这四组流水线寄存器定义为reg数组,再在时钟always块内更新。此处给出IF/ID寄存器定义的参考写法:
// IF/ID流水线寄存器:取指后锁存指令与PC+4 reg [31:0] IF_ID_IR; // 从指令存储器读出的原始指令 reg [31:0] IF_ID_PC4; // 当前PC+4,用于后续分支目标计算 reg IF_ID_VALID; // 指令有效位,flush时清零,stall时保持 always @(posedge clk) begin if (stall_if_id) begin // 停顿:不更新数据,保持上一拍状态 IF_ID_IR <= IF_ID_IR; IF_ID_PC4 <= IF_ID_PC4; IF_ID_VALID <= IF_ID_VALID; end else if (flush_if_id) begin // 刷新:清除流水线中的错误指令 IF_ID_IR <= 32'b0; IF_ID_PC4 <= 32'b0; IF_ID_VALID <= 1'b0; end else begin // 正常流动 IF_ID_IR <= inst_mem_read_data; IF_ID_PC4 <= pc_plus4; IF_ID_VALID <= 1'b1; end end这里最关键是VALID位。很多初学者只搬运数据字段,忘了携带valid,flush时数据清空了但valid还是1,后续级在组合逻辑里会拿全零指令当合法指令执行,尤其容易在跳转刷新后把nop当addi跑一遍。VALID位参与所有后级的分支判断使能、写回使能,建议把它加入每一级寄存器的首字段,综合时也能减少不必要的翻转。
3.2 寄存器堆的双口读写与写回时机:读写同时发生怎么办
MIPS寄存器堆在ID级需要读两个源寄存器,在WB级需要写一个目的寄存器,常规实现是使用两个读口、一个写口的同步RAM。课程设计里最容易踩坑的是“写优先还是读优先”的语义。在同一个时钟沿上,WB级写入寄存器堆的数据,能不能被下一拍ID级的读操作立刻看到?答案要看设计是上升沿写、上升沿读,还是读写错开。
比较稳妥的资源实现,是让寄存器堆采用“写入后立刻可读”的写通(write-through)风格:写地址与读地址相同时,读口直接旁路写数据。对应的读逻辑不能只依赖寄存器堆阵列,还要加一层旁路判断。以下几行代码是常见做法:
// 寄存器堆:同步写、异步读 + 写通旁路 always @(posedge clk) begin if (wb_reg_write) regfile[wb_reg_addr] <= wb_write_data; end assign rs_data = (rs_addr == wb_reg_addr && wb_reg_write && rs_addr != 0) ? wb_write_data : regfile[rs_addr];参数说明:rs_addr是ID级读端口1地址,wb_reg_addr是WB级写地址,wb_reg_write是写使能。旁路条件是“同一周期读地址等于写地址且写使能有效”,此时直接取wb_write_data;如果读地址为0,则不应旁路,因为MIPS约定寄存器0恒为0,不能因写端口拼凑数据而破坏这个约定。资源里如果看到读逻辑里没处理reg_addr==0的情况,需要自己补上,否则后续add与slt都会在用到r0时出现罕见的偶发错误。
3.3 访存级与外设统一编址:load和store的地址边界检查
MEM级的数据存储器访问,资源一般按字寻址或字节寻址两种方式之一实现。课程设计建议采用字节寻址空间,但每次访问只取一个字。lw要求地址按4字节对齐,非对齐访问要么报错要么忽略,很多资源会选择直接忽略并输出高阻,这是把问题藏起来了。如果你想做得更完整,可以在MEM级加一个地址对齐检查信号,非对齐时拉高一个mem_align_err标志并阻塞写回。
地址计算里有个细节:ALU输出的地址是字节地址,而数据存储器的物理端口如果按字组织,需要对地址低两位做处理。以按字组织的存储器为例,访问逻辑应按如下方式索引:
// 访存读使能与字地址映射 wire [7:0] byte_addr = alu_result[9:2]; // 取字地址,按4字节对齐 assign data_mem_read_en = mem_read && !mem_align_err; assign mem_rd_data = data_mem[byte_addr];注意这里的alu_result[1:0]被舍弃了。如果lw/sw指令的地址偏移不是4的倍数,你会在波形里看到存储阵列读出的是相邻地址的内容,且没有任何报错。这个问题在最终验收演示时很容易被忽视,但如果机器码里存在伪指令展开的非对齐地址,程序就会在运行时静默读错值。建议在测试程序里专门放几条非对齐的lw,验证你的设计是否具备“宁可报错也不读错”的约束。
4. 动态调度与数据冒险处理:转发、停顿与分支预测的取舍
4.1 三类数据冒险:哪些需要转发,哪些需要用停顿解决
数据冒险是五级流水线的灵魂考题。R型指令、I型指令与lw/sw交织在一起时,会产生RAW、WAR、WAW三类依赖。在五级按序流水线中,WAR和WAW不会出现,因为所有指令都按程序顺序写回,只有RAW需要我们正面处理。
RAW依赖的核心场景是:上一条指令的写回结果还未到达寄存器堆,下一条指令就要读源操作数。解决思路有两种:一是等,二是抄近路。等的方式叫stall,会冻结流水线;抄近路的方式叫forwarding,把还在流水线中后的数据直接引到EX级ALU的输入口。资源里会看到两者的配合,建议的指导原则是:能转发绝不停顿,只有load-use冲突和分支预测失败才停顿。
控制型冒险则来自beq/bne/j指令。跳转指令在EX级才能得出比较结果,如果按最简单的方式做,跳转成功时需要冲刷其后进入流水线的两条指令(IF/ID与ID/EX),代价是两个周期的惩罚。资源里如果包含动态分支预测,通常会把预测结果提前到ID级,通过分支历史表(BHT)猜方向、猜目标,将错误惩罚压缩到一到两个周期。
4.2 转发网络的实现:EX/MEM与MEM/WB两条短路路径
转发要从后两级分别引出数据。EX/MEM级寄存器里保存着上一条指令的ALU结果,MEM/WB级寄存器里保存着更远一条指令的写回产物,这两处都可能是当前EX级源操作数的最新值。判断条件需要同时检查RegWrite是否有效、写入寄存器号是否为0、以及写入寄存器号是否与当前读端口寄存器号相等。
一个容易忽略的优先级问题是:如果EX/MEM与MEM/WB同时命中同一寄存器号,应该选哪一路?答案是选EX/MEM,因为它的数据更新,更接近当前执行点,覆盖更晚的写回。以下是一段经典的转发选择逻辑,直接写在EX级的源操作数选择处:
// 转发源选择:优先使用最近的EX/MEM级数据 wire ex_fwd_hit = ex_mem_reg_write && (ex_mem_rd != 0) && (ex_mem_rd == id_ex_rs); wire mem_fwd_hit = mem_wb_reg_write && (mem_wb_rd != 0) && (mem_wb_rd == id_ex_rs); assign alu_src1 = ex_fwd_hit ? ex_mem_alu_result : mem_fwd_hit ? mem_wb_write_data : id_ex_rs_data;参数说明:id_ex_rs是当前EX级指令的源寄存器号,ex_mem_rd与mem_wb_rd分别是流水线上后方两条指令的目标寄存器号。判断里特意排除寄存器号0,是因为R0为恒值,不该被旁路覆盖。这里还要留意,转发数据来源可以是ALU结果,也可以是访存读出数据,后者在MEM/WB级才有完整数据,所以对load的结果做转发时,只能从MEM/WB级掏数据,而不能从EX/MEM级提前掏,否则会拿到一个未定的存储器读值。
4.3 load-use冲突的停顿一拍:控制信号冻结而不是清空
load-use冲突指lw指令后面紧跟一条要读取该加载结果的指令。由于lw的访存数据在MEM级末尾才就绪,即便从MEM/WB级转发到EX级,也只能覆盖一条指令,依然让紧随其后的那条指令在EX级读不到数据。此时需要插入一个周期的气泡,也就是把ID/EX之后的指令整体冻结一拍。
停顿的实现方式有两种直观思路。第一种是直接把PC冻结、IF/ID寄存器冻结,同时把ID/EX控制信号全部清空,让EX级自动执行一条nop;第二种是只冻结PC与IF/ID,ID/EX继续放行当前译码结果,不插入气泡。两种方式都会影响后续流水线的对齐,资源里多数选择第一种,因为实现上更清晰。需要特别注意的是,停顿只清空ID/EX的控制信号,不能清空数据字段,否则后续的写回使能丢失,寄存器堆永远不会收到这次写。
一段常用的load-use检测逻辑如下:
// load-use冒险检测:lw的目标寄存器是下一条指令的源操作数 assign load_use_hazard = id_ex_mem_read && (id_ex_rd == if_id_rs || id_ex_rd == if_id_rt); assign stall_pc = load_use_hazard; assign stall_if_id = load_use_hazard; assign flush_id_ex = load_use_hazard;这里load_use_hazard拉高后,PC保持不动,IF/ID保持不动,ID/EX被刷新成nop,EX级在下一拍执行一条空操作,MEM/WB级的转发数据在同一拍到达,第三拍恢复到正常执行。如果资源里没有单独区分stall_pc和flush_id_ex,而是把ID/EX也冻结,那就会变成每两个周期执行一条指令,流水线效率直接打对折。
4.4 动态分支预测:2位饱和计数器的实现与流水线刷新范围
动态分支预测是这份资源把自己定位成“动态流水线”的核心证据。常见实现是分支历史表BHT,每个表项用2位饱和计数器记录跳转偏置。2位的状态机从“强不跳”到“弱不跳”到“弱跳转”再到“强跳转”,每发生一次跳转或非跳转,计数器沿一个方向连续变化,只有连续两次同样结果才翻转预测。这样做的优势是能适应循环体内的分支抖动模式。
BHT的索引通常取PC的一段低位。课程设计里我一般取PC[8:2]做128项BHT,因为PC[1:0]恒为0,不参与索引,否则表项利用率会减半。预测器只负责给出“是否跳”的二元结果,不负责计算跳转目标,目标地址仍由ID级计算出的branch_target提供。代码如下:
// 2位饱和分支预测器 reg [1:0] bht [0:127]; wire [6:0] bht_idx = pc[8:2]; assign branch_pred_taken = bht[bht_idx][1]; // 高位置1时预判跳转 always @(posedge clk) begin if (branch_resolved) begin case (bht[bht_idx]) 2'b00: bht[bht_idx] <= taken ? 2'b01 : 2'b00; 2'b01: bht[bht_idx] <= taken ? 2'b10 : 2'b00; 2'b10: bht[bht_idx] <= taken ? 2'b11 : 2'b01; 2'b11: bht[bht_idx] <= taken ? 2'b11 : 2'b10; endcase end end预测命中时流水线不插入任何气泡;预测失败时把IF/ID和ID/EX同时刷新。常见的坑是只刷新IF/ID不刷新ID/EX,导致后面一条已经进入译码级的错误指令继续执行并写回。刷新信号需要在分支结果出来的那个周期拉高,并且必须让PC在下一个周期选择正确的新地址而不是PC+4,否则跳转目标根本进不了流水线。
5. 仿真与上板避坑:五个高频深坑与排查记录
5.1 波形全是X态:复位与初始化的时间没对清楚
现象:一运行仿真,PC、指令、寄存器堆输出全部显示为X。
原因:设计中没有显式复位信号,或者复位信号只在initial块里短暂拉低,而时钟生成块与复位释放顺序错乱,导致寄存器堆和流水线寄存器在复位释放前已经采到了X态,X态在组合逻辑里传播开。
解决:给所有时序寄存器补上一个同步或异步复位。我的习惯是统一用异步复位、同步释放的rst_n,复位释放时确保时钟沿尚未到来。在testbench里先拉低rst_n保持5个时钟周期,再释放,再等一拍开始取指。同时把PC初始值显式赋为0x00000000(或复位向量0x0000_0000),避免依赖工具默认值。
5.2 仿真死循环:时钟生成与$display顺序的教训
现象:testbench里用forever #5 clk = ~clk;生成时钟,但仿真跑到第几百个周期后突然卡死,Vivado Console不再打印任何信息。
原因:死循环不在时钟生成,而在某个驱动信号的initial块里写了while(1)且没有等待时序推进,或者某个组合逻辑环路没有在时钟沿隔离,导致仿真器在同一time step内反复计算传播。
解决:排查所有不含时钟沿的组合always块,确认每一个都是电平敏感且不存在反馈回路。testbench里不要用while(1)等待事件,改用在时钟沿后加固定延迟。常见做法是在每个posedge clk后执行一句@(posedge clk);再继续下一段激励。
5.3 beq误跳:分支比较使用的是哪一级的数据
现象:两数相等时beq没有跳转,两数不等时反而跳转,时序波形看ALU输入完全正确。
原因:分支判断在EX级,但比较器用的源操作数是从ID/EX寄存器直接拿的,没有经过转发网络。结果就是当前beq执行时,它依赖的rs/rt值还是从ID级读出的旧值,后级转发数据到不了它的比较器输入口。
解决:把beq的源操作数也被转发网络覆盖。即比较器的两个输入不是ID/EX_rs_data和ID/EX_rt_data,而是经过转发选择逻辑之后的alu_src1与alu_src2。如果你设计里beq判断在ID级提前完成,那就要额外在前级加旁路,否则害处更大。
5.4 控制信号默认全0收不到:case的default写成0的风险
现象:ALU控制信号alu_op在指令助记符正确的情况下总是0,导致所有运算指令都被当成add执行。
原因:case语句里漏写了default分支,或者default分支被写成0 3位,某些funct码被排除在匹配列表外后落入默认路径。
解决:把default定义成“报错值”,而不是常规运算值。我给alu_op默认值填充为3'b111,在数据通路里这个值不会对应任何真运算操作,借此在波形里一眼看出哪条指令译码失败。另一个习惯是把所有已实现的funct码全列在case里,缺一不可。
5.5 综合通过但上板乱跳:异步复位没有统一树的代价
现象:仿真完全正确,约束文件时序也满足,下载到开发板上跑一次,程序跑到一半随机跳转到错误地址。
原因:复位信号rst_n在内部逻辑里既有异步复位又有逻辑置位,导致多个触发器在不同的时间点脱离复位状态,取指状态错位。另一个可能性是拨码开关直接作为异步复位源,抖动造成复位释放时间不一致。
解决:板级复位信号要先经过两级同步器再接进设计,并保证所有触发器的复位端都由同一个复位网络驱动。不要在数据通路里再把rst_n当普通信号再次取反使用,否则综合工具会铺出额外的复位路径。
6. 验证闭环:锁定每一级行为,让bug无处可藏
6.1 自检式断言:把$display升级成$error
课程设计阶段最常见的验证方式是打印整条指令的执行过程,但打印结果只能给人看,不能自动拦住错误。更好的做法是写一组自检断言宏,在仿真过程中直接判定执行结果是否符合预期。以下是我在这类资源里推荐加入的断言风格:
`define CHECK_SIGNAL(cond, msg) \ if (!(cond)) begin \ $error("ASSERT FAIL: %s @ %0t", msg, $time); \ $finish; \ end调用时放在testbench的时钟块里,比如在执行完一条add指令后的WB写回时刻检查寄存器堆的值:
// 在测试序列中验证addi r2, r0, 5之后r2的数值 @(posedge clk); `CHECK_SIGNAL(mem_wb_reg_addr == 2 && mem_wb_write_data == 32'd5, "r2 should be 5")参数说明:mem_wb_reg_addr是WB级写回的目标寄存器号,mem_wb_write_data是写回数据。这个断言会在那一拍不满足时直接打印错误时间并暂停仿真,定位精度远高于事后翻波形。注意断言语句必须放在与流水线级同步的位置,否则在错误拍没有采样到真实数据。
6.2 回归清单:每次改动后强制跑的扫描序列
流水线CPU的bug通常不是改一处就能解决的,它的逻辑关联性很强,单点修复会暴露下一处问题。我常用的回归序列分三类:第一类是基础算术负载,依次执行addi、add、sub、and、or、slt,并让后一条指令的源操作数依赖前一条的写回,覆盖RAW转发路径;第二类是load-use压力测试,安排lw后面紧跟一条使用加载值的R型指令,覆盖停顿与转发协同路径;第三类是跳转冲刺,在循环体内放beq回跳、beq不跳、j无条件跳、连续三次同一分支预测失败,验证BHT状态翻转和刷新逻辑。
建议在仿真环境里跑一个脚本,循环执行这三组测试并输出“PASS”或“FAIL”字样:
#!/bin/bash # 回归测试:依次运行三种测试向量并统计结果 for case in basic_loadstore loaduse branchsuite; do iverilog -o sim_$case cpu.v tb_${case}.v 2>&1 | tee log_$case.txt ./sim_$case | grep -E "PASS|FAIL" | tail -n 1 done这个脚本把每次回归的收尾输出收敛成一行结论,省去逐行看波形的重复劳动。
这里也说一个此前踩过的教训:改完EX级转发后,忘了把testbench里的断言检查点从MEM级挪到WB级,结果波形显示数据完全正确,但断言一直在报错,查了很久才发现是检查位置的时序差了一拍。现在每次改动数据通路,我都会先把断言所在的流水线级与设计文档对照一遍,再跑回归。希望这套验证习惯,能让你的五级流水线少走些弯路。
本文还有配套的精品资源,点击获取