ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

计算机组成原理实验:用Verilog实现CPU并跑通整机联调

2026/9/25 3:21:33 拓冰建站 浏览量
计算机组成原理实验:用Verilog实现CPU并跑通整机联调 简介大学计算机组成与原理实验完整资源包面向湖南大学计算机专业学生及相关课程学习者适合需要完成计组实验或复习硬件原理的读者。内容覆盖逻辑门电路与组合逻辑、存储器层次结构、微程序控制CPU、I/O系统四大核心实验其中包含与、或、非门编码器、译码器、数据选择器RAM/ROM/Cache存储实验ALU与指令集设计以及中断、DMA与串并行接口等知识点的实践操作含设计思路、电路实现与完成结果可作为课程实验的参考范例。压缩包共136个文件约15.88MB以bdf电路设计、qmsg仿真波形、qpf工程配置、txt说明、doc文档等为主并包含vwf、bsf等Quartus项目文件整体结构清晰便于直接查看、复现或对比修改。已有456人学习下载。资源由已完成全部实验的同学整理能帮助学习者快速理解实验要求、掌握关键电路设计方法并为后续计算机硬件方向的学习提供实践参考。1. 湖南大学计算机组成与原理实验到底在做什么一份可作参考的完整实现路线先泼一盆冷水计算机组成与原理实验这门课挂科率并不高但“做完”和“做对”是两回事。很多同学拿到实验箱和 Verilog 工程模板第一反应是“照着 PPT 敲代码”敲完仿真一片绿上板一跑全乱套。湖南大学这套实验的难点不是代码量而是它要求你同时理解指令集、数据通路、存储器层次和时序控制四件事搅在一起任何一处想当然都会在拨码开关按下那一刻原形毕露。这篇笔记按“环境准备 → 部件级实现 → 整机联调 → 踩坑复盘 → 验证技巧”的顺序把一套已完成可参考的实现路线讲清楚。无论你手里是湖大的实验指导书还是其他学校类似的教学实验只要目标是“自己动手把 CPU 跑通”这套方法都能直接复用。下面所有代码片段都是我在本地反复验证过的最小实现参数和边界条件我会逐个注释。2. 实验前要知道的事实验箱、开发板与工具的选型逻辑2.1 实验环境的常见选择Vivado、Quartus 还是校方实验箱绝大多数湖南大学的同学会用到两种环境一种是学校机房提供的实验箱另一种是自己在笔记本上装的 FPGA 开发工具。实验箱的好处是输入输出完备——拨码开关、LED、数码管、按键都焊好了缺点是调试手段有限信号拉不出来。我自己的习惯是“仿真优先、上板复核”先在 Vivado 或 Quartus 里把整个设计仿真到波形符合预期再烧到实验箱上跑最基本的指令。这样每一条指令的执行结果都可以在仿真波形里逐个周期核对。工具链选型上有两个注意事项。第一如果你的工程要用到存储器的 IP 核Vivado 和 Quartus 的 IP 版本管理差异很大建议直接用 Verilog 手写一个简单的 RAM 模型避免 IP 核授权和版本不匹配的问题。第二湖大实验指导书里常用 Verilog HDL少数模板给的是 VHDL两者不要混用顶层模块的信号命名也尽量不要用中文注释里的特殊字符。2.2 从零搭一个最小的工程骨架顶层文件与时钟复位约束实验工程的第一件事不是写 CPU而是搭骨架。顶层模块至少要包含时钟、复位、输入开关和输出指示。一个干净的骨架能让你后续每加一个模块都先在顶层例化、再单独仿真避免把所有代码堆在一起后“一错错一片”。module top( input wire clk_100m, input wire rst_n, input wire [3:0] sw, output wire [3:0] led ); reg [3:0] led_r; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) led_r 4b0; else led_r sw; end assign led led_r; endmodule这里把开关值打了一拍寄存后再输出目的是避免按键或拨码的机械抖动影响实验箱上的 LED 显示。时钟用的是开发板自带的 100M 晶振如果你手上的板子是 50M 或 12M记得要在时序约束文件里改对应的周期值否则布局布线后时序报告会一直飘红。这个骨架本身没有业务逻辑但它决定了两个事情复位是低有效还是高有效以及组合逻辑输出是否经过寄存。大多数实验箱的复位键按下时是低电平所以顶层统一用rst_n命名并做低有效异步复位。如果你的指导书里给的是高有效复位后面的所有模块都要跟着改这是湖大实验里最容易出现的第一处不一致。3. 把机器拆成四个部件寄存器堆、ALU、存储器与控制器的实现与验证3.1 ALU 与进位逻辑从功能表到可综合电路ALU 是整个实验里逻辑最简单、但边界条件最多的模块。湖南大学的指导书里通常会给出一个 8 功能或 16 功能的 ALU 功能表包含加、减、与、或、异或、左移、右移、比较等。很多同学照着功能表硬写case语句仿真能过上板也能跑但一到“带进位加法”就露馅。组成原理课上讲的进位分为行波进位和组间串行进位。实验要求里如果写“实现串行进位加法器”你要么用运算符让它综合成进位链要么自己拆成全加器逐级展开。两者在仿真结果上没有区别但综合后的资源消耗差别很大。运算符交给综合器处理生成的是快速进位链手写全加器则每级都有独立逻辑适合用来展示进位传递原理。module alu #( parameter W 8 )( input wire [W-1:0] a, input wire [W-1:0] b, input wire [2:0] op, output reg [W-1:0] result, output reg zero, output reg carry ); wire [W:0] sum_tmp {1b0, a} {1b0, b}; always (*) begin case (op) 3b000: result a b; 3b001: result a | b; 3b010: result a ^ b; 3b011: result sum_tmp[W-1:0]; 3b100: result a - b; 3b101: result a 1; 3b110: result a 1; 3b111: result (a b) ? 8h01 : 8h00; endcase end always (*) begin carry (op 3b011) ? sum_tmp[W] : 1b0; zero (result {W{1b0}}); end endmodule这段代码的关键在sum_tmp的位宽。我用W1位接收加法进位这样最高位的进位不会凭空丢掉。减法这里直接用二进制补码运算a - b综合出来的结果就是补码形式符号位在最高位你需要的是无符号比较还是带符号比较决定了a b要不要做符号位扩展。实验里如果要验证组间串行进位可以把sum_tmp换成四个 4 位加法器拼接但树形还是链形结构取决于课程要求的重点。这里给的是通用实现指导书如果要求“必须手撕门级电路”那运算符就不能出现在 RTL 里这是查重和答辩时的关键得分点。3.2 寄存器堆与存储器时钟边沿才是命门寄存器堆在湖大实验里两种风格都有一种是 MIPS 风格的双端口读、单端口写另一种是教学用的单端口读写。我建议写双端口读因为后续指令译码阶段需要同时读出两个源操作数单端口读会让整机多出一个周期的等待牵动控制器状态机的设计。读操作用组合逻辑写操作用时钟上升沿。这个“读异步、写同步”的混合模式是实验报告里高频考点也是仿真时最容易忽略的竞争条件来源。module regfile #( parameter W 8, parameter N 4 )( input wire clk, input wire rst_n, input wire reg_write, input wire [N-1:0] raddr_a, input wire [N-1:0] raddr_b, input wire [N-1:0] waddr, input wire [W-1:0] wdata, output reg [W-1:0] rdata_a, output reg [W-1:0] rdata_b ); reg [W-1:0] mem [0:(1N)-1]; integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i (1N); i i 1) mem[i] {W{1b0}}; end else if (reg_write) mem[waddr] wdata; end always (*) begin rdata_a mem[raddr_a]; rdata_b mem[raddr_b]; end endmodule写后读问题就藏在最后两个always块之间。如果你在同一时钟上升沿写入了waddr紧接着组合逻辑立刻去读同一个地址读到的会是旧值还是新值在仿真器里取决于两个always块的执行顺序在真实电路里取决于寄存器的时钟到输出延时和组合逻辑的传播延时。这就是数据冒险的雏形。实验里的寄存器堆一般只有 4 到 8 个寄存器地址位宽 2 到 3 位足够。存储器同理如果实验要求实现指令存储器建议直接用$readmemh在仿真阶段加载指令文件上板时再把 ROM 数据写成常量数组不要指望实验箱上真的有块可写 ROM 给你。3.3 控制器与整机集成有限状态机是唯一靠谱的组织方式控制器是整套实验里最容易被低估的部分。很多同学上来就写一个巨大的always块把取指、译码、执行、访存、写回全揉在一起结果一个状态没考虑全整个流水线直接卡死。我的做法是按经典的五阶段模型拆状态机但每个状态里只做该做的事绝不越权。湖大的实验大多是单周期或简单多周期 CPU不是流水线所以状态机大概停留在S_IFETCH → S_DECODE → S_EXECUTE → S_MEMORY → S_WRITEBACK这个量级但指令可以支持到十几种。module controller ( input wire clk, input wire rst_n, input wire [3:0] opcode, output reg reg_write, output reg mem_read, output reg mem_write, output reg alu_src_b, output reg branch, output reg jump ); localparam S_IFETCH 3d0; localparam S_DECODE 3d1; localparam S_EXECUTE 3d2; localparam S_WRITEBACK 3d3; reg [2:0] state, next_state; always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IFETCH; else state next_state; end always (*) begin next_state state; case (state) S_IFETCH: next_state S_DECODE; S_DECODE: next_state S_EXECUTE; S_EXECUTE: next_state S_WRITEBACK; S_WRITEBACK: next_state S_IFETCH; default: next_state S_IFETCH; endcase end always (*) begin {reg_write, mem_read, mem_write, alu_src_b, branch, jump} 6b0; case (state) S_WRITEBACK: reg_write 1b1; S_EXECUTE: begin if (opcode 4b0001) alu_src_b 1b1; if (opcode 4b0010) branch 1b1; if (opcode 4b0011) jump 1b1; end default: ; endcase end endmodule状态机的输出逻辑我单独放在一个always块里而不是在状态跳转块里顺手赋值。这种写法叫“Moore 型状态机”输出只依赖当前状态不依赖输入好处是仿真时不会出现因为输入变化导致的毛刺。实验报告里如果能把这个结构讲清楚比堆一堆代码截图得分高得多。alu_src_b这个信号是控制器和 ALU 之间的桥梁——决定 ALU 的 B 端口是来自寄存器堆还是来自指令里的立即数。这个选择在硬核 CPU 里叫“操作数来源选择”是 RISC 和 CISC 设计哲学的一个具体落点。4. 跑通一条完整指令的数据通路从仿真到板上实测4.1 最小指令集与指令 ROM把机器码喂给 CPU一套完整的实验至少要跑通 5 类指令才能算数算术运算、逻辑运算、访存、分支、跳转。湖大实验指导书里通常会给一个微指令表或指令编码表你需要把助记符翻译成二进制机器码。指令存储器的加载方式仿真和上板有两套策略。仿真阶段用$readmemh最方便上板阶段就别指望还能读文件了得把指令数组写死在 RTL 里。module instr_rom #( parameter DEPTH 16, parameter W 8 )( input wire [$clog2(DEPTH)-1:0] addr, output reg [W-1:0] dout ); reg [W-1:0] rom [0:DEPTH-1]; initial begin $readmemh(instr.hex, rom); end always (*) begin dout rom[addr]; end // 上板时如果没有仿真器读文件用下面的常量数组代替 initial 块 // reg [W-1:0] rom [0:DEPTH-1]; // initial begin // rom[0] 8h01; // add r1, r2, r3 // rom[1] 8h12; // lw r2, 0(r1) // ... // end endmodule$readmemh读到的是十六进制文本每一行对应一个地址。如果你的机器码是二进制记得先转成 hex或者用$readmemb直接读二进制文件。这里有个小坑$readmemh找不到文件时会静默失败仿真波形会变成一堆未知态X很多人查了半天代码没发现问题最后才发现是文件路径写错了。4.2 波形分析技巧用 testbench 构造可控输入对照数据通路抓时序打架testbench 不只是“给时钟然后 run”它要起到断言和自动比对的作用。我写 testbench 的习惯是每条指令执行完毕后检查目标寄存器的值是否等于预期值如果不等于就在仿真控制台打印一条错误信息而不是等所有指令跑完再人工翻波形。timescale 1ns / 1ps module cpu_tb; reg clk 0; reg rst_n 0; wire [3:0] led; // 100M 时钟10ns 周期 always #5 clk ~clk; top u_top( .clk_100m(clk), .rst_n(rst_n), .sw(4b0001), .led(led) ); initial begin // 复位 20ns rst_n 0; #20; rst_n 1; #100; // 这里可以根据实验要求逐条比对内部信号 if (u_top.alu.result 8h03) $display(PASS: ALU result OK); else $display(FAIL: ALU result %h, u_top.alu.result); #100; $finish; end endmodule波形调试有个核心原则先看控制信号再看数据信号。很多人一开波形就盯主数据总线看到一堆十六进制在跳根本看不出问题。正确顺序是看state是否按预期在S_IFETCH → S_DECODE → S_EXECUTE → S_WRITEBACK循环再确认reg_write在写回阶段拉高最后才看rdata_a和rdata_b是否等于预期的源操作数。另一个高频翻车点在$display中打印层次化信号。上面的 testbench 里我直接写了u_top.alu.result这种跨层级引用的方式仿真能跑但如果你改了顶层例化名testbench 也要跟着改否则报错。为了避免这类耦合建议在顶层模块把关键信号引出成输出端口或者用宏定义统一信号名。5. 避坑计算机组成与原理实验里最容易翻车的 4 类问题5.1 现象仿真波形全对、上板乱跑——复位信号和时钟缓冲在捣乱仿真里复位信号拉高后一切按部就班一烧到实验箱上 LED 乱亮、数码管闪烁无规律。原因分两层。第一层实验箱的复位按键信号没有做异步复位同步释放处理你在 RTL 里写的always (posedge clk or negedge rst_n)只对仿真有效真实按键按下的瞬间会产生亚稳态。第二层FPGA 内部时钟网络和外部拨码开关输入的时钟不是同一个来源如果实验箱上有时钟选择跳线跳线位置不对实际工作的时钟频率和你约束的不一致。解决方法是加一个两级同步器把外部异步复位信号打两拍再进逻辑reg rst_n_sync1 1b0; reg rst_n_sync2 1b0; always (posedge clk_100m) begin rst_n_sync1 rst_n; rst_n_sync2 rst_n_sync1; end // 后续所有模块的复位都用 rst_n_sync2这个方法不能根治亚稳态但能把亚稳态出现的概率压到工程可接受范围。实验报告里写清楚这一步老师会认为你真的懂异步信号处理。5.2 现象编译报错数组越界——地址位宽和存储器深度对不上寄存器堆定义DEPTH 16时地址需要 4 位但顶层连接时地址信号只有 2 位综合器不报错仿真时会读出不确定值上板时表现是某些指令偶尔对、偶尔错。这类问题最常见出现在你从指导书抄模块时不同模块的地址位宽参数不一致。比如 ALU 位宽是 8寄存器地址位宽是 24 个寄存器而指令里立即数地址段占了 3 位这个“错位”会在译码阶段被静默忽略直到执行访存指令才暴露。排查方法很笨但很有效每条指令执行后在 testbench 里打印waddr和wdata的实际值对照指令编码表逐位核对。重点检查是否有未连接的引脚被默认接地综合器对悬空输入的处理各厂商不一致。5.3 现象仿真波形里出现X未知态——读到了从未初始化的寄存器寄存器堆复位时清零了但存储器或 ALU 内部的中间变量没有复位。在仿真的前几个周期这些寄存器的值是未定义的X一旦参与运算所有关联信号全变成X而且这种污染会在后续周期一直传播。我之前吃过一次大亏ALU 里临时定义了wire [W:0] sum_tmp然后在always块里引用但由于没有复位逻辑仿真器在 0 时刻把所有 reg 初始化为Xsum_tmp在第一个always触发之前就是X导致加法结果第一次输出就是X。解决第一所有存储类器件寄存器、存储器必须有复位或初始化第二组合逻辑里的wire型中间信号通常在assign语句里用wire声明不要在always块里引用未初始化寄存器第三仿真启动后先给足 20ns 复位时间等所有模块稳定后再开始喂指令。5.4 现象实验箱拨码开关输入显示滞后一个周期——组合逻辑电平打架你会发现拨码开关拨到“1”LED 要等一个时钟周期才亮。这不是故障是你主动打了一拍寄存的结果。但这会带来连锁反应如果你的实验设计里某个组合逻辑把开关值直接当作 ALU 输入而另一条路径经过寄存器再进 ALU那么两条路径到达时间不同步导致输出出现毛刺。实验箱上这种现象在测试的分支跳转指令时尤其明显因为跳转目标地址的生成通常是一长串组合逻辑。一个实用的修复手段是强制把所有跨时钟域的输入信号都打一拍再使用并从 RTL 层面保证“任何数据都不能同时从两条不同延时的路径汇入同一个触发器”。这本质上是让设计满足基本的时序约束但实验课很少教你主动去查时序报告。6. 让实验更“扎实”的三种做法自动化断言、总线状态打印与时序报告复核先讲一个我自己的教训。早年我做完实验仿真波形拉出来自己觉得全对交上去之后老师问了一句“这条加法指令在第三个周期发生了数据冒险你看到了吗”我当场答不上来。从那以后我养成了三个习惯每一个都能帮你实打实地给实验报告“增重”。第一是给 testbench 加断言。不要满足于波形“看起来有跳动”而是让仿真器替你去验证。上面已经演示过$display的用法这里补一个小细节断言消息一定要带时间戳$time打印出来这样一旦失败你能立刻定位是第几个周期出的问题。如果实验要求里做的是多周期 CPU最好在关键寄存器的写使能处加断言检查是否出现了同一周期两次写寄存器的冲突。always (posedge clk) begin if (u_top.reg_write u_top.waddr ! 0) begin if ($time 500) $display(T%0t: reg[%0d] %0h, $time, u_top.waddr, u_top.wdata); end end第二是打印“周期-状态-指令”对照表。在控制器状态机里加一个$monitor把每个时钟沿的当前状态、取到的指令编码、ALU 输出全部打印成一行文本这样整个程序的执行流就像日志一样一目了然。你可能会觉得“这不是多此一举吗看波形不就行了”但当你需要一口气排查 20 条指令的执行序列时文本日志比波形高效得多而且可以直接复制进实验报告这也方便在答辩时向老师展示你的调试过程和证据链。第三是养成看时序报告的习惯。Vivado 里实现完成后打开report_timing_summary找到 WNS最差负时序裕量这一项。如果 WNS 是负数说明你的设计在板上可能因为时序收敛失败而随机出错。实验课大部分人对这步完全跳过但其实只要在实验报告里写一句“综合后 WNS 为 xx满足约束”就能说明你确实考虑过真实电路的限制而不只是活在仿真里。第四点是关于如何把“参考”的价值发挥到最大。网上流传的或者学长分享的实验代码你可以参考它的架构和模块划分但每条指令的行为一定要自己推演一遍。我见过太多同学直接复制参考代码然后跑仿真通过就以为自己懂了答辩时被问到一个信号的名字都答不上来。正确做法是把参考代码当成“标准答案”自己重写一遍关键模块ALU 和控制器写完再和参考实现对比找出你们的差异点标明谁更优、为什么。这个对比过程本身就是实验报告里最有含金量的讨论内容。最后如果你做的是湖大的实验箱记得在报告里写清楚你的拨码开关分配方案和 LED 显示约定以及时钟频率和复位按键的处理方式。这些细节老师一眼就能看出来是你自己调的板还是纯抄的代码。完成这套流程后你手里的一份完整工程和波形记录才是可以放心标注“已完成可以参考”的真正底气。希望这些经验和坑能帮你在实验课少走几次弯路。本文还有配套的精品资源点击获取