1. 项目概述:为什么单周期CPU实验是计算机组成原理的“分水岭”
在海大(中国海洋大学)计算机学院的《计算机组成原理》课程体系里,“实验4:单周期CPU(Verilog)”从来不是一份普通作业——它是学生第一次亲手把指令集架构、数据通路、控制逻辑这三块抽象理论真正焊接到一起的实操节点。我带过六届本科生实验课,亲眼见过太多同学在做完ALU、寄存器堆、存储器这些模块后信心满满,结果一碰单周期CPU就卡在“为什么PC没跳转”“为什么MEM阶段写不进数据”“为什么addi指令总读错立即数”这类问题上,反复烧录、反复仿真、反复抓头发。这不是代码写错了,而是对“时序”和“控制信号协同”的理解出现了断层。
这个实验的核心关键词非常明确:MIPS32指令集、单周期数据通路、Verilog行为级建模、Vivado综合与仿真验证。它不追求性能,也不模拟流水线冒险,而是用最“笨”但最透明的方式,把一条指令从取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回(WB)五个阶段,在一个时钟周期内全部完成。这种设计让每个信号的来龙去脉都清晰可见,就像拆开一台机械手表,齿轮咬合关系一目了然。但正因如此,它对信号生成的精确性、路径延迟的预判、测试激励的覆盖度提出了极高要求——任何一处控制信号晚一个节拍,整个CPU就停摆;任何一个多路选择器选错源,结果就全错。
适合谁来深挖这个实验?首先是海大本校正在做该实验的同学,你们手里的实验指导书可能只给了框架代码,缺的是关键信号的推导逻辑和调试方法;其次是准备考研复试的学生,王道、天勤教材里关于单周期CPU的图示很多,但真正能跑起来、能改、能调的Verilog工程极少;还有自学数字电路的工程师,想用FPGA验证自己对经典RISC架构的理解是否扎实。我这次复现的版本,基于Vivado 2023.1,兼容Basys3(Xilinx Artix-7)开发板,所有模块均采用可综合的同步设计风格,不依赖任何非标准库,连testbench都做了分层激励——从单条指令验证,到连续5条指令流水式测试,再到完整MIPS汇编小程序(如求最大值、阶乘)的端到端运行。下面,我们就从顶层设计开始,一层层剥开这个“单周期CPU”的硬壳。
2. 整体架构设计与方案选型逻辑
2.1 为什么坚持“单周期”而非“流水线”?
很多初学者看到“CPU设计”第一反应就是流水线,觉得更“先进”。但在教学场景下,单周期是不可替代的基石。它的价值不在于效率,而在于因果链的绝对可追溯性。在流水线CPU中,一条指令的执行结果可能要等3个周期后才出现在寄存器堆,中间还夹杂着转发、阻塞、分支预测等复杂机制,一旦出错,你根本不知道是IF阶段取错了指令,还是EX阶段ALU算错了,抑或是MEM阶段数据没写对。而单周期CPU里,所有操作都在同一个clk上升沿触发,所有结果都在下一个clk到来前稳定输出。你只要在仿真波形里定格在t=100ns这个时刻,就能同时看到PC的值、IR的内容、ALU的输出、MemData的输入、RegWrite信号的状态——它们之间是严格的组合逻辑+寄存器更新关系,没有跨周期的隐含状态。
我曾用两种方案对比过教学效果:一组学生先做单周期,再过渡到五级流水线;另一组直接上流水线。结果前者在第三周就能独立分析分支冲突,后者直到结课还在纠结“为什么bne指令后紧跟的指令被取到了”。原因很简单:单周期强迫你把每一条MIPS指令的控制信号真值表背下来,把每个MUX的选择逻辑推导出来,把每个寄存器的写使能条件列清楚。这种“笨功夫”,恰恰是建立硬件直觉的唯一捷径。
2.2 指令集为何锁定MIPS32?
当前主流教学CPU实验基本围绕MIPS32展开,这是经过长期验证的最优选择。它比x86简洁(无变长指令、无复杂寻址模式),比ARMv7精炼(无条件执行、无Thumb状态切换),又比RISC-V的RV32I更成熟(工具链、教材、例题资源极其丰富)。海大实验指导书指定的指令子集是典型的教学裁剪版:R型(add, sub, and, or, slt)、I型(addi, lw, sw, beq)和J型(j),共9条指令。这个集合足够覆盖数据通路所有关键路径:R型走ALU→RegWrite;I型中的lw/sw走Data Memory读写;beq需要比较ALU输出并跳转;j则直接修改PC。少一条,数据通路就有冗余;多一条,比如加入syscall,就会引入异常处理模块,彻底偏离单周期教学目标。
特别注意:MIPS32是大端序(Big-Endian),但FPGA内部存储器默认小端,这点在实现lw/sw时极易踩坑。我的方案在Memory模块内部做了字节序适配,确保Verilog代码里mem[addr]读到的就是MIPS规范定义的字节,避免学生在调试时陷入“明明地址对了,数据却反了”的迷宫。
2.3 工具链为何选用Vivado而非ModelSim或Quartus?
Vivado已成为Xilinx FPGA开发的事实标准,其优势在单周期CPU实验中尤为突出:
- 集成度高:从Verilog编写、语法检查、RTL分析、综合、实现(布局布线)、到比特流生成、硬件下载、ILA(Integrated Logic Analyzer)在线调试,全在同一个GUI里完成。学生不用在ModelSim里仿真完,再切到Quartus里综合,最后用SignalTap看波形——这种割裂极大增加学习成本。
- 仿真与硬件一致性好:Vivado自带的XSIM仿真器与后端综合器共享同一套语义解析引擎,你在仿真里跑通的代码,几乎100%能成功综合到FPGA上。而用第三方仿真器,常出现“仿真OK,上板失败”的情况,根源往往是某些非可综合写法(如
initial块里用#10延时)被仿真器容忍,却被综合器直接忽略。 - 调试能力强大:ILA核可以实时抓取片上信号,支持触发条件设置、深度采样、波形导出。当你的CPU在板子上跑飞了,不用猜,直接把PC、IR、ALUOut、MemData等关键信号接进ILA,一帧波形就能定位到第几条指令出错。我在海大实验室给学生演示时,常把ILA探针接到
ctrl_regwrite信号上,当看到它在不该写的时候拉高,就知道控制单元逻辑有漏洞。
提示:Vivado 2023.1及以后版本已取消对Windows 7的支持,务必使用Win10/Win11或Ubuntu 20.04+。安装时勾选“Vivado Simulator”和“Documentation”,前者用于仿真,后者里的UG901(Vivado Design Suite User Guide)是遇到报错时最权威的查证来源。
3. 核心模块拆解与关键信号推导
3.1 数据通路:五大部件如何物理连接?
单周期CPU的数据通路本质是一张由寄存器、ALU、存储器构成的“高速公路网”,所有部件通过总线互联,控制信号则是指挥交通的红绿灯。我们按信号流向梳理:
- PC(Program Counter):32位计数器,初始值为0x00000000。它的输出
pc_out一路送入Instruction Memory(IM)作为地址,另一路加4后送入Branch Adder(用于beq/j跳转计算)。 - Instruction Memory(IM):只读存储器,深度1024×32bit(对应4KB代码空间)。输入是
pc_out,输出是32位指令inst_out。这里有个易错点:IM的地址线是pc_out[31:2],因为MIPS指令按4字节对齐,最低两位恒为0,直接截掉可节省地址线资源。 - Control Unit(CU):核心大脑。输入是
inst_out[31:26](opcode),输出8个控制信号:RegDst(决定rd/rs选择)、ALUSrc(决定ALU第二操作数来源)、MemtoReg(决定WB阶段数据来源)、RegWrite(寄存器堆写使能)、MemRead(IM读使能)、MemWrite(DM写使能)、Branch(beq跳转使能)、ALUOp(ALU操作类型)。CU的真值表必须严格对照MIPS32手册,例如add指令(opcode=0x00, funct=0x20)要求RegDst=1, ALUSrc=0, MemtoReg=0, RegWrite=1, MemRead=0, MemWrite=0, Branch=0, ALUOp=10。 - Register File(RF):32×32bit寄存器堆。双读端口(rs/rt)、单写端口(rd)。读地址
rs_addr/rt_addr来自inst_out[25:21]/inst_out[20:16],写地址rd_addr由RegDst控制:RegDst=1时取inst_out[15:11](rd),RegDst=0时取inst_out[20:16](rt)。写数据wr_data来自ALU或Data Memory,由MemtoReg选择。 - ALU:执行所有算术逻辑运算。输入A来自RF的rs输出,输入B由
ALUSrc选择:ALUSrc=0时取RF的rt输出,ALUSrc=1时取inst_out[15:0](立即数符号扩展后)。ALUOp决定运算类型,例如ALUOp=10对应R型指令,此时ALU根据funct字段(inst_out[5:0])选择add/sub/and/or/slt。 - Data Memory(DM):读写存储器,深度1024×32bit。地址来自ALU输出(lw/sw的基址+偏移),写数据来自RF的rt输出(sw),读数据输出到WB阶段。注意:lw/sw的地址计算在EX阶段完成,DM的读写在MEM阶段触发,所以ALUOut必须锁存到MEM阶段才能用。
这张通路图里,最易被忽视的是信号延迟匹配。例如,pc_out到IM输出inst_out有1个时钟周期延迟,inst_out到CU输出控制信号有组合逻辑延迟,CU信号到RF读地址也有延迟。所有这些延迟必须小于一个时钟周期(假设主频100MHz,周期10ns),否则时序违例。Vivado的Timing Report会明确告诉你哪条路径超限,常见优化手段是:在关键路径插入寄存器打拍(如把ALUOut先存到EX_Reg再送到MEM),但这会改变单周期语义,教学实验中应避免,转而优化逻辑层级(如用Case语句代替If-Else嵌套)。
3.2 控制单元:从Opcode到控制信号的映射逻辑
CU是单周期CPU的“翻译官”,它把32位二进制指令翻译成8个布尔信号。手工写真值表容易出错,我的做法是用Verilog的case语句结构化实现,并添加注释说明每条指令的信号含义:
// 控制单元核心逻辑(简化版) always @(*) begin case (opcode) 6'b000000: begin // R-type RegDst = 1'b1; ALUSrc = 1'b0; MemtoReg = 1'b0; RegWrite = 1'b1; MemRead = 1'b0; MemWrite = 1'b0; Branch = 1'b0; ALUOp = 2'b10; // R-type operation end 6'b001000: begin // addi RegDst = 1'b0; ALUSrc = 1'b1; MemtoReg = 1'b0; RegWrite = 1'b1; MemRead = 1'b0; MemWrite = 1'b0; Branch = 1'b0; ALUOp = 2'b00; // add operation end 6'b100011: begin // lw RegDst = 1'b0; ALUSrc = 1'b1; MemtoReg = 1'b1; RegWrite = 1'b1; MemRead = 1'b1; MemWrite = 1'b0; Branch = 1'b0; ALUOp = 2'b00; // add operation end 6'b101011: begin // sw RegDst = 1'bx; // don't care ALUSrc = 1'b1; MemtoReg = 1'bx; // don't care RegWrite = 1'b0; MemRead = 1'b0; MemWrite = 1'b1; Branch = 1'b0; ALUOp = 2'b00; // add operation end 6'b000100: begin // beq RegDst = 1'bx; ALUSrc = 1'b0; MemtoReg = 1'bx; RegWrite = 1'b0; MemRead = 1'b0; MemWrite = 1'b0; Branch = 1'b1; ALUOp = 2'b01; // subtract for branch compare end 6'b000010: begin // j RegDst = 1'bx; ALUSrc = 1'bx; MemtoReg = 1'bx; RegWrite = 1'b0; MemRead = 1'b0; MemWrite = 1'b0; Branch = 1'b0; ALUOp = 2'bxx; // don't care end default: begin // illegal instruction RegDst = 1'b0; ALUSrc = 1'b0; MemtoReg = 1'b0; RegWrite = 1'b0; MemRead = 1'b0; MemWrite = 1'b0; Branch = 1'b0; ALUOp = 2'b00; end endcase end关键细节:
default分支必须存在,防止latch生成。Vivado综合时若发现未覆盖所有case,会自动补全,但补全逻辑不可控,可能导致意外行为。1'bx表示“无关项”,在综合时会被优化掉,减少逻辑资源占用。但仿真时需明确赋值,否则波形显示为未知态(X),影响调试。我的习惯是在仿真testbench里对所有信号初始化为0,再在CU里显式写出1'bx。ALUOp只有3种有效值:2'b00(add)、2'b01(sub)、2'b10(R-type),其他值在ALU内部用default兜底,避免未定义行为。
3.3 ALU设计:如何用Case语句实现多功能运算?
ALU是数据通路的“心脏”,它接收两个32位输入和2位操作码,输出32位结果和1位零标志(Zero)。教学版ALU无需追求高性能,重点是功能完备和可读性强:
// ALU核心逻辑 always @(*) begin case (aluop) 2'b00: aluout = a + b; // add, addi, lw, sw 2'b01: aluout = a - b; // sub, beq 2'b10: begin // R-type case (funct) 6'b100000: aluout = a + b; // add 6'b100010: aluout = a - b; // sub 6'b100100: aluout = a & b; // and 6'b100101: aluout = a | b; // or 6'b101010: aluout = (a < b) ? 32'h1 : 32'h0; // slt default: aluout = 32'h0; endcase end default: aluout = 32'h0; endcase zero = (aluout == 32'h0) ? 1'b1 : 1'b0; end这里有两个实战技巧:
- 零标志生成时机:
zero信号必须在ALU输出稳定后立刻计算,不能等到WB阶段。因为beq指令的跳转判断依赖zero && Branch,如果zero延迟,会导致跳转失效。 - slt指令的陷阱:MIPS的slt是“有符号比较”,即把a和b当作补码解释。Verilog中
a < b默认是有符号比较,但前提是a和b声明为signed类型。我的做法是在ALU模块顶部加typedef logic signed [31:0] int32;,然后将a/b声明为int32,确保比较逻辑正确。否则,slt $t0,$t1,$t2在$t1=0xffffffff(-1)、$t2=0x00000001(1)时会错误返回0。
4. 实操全流程:从Vivado创建到上板验证
4.1 Vivado工程创建与文件组织
打开Vivado 2023.1,选择“Create Project” → “Next” → 输入工程名(如single_cycle_cpu)→ 选择“RTL Project” → 勾选“Do not specify sources at this time” → “Next” → 选择板卡:Basys3(Xilinx Artix-7 XC7A35T-1CPG236C)→ “Finish”。
关键步骤:正确设置约束文件(XDC)。Basys3的时钟引脚是E3(100MHz),必须在XDC中声明:
# basys3.xdc set_property PACKAGE_PIN E3 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} [get_ports clk]同时,为LED和开关分配引脚(用于观察CPU状态):
# LED[0]显示RegWrite,LED[1]显示MemWrite,SW[0]作为复位按钮 set_property PACKAGE_PIN U16 [get_ports rst_n] # SW0 set_property IOSTANDARD LVCMOS33 [get_ports rst_n] set_property PACKAGE_PIN U15 [get_ports led_regwrite] # LED0 set_property IOSTANDARD LVCMOS33 [get_ports led_regwrite] set_property PACKAGE_PIN V16 [get_ports led_memwrite] # LED1 set_property IOSTANDARD LVCMOS33 [get_ports led_memwrite]注意:Basys3的复位开关是低电平有效(SW0按下时输出0),所以顶层模块的
rst_n信号需在内部做rst <= ~rst_n转换,确保CPU在开关释放时复位。
文件组织采用分层结构,便于维护:
single_cycle_cpu/ ├── src/ # RTL源码 │ ├── cpu_top.v # 顶层模块 │ ├── pc.v # PC计数器 │ ├── im.v # 指令存储器 │ ├── cu.v # 控制单元 │ ├── rf.v # 寄存器堆 │ ├── alu.v # 算术逻辑单元 │ ├── dm.v # 数据存储器 │ └── extender.v # 立即数扩展器 ├── testbench/ # 测试平台 │ └── tb_cpu.v # 顶层testbench ├── sim/ # 仿真脚本 │ └── run_sim.tcl # XSIM运行脚本 └── constraints/ # 约束文件 └── basys3.xdc4.2 关键模块Verilog实现要点
顶层模块cpu_top.v:信号粘合与状态观测
module cpu_top ( input logic clk, input logic rst_n, output logic [31:0] pc_out, output logic [31:0] inst_out, output logic [31:0] aluout, output logic regwrite, output logic memwrite, output logic [31:0] memdata_out ); // 内部信号声明 logic [31:0] pc_next, pc_add4; logic [31:0] inst, ir; logic [31:0] rs_data, rt_data, rd_data, imm_ext; logic [31:0] alu_a, alu_b, alu_result; logic [31:0] mem_addr, mem_wrdata; logic [31:0] mem_rddata; logic regdst, alusrc, memtoreg, regwrite_int, memread, memwrite_int, branch, aluop_2; logic [1:0] aluop; // 实例化各模块 pc uut_pc ( .clk(clk), .rst_n(rst_n), .pc_out(pc_out), .pc_next(pc_next) ); im uut_im ( .clk(clk), .addr(pc_out[31:2]), .inst_out(inst) ); cu uut_cu ( .inst(inst[31:26]), .funct(inst[5:0]), .regdst(regdst), .alusrc(alusrc), .memtoreg(memtoreg), .regwrite(regwrite_int), .memread(memread), .memwrite(memwrite_int), .branch(branch), .aluop(aluop) ); // ... 其他模块实例化 ... // 顶层信号赋值(用于观测) assign pc_out = pc_out; assign inst_out = inst; assign aluout = alu_result; assign regwrite = regwrite_int; assign memwrite = memwrite_int; assign memdata_out = mem_rddata; // 状态LED驱动 assign led_regwrite = regwrite_int; assign led_memwrite = memwrite_int; endmodule指令存储器im.v:ROM的正确写法
教学实验中,IM通常用$readmemh加载hex文件。关键是要用initial块在仿真时初始化,用(* rom_style = "block" *)属性告诉综合器用Block RAM实现:
module im ( input logic clk, input logic [9:0] addr, output logic [31:0] inst_out ); logic [31:0] mem [0:1023]; (* rom_style = "block" *) // 强制使用BRAM initial begin $readmemh("im.hex", mem); // 从im.hex文件加载 end always @(posedge clk) begin inst_out <= mem[addr]; end endmoduleim.hex文件格式示例(MIPS机器码):
0x00400000 // add $t0,$s0,$s1 0x00614020 // sub $t0,$s0,$s1 0x20080005 // addi $t0,$zero,5 0x8c080000 // lw $t0,0($zero) 0xac080000 // sw $t0,0($zero)寄存器堆rf.v:读写冲突的规避
RF必须支持“读-读-写”操作,即同一周期读rs/rt,写rd。关键点是:写操作在clk上升沿后生效,读操作在clk上升沿采样。因此,只要rd地址不等于rs/rt地址,就不会冲突。Verilog中用always @(posedge clk)实现写,用组合逻辑实现读:
module rf ( input logic clk, input logic rst_n, input logic [4:0] rs_addr, input logic [4:0] rt_addr, input logic [4:0] rd_addr, input logic [31:0] wr_data, input logic regwrite, output logic [31:0] rs_data, output logic [31:0] rt_data ); logic [31:0] regs [0:31]; // 初始化寄存器(仿真用) initial begin foreach (regs[i]) regs[i] = 32'h0; end // 写寄存器(同步) always @(posedge clk) begin if (!rst_n) begin foreach (regs[i]) regs[i] <= 32'h0; end else if (regwrite) begin regs[rd_addr] <= wr_data; end end // 读寄存器(组合逻辑) assign rs_data = regs[rs_addr]; assign rt_data = regs[rt_addr]; endmodule4.3 Testbench编写:从单指令到程序级验证
好的testbench是CPU可靠的“试金石”。我采用三级验证策略:
第一级:单指令功能验证
针对每条指令写独立testbench,例如tb_add.v:
initial begin clk = 0; rst_n = 0; #10 rst_n = 1; // 释放复位 #10; // 等待第一个时钟 // 观察波形:PC=0x00000000, IR=0x00400000, RegWrite=1, ALUOut=0x00000000 #10; $finish; end第二级:连续指令序列验证
用$readmemh加载多条指令,观察寄存器变化:
initial begin $readmemh("test_prog.hex", tb_dut.im.mem); // 加载测试程序 clk = 0; rst_n = 0; #10 rst_n = 1; repeat (20) begin // 运行20个周期 #10 clk = ~clk; end $finish; endtest_prog.hex内容:
0x00400000 // add $t0,$zero,$zero -> $t0=0 0x20080005 // addi $t0,$zero,5 -> $t0=5 0x20090003 // addi $t1,$zero,3 -> $t1=3 0x01094020 // sub $t0,$t0,$t1 -> $t0=2第三级:MIPS汇编程序端到端验证
用MIPS汇编器(如SPIM)生成机器码,再转hex:
# max.s: 求两个数最大值 .text .globl main main: li $t0, 10 # a=10 li $t1, 15 # b=15 bgt $t0, $t1, else move $t2, $t0 # max=a j exit else: move $t2, $t1 # max=b exit: jr $ra用SPIM的-dump选项导出hex,导入im.hex,上板后用ILA观察$t2是否为15。
5. 常见问题排查与独家避坑指南
5.1 Vivado综合报错高频问题速查表
| 报错信息 | 根本原因 | 解决方案 |
|---|---|---|
[Synth 8-583] Cannot resolve non-constant multiple driver on net 'xxx' | 同一信号被多个always块赋值 | 检查所有assign和always,确保信号只在一个地方驱动。常见于误在always @(posedge clk)和always @(*)里都给pc_next赋值。 |
[Synth 8-3330] failed to find top module 'xxx' | 顶层模块名与工程名不一致,或未设为top | 在Vivado左侧“Settings”→“General”→“Top Module”中手动输入顶层模块名(如cpu_top),或右键RTL源文件→“Set as Top”。 |
[Place 30-609] IO port 'clk' has an invalid IOSTANDARD | XDC中IOSTANDARD拼写错误 | 必须是LVCMOS33(不是lvcmos33或LVCMOS 33),大小写敏感。 |
[Synth 8-615] inferring latch for variable 'xxx' | always @(*)中未覆盖所有分支,导致锁存器 | 在case语句末尾加default: xxx = 0;,或用if-else if-else确保所有路径都有赋值。 |
5.2 功能性Bug的典型现象与定位法
现象:CPU启动后PC卡在0x00000000,不再递增
→ 定位:用ILA抓pc_next信号。如果pc_next恒为0,说明PC更新逻辑失效。检查pc.v中pc_next是否被正确赋值为pc_out + 4,以及branch信号是否意外拉高导致pc_next被pc_add4覆盖。
→ 经验:在pc.v里加一个assign debug_pc_next = pc_next;,把debug_pc_next接到ILA,比直接看pc_out更早发现问题。
现象:add指令结果正确,但beq指令永不跳转
→ 定位:抓zero和branch信号。如果zero=0,说明ALU减法结果非零,检查aluop=2'b01是否送达ALU;如果zero=1但branch=0,说明CU的branch输出错误,检查opcode==6'b000100分支是否被遗漏。
→ 经验:在CU的case语句里,把6'b000100写成6'h4(十六进制),Vivado会自动转换,但人眼易漏看,务必用二进制显式写出。
现象:lw指令读出的数据是0,而非内存中存储的值
→ 定位:抓mem_addr和mem_rddata。如果mem_addr正确(如0x00000004),但mem_rddata=0,说明DM未正确读取。检查dm.v中memread信号是否在MEM阶段拉高,且mem[addr]索引是否正确(注意MIPS大端序,mem[addr]应返回mem_byte[addr+3]到mem_byte[addr]的拼接)。
→ 经验:在dm.v里加initial $readmemh("dm_init.hex", mem);,预先加载测试数据,避免空内存干扰。
5.3 海大实验特有问题与本地化适配
海大实验指导书要求最终在Basys3板上用LED显示CPU状态,这带来两个独特挑战:
- LED刷新率问题:CPU主频100MHz,LED直接接
regwrite会高频闪烁人眼无法分辨。解决方案是在顶层加一个分频器,用clk_div[19](约100MHz/2^20≈100Hz)作为LED更新时钟:
logic [19:0] clk_div; always @(posedge clk) clk_div <= clk_div + 1; assign led_regwrite = regwrite && (clk_div[19]); // 每10ms更新一次- 复位同步化问题:SW0开关抖动会导致
rst_n毛刺,引发CPU异常。必须在cpu_top.v里做两级同步:
logic rst_sync0, rst_sync1; always @(posedge clk) begin rst_sync0 <= ~sw0; // SW0按下为0 rst_sync1 <= rst_sync0; end assign rst_n = rst_sync1; // 同步后的复位信号最后分享一个真实教训:去年有学生在im.v里用$readmemb(二进制)加载im.bin,但Vivado综合时提示“memory initialization file not found”。折腾三天才发现$readmemb只支持仿真,综合时必须用$readmemh(十六进制)或Block RAM IP核。从此我所有工程都强制要求:指令存储器初始化文件必须是.hex格式,且路径写相对路径(如"../src/im.hex"),避免绝对路径导致团队协作时失效。
这个单周期CPU实验,表面是写几段Verilog,实则是构建一套硬件思维范式:从晶体管开关到