简介面向高校计算机专业师生的中央处理器设计与体系结构实验指导手册教师版配套LS-CPU-EXB-002实验箱及Xilinx Vivado平台适合《数字逻辑》《计算机组成原理》《计算机体系结构》等课程的实验教学与自主研习。手册围绕一套完整实验链路展开实验一、二分别完成定点加法与定点乘法帮助理解算术逻辑单元的基本运算实验三实现寄存器堆包括通用寄存器与状态寄存器实验四完成算术逻辑单元模块执行算术和逻辑操作实验五搭建存储器模型实验六、七则依次实现单周期与多周期中央处理器并对比不同时钟策略课程设计延伸到静态5级流水线中央处理器及优化系统。附录列出所实现的指令集和对应的寄存器映射方便读者对照实验电路验证。资源仅1个PDF文件大小约6.09MB包含实验目的、任务、结构框图、Verilog设计流程与调试要点目录清晰便于教师备课和学生按阶段实践。已有680人浏览学习适合需要从底层理解计算机系统运作、系统掌握处理器设计流程的教师和高年级学生参考。1. 从“”到流水线一门实验课如何拆穿 CPU 的黑匣子在 Verilog 里做加法器最直接的办法不是画全加器而是敲一个“”号——综合工具会自动把它换成专用进位链面积更小、时序更快。这本实验指导手册最反直觉的地方就在这里它带你从一个“”号出发最后落到一条完整的 5 级流水线 CPU中间每个实验都对应一个真实的体系结构决策。为什么要从定点加法学起因为加法是所有运算、寻址、比较的公共底座。为什么要一步步搭寄存器堆、ALU、存储器再做到单周期、多周期 CPU因为只有亲手实现过控制器你才会明白指令是怎么被“翻译”成电路动作的。这篇解读就照着手册的实验顺序往下拆每一层机制的取舍在哪里仿真与上板之间又会栽哪些跟头。2. 定点加法与乘法为什么一个“”比自己搭全加器更优2.1 一个“”号背后综合工具在悄悄替换你的电路手册实验一的参考实现只有短短几行timescale 1ns / 1ps module adder( input [31:0] operand1, input [31:0] operand2, input cin, output [31:0] result, output cout ); assign {cout, result} operand1 operand2 cin; endmodule拼接符{cout, result}把 33 位结果一次性赋值最高位是向高位的进位低 32 位是与输入等宽的加法结果。这段代码值得注意的不是功能而是注释里那句“直接使用‘’会自动调用库里的加法器”。很多初学者把硬件描述语言当高级语言用以为只是语法糖综合工具随便给你兜个底。实际上现代综合工具对加法有专门优化FPGA 的每个查找表LUT后面都跟着一条专用快速进位链综合工具会把映射到这条硬连线结构上形成类似超前进位的快速传播路径。手工用全加器搭 32 位行波进位加法器最坏情况下进位要从最低位一路串到最高位延迟随位宽线性增长用门级逻辑搭超前进位进位逻辑面积又暴涨。FPGA 里的专用进位链正好卡在中间既有接近超前进位的速度又几乎不消耗额外 LUT。所以手册建议“如果教师没有特殊要求优先用”不是一个偷懒的选择而是基于器件结构的工程判断。这背后是硬件设计里一条反复出现的规律与其自己造轮子不如让工具调用经过验证、针对器件优化过的原语。2.2 定点乘法移位累加为何是 CPU 性能的隐患实验二把难度从加法抬到乘法。乘法从原理上可以拆成“移位 累加”乘数每一位为 1 时把被乘数左移对应位数后加入部分积32 位乘 32 位最多要累加 32 个部分积。这个朴素思路在教材里很直观在硬件上却是性能噩梦。加法只要处理一条进位链延迟是 O(n)乘法要处理 n 行部分积的压缩复杂度是 O(n²)。如果 CPU 里每一次乘法都老老实实串行累加 32 拍乘法指令会把流水线拖垮。所以现代乘法器普遍用部分积压缩树先把 32 行部分积用全加器/半加器压缩成两行再用一个超前进位加法器收尾。手册实验二的任务正是让学生从“理解移位累加”过渡到“实现一个可用的乘法模块”这一步直接关系到后面 CPU 实现时乘法指令能不能在一个有限周期数内完成。这也是多周期 CPU 设计里一个伏笔如果乘法器不支持单周期出结果控制器就得给乘法指令安排额外的执行周期。你在实验二里选择的乘法实现方案会直接影响实验七多周期 CPU 的状态机设计——这是后来我在做课程设计时才想明白的关联。2.3 黑盒复用LCD 触摸屏模块与随机激励验证定点加法本身太简单真正的门槛在上板验证。若是用拨码开关输入加数、LED 显示结果32 位加法需要 64 个拨码开关加 32 个 LED实验箱根本提供不了这么多 IO。这套实验系统的解法是 LCD 触摸屏硬件驱动被封装成 dcp 网表文件以黑盒形式提供给调用者。这里有个很专业的设计取舍。触摸屏驱动的时序逻辑复杂如果让学生从头写大概率会迷失在 LCD 初始化、扫描、触控采样这些细节里丢了主线。封装成黑盒后学生只需要在顶层模块里调用lcd_module像接插座一样接好引脚就行。手册明确说这是“避免学生过分关注而导致迷失在 LCD 触摸屏实现中”。这种“把次要复杂度藏起来”的做法和 CPU 设计里的指令集抽象如出一辙让使用者通过接口理解功能而不是被内部实现淹没。功能验证环节同样体现工程思维。手册给的 testbench 不是枚举有限测试向量而是用随机激励always #10 operand1 $random; always #10 operand2 $random; always #10 cin {$random} % 2;$random是 Verilog 系统任务每次调用产生一个 32 位有符号随机数#10表示每 10ns 触发一次赋值。注意第三行的{$random}加了拼接符作用是把随机数先转成非负数再% 2得到 0 或 1——如果不加拼接符$random % 2在有符号数下可能得到负数仿真波形里看就是红色未知态。手册用这句话带过了这个细节但它正是仿真里最常见的玄学翻车点之一。随机激励的验证逻辑是统计性的每 10ns 换一组随机输入跑足够长时间后如果几十上百组随机数据都没有出错就认为功能“趋于稳定”。手册里那句“我们认为功能趋于稳定可以认为是正确的”措辞很严谨——这不是形式化证明而是覆盖率足够高后的工程信心。观察波形时把数据 Radix 改成 16 进制手算一组b897be71 42f24186 1 fb89fff7能对上就可以进入综合布线环节了。3. 寄存器堆与 ALU数据通路的两个心脏3.1 三端口寄存器堆双读单写由指令格式决定寄存器堆在 CPU 里承担“工作台”角色所有运算都要先把数据从寄存器读出来算完再写回去。手册实验三的核心问题是为什么寄存器堆通常设计成两个读端口、一个写端口答案藏在指令格式里。以 MIPS 风格的 R 型指令为例一条指令同时给出两个源寄存器号和一个目的寄存器号比如add rd, rs, rt。翻译成硬件需求就是一个时钟周期里要能同时读出 rs 和 rt 两份数据同时可能写入 rd。所以最少需要两个读端口和一个写端口。这不是拍脑袋定的是 ISA 给出的约束硬件接口只是顺从它。典型的寄存器堆骨架如下reg [31:0] regfile [0:31]; // 写端口时钟上升沿写入 always (posedge clk) begin if (we (addr_w ! 0)) regfile[addr_w] wdata; end // 读端口组合逻辑直接输出 assign rdata1 (addr1 0) ? 32b0 : regfile[addr1]; assign rdata2 (addr2 0) ? 32b0 : regfile[addr2];几个细节要讲清楚regfile是一个 32 个 32 位寄存器组成的数组实际硬件实现时综合工具会把它映射成分布式 RAM 或寄存器阵列。写端口是时序逻辑必须在时钟沿到达且写使能we有效时才会更新。读端口是纯组合逻辑地址一变数据跟着变不依赖时钟。两条读端口各自都有addr 为 0 时输出 0的保护——这是 MIPS 指令集的约定0 号寄存器永远读出 0写 0 号寄存器不会生效。这条约定看起来是软件层面的规则实现在硬件里就是一行判断。这里有个初学者容易忽略的点如果读是组合逻辑那么从“地址稳定”到“数据稳定”之间存在一个读延迟这段时间内数据可能无效。CPU 数据通路必须把这个延迟算进周期预算否则后级 ALU 采样的可能是上一轮的数据。3.2 ALU 操作码指令字段到控制信号的译码实验四的 ALU 是 CPU 的执行核心。但直接设计 ALU 前得先想清楚一个问题CPU 指令里的操作码和 ALU 的端口控制信号不是一回事。MIPS 指令里add和sub的指令码不同但 ALU 的输入端口上通常只需要一组短得多的操作码比如00表示加、01表示减、10表示与、11表示或。指令字段到 ALU 操作码的翻译由控制单元完成。典型 ALU 的输入输出如下module alu( input [31:0] a, input [31:0] b, input [2:0] alu_op, // ALU 控制信号由控制单元译码得到 output [31:0] result, output zero );zero标志用于分支判断它的产生方式是把 result 的每一位做或运算后取反——只有 result 恰好为 32b0 时zero 才为 1。sltset less than这类置位指令的实现思路是把 a 和 b 转换为减法判断结果的符号位逻辑上写成slt (a b)硬件上通常通过符号位比较实现。对比一下实现slt需要的减法逻辑、实现add需要的加法逻辑、实现and/or需要的位运算逻辑三者在同一组输入 a、b 上并行做不同运算最后由alu_op选择一个作为 result 输出。这就是 ALU 的典型内部结构功能单元并行计算结果多路选择。做 ALU 时最大的坑不是运算逻辑而是扩展性多周期 CPU 里ALU 有时要算 PC4取指地址自增有时要算分支目标地址有时要算运算结果。实验三寄存器堆和实验四 ALU 单独做时都不难难的是把这两个模块合进数据通路时控制信号怎么在每条指令的不同阶段切来切去。3.3 读后写窗口寄存器堆的时序设计寄存器堆有一个容易被仿真掩盖的时序问题。假设一条指令要写寄存器紧接着下一条指令要读同一个寄存器——在单周期 CPU 里同一周期内写和读不会同时发生但在流水线 CPU 里IF/ID 读寄存器和 WB 写寄存器可能在同一周期重叠执行前一条指令在写回阶段把结果写进寄存器堆后一条指令在译码阶段读同一个寄存器。如果寄存器堆是“上升沿写入”而读端口是组合逻辑那么在一个时钟周期内前半段读到的是旧值上升沿一过后新值才出现。读端口在上升沿前采样的指令拿到的是旧数据这就触发了典型的数据冒险。教学 CPU 里常见两种解法。一种是寄存器堆采用“下降沿写、上升沿读”的约定让写动作发生在读动作之后避开同一个沿上的竞争另一种是写回阶段的结果直接通过转发通路送往需要它的执行阶段不依赖寄存器堆的更新。手册实验三落脚在寄存器堆本身实验六的单周期 CPU 因为每周期只执行一条指令不会暴露 RAW 竞争但到了课程设计的 5 级流水线这个窗口会变成你第一个要面对的真实冒险。能提前在寄存器堆的仿真波形里找到这个窗口后面处理转发和暂停会顺很多。4. 存储器实现指令与数据为什么必须分开存取4.1 指令存储与数据存储分开还是合并实验五的存储器实现要面对的第一个选择题是指令存储器和数据存储器是共用一块 RAM 还是分开两块教材上把它表述为冯·诺依曼结构和哈佛结构之争落到这次实现里其实是个工程决策。单周期 CPU 的一个基本事实是每个周期都要取指同时 load/store 指令还要访问数据。如果指令和数据共用一块物理存储器就必须等到取指完成后才能读数据读数据时取指总线闲着——数据通路上凭空多出一段串行等待。教学 CPU 通常选择指令存储器和数据存储器分开各用一块独立的 RAM 块Block RAM让取指和访存并行路径清晰控制逻辑也简单。代价是面积翻倍而且指令 ROM 里装着程序数据 RAM 里装着运行期变量两者不能动态共享。这个选择还牵出一个细节指令存储器本质上是只读的。手册里单周期 CPU 实验实现的是固定 MIPS 指令集程序提前写入指令存储器的初始化文件里。所以在硬件里指令存储器往往实现为 ROM 或者带有初始化数据的 RAM而数据存储器必须支持读写。读和写对时序的要求完全不同这直接导向存储器的端口设计。4.2 单端口与双端口 RAM面积与并行的交换存储器的端口数量是一个和面积强相关的参数。单端口 RAM 只有一组地址线和一组数据线一个时钟周期要么读要么写双端口 RAM 两组端口独立可以一个端口读、另一个端口写。类型端口能力硬件成本适合场景单端口每周期要么读要么写低Block RAM 原生支持多周期 CPU 数据存储器、单周期指令存储器伪双端口一个读口一个写口地址独立中读和写可以同时单周期 CPU 数据存储器读数据、写数据分离真双端口两个独立读写端口高面积接近翻倍多核共享存储、复杂流水线结构多周期 CPU 的经典设计里数据存储器用单端口就够因为它的访存阶段要么是 load读要么是 store写不会同时发生。但单周期 CPU 的数据存储器更常用伪双端口load 指令在数据存储器读store 指令在数据存储器写两条指令不会在同一个周期访问可一旦指令集扩展出同时要求读和写的操作伪双端口就会成为瓶颈。手册把存储器单列一个实验目的就是让你先把这个参数吃透再进入 CPU 集成。4.3 同步读还是异步读仿真与上板不一致的根源存储器实现里最容易翻车的是读时序。用 reg 数组搭的存储器读可以是纯组合逻辑地址一变数据立即跟着变那叫异步读但如果用 FPGA 的 Block RAM 原语读操作往往需要等一个时钟沿数据在下一个周期才稳定下来那叫同步读。// 异步读地址变化 - 数据组合输出 assign dout mem[addr]; // 同步读时钟沿后数据更新 always (posedge clk) if (read_en) dout mem[addr];这两种写法在行为仿真里都能看到正确的数据但时序完全不一样。异步读的 dout 在地址变化后立刻变化同步读的 dout 在时钟沿之后才更新相当于多了整整一个周期的读延迟。如果你的 CPU 控制逻辑按异步读设计上板后却用了综合工具自动推断出的 Block RAM 同步读波形上数据会比预期晚一拍整条流水线的节奏全乱。血泪经验是写存储器前先想清楚自己到底需要同步读还是异步读然后在整个数据通路里保持一致。教学 CPU 里寄存器堆通常用异步读因为译码阶段要立刻拿到源操作数数据存储器的读可以用同步读但控制单元必须把读延迟算进访存阶段的周期里。这个选择没有绝对的对错只有一致不一致。5. 从单周期到多周期控制逻辑的分解与常见问题排查5.1 单周期 CPU最慢指令决定时钟周期实验六实现单周期 CPU它的核心规则是每一条指令都在一个时钟周期内完成全部操作。从取指、译码、读寄存器、运算、访存到写回所有部件在这一个周期里串行工作。这种设计最致命的点是时钟周期必须按最坏路径的延迟来定。最坏路径几乎总在 load 指令上PC 从指令存储器取指寄存器堆读出基址和偏移ALU 计算访存地址数据存储器读出数据再写回寄存器堆。这条路径上所有部件的延迟相加就是时钟周期的下限。算术逻辑指令不访存但它的周期也得按 load 的周期来跑。结果是单周期 CPU 的 CPI 恒等于 1看似很理想但周期时间被最慢指令拖长整体性能并不高。这就是单周期方案“逻辑简单、性能吃亏”的本质矛盾。手册附录 A1 列出单周期 CPU 实现的指令集可以看到它只包含最基本的加减、逻辑运算、存取和分支指令。指令集刻意收敛是为了让数据通路和控制逻辑保持简单这个阶段的目标是建立起“指令 → 控制信号 → 部件动作”的直觉。你写控制单元时要为每一条指令生成一组确定的控制信号寄存器写使能、存储器写使能、ALU 操作码、多路选择器选择信号这些信号在一个周期内都保持稳定不做任何分时复用。5.2 多周期 CPU把一条指令拆成一组状态实验七的多周期 CPU解决单周期“所有部件同时工作”的浪费。思路是把一条指令的执行过程拆成若干步骤每个步骤一个时钟周期一个周期里只启用部分部件。典型拆法是取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回(WB)五步。关键变化在于部件的分时复用ALU 在取指阶段算 PC4在执行阶段算真正的运算结果数据存储器在取指阶段读指令在访存阶段读写数据。控制逻辑从一组并行输出的组合逻辑变成一台状态机当前状态是哪一步下一步该走到哪。状态机的实现方式通常是 case 语句按指令类型和当前状态组合出控制信号。单周期和多周期的对比在手册的两个实验里体现得很清楚维度单周期 CPU多周期 CPU周期定义一条指令一周期一条指令多周期最慢路径所有部件串联一次每周期只走一个子步骤硬件利用率低所有部件每周期都动高部件按需启用控制逻辑组合逻辑有限状态机时钟频率被最长路径卡住可以更高各步骤独立优化5.3 排查五个最容易翻车的点1. 行为仿真正确、上板后完全乱跑现象testbench 里波形正确下载到实验箱后要么无输出、要么偶发错误。原因引脚约束xdc 文件没生效顶层端口和 FPGA 引脚没绑上端口悬空或被分配到错误引脚。解决综合后先查看 I/O Ports 报告确认每个顶层 input/output 都已经有 PACKAGE_PIN 约束检查 get_ports 名字是否和 Verilog 顶层端口完全一致大小写、总线索引都要对。手册里给的set_property PACKAGE_PIN AC19 [get_ports clk]这类约束写错一个字母综合工具不会报错但上板就会静默失败。2. 读存储器的数据总比预期晚一拍现象仿真时 load 指令读出来的数据不对波形上看总在预期时刻之后才出现。原因存储器的读被设计成同步读时钟沿后更新数据而控制逻辑按异步读地址变化后数据立即出现设计。解决回到存储器的实现代码检查 dout 是assign组合输出还是always (posedge clk)时序输出如果是同步读需要在控制状态里预留一拍。3. 多周期 CPU 状态机卡死复位后也动不了现象跑仿真时状态一直停在某个值时钟怎么打都不往前走。原因case 语句没有覆盖所有状态组合或者状态转移条件互相矛盾综合后生成了锁存器latch。解决case 必须写 default 分支默认为回到取指状态所有输出信号在进入 case 前先赋默认值避免综合器推断出 latch。波形排查时重点看状态的次态逻辑在 state 信号上添加条件断点观察是哪个输入组合让它困住。4. testbench 里随机数产生负数导致仿真相貌异常现象always #10 cin $random % 2;跑出来的 cin 时而 0 时而 1但 operand 的波形偶尔出现负值或未知态。原因$random返回有符号数对有符号数取余会得到负数。解决按手册写法改成{$random} % 2拼接符先把随机数包装成非负数再取余。看起来只是括号问题实际上是有符号数和无符号数语义的典型陷阱。5. dcp 网表文件添加失败工程一直报模块找不到现象添加触摸屏模块的网表文件后用不了模块上有个问号综合时提示未知模块。原因网表文件针对的版本和当前综合工具版本不匹配。解决手册明确提到某些工具版本下需要先把 dcp 文件解压再添加。实验箱配套的 LCD 触摸屏封装成黑盒 dcp 网表这块最容易踩的坑就是版本兼容添加前先确认工具版本和网表文件的匹配关系。6. 静态 5 级流水线冒险、转发与设计取舍课程设计把实验推进到静态 5 级流水线这是整本手册的制高点。流水线把一条指令的执行分成取指、译码、执行、访存、写回五个阶段每个阶段由独立的硬件完成不同指令在不同阶段重叠推进。吞吐量上去了代价也随之而来相邻指令在流水线里并行执行前一条还没写完后一条就要读数据数据冒险、控制冒险接踵而至。静态 5 级流水线的设计取舍恰好在“复杂度”和“教学价值”之间取了一个平衡点。静态意味着指令严格按顺序流经所有流水段不做乱序发射、不做动态调度。换来的是异常处理和寄存器写回的顺序性天然清晰CP0 寄存器可以简单地保存 EPC、Cause 等异常现场流水线遇到异常时冲刷后续指令即可。如果换成动态调度转发逻辑、保留站、重排序缓冲的复杂度会成倍上升反而不适合作为组成原理课程的终点。处理数据冒险的标准手段是转发把写回阶段的结果直接送到执行阶段的 ALU 输入绕过寄存器堆。但 load 指令的转发特别难因为它要等访存完成才知道结果紧跟在 load 后的指令就必须暂停一拍。这暴露了流水线的另一个本质矛盾流水段拆得越细频率越高但冒险出现的频率和代价也越高。转发路径变长会拖慢关键路径暂停逻辑变复杂会牺牲 CPI这些此消彼长的博弈就是体系结构设计最迷人的部分。手册后面还安排了“优化 CPU 系统”作为拓展方向正是指令调度、资源分配这些更深的内容。从那以后我每写一个数据通路模块都会先画一张时序图把每个读写动作标在时钟沿上再沿着流水线把相邻指令的依赖窗口标出来最后才动手写代码。这道工序帮我挡掉了大半的冒险问题希望帮到你。本文还有配套的精品资源点击获取