
简介这是配合龙芯教学实验箱的CPU设计与体系结构实验指导手册教师版面向高校计算机专业学生和任课教师用于数字逻辑、计算机组成原理、计算机体系结构等课程实验教学。手册分为CPU设计与CPU优化两大部分按难度递进安排实验包括定点加法、定点乘法、寄存器堆、ALU、存储器实现进而到单周期CPU、多周期CPU及静态5级流水线CPU设计与系统优化并附有实现的MIPS指令集和CP0寄存器定义。资源为单个PDF文件大小6.09MB已有680人学习下载。内容不仅给出各实验目的、设备要求、任务步骤和Verilog设计流程还结合FPGA平台讲解电路搭建与信号观察方法教师可以直接用于备课指导学生能够逐步构建从基本运算单元到完整CPU的系统深入理解数据通路、控制逻辑和流水线优化。附录中的指令集与寄存器清单方便实验时随时查阅和验证。1. 先搞清楚这份手册到底帮你解决什么问题第一次带《计算机组成原理》实验课的老师最头疼的不是准备PPT而是怎么让学生在有限课时里亲手把一颗CPU“点亮”。这份实验指导手册解决的就是从零到一的问题它以 LS-CPU-EXB-002 实验箱为载体把定点加法、定点乘法、寄存器堆、ALU、存储器、单周期 CPU、多周期 CPU、静态 5 级流水线串成一条可动手复现的路线。资源文件名里的 EXB-003 只是版本批次实际平台和实验内容以手册正文为准。适合两类人要组织 FPGA 实验课的老师以及想把“CPU 工作原理”从概念黑匣子变成自己 Verilog 代码的本科生。你不需要先学完体系结构再动手跟着实验一往下做就行。2. 硬件与软件栈LS-CPU-EXB-002 实验箱和 Vivado 整体流程2.1 实验箱硬件组成与你要关注的接口手册一开篇就把实验箱的配套资源列清楚了FPGA 实验板、Xilinx 下载线、串口线含 USB 串口转换器、电源线含适配器。真正需要你上心的是 FPGA 板上的器件型号。新建 Vivado 工程时器件筛选要在 family 选 Artix 7package 选 fbg676最后落到xc7a200tfbg676-2上。这个型号在整本手册里是统一的单周期 CPU、多周期 CPU、流水线 CPU 都要用它做综合布局布线选错封装后面引脚约束文件直接失效。实验箱最有特色的部分是 LCD 触摸屏。按手册里的说法硬件驱动已经做进了板子不需要处理器核参与触摸屏就能独立完成显示和输入功能并且对外提供了“简单清晰的调用接口”。这意味着你在做实验一“定点加法”时不需要自己写 VGA 驱动或者串口屏协议只要调封装好的模块就能把两个加数显示在屏幕上再用触摸功能输入数据。手册里这个设计意图很明确让学生的注意力集中在 CPU 模块本身而不是被外设驱动带偏。后续六个实验的上板验证环节几乎都靠这块触摸屏撑场面。实验内容安排的层次也很清楚手册把它分成 CPU 设计和 CPU 优化两部分。前者面向组成原理课程从实验一到实验七最后接一个课程设计后者面向体系结构课程对应课程设计和课程设计拓展。具体清单如下。编号实验内容一句话定位上板验证方式实验一数据运算定点加法熟悉平台、仿真、上板的最小闭环LCD 触摸屏输入/显示实验二数据运算定点乘法从组合逻辑向时序逻辑过渡LCD 触摸屏输入/显示实验三寄存器堆实现为 CPU 准备通用寄存器组触摸屏读寄存器值实验四ALU 模块实现实现算术逻辑运算核心触摸屏显示运算结果实验五存储器实现用 FPGA 内部 BRAM 搭存储模型触摸屏写读数据实验六单周期 CPU 实现一条指令在一个周期内跑完板上运行程序并显示实验七多周期 CPU 实现用状态机拆分指令执行步骤板上运行程序并显示课程设计静态 5 级流水线 CPU接触数据冒险与控制冒险板上跑指令序列拓展优化 CPU 系统转发、调度、异常处理综合报告与上板验证从这张表能看出它不是零散的小实验而是一条从单元电路到完整处理器的递进路线。后面每个实验都是在前面模块的基础上组装。2.2 从新建工程到上板一套贯穿所有实验的 Vivado 流程手册里每个实验的操作步骤几乎都复用同一套流程我把它拆成六步。第一步是新建工程打开 VivadoFile → New Project工程类型选 RTL Project勾选 “Do not specify sources at this time”先建一个空工程。第二步添加设计源文件可以直接在工程里新建.v文件也可以把外部编辑器写好的文件通过 Add Sources 加进来。第三步是添加外围展示模块和 LCD 模块也就是把lcd_module的网表文件加进工程。第四步写 testbench做行为仿真确认功能正确。第五步添加.xdc引脚约束文件。第六步综合、布局布线、生成比特流下载到实验箱上验证。这套流程在实验一里被讲得最细因为后续实验只是换不同的module实现外围结构基本不变。有个细节容易忽略Vivado 工程管理器里如果某个模块前面带了问号说明这个源文件还没添加。手册里lcd_module是以.dcp格式提供的网表文件不是.v源码目的就是不让学生看到 LCD 驱动内部实现。另外手册特意提了一句某些版本的 Vivado 需要先把 dcp 文件解压再添加不然工程识别不到。我按这个提示操作综合阶段确实没有再报黑盒错误。2.3 顶层模块怎么组织以 adder_display 为模板实验一给出的顶层结构是整个系列实验的标准模板。顶层模块叫adder_display里面调用两个子模块一个是功能主体adder另一个是外围展示模块lcd_module。这种“功能模块 展示模块”的剥离方式值得你留意。功能模块只关心输入输出信号完全不碰引脚展示模块负责把信号映射到屏幕或按键上引脚绑定只出现在顶层模块的.xdc约束里。三层各干各的事。手册在实验一里花了大量篇幅讲adder_display意思是以后所有实验的外围模块都可以照这个写法改。你在做实验三寄存器堆的时候只需要把顶层显示模块里的adder换成regfile再把显示数据换成读端口输出其他结构不用动。这也解释了为什么实验一到实验七的工程搭建过程几乎一样——它们共用同一套显示框架你只需要替换核心计算模块。理解了这个套路后面三次上板实验你在工程配置上基本不会卡壳。3. 基础模块实验拆解从定点加法到存储器3.1 实验一定点加法最小的完整 Verilog 工程实验一的参考实现非常短adder.v里就一个模块核心代码只有一行 assign。它直接用了 Verilog 的运算符而不是自己搭全加器。这样写的好处是综合工具会自动调用库里的加法器 IP面积和时序往往比手搭的加法器更优。timescale 1ns / 1ps // 文件名: adder.v // 描述加法器直接使用会自动调用库里的加法器 module adder( input [31:0] operand1, input [31:0] operand2, input cin, output [31:0] result, output cout ); assign {cout, result} operand1 operand2 cin; endmoduleassign后面把cout和result拼成一个 33 位的数低 32 位是加法结果最高位是进位输出。这是处理“加法结果进位”最直接的写法也符合教学里对全加器演进的预期。如果你拿到的是实验箱配的教师版手册会发现作者在这里特意做了注释说明这是参考设计教师可以要求学员自己搭全加器也可以直接用两种方案都能完成实验。仿真部分手册给的testbench.v很考验功底因为它用$random自动生成了随机激励而不是手动写死几组测试数。代码里一共有三行关键赋值分别作用于两个 32 位加数和进位输入。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这里有一个常见的理解偏差$random返回的是有符号数直接赋给 32 位寄存器没问题但如果用在取模上负数取模可能得到负值。手册里对cin的赋值用了拼接符{$random}把它变成非负数再取模这样只有 0 或 1 两种结果。第 29 到 31 行的三个always块每 10ns 触发一次相当于每 10ns 换一组新的随机输入。仿真跑起来后你在波形窗口里看到的不是固定波形而是持续跳变的随机激励这比手写几十组测试向量省事得多。仿真时的操作细节手册也写得很具体在 Scopes 窗口选中adder对应的源文件对象在 Objects 窗口左键选中要观察的信号右键 Add To Wave Window 添加信号。如果添加之后波形窗口没有更新需要点 Relaunch 重新载入仿真。还有就是波形默认显示二进制一个 32 位数看起来像一长串 0 和 1检查起来太费眼睛。选中信号右键Radix 改成 Hexadecimal显示就清爽了。这套操作习惯会延续到实验六、实验七越早熟悉越好。3.2 实验二定点乘法开始挑战时序实验二的主题从加法跳到乘法难度明显上一个台阶。如果是两个 32 位数相乘结果位宽扩展到 64 位不能像加法那样一条 assign 就完事。手册在这里没有继续给完整代码而是留了方案选择的余地你可以自己用移位累加实现乘法器也可以继续用*运算符由工具推导出乘法器模块。但从实验目的出发自己实现乘法器才是理解“部分积”和“移位累加”的关键。我拆这类实验时一般会建议学生先做一个 8 位乘 8 位的乘法器练手跑通之后扩到 32 位。如果用*直接做 32 位乘法综合工具会映射到 FPGA 里的 DSP 硬核面积和时序都好看适合赶进度如果自己搭阵列乘法器代码量会明显增加但你会真正理解为什么乘法比加法慢以及多周期 CPU 里为什么要把乘法单独拆出去。这个实验还有一个容易被忽略的点上板显示 64 位结果时触摸屏一次显示不完通常要分高 32 位和低 32 位两次显示或者做一个“切换显示高低位”的按键逻辑。这属于外围展示模块的设计范畴也是实验二和实验一在工程搭建上为数不多的区别。3.3 实验三与实验四寄存器堆和 ALU寄存器堆是所有后续 CPU 实验的地基。手册实验三要求你实现一组通用寄存器从规格上看它对应的是 MIPS 32 个 32 位寄存器。单周期 CPU 里指令译码阶段需要同时读两个源操作数写回阶段要写一个目标寄存器所以寄存器堆最少要提供两个读端口和一个写端口。常见写法是这样的// 寄存器堆常见写法写端口用时钟沿触发读端口用组合逻辑 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] regs [0:31]; integer i; initial begin for (i 0; i 32; i i 1) regs[i] 32b0; end always (posedge clk) begin if (we waddr ! 5b0) regs[waddr] wdata; // MIPS 的 $0 寄存器恒为 0 end assign rdata1 regs[raddr1]; assign rdata2 regs[raddr2]; endmodule这段代码里有三个关键点。第一regs [0:31]是一个长度为 32、位宽 32 的二维数组在 FPGA 里会被综合成一块分布式的寄存器阵列。第二写端口是时序逻辑只在时钟上升沿写入读端口则是纯组合逻辑地址一变输出立刻跟着变这和单周期 CPU 对数据通路的要求一致。第三MIPS 约定$0寄存器永远读出来是 0所以写地址为 0 时要屏蔽写入否则程序里对$0的误写会污染整个系统。你以后写单周期 CPU 的译码逻辑时很多控制信号都依赖这两个读端口的稳定性。实验四的 ALU 模块就是在寄存器堆外面包一层运算逻辑。按手册里的定位ALU 要覆盖算术和逻辑两类操作常见接口是这样设计的端口方向位宽说明Ainput32源操作数 1Binput32源操作数 2ALUOpinput4运算控制码Resultoutput32运算结果Zerooutput1结果为 0 标志分支指令用ALU 实现本身不复杂核心是一个case语句按ALUOp选择加法、减法、与、或、异或、左移、右移、比较等操作。但是实验四真正的坑在于Zero信号它必须在所有运算结束后根据整个 32 位结果判断是否全 0如果你的 ALU 用case输出ResultZero得用独立的assign判断不能只在一个分支里赋值否则综合时容易产生锁存器。3.4 实验五存储器实现存储器和寄存器堆虽然都是“存东西”但实现原理完全不同。寄存器堆用触发器阵列读写灵活存储器要模拟存储器模型面向的是 FPGA 内部的 BRAM 资源。手册实验五要你实现一个可供 CPU 使用的存储模块关键参数是数据位宽和地址位宽。这里需要区分两个概念指令存储器和数据存储器。在一个单周期 CPU 里指令存储器和数据存储器通常是分开的。指令存储器只读地址从 PC 来输出直接进译码器数据存储器可读可写地址来自 ALU 的计算结果写入数据来自寄存器堆的第二个读端口。实验五在做上板验证时常见做法是把存储器里的若干地址映射到触摸屏上显示通过触摸输入地址和数据再读出来核对。这样你既验证了存储器的读写时序也顺便验证了地址译码逻辑。手册在实验五里特意强调要“掌握利用显示屏观察特定信号的方法”。这和我平时调试存储器的习惯是一致的仿真波形只能证明你的电路在理想激励下是对的上板后真正会用这块存储器的场景是运行指令序列所以存储器实验的一个重要目标是让你建立“地址、数据、读写使能、时钟”四要素的时序直觉。后面单周期 CPU 里一旦跑不出预期结果九成问题都出在存储器读写时序上早早在实验五里把这种感觉建立起来后面能少调很多 bug。4. CPU 实现实验拆解单周期、多周期与静态 5 级流水线4.1 实验六单周期 CPU一条指令在一个周期内跑完单周期 CPU 是整本手册从“模块”迈向“系统”的分界点。它的核心特征是一条指令从取指到写回所有操作都在一个时钟周期内完成没有中间状态。你不需要设计状态机只需要把取指、译码、执行、访存、写回五条数据通路全部用组合逻辑串起来最终在一个时钟沿之间完成全部动作。代价是时钟频率上不去因为单个周期的时间要由最慢的指令决定典型的就是lw从存储器读数据到寄存器。手册在附录 A1 里给出了单周期 CPU 必须实现的指令集。从教学顺序判断这个集合覆盖了 MIPS 的基础指令类型寄存器和立即数的算术逻辑运算、访存、分支、跳转。附录 A1 的作用不是摆设它是你设计控制信号真值表的依据。你每加一条指令就要在译码逻辑里新增一个case分支明确这条指令要打开哪些控制信号寄存器写使能、存储器写使能、ALUSrc、RegDst、Branch、Jump。我在设计单周期 CPU 时习惯把所有指令的控制信号列成一张真值表再对着真值表写 Verilog这样比边写边想少出错。单周期 CPU 的 Verilog 结构一般是“顶层 CPU 模块 若干功能子模块”的组合。顶层模块里实例化 PC、指令存储器、寄存器堆、ALU、数据存储器、立即数扩展单元并用一批控制信号把它们连接起来。手册里特别提到这一章的.v文件数量会明显增加形成一定的调用层次这也是它和前几章实验最大的不同。你在工程管理区看到的层次关系基本就是一张 CPU 结构框图。仿真阶段单周期 CPU 的 testbench 需要构造一段真实的指令序列而不是随机激励。最简单的做法是在指令存储器初始化文件里预置一小段程序比如用addi给两个寄存器赋值再用add相加写回第三个寄存器最后用beq判断跳转。通过观察 PC 的变化和寄存器堆的写回值验证每一条指令是否按预期执行。这一步能跑通单周期 CPU 的主体功能就算成立。4.2 实验七多周期 CPU用状态机切分指令多周期 CPU 改变的是整个数据通路的组织方式指令执行被拆成若干步每一步占一个时钟周期用状态机控制当前执行到哪一步。手册实验七的关键词是“将不同的操作分配到不同的时钟周期以提高效率”。和单周期相比多周期 CPU 可以把一条指令拆成取指、译码、执行、访存、写回五个阶段硬件上可以复用 ALU 和存储器单周期里需要两份存储器的问题在这里也得到了缓解。状态机的设计是多周期 CPU 实验的核心。常见做法是用S0到S8这样的状态编码每个状态完成一个固定的微操作。以lw指令为例它至少要经历四个状态取指、读寄存器堆算地址、访问存储器、写回寄存器堆。每种指令在状态机里走的路径不一样译码逻辑需要根据指令类型决定下一个状态跳转到哪里。附录 A2 里列的是多周期 CPU 新增支持的指令相比单周期版本多出来的部分主要是对复杂指令的拆解支持。我在做多周期 CPU 时有一个习惯先把所有指令的执行步骤画成状态转移图标清楚每个状态里要拉高的控制信号再写状态机代码。如果跳过画图直接写case很容易在状态跳转上翻车。多周期 CPU 的另一个考验是上板调试状态机跑得快你肉眼根本看不清楚到底停在哪一步所以一般会在顶层模块里加一个状态显示逻辑把当前状态编码送到触摸屏或 LED 上这样出问题时能直接看到卡在哪个状态。4.3 课程设计静态 5 级流水线 CPU 与 CP0 寄存器课程设计部分直接跨到静态 5 级流水线这是整本手册里技术密度最高的一章。流水线把指令执行拆成五个阶段取指 IF、译码 ID、执行 EX、访存 MEM、写回 WB每两个阶段之间由流水线寄存器隔开多个指令可以在不同阶段同时执行。理论上每条指令仍然要经历五个阶段但因为各个阶段重叠执行CPU 的吞吐率大幅提升。但流水线的代价是逻辑复杂度。你需要处理两类冒险数据冒险和分支冒险。数据冒险典型场景是一条add后面紧跟着一条使用该结果的sub第二条指令在 ID 阶段读寄存器时第一条指令还没到 WB 阶段读到的就是旧值。静态 5 级流水线不做乱序执行解决思路要么是插入停顿气泡要么是新增一条转发通路。转发单元需要比较两条指令之间的寄存器地址把 EX 阶段的结果直接旁路给后面的指令这要求你能熟练操作流水线寄存器里的字段。分支冒险更麻烦。beq指令要等到 EX 阶段才能算出跳转方向但下一条指令已经在 IF 阶段被取进来了怎么处理取决于你的流水线实现。常见做法是默认不跳转一旦发现分支条件成立就把后面已经取入的指令全部作废代价是每遇到一条分支指令就要浪费一个周期。手册里要求你实现的是“静态”5 级流水线意味着没有动态调度硬件只负责按固定顺序执行指令之间的冲突要靠编译器或者程序员提前排好。附录 A3 列出了流水线版本新增支持的指令附录 B 则给出了实现的 CP0 寄存器。CP0 寄存器是 MIPS 处理器里负责异常和中断处理的特殊寄存器包含状态寄存器、原因寄存器、EPC 等。在流水线 CPU 里你要处理异常发生的时机和恢复现场的逻辑异常发生后处理器要记录当前指令地址到 EPC跳转到异常处理入口并在处理结束后返回。手册附录 B 列出的 CP0 寄存器是实现这个流程的关键。课程设计拓展“优化 CPU 系统”就是在 5 级流水线的基础上做改进常见方向有增加指令转发、优化分支预测、添加中断支持。能把这部分做完这门课的成绩基本就是一档了。5. 常见问题与避坑记录手册流程里容易翻车的五个点5.1 testbench 没有设为顶层仿真一片空白现象添加了 testbench.v 后运行行为仿真波形窗口空白Vivado 没有把 testbench 识别为仿真顶层。原因工程里同时存在多个顶层候选文件时Vivado 默认选择的是你最早添加到工程里的设计文件而不是 testbench。解决在 Sources 面板里右键 testbench.v选择 Set as Top再重新运行仿真。这个操作在每次新建仿真工程时都应该养成习惯。5.2 新加的信号在波形窗口里怎么都看不到现象你在 Objects 窗口选中信号并 Add To Wave Window波形窗口里却没有出现对应数据。原因Vivado 在仿真运行期间添加的观察信号不会自动刷新到当前波形视图需要重新载入仿真才能生效。解决添加完所有要观察的信号后点工具栏的 Relaunch 重新启动仿真新信号的数据就出来了。我在这上面浪费过半小时后来直接固定操作顺序先添加全部信号再 Relaunch。5.3 lcd_module 模块带问号综合报 No such module现象工程管理区里 lcd_module 前面有个问号综合时报找不到模块。原因lcd_module是以.dcp网表文件提供的黑盒模块你没有把 dcp 文件添加到工程里或者你用的 Vivado 版本比较老需要先解压 dcp 文件再添加。解决在 Add Sources 里选择 Add or create design sources找到配套的lcd_module.dcp添加成功后问号消失。注意 dcp 文件不需要也不能当普通 Verilog 源文件打开。5.4 32 位数据用二进制显示眼睛直接看花现象仿真波形里一长串 0 和 1你根本分不清 result 到底是哪个值。原因Vivado 默认以二进制格式显示所有波形信号。解决在波形窗口的 Name 列选中信号右键选择 Radix改成 Hexadecimal或者对无符号数据选 Unsigned Decimal。这个设置对单个信号独立生效不会影响其他信号。我在检查 32 位加法结果时基本都是十六进制显示只有看单比特控制信号时才切回二进制。5.5 手册里的手算示例和仿真波形对不上现象手册实验一给了一组手算检验数据声称b897be71 42f24186 1 fb89fff7但你的仿真波形结果却是fb89fff8进位输出也不一致。原因这是一个典型的文档笔误。按二进制加法逐字节计算b897be71 42f24186本身已经是fb89fff7再加上低位进位cin1结果应该是fb89fff8。手册里很可能是在写示例时把“加数和被加数的和”误写成了“包含进位后的最终结果”。解决遇到仿真结果和文档示例不一致不要先怀疑自己的代码先用 Python、Windows 计算器或者手算把期望值算清楚再去对照波形。这不是个别情况改动频率越高的大型实验参考文档里越是容易出现这种手算错误。6. 拿到手册之后怎么改成自己的实验从换引脚到扩展指令集这份手册虽然是教师版但改造空间很大你可以把它当一套“CPU 实验框架”来用。最常见的二次开发是换板子你手上如果是一块其他型号的 Artix-7 开发板顶层模块和 Verilog 代码基本不用动只需要重写.xdc约束文件。把PACKAGE_PIN改成新板原理图对应的引脚IOSTANDARD保持 LVCMOS33 不变再核对一下时钟引脚和复位引脚的位置。注意.xdc里的clk、resetn、led_cout、sw_cin这些信号名字必须和顶层模块端口名完全一致否则会报 constraint mismatch。第二个改造点是把lcd_module这个黑盒换掉。如果你需要在自己的板子上跑这套实验但手里没有配套的 LCD 网表文件可以用 UART 串口模块代替触摸屏把加法结果、寄存器值或 PC 值通过串口打印到上位机。实现思路是保留adder_display的顶层结构把lcd_module实例换成uart_tx再把显示数据的编码逻辑改成 16 进制 ASCII 输出。这样做虽然少了触摸交互但调试信息更直观。第三个改造方向是扩展指令集。单周期 CPU 附录 A1 只实现了基础指令集你可以把slt、lui、jr这些指令补进去。以lui为例你需要在译码模块里新增一个case分支控制 ALUSrc、RegDst 和 Imm 扩展方式同时把指令码的识别条件加到译码表里。每加一条指令至少要同步修改三处指令译码的控制信号、立即数扩展逻辑、数据通路里新增的连线。这个流程做过一次你就明白为什么指令集是 CPU 设计里牵一发而动全身的部分。如果是用这份手册组织自己的课程设计我建议别让学生直接拿实验六的代码改流水线而是让他们从实验一一路做下来把每个模块的仿真波形单独截图留档。这样到最后做流水线课程设计时手里的基础模块都是自己验证过的调试时间能省下一大半。我个人的习惯是每完成一个实验就做一次工程备份命名带版本号因为 CPU 实验改到后期经常出现“上个版本还能跑改完就不行了”的情况有备份才有后悔药。希望这份手册的拆解对你有帮助拿到资源后先照着实验一完整跑一遍流程再决定从哪个实验开始二次开发。本文还有配套的精品资源点击获取