简介这份教师版实验指导手册围绕龙芯 LS-CPU-EXB-002 教学实验平台展开面向开设数字逻辑、计算机组成原理与计算机体系结构课程的高校师生用于在 FPGA 实验箱上完成从基础运算部件到完整 CPU 的验证型与综合型实验。全文以实验驱动依次讲解定点加法与定点乘法、寄存器堆、ALU 模块、存储器、单周期与多周期 CPU 的设计实现并延伸到静态 5 级流水线 CPU 的课程设计与 CPU 系统优化拓展附录还整理了所实现的 MIPS 指令集与 CP0 寄存器便于对照查阅指令格式与执行流程。资源包为 1 个 PDF 文件约 6.09MB可直接打印或屏幕阅读目录层级清晰按实验序号即可定位到实验目的、设备、任务与设计步骤。目前已有 681 人学习适合需要备课的教师与做课程设计的学生按图索骥地搭建实验方案。1. 一份能上板的 CPU 实验手册值不值得照着做一遍很多组成原理课设卡在一个很尴尬的位置理论课讲清楚了数据通路图也画了代码也写了一到 Vivado 综合就红了或者干脆下载到板子上 LCD 一片黑。这份 LS-CPU-EXB-002 的《CPU 设计与体系结构实验指导手册》解决的就是这段“从图纸到板子”的落差。它把七个实验加两个课程设计串成一条线定点加法、定点乘法、寄存器堆、ALU、存储器、单周期 CPU、多周期 CPU最后推到静态 5 级流水线。每个实验都给了可综合的 verilog 代码、testbench、引脚约束以及一个用 LCD 触摸屏做输入输出的展示外壳。适合三类人正在做 CPU 课设的本科生、要带这门实验的助教、以及想补一补数字逻辑落地经验的后端/嵌入式工程师。它不教你计算机体系结构的原理它教你怎么让一条 MIPS 指令在一块 Artix-7 上真的跑出结果。2. 先把工程骨架立住器件型号、黑盒网表与引脚约束的三处硬要求拿到手册先别急着看 CPU先把一个 32 位加法器完整走一遍“建工程 → 写 RTL → 仿真 → 加约束 → 上板”这条链路跑通了后面七个实验就是往里换模块。这一步里最容易翻车的不是代码是工程配置。器件型号选错、黑盒网表没加进去、约束文件漏了电平标准任何一个都会让综合或实现阶段直接失败。2.1 平台拆解FPGA 板、LCD 黑盒与工具链版本实验箱里能用的东西就四样一块 Artix-7 的 FPGA 实验板、一根下载线、一根带 USB 转串口的串口线、一个电源适配器。板上有一块 LCD 触摸屏手册里明确说了触摸屏的驱动被封装成一个黑盒网表lcd_module.dcp不开放源码。这个设计是有意为之——让你把注意力放在 CPU 上而不是陷进 LCD 时序里。你要做的只是例化它、接对端口。工具链是 Xilinx Vivado器件型号锁定xc7a200tfbg676-2Artix-7 系列、fbg676 封装、速度等级 -2。手册里提到一个版本差异用 Vivado 2012.3 时需要先把 dcp 文件解压再添加之后的版本直接添加.dcp即可。这个细节看着小但在老机房环境里就是“加不进去”和“加得进去”的区别。提示先确认机房的 Vivado 版本再决定 dcp 要不要解压。版本对不上时综合阶段会报黑盒模块找不到而不是在添加文件时报错排查方向很容易跑偏。2.2 新建工程用 tcl 把配置固化下来图形界面点一遍当然可以但每次实验都重来一遍容易漏项。我一般会把工程创建写成 tcl 脚本换机器时直接source一次省掉“这次忘了勾 Do not specify sources”的重复劳动。# create_project.tcl # 在 Vivado 的 Tcl Console 里 source 这个文件一键重建工程骨架 create_project cpu_lab ./cpu_lab -part xc7a200tfbg676-2 -force # 顶层语言锁定 Verilog避免混入 SystemVerilog 语法后综合报错 set_property target_language Verilog [current_project] # 设计源文件 add_files -norecurse ./rtl/adder.v add_files -norecurse ./rtl/adder_display.v # LCD 黑盒网表必须作为设计文件加入不能放进仿真文件集 add_files -norecurse ./ip/lcd_module.dcp # 仿真文件集与约束文件集分开添加 add_files -fileset sim_1 -norecurse ./sim/testbench.v add_files -fileset constrs_1 -norecurse ./constr/adder.xdc # 顶层设置综合顶层是展示模块仿真顶层是 testbench set_property top adder_display [current_fileset] set_property top testbench [get_filesets sim_1] # 刷新编译顺序让子模块先于顶层被解析 update_compile_order -fileset sources_1这段脚本的关键参数有三个。-part xc7a200tfbg676-2决定后面所有原语和 IP 的可用性写错的话综合能过但实现会报无可用资源。-fileset sim_1和-fileset constrs_1把仿真文件和约束文件分到不同文件集混在一起会导致综合时把 testbench 也当成可综合逻辑。最后的update_compile_order不做的话模块层次里lcd_module会一直显示问号因为工具还没解析到它。工程建好后在 Project Manager 里应该看到这样的层次顶层adder_display下面挂两个子模块一个是adder一个是lcd_module。如果lcd_module前面带问号说明 dcp 没加对先解决这个再往下走。2.3 引脚约束PACKAGE_PIN 与 IOSTANDARD 必须成对出现约束文件是把顶层端口绑到 FPGA 物理引脚上的唯一手段后缀.xdc。这份手册给的加法器约束文件里每个端口都写了两行一行指定引脚号一行指定电平标准。少写第二行实现阶段会直接报 UCIO-1/NSTD-1 这类错误。# adder.xdc 片段时钟、复位、LED 与拨码开关 set_property PACKAGE_PIN AC19 [get_ports clk] set_property PACKAGE_PIN Y3 [get_ports resetn] set_property PACKAGE_PIN A3 [get_ports led_cout] set_property PACKAGE_PIN AC21 [get_ports input_sel] set_property PACKAGE_PIN Y6 [get_ports sw_cin] set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports resetn] set_property IOSTANDARD LVCMOS33 [get_ports led_cout] set_property IOSTANDARD LVCMOS33 [get_ports input_sel] set_property IOSTANDARD LVCMOS33 [get_ports sw_cin] # LCD 触摸屏16 位双向数据线要按位展开绑定 set_property PACKAGE_PIN J25 [get_ports lcd_rst] set_property PACKAGE_PIN H18 [get_ports lcd_cs] set_property PACKAGE_PIN K16 [get_ports lcd_rs] set_property PACKAGE_PIN L8 [get_ports lcd_wr] set_property PACKAGE_PIN K8 [get_ports lcd_rd] set_property PACKAGE_PIN J15 [get_ports lcd_bl_ctr] set_property PACKAGE_PIN H9 [get_ports {lcd_data_io[0]}] set_property PACKAGE_PIN K17 [get_ports {lcd_data_io[1]}] # …… lcd_data_io[2] 到 [15] 按手册引脚表逐位补齐 set_property IOSTANDARD LVCMOS33 [get_ports lcd_rst] set_property IOSTANDARD LVCMOS33 [get_ports lcd_cs]参数上有两点值得说。LVCMOS33表示 3.3V 电平这块板子的 IO bank 电压就是 3.3V写成LVCMOS18不会报错但上板行为不可预期。总线端口必须写成{lcd_data_io[0]}这种带花括号的逐位形式直接写lcd_data_io会报端口位宽不匹配。2.4 仿真流程从 Scopes 到波形进制代码加完先别综合跑行为级仿真。点左侧导航栏的 Run Simulation → Run Behavioral Simulation没有语法错误就会打开波形窗口。左侧 Scopes 列选源文件对象中间 Objects 列列出该对象下所有信号选中后右键 Add To Wave Window 加进波形。加完新信号必须点一次 Relaunch否则波形窗口里看不到它——这是新手最常问的“为什么我加了信号还是空的”。默认波形是二进制32 位的加法结果看二进制基本没法读。在 Name 列选中信号右键 → Radix → Hexadecimal切成 16 进制。手册里给了一组对照数据b897be71 42f24186 1 fb89fff7可以先手工验算一遍再对波形。3. 数据运算两连击32 位加法器与移位累加乘法器的代码与波形数据运算这两个实验看着简单实际是整条链路的地基。加法器教你“怎么写才能让综合器给出好结果”乘法器教你“怎么用状态机把一个多周期运算拆开”。这两个能力直接对应后面的 ALU 和流水线。3.1 加法器为什么直接用 “” 比自己搭全加器更划算手册给的参考实现只有一行核心逻辑// adder.v32 位加法器带低位进位输入 timescale 1ns / 1ps module adder ( input wire [31:0] operand1, input wire [31:0] operand2, input wire cin, output wire [31:0] result, output wire cout ); // 左侧拼接成 33 位右侧运算会在 33 位上下文中进行 // 因此最高位的进位不会丢直接落到 cout 上 assign {cout, result} operand1 operand2 cin; endmodule逻辑说明{cout, result}是 33 位的拼接赋值语句的上下文位宽会向上扩展到 33 位所以operand1 operand2 cin这三个 32 位操作数参与运算时也按 33 位处理第 33 位正好是进位输出。如果写成assign result operand1 operand2 cin;进位会被截掉cout永远是 0。参数说明两个 32 位操作数加一个 1 位进位输入输出 32 位结果加 1 位进位这是标准的行波进位加法器接口。手册里明确说直接写综合工具会调用内部库里的加法 IP面积和时序都比自己用全加器搭更优。自己搭也不是不行但手写的行波进位链在 32 位下关键路径太长时序很难收敛。3.2 testbench只给激励不够要能自动比对手册给的原始 testbench 只产生随机激励靠人眼看波形判断对错。这种写法在 32 位加法下勉强能用到了多周期 CPU 就完全不可靠。我一般会加一段自动比对逻辑// testbench.v带自动比对的加法器测试平台 timescale 1ns / 1ps module testbench; reg [31:0] operand1, operand2; reg cin; wire [31:0] result; wire cout; adder uut ( .operand1(operand1), .operand2(operand2), .cin (cin), .result (result), .cout (cout) ); integer i; reg [32:0] expect; // 33 位用来保存期望的 {cout, result} initial begin operand1 32b0; operand2 32b0; cin 1b0; #100; // 等全局复位稳定 for (i 0; i 200; i i 1) begin operand1 $random; // 32 位随机数 operand2 $random; cin {$random} % 2; // 拼接符保证结果非负 #10; expect {1b0, operand1} {1b0, operand2} cin; if ({cout, result} ! expect) begin $display(MISMATCH #%0d: %h %h %b %h_%b, expect %h, i, operand1, operand2, cin, cout, result, expect); $stop; end end $display(ALL PASS); $finish; end endmodule逻辑说明expect声明为 33 位两个操作数先各自补一位 0 扩展到 33 位再相加这样期望值的进位不会被截断。比较用的是!而不是!因为四值逻辑下!遇到 X 会返回 X条件判断会漏掉错误。$stop让仿真在第一次不匹配时暂停方便直接在波形里定位那组数据。参数说明#10是每个测试向量之间的间隔10 个时间单位对应timescale里的 1ns。200 组随机向量足够覆盖进位链的边界情况如果只想跑手册里那组固定数据把循环去掉、直接赋值b897be71和42f24186即可。3.3 定点乘法移位累加状态机与 64 位结果乘法比加法多两个约束结果要 64 位运算需要多个周期。手册里实验二是定点乘法常见做法是用移位累加32 个时钟周期出一组结果。// mult_seq.v32x32 无符号乘法移位累加实现 module mult_seq ( input wire clk, input wire rstn, input wire start, // 单周期脉冲启动一次乘法 input wire [31:0] a, input wire [31:0] b, output reg [63:0] p, // 64 位乘积 output reg done // 计算完成标志保持一拍 ); reg [4:0] cnt; // 剩余迭代次数从 32 递减到 0 reg [63:0] acc; // 累加器同时承担左移 reg [31:0] mcand; // 被乘数每拍左移一位 reg [31:0] mplier; // 乘数每拍右移一位 reg busy; always (posedge clk or negedge rstn) begin if (!rstn) begin acc 64b0; cnt 5d0; busy 1b0; done 1b0; p 64b0; end else if (start !busy) begin acc 64b0; mcand a; mplier b; cnt 5d32; busy 1b1; done 1b0; end else if (busy) begin if (cnt 5d0) begin busy 1b0; done 1b1; p acc; end else begin if (mplier[0]) acc acc {32b0, mcand}; mcand {mcand[30:0], 1b0}; mplier {1b0, mplier[31:1]}; cnt cnt - 5d1; end end end endmodule逻辑说明核心是三件事同步进行——乘数最低位为 1 就把被乘数加到累加器被乘数左移一位乘数右移一位。迭代 32 次后累加器里就是完整的 64 位乘积。start用单周期脉冲触发busy防止计算中途被重新启动done保持一拍供上层采样。参数说明cnt用 5 位是因为要表示 325d324 位放不下。这里实现的是无符号乘法如果要做有符号乘法需要先取绝对值、记录符号位、最后再补符号或者直接用$signed让综合器处理代价是面积变大。3.4 乘法器的三种实现与选型同一个乘法在这块板子上至少有三种做法面积和时序差得很远实现方式延迟资源占用适用场景移位累加状态机32 个时钟周期一个 64 位加法器 若干寄存器实验教学看清每一步组合阵列乘法器单周期大量全加器关键路径长单周期 CPU 里的mult指令调用 DSP 硬核13 个时钟周期一个 DSP48 块追求时序收敛的流水线设计手册里的实验二落在第一行目的是让你把移位和累加的过程摆到波形上看清楚。等做到课程设计的流水线 CPU 时乘法一般会改成调用 DSP 或者拆成多周期否则关键路径根本过不去。这个取舍在做实验六、实验七的时候会变得非常具体——单周期 CPU 里如果塞一个组合乘法器时钟频率会被压得很低。4. 从寄存器堆到整机ALU、存储器与单周期/多周期数据通路怎么拼三个部件实验寄存器堆、ALU、存储器是零件单周期和多周期是把零件拼成整机。零件阶段的接口定义错了整机阶段就要返工所以这一章的重点是接口和时序而不是功能。4.1 寄存器堆双读一写与 $0 恒零MIPS 的寄存器堆是 32 个 32 位寄存器两个组合读端口、一个时序写端口。看起来简单但$0的处理和同拍读写的行为是两个真实的坑。// regfile.v32x32 寄存器堆双读一写 module regfile ( input wire clk, input wire we, // 写使能 input wire [4:0] raddr1, input wire [4:0] raddr2, input wire [4:0] waddr, input wire [31:0] wdata, output wire [31:0] rdata1, output wire [31:0] rdata2 ); reg [31:0] r [0:31]; integer i; // 读端口是组合逻辑地址一变数据就跟着变 assign rdata1 (raddr1 5d0) ? 32b0 : r[raddr1]; assign rdata2 (raddr2 5d0) ? 32b0 : r[raddr2]; // 写端口在时钟上升沿生效$0 不允许被写入 always (posedge clk) begin if (we (waddr ! 5d0)) r[waddr] wdata; end // 上电初始化仿真时避免读出 X initial begin for (i 0; i 32; i i 1) r[i] 32b0; end endmodule逻辑说明读端口必须对raddr 0做显式判断因为物理上的r[0]可能被写过如果写使能没挡住显式返回 0 才能保证 MIPS 语义。写端口用非阻塞赋值保证和读端口在同一拍内的行为可预期。参数说明we是写使能由控制器给出waddr ! 5d0是第二道保险防止写$0。initial块只用于仿真初始化综合时会忽略上板时寄存器初值是 FPGA 的默认值通常是 0但不能依赖它CPU 的复位逻辑要自己写。同一个时钟沿写入、读端口读同一地址读出来的是旧值还是新值在纯组合读的实现里读到的是旧值。如果后面的流水线需要“写后读”在同一拍生效要么在写回级加旁路要么在译码级做转发。这个细节在单周期里不明显在流水线里是数据冒险的源头之一。4.2 ALU操作码译码与标志位ALU 是纯组合逻辑关键是case写全、默认值给上避免综合出 latch。// alu.v支持加减、逻辑、移位与比较 module alu ( input wire [31:0] a, input wire [31:0] b, input wire [3:0] op, output reg [31:0] y, output wire zero ); wire [32:0] sum {1b0, a} {1b0, b}; wire [32:0] dif {1b0, a} - {1b0, b}; always (*) begin case (op) 4b0000: y sum[31:0]; // add 4b0001: y dif[31:0]; // sub 4b0010: y a b; // and 4b0011: y a | b; // or 4b0100: y a ^ b; // xor 4b0101: y b a[4:0]; // sllv移位量取低 5 位 4b0110: y b a[4:0]; // srlv 4b0111: y ($signed(b) a[4:0]);// srav算术右移 4b1000: y ($signed(a) $signed(b)) ? 32d1 : 32d0; // slt default: y 32b0; // 必须写否则综合出 latch endcase end assign zero (y 32b0); endmodule逻辑说明sum和dif都扩展到 33 位是为了在需要进位或借位的地方能取到最高位这里只用了低 32 位但扩展到 33 位能避免减法溢出时的误判。移位量取a[4:0]是因为 32 位数据最多移 31 位高位的值无意义。参数说明op是 4 位操作码具体编码要和控制器里的译码保持一致改一处就要改另一处建议在文件头用注释把编码表固定下来。zero是组合输出分支指令直接用它做判断。4.3 存储器分布式 RAM、块 RAM 与地址对齐数据存储器实验要关注两件事综合成什么资源、地址怎么对齐。// dmem.v256 字的数据存储器按字寻址 module dmem ( input wire clk, input wire we, input wire [31:0] addr, input wire [31:0] wdata, output wire [31:0] rdata ); reg [31:0] mem [0:255]; // addr[1:0] 忽略实现字对齐addr[9:2] 作为 RAM 索引 assign rdata mem[addr[9:2]]; always (posedge clk) begin if (we) mem[addr[9:2]] wdata; end // 仿真时从文件加载初值上板时由 bitstream 固化 initial begin $readmemh(dmem_init.hex, mem); end endmodule逻辑说明addr[1:0]被直接丢弃因为按字访问时低两位没有意义。读是异步的、写是同步的这种写法在 Vivado 里会被推断成分布式 RAMLUTRAM而不是块 RAMBRAM。如果想用 BRAM读端口必须加时钟——加一拍读延迟但能省下大量 LUT。参数说明256 个字对应 1KB 容量索引位宽 8 位所以取addr[9:2]。$readmemh加载的是十六进制文本每行一个 32 位数综合时会被忽略上板前要把初始化数据写进 bitstream 或者用 boot ROM 的方式处理。4.4 单周期 CPU数据通路与关键路径单周期 CPU 的拼法是把 PC、指令存储器、寄存器堆、ALU、数据存储器、控制器用组合逻辑连起来每条指令在一个时钟周期内走完全部阶段。手册里实验六的目标是跑通一批基础指令附录 A1 列了要实现的指令清单。单周期的问题不在功能在时序。时钟周期必须容纳最长的那条指令——通常是lw它要经过 PC → 指令存储器 → 寄存器堆读 → ALU 计算地址 → 数据存储器读 → 寄存器堆写一条组合路径串到底。如果这条路径延迟 20ns时钟周期就只能做到 20ns 以上其他短指令全在浪费。部件输入输出时序属性PCclk, rstn, npcpc时序指令存储器pcinstr组合或加一拍寄存器堆raddr1/2, waddr, wdata, werdata1/2读组合、写时序ALUa, b, opy, zero组合数据存储器addr, wdata, werdata读组合、写时序控制器op, funct各控制信号组合4.5 多周期 CPU状态划分与 CPI 的账多周期把一条指令拆成若干步每步一个时钟周期部件可以复用。手册里实验七的目标是在单周期的基础上新增指令附录 A2同时把数据通路改成状态机驱动。典型划分是五个状态取指IF、译码ID、执行EX、访存MEM、写回WB。不是每条指令都走满五步add到 EX 就结束lw要走到 WBbeq在 EX 就算完分支地址。状态转移由控制器里的case (state)决定下一状态取决于当前状态和指令类型。// ctrl_multi.v多周期控制器的状态划分骨架 localparam S_IF 3d0, S_ID 3d1, S_EX 3d2, S_MEM 3d3, S_WB 3d4; always (posedge clk or negedge rstn) begin if (!rstn) state S_IF; else begin case (state) S_IF: state S_ID; S_ID: state S_EX; S_EX: state (is_lw || is_sw) ? S_MEM : S_WB; S_MEM: state S_WB; S_WB: state S_IF; default: state S_IF; endcase end end逻辑说明状态转移只看当前状态和指令类型不看数据这是多周期控制器的基本结构。is_lw、is_sw这类标志由译码阶段产生存到寄存器里供后续状态使用。参数说明状态编码用 3 位五个状态够用多出来的编码在default里回到S_IF防止跑飞。多周期的 CPI 不是 1lw是 5 个周期add是 4 个周期平均 CPI 要按指令 mix 算这也是它相对单周期的优势所在——单周期 CPI 恒为 1 但时钟周期长多周期时钟周期短但 CPI 大于 1两条路的性能账要分开算。5. 避坑排查综合报错、波形全 X、上板黑屏的五个现场下面五条是这条实验链路上出现频率最高的问题按“现象 → 原因 → 解决”拆开写。每一条我都实际遇到过或者见人踩过按这个顺序排查能省不少时间。5.1 综合与实现阶段的两个硬报错坑一层次里lcd_module一直显示问号综合报黑盒未解析。现象工程建好后 Project Manager 里lcd_module前面带问号综合阶段报类似 “module lcd_module not found” 的错误或者综合能过但实现阶段报黑盒端口不匹配。原因.dcp文件没有被正确加入设计文件集或者版本不匹配。Vivado 2012.3 添加 dcp 前需要先解压后续版本可以直接添加另外 dcp 必须放进sources_1而不是sim_1放错文件集时综合阶段根本看不到它。解决在 Tcl Console 里执行get_files lcd_module.dcp确认文件在工程里再用get_property FILESET [get_files lcd_module.dcp]确认它属于sources_1。版本不符就按手册说明解压后重新添加添加完执行一次update_compile_order。坑二实现阶段报 UCIO-1 / NSTD-1说引脚未约束或电平标准缺失。现象综合能过实现阶段报 “Unconstrained logical port” 或者 “I/O standard not specified”严重时直接终止。原因.xdc里只写了PACKAGE_PIN没写IOSTANDARD或者顶层模块新增了端口但约束文件没同步更新。手册里的约束文件是对着adder_display的端口写的改顶层端口名之后必须同步改约束。解决每个PACKAGE_PIN后面紧跟一行IOSTANDARD LVCMOS33端口名要和顶层模块的端口声明逐字一致。改完在 Tcl Console 跑一次report_io把未约束的端口列出来逐个补。5.2 仿真与上板阶段的三个现象坑三波形窗口里信号全是 X或者一直是 0。现象仿真跑起来了但加进波形的信号没有跳变或者从头到尾是红线X 态。原因三种可能——激励没产生initial块里没赋值或者#100等待时间太长看起来像没动、信号加进波形后没点 Relaunch、testbench 没有设为仿真顶层。解决先确认 testbench 文件前面有三个方形的 top 标志没有就右键 Set as Top再加信号后点一次 Relaunch最后检查initial块里的赋值和#延迟把#100临时改小到#10看有没有跳变。坑四仿真完全正确下载到板子上 LCD 不亮或者加法结果不对。现象行为仿真波形一切正常生成 bitstream 下载后LCD 屏幕黑屏、或者显示的加法和预期不符。原因按概率排第一是约束文件里的引脚号和实际板子不符尤其是 LCD 的 16 位数据线错一位就整屏不显示第二是复位信号极性搞反resetn是低有效接成高有效就一直处于复位第三是时钟引脚没绑对导致整个设计没有时钟。解决先拿一个最简单的 LED 闪烁工程验证约束文件和下载链路是通的再换回加法器工程。LCD 不亮时优先核对lcd_data_io[0]到[15]这 16 个引脚逐个对照手册引脚表。复位极性用report_property或者直接看代码里always (posedge clk or negedge rstn)的敏感列表。坑五多周期 CPU 跑飞状态机停在某个状态不动。现象多周期 CPU 仿真时状态机进入某个状态后不再跳转PC 不更新波形上state信号一条直线。原因状态转移的case语句漏了分支某个指令类型进来后没有对应的下一状态落进default又被拉回S_IF形成死循环或者is_lw/is_sw这类标志寄存器没有在译码阶段正确锁存导致条件判断永远为假。解决在case的default分支里加$display打印当前状态和指令仿真时看看到底是哪条指令进来导致的把is_lw、is_sw这些中间信号加到波形窗口确认它们在 ID 阶段被正确赋值并在后续状态保持。注意这五条里坑二和坑四属于“代码没错但上不了板”排查成本最高。我一般会在每次换顶层模块之后先跑一次report_io和一次 LED 闪烁验证把这类问题和逻辑错误分开。6. 5 级流水的验证账冒险处理与指令序列对比的具体做法课程设计部分的静态 5 级流水线是把前面所有实验合起来再插入四级流水寄存器IF/ID、ID/EX、EX/MEM、MEM/WB。功能上它和单周期等价难的是三件事数据冒险、控制冒险、以及验证。数据冒险用转发加重载解决。EX 级的操作数优先从 EX/MEM 和 MEM/WB 两级流水寄存器转发转发命中就不阻塞碰到 load-use也就是上一条是lw、下一条立刻要用它的结果必须阻塞一个周期。控制冒险在 EX 级判定分支需要冲刷 IF/ID 和 ID/EX 两级。这三条规则写在纸上一页落到代码里是几十行旁路逻辑任何一条写漏都会表现为“单条指令对、指令序列错”。冒险类型触发条件处理手段代价RAW 数据冒险后一条指令读前一条的写结果EX/MEM、MEM/WB 转发0 周期load-uselw后紧接使用其目标寄存器阻塞一拍 转发1 周期控制冒险分支指令在 EX 级判定冲刷两级流水寄存器2 周期结构冒险取指和访存同时用存储器指令/数据存储器分离0 周期验证方法我推荐“分两级对比”。第一级是单指令验证把附录 A3 里新增的每条指令单独跑一遍波形上确认写回值正确这一步把指令实现的错误筛掉。第二级是指令序列对比写一段十几条指令的小程序手工算出每条指令执行后的寄存器值和仿真波形里regfile的内容逐条比对。这一步能抓出转发和阻塞的时序错误——单条指令全对、序列一出错问题基本都在旁路逻辑。再补一个技巧把 CPI 和时序报告放在一起看。流水线跑通之后用report_timing_summary看关键路径落在哪一级如果落在 ALU 或者乘法器上说明这一级的组合逻辑太长可以考虑把乘法拆成两拍如果落在转发网络上说明旁路的选择逻辑写得太复杂可以用优先级编码简化。CPI 的算法是总周期数除以指令数理想值接近 1实际会因为 load-use 和控制冒险略高算出来和预期对不上先查冒险处理的阻塞条件再查状态机有没有多走。血泪经验是不要等整条流水线写完再仿真。每加一级流水寄存器就先跑一遍单指令每加一条转发路径就补一组序列测试。从那以后我每次做流水线类的设计都强制先跑一遍单指令再跑序列宁可前期多花两小时也不想在最后阶段对着几百个周期的波形找一个转发的选择条件。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?