FPGA CPU设计实验:从32位加法器到五级流水线

发布时间:2026/10/25 1:07:41
FPGA CPU设计实验:从32位加法器到五级流水线 简介面向高校计算机组成原理与体系结构课程实验的教师版指导手册围绕龙芯LS-CPU-EXB-002教学实验平台展开帮助师生完成从基础运算部件到完整CPU的实现与验证。PDF单文件打包约6.09MB内容按实验难度递进编排覆盖定点加法与乘法、寄存器堆、ALU、存储器、单周期和多周期CPU并延伸到静态5级流水线CPU课程设计及系统优化涉及加法器进位、乘法移位累加、寄存器类型、ALU运算与存储器模型等关键知识点。附录整理MIPS指令集与CP0寄存器便于对照指令执行过程、端口设计和调试要点。手册结合FPGA实验平台与Verilog电路设计给出各实验的目的、设备、任务与实现路径适合教师备课指导和学生在课程设计中搭建数据通路、理解流水线性能优化。已有680人学习可作为课程实验教学与综合设计参考。1. 从一份实验讲义到一颗能跑的 CPU很多人拿到这份 CPU 设计与体系结构实验指导手册第一反应是这不就是一份课程讲义吗翻两页就搁下了。真正翻到后面才会发现它把一条完整的路径摆在你面前从一行 32 位加法器代码开始一个实验一个模块地往上垒最后垒出一颗能上板运行、带五级流水线、能处理 CP0 异常的五级 CPU 核。这份资源适合两类人一类是刚学完数字逻辑、想弄明白CPU 到底是怎么被搭出来的的学生另一类是要带实验、需要一套能直接照抄的工程流程和验证方法的教师或助教。它不讲抽象理论讲的是工程里每一个.v文件该怎么写、约束该怎么绑、波形该看哪一段。2. 实验体系全景八个实验如何垒成一颗 CPU这套手册的组织方式不是按知识点罗列而是按能力依赖排的。前面的实验是后面的前置条件跳过任何一个后面的 CPU 就搭不起来。理解这张依赖图比背指令格式重要得多。2.1 八个实验的能力阶梯与依赖关系整体上是四层结构。第一层是工具入门实验一定点加法本质上不是教你加法是教你在 FPGA 上走完写代码—仿真—约束—综合—下载—板上验证这一整条链路同时熟悉实验箱的触摸屏交互。这层跑不通后面全是空谈。第二层是三大基础部件实验二定点乘法理解移位累加、实验三寄存器堆、实验四 ALU、实验五存储器。这四个模块是数据通路的零件库它们的端口定义会直接决定后面 CPU 顶层能不能顺利连线。我在看学生的工程时发现翻车最多的不是 CPU 逻辑写错而是寄存器堆的读写时序和 ALU 的位宽在前面的实验里就没对齐到了单周期 CPU 阶段才发现要推倒重来。第三层是整机集成实验六单周期 CPU、实验七多周期 CPU。单周期把所有指令压在一个时钟周期内完成结构最直白也最容易验证——每条指令的执行路径都能在波形上一眼看穿。多周期把一条指令拆成取指、译码、执行、访存、写回若干节拍引入状态机做调度成本和复杂度都上一个台阶。第四层是课程设计与拓展静态五级流水线 CPU以及在其上做优化。流水线要处理数据相关、控制相关、结构相关还要引入流水寄存器、前递通路和阻塞逻辑这是整份手册里技术密度最高的部分。阶段实验产出模块对后续的支撑入门实验一 定点加法adder 展示外围打通工具链与板上验证机制零件实验二 定点乘法乘法器移位、累加类指令的硬件基础零件实验三 寄存器堆regfile指令读操作数与写回结果零件实验四 ALUalu运算类指令统一出口零件实验五 存储器imem / dmem取指与 load/store整机实验六 单周期 CPUcpu_single数据通路与控制信号完整闭环整机实验七 多周期 CPUcpu_multi状态机调度、节拍划分进阶课程设计 五级流水cpu_pipe吞吐率优化、相关处理拓展优化 CPU 系统分支预测、旁路等体系结构层面的取舍2.2 工具链与工程文件组织方式技术栈很朴素Verilog HDL 写 RTL用主流 FPGA 厂商的 EDA 套件做仿真、综合、布局布线和下载目标器件是一颗引脚数够接 LCD 的中端 FPGA板上有一个封装成网表的 LCD 触摸屏模块供调用。指令集是 MIPS 子集附录里按单周期、多周期、五级流水三个阶段分别列了各自实现的指令清单和 CP0 寄存器这个拆分很关键——它意味着每升级一次 CPU指令集就扩一次不会一次性甩给你一百条指令。工程里的文件职责大致是这样文件职责备注adder.v/alu.v/regfile.v/dmem.v功能模块 RTL每个实验的主体可被顶层反复例化*_display.v板级展示外围负责把内部信号接到 LCD 或 LEDlcd_module.dcp触摸屏黑盒网表已综合的网表只给端口不给源码testbench.v仿真激励只产生输入不含自动比对*.xdc引脚与时序约束顶层端口到板卡引脚的绑定关于 LCD 模块的处理这套手册做了一件很务实的事把触摸屏驱动封装成.dcp网表学生只看到端口看不到内部实现。这样做的用意很明确——屏的驱动里有大量上电时序、坐标换算、扫描刷新的细节一个本科生如果陷进去两周也爬不出来。作为一个黑匣子它把注意力还给了 CPU 本身。这个取舍在教学设计上是成立的我自己带实验也倾向这么干。需要注意的一点是网表文件对 EDA 工具的版本比较敏感工程里要统一用同一套工具链打开否则添加.dcp时会报格式不识别。这类问题在手册里只提了一句但实际踩到的人不少。3. 实验一拆解32 位加法器如何一路走到上板实验一看似最简单但它是唯一一个把RTL → 仿真 → 外围 → 约束 → 上板整条链路走完的实验。后面所有实验都是在这条链路上复制粘贴、逐步加模块。所以这一章值得拆细。3.1 加法器主体一行 assign 背后的综合取舍主体代码就一行有效逻辑timescale 1ns / 1ps // 32 位定点加法器两个操作数 进位输入输出和与进位输出 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 位把进位和和拼在一起接收进位就自然落到了最高位不用额外写判断。综合工具看到这个写法会自动调用器件里现成的进位链资源面积和时序通常优于自己用全加器一级一级搭出来的树形结构。参数上没什么可调的operand1、operand2是 32 位无符号/有符号共用cin是最低位进位result是低 32 位和cout是向高位的进位。有符号和无符号在这里不做区分因为二进制补码的加法电路本身是同一套。手册里明确给了两条路用全加器自己搭或者直接写。作为教学前者能让你看见进位传播的过程作为工程后者才是正解。我的建议是两条都写一遍自己搭的那版拿来做波形对比理解完就丢掉工程里继续用。3.2 testbench 与波形验证随机激励怎么下仿真激励写得很精简没有自动比对只负责灌数据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; // 等全局复位稳定 // 定点验证一组已知数据 operand1 32hb897be71; operand2 32h42f24186; cin 1b1; #20; // 期望 {cout, result} 33h1_fb89fff7 end // 随机激励每 10ns 换一组数 always #10 operand1 $random; always #10 operand2 $random; always #10 cin {$random} % 2; endmodule#100是留出复位稳定时间#10是激励周期10ns 对应 100MHz 的节拍跟板上时钟量级一致比较直观。$random产生有符号 32 位随机数赋给reg [31:0]会按位截断做加法无所谓正负。{$random} % 2里的拼接符{}是把有符号数转成无符号再对 2 取余得到稳定的 0/1 序列——不加这个拼接%在负数上的行为会让人抓狂属于典型的仿真玄学。波形窗口默认二进制显示32 位的数根本没法看。选中信号右键改 Radix 为十六进制是每次仿真都要做的第一个动作。手册里给的校验样例是b897be71 42f24186 1 fb89fff7且进位为 1手算一遍再对波形比盲目信任仿真结果靠谱。3.3 外围展示模块与引脚约束从仿真到上板板上验证的难点在于 IO 不够。32 位加法需要 64 个拨码开关加 32 个 LED实验箱根本给不出这么多引脚。所以手册引入了一个展示外围模块用 LCD 触摸屏完成两个加数的输入和结果的显示module adder_display( input clk, input resetn, input input_sel, // 拨码切换当前正在录入的加数 input sw_cin, // 拨码进位输入 output led_cout, // LED进位输出肉眼可见 // LCD 侧引脚全部直连到黑盒 output lcd_rst, output lcd_cs, output lcd_rs, output lcd_wr, output lcd_rd, output lcd_bl_ctr, inout [15:0] lcd_data_io, output ct_int, inout ct_sda, output ct_scl, output ct_rstn ); wire [31:0] op1, op2, sum; wire cout; // 加法主体 adder u_adder( .operand1(op1), .operand2(op2), .cin(sw_cin), .result (sum), .cout (cout) ); assign led_cout cout; // 触摸屏黑盒内部时序不用管把端口按时序接出来即可 lcd_module u_lcd( .clk(clk), .rst_n(resetn), .op1(op1), .op2(op2), .result(sum), .cout(cout), .input_sel(input_sel), .lcd_rst(lcd_rst), .lcd_cs(lcd_cs), .lcd_rs(lcd_rs), .lcd_wr(lcd_wr), .lcd_rd(lcd_rd), .lcd_bl_ctr(lcd_bl_ctr), .lcd_data_io(lcd_data_io), .ct_int(ct_int), .ct_sda(ct_sda), .ct_scl(ct_scl), .ct_rstn(ct_rstn) ); endmodule这里的端口定义是示意实际以配套网表的端口名为准但结构是固定的顶层只做三件事——例化功能模块、例化黑盒、把内部信号连到板上引脚。input_sel是个很聪明的设计一个拨码开关切换现在输入的是加数一还是加数二用一组触摸输入解决两个 32 位操作数的录入。约束文件把顶层端口绑到物理引脚上格式如下引脚号以实际板卡为准# 时钟与基本 IO 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] # 电平标准统一写 LVCMOS33漏写会在实现阶段报错 set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports resetn] set_property IOSTANDARD LVCMOS33 [get_ports led_cout] # LCD 数据总线是双向的16 根要逐位绑定 set_property PACKAGE_PIN H9 [get_ports {lcd_data_io[0]}] set_property PACKAGE_PIN K17 [get_ports {lcd_data_io[1]}]PACKAGE_PIN是物理引脚编号IOSTANDARD是 IO 电平标准这两个必须成对出现。总线信号用{lcd_data_io[0]}这种带下标的写法单独绑不能用[get_ports lcd_data_io]一把梭否则实现阶段会报位宽不匹配。漏写IOSTANDARD是最常见的低级错误综合能过、实现直接挂。4. 从零件到整机寄存器堆、ALU、存储器的接口设计这三个模块是数据通路的骨架它们的端口一旦定下来单周期 CPU 的连线基本就确定了。所以这一章的重点不是功能怎么实现而是接口怎么定才不给后面埋雷。4.1 寄存器堆双读单写与 0 号寄存器MIPS 的寄存器堆是典型的双端口读、单端口写module regfile( input clk, input we, // 写使能由控制单元给出 input [4:0] waddr, // 写地址5 位对应 32 个寄存器 input [31:0] wdata, // 写数据来自 ALU 或访存结果 input [4:0] raddr1, // 读端口 1 地址 input [4:0] raddr2, // 读端口 2 地址 output [31:0] rdata1, output [31:0] rdata2 ); reg [31:0] rf [0:31]; integer i; // 上电清零仿真时避免 X 态扩散 initial begin for (i 0; i 32; i i 1) rf[i] 32b0; end // 0 号寄存器恒为 0读的时候直接短路 assign rdata1 (raddr1 5d0) ? 32b0 : rf[raddr1]; assign rdata2 (raddr2 5d0) ? 32b0 : rf[raddr2]; // 写操作在时钟上升沿生效写 0 号寄存器无效 always (posedge clk) begin if (we waddr ! 5d0) rf[waddr] wdata; end endmodule读是组合逻辑地址一变结果立刻变这是为了让取操作数和执行在同一个周期内完成。写是时序逻辑上升沿触发保证写回发生在周期末。raddr 0时短路返回 0比在写端口拦截更彻底——即使有人误写了 0 号寄存器读出来依然是 0。这里有个容易忽略的点写和读如果发生在同一周期、同一地址读出来的是旧值还是新值上面这段代码读是组合的写是时序的所以同周期读到旧值。这个行为在单周期 CPU 里没问题但到了流水线阶段如果 WB 段写、ID 段读同一寄存器就会读到旧值必须靠前递或者写优先改造来解决。这也是后面流水线部分要重点处理的地方。4.2 ALU运算选择与零标志ALU 的结构是把所有运算并行算出来用alu_op选一个输出module alu( input [31:0] a, input [31:0] b, input [3:0] alu_op, // 运算选择由控制单元译码产生 output reg [31:0] y, output zero // 结果为零标志给分支指令用 ); assign zero (y 32b0); always (*) begin case (alu_op) 4b0000: y a b; // add / addi / lw / sw 4b0001: y a - b; // sub / beq 4b0010: y a b; // and / andi 4b0011: y a | b; // or / ori 4b0100: y a ^ b; // xor / xori 4b0101: y ~(a | b); // nor 4b0110: y a b[4:0]; // sll移位量取低 5 位 4b0111: y a b[4:0]; // srl 4b1000: y ($signed(a) $signed(b)) ? 32d1 : 32d0; // slt有符号比较 default: y 32b0; endcase end endmodulealu_op是 4 位够编 16 种运算实际用到的不到 10 种留足扩展余量。移位量取b[4:0]因为 32 位寄存器最大移位 31 位5 位正好多出来的位必须截掉否则综合出来是个超宽的桶形移位器白白吃资源。zero标志直接从y组合出来供beq/bne使用。注意它是组合逻辑会跟着alu_op和输入实时变化在流水线里用的时候要小心毛刺。slt用$signed强转做有符号比较这是最容易写错的一条。如果漏了$signed负数会被当成大正数slt结果直接反了而且这种错在简单测试用例里不一定暴露往往到跑排序算法才崩。4.3 存储器取指与访存的双模型指令存储器和数据存储器在单周期 CPU 里通常分成两个模块避免结构冲突module dmem( input clk, input we, // 写使能来自控制单元的 MemWrite input [31:0] addr, // 字节地址 input [31:0] wdata, output [31:0] rdata ); // 1024 个字够跑课程设计里的测试程序 reg [31:0] mem [0:1023]; // 字对齐地址低 2 位丢弃再折算成字索引 assign rdata mem[addr[11:2]]; always (posedge clk) begin if (we) mem[addr[11:2]] wdata; end endmoduleaddr[11:2]这个切片是关键。CPU 发出的是字节地址但存储器按字组织所以要丢掉低 2 位再索引容量 1024 字对应地址位宽 10 位。读是组合的异步读写是同步的这个组合在单周期里能跑通但到流水线阶段就会成为时序瓶颈——一个周期内要完成算地址 → 读数据 → 用数据路径太长。实际工程里通常换成同步读 RAM然后把访存拆成两级流水代价是多一个周期的延迟。指令存储器imem结构类似只是只读通常用initial块或$readmemh从文件加载机器码。这里有个细节程序文件路径要写相对路径工程换个目录就得改建议放在工程根目录下的固定子目录里统一管理。5. 上板避坑从仿真通过到板子亮灯之间的鸿沟仿真波形全对、综合零警告下载到板上却是黑屏——这是每个做 FPGA 实验的人都经历过的时刻。下面几条是这套实验里出现频率最高的问题。5.1 工程与综合阶段的问题现象添加.dcp网表时提示格式无法识别或者加进去之后模块前面一直挂着问号。原因网表文件是特定版本工具综合出来的产物跨版本不兼容另外添加时必须选Add or create design sources里的网表类型选成普通源文件是不会被识别的。解决统一工具链版本工程建好后不要再混用添加时确认文件类型如果模块前仍有问号右键该模块手动指定为黑盒或者检查层次结构里是否漏了顶层声明。现象综合能过实现阶段报大量 IO 约束错误。原因IOSTANDARD漏写或者总线信号用了整体绑定而不是逐位绑定或者顶层端口名和约束文件里的名字对不上改过端口名没同步改 xdc。解决养成改端口先改约束的习惯每个PACKAGE_PIN后面紧跟一行IOSTANDARD总线一律展开逐位绑定实现前先跑一遍约束语法检查。现象波形里一堆 X 态加法结果全是红线。原因某个输入从头到尾没被赋值或者initial块里没给寄存器堆初值X 态沿着组合逻辑一路扩散。解决testbench 的initial里把所有输入显式置 0寄存器堆、存储器这类阵列加上电清零在波形里顺着 X 往上追找到第一个变 X 的信号那里才是根因后面的都是被污染的。5.2 上板与调试阶段的问题现象程序下载成功LCD 屏不亮或者只有背光没有内容。原因屏的上电时序有要求复位信号lcd_rst的极性搞反了或者黑盒模块的时钟没接上一直停在那里。解决先单独测屏——不接任何 CPU 逻辑只例化黑盒看能不能点亮并画出静态字符。这一步过了再接业务逻辑能把屏的问题和CPU 的问题彻底分开。极性这种细节看网表端口名里的_n后缀带n的一般是低有效。现象板上结果和仿真对不上加法总是差一点。原因按键和触摸输入没有做消抖一次触摸被识别成多次或者input_sel切换时没有锁存中途改了正在录入的操作数。解决输入侧加消抖一般是打两拍再取边沿切换操作数时把已录入的值锁进寄存器不要在组合路径上直接用触摸坐标算数。这类问题在仿真里永远复现不了只能上板抓。现象换一台机器打开工程编译报找不到文件。原因工程里用了绝对路径引用.dcp、.xdc或者程序初始化文件。解决所有路径改成相对工程根目录的相对路径工程整体打包迁移。这条是血泪经验吃过一次亏之后我把所有外部文件都塞进工程目录里了。6. 二次开发把单周期改成五级流水线时我会盯住的三件事单周期 CPU 跑通之后往五级流水改是这套手册的终点也是大多数人真正开始理解体系结构的起点。我的做法是先不加任何冒险处理把流水寄存器老老实实插进去让五条指令能同时在流水线上流动跑一个没有数据相关的测试程序确认吞吐率确实上去了——这一步叫裸流水能跑通说明阶段划分是对的。裸流水跑通之后再逐个加冒险处理每加一个就用一组针对性的汇编用例验证不要一次性全加上去否则出了错根本定位不到是哪块逻辑。第一件事是流水寄存器的复位值。IF/ID、ID/EX、EX/MEM、MEM/WB 这四级寄存器控制信号那几位必须能清零否则流水线里会飘着无效指令的控制信号把后面本来该阻塞的指令放过去。我一般会给控制信号单独做一套清零即插入 nop的逻辑比逐位判断省事。// ID/EX 流水寄存器控制信号清零等价于插入一条 nop always (posedge clk or negedge resetn) begin if (!resetn) begin ex_alu_op 4b0; ex_regwrite 1b0; // 关键清零即不写回 ex_memwrite 1b0; ex_memread 1b0; end else if (flush) begin ex_alu_op 4b0; ex_regwrite 1b0; ex_memwrite 1b0; ex_memread 1b0; end else begin // 正常通路把 ID 段译码结果打过来 ex_alu_op id_alu_op; ex_regwrite id_regwrite; ex_memwrite id_memwrite; ex_memread id_memread; end endflush是分支跳转时清空错误路径指令用的和复位走同一套清零逻辑代码可以复用。第二件事是前递的优先级。EX 段算出来的结果要抢先于 MEM 段的结果因为 EX 段更新。写成if (ex_regwrite ex_waddr id_rs) ... else if (mem_regwrite ...)这种链式判断顺序反了会出现用了旧值但看起来也能跑的隐蔽 bug只有特定指令序列才暴露。第三件事是 load-use 冒险必须阻塞一拍。这条没有捷径lw后面紧跟一条用到该寄存器的指令前递也救不了只能让流水线停一个周期。判断条件是EX 段是 load 且目标寄存器等于 ID 段源寄存器一旦命中就冻结 PC 和 IF/ID同时给 ID/EX 插 nop。验证方法我习惯用三层先跑指令级的功能用例每条指令一条看写回值再跑一段带循环和分支的小程序比如冒泡排序最后上板跑真实负载。前两层在仿真里做第三层才下载。跳过前两层直接上板等于把调试成本放大十倍。从那以后我每次动流水线都强制先把裸流水 无相关测试程序这一关单独走一遍确认干净了再往里加冒险处理——这个习惯帮我省掉了无数次在几百行波形里大海捞针。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询