资讯详情

FPGA加法器实验全解析:从Verilog代码到上板调试

📅 2026/9/17 1:55:46 | 华诺云谱 👁 阅读
FPGA加法器实验全解析:从Verilog代码到上板调试
如果你刚接触FPGA十有八九会被推荐的第一个正经实验不是流水灯而是加法器设计。山东大学FPGA课程实验一就把加法器放在第一关这个安排看起来简单但背后藏着一整套数字电路的基本功真值表、组合逻辑、Verilog语法、testbench仿真再到最终上板的管脚约束。我当年也是从这块板子一路踩坑过来的后来带过几届学弟发现大家遇到的坑几乎一模一样。这篇就把这个实验从头到尾拆开讲明白包括代码怎么写、仿真怎么调、上板为什么不亮希望能帮你少走点弯路。1. 实验背景与核心思路1.1 为什么第一课选择加法器加法器是数字系统最基础的运算单元CPU里的ALU、计数器的累加逻辑、地址生成器说到底都离不开它。学校里很多课程设计会用交通灯、电子琴这类花哨题目但山东大学FPGA课程偏偏选加法器当开胃菜我觉得原因很实在加法器能让你在一节课内同时接触到模块端口怎么定义、组合逻辑怎么写、仿真激励怎么给以及最重要的——“电路不是程序”这个思维转换。很多同学是学过C语言再来学FPGA的上手就喜欢写for循环、写变量自加总想用软件思维去写硬件。加法器这个小东西没有时序状态没有复杂状态机正好把你逼到一个最朴素的问题上怎么用语言描述一条信号从输入到输出的传播路径。理解了这个后面学流水灯、状态机、FIFO都会顺很多。1.2 从真值表到Verilog三种实现思路对比我给这个实验分了三个层次你们可以根据自己的进度选择做哪一种。第一种是逻辑门级描述。把加减法退化成最原始的与门、或门、异或门完全照着手绘电路图去例化门电路。这种方法最繁琐但最接近硬件本质能让你看清每一位是如何独立运算的。第二种是数据流描述。用assign语句配合运算符一行代码解决。这种方法看起来是“作弊”但它体现了Verilog作为硬件描述语言的抽象能力你描述的是“期望什么结果”工具会自动推断出加法器电路。第三种是行为级描述。在always块里用case或者if列出所有输入组合相当于把真值表直接翻译成代码。这种写法对逻辑综合工具最友好是工业界最常用的方式。我建议你们三种都试一遍然后对比综合后的RTL视图。你会发现工具生成的电路和你手绘的门级结构几乎一样那种“原来代码真的会变成电路”的感觉比刷十遍理论课都有用。2. 设计原理与代码实现2.1 半加器和全加器的逻辑拆解在动手写代码前先得把底层的铁律搞清楚。一个二进制加法不考虑来自低位的进位时就是半加器。设两个输入为a和b输出为和sum、进位cout真值表如下absumcout0000011010101101从真值表能直接写出逻辑表达式sum a ^ bcout a b。异或门就是“只看两个数是否不同”与门就是“两个都为1时才产生进位”。但实际做多位加法时每一位还要考虑来自低位的进位cin这就是全加器。全加器有三个输入a、b、cin输出sum和cout。注意这里的cin是“carry in”代表来自低位的进位输入。表达式变成sum a ^ b ^ cincout (a b) | (cin (a ^ b))很多人问为什么要写成这样其实就是穷举8种输入状态化简的结果。你可以用卡诺图验证也可以直接理解为只要a、b同为1必然产生进位如果a和b只有一个为1但低位的进位是1那也会产生进位并传给高位。2.2 多位加法器的两种经典写法掌握了单bit全加器多位加法就是把它复制多份。最直观的方式叫行波进位加法器Ripple Carry AdderRCA把每个全加器的cout接到下一位的cin。第一位cin接地0最高位cout作为整个加法器的溢出标志。这种结构简单缺点是高位要等低位的进位逐级传上来位宽越大延迟越高。另一种叫超前进位加法器Carry Lookahead AdderCLA它通过逻辑电路预先算出每一位的进位不需要等待逐级传递速度更快但电路更复杂。对于课程实验RCA已经足够而且能让你更清楚进位的传播关系。不过真的用Verilog实现时大多数人不会自己展开门级电路而是直接写assign {cout, sum} a b cin;这里的{cout, sum}意思是将进位和和的各位拼接成一个大的向量。位宽要匹配比如a和b是4位sum就是4位cout是1位拼接后就是5位可以容纳最大结果1515131也就是5位二进制。这一行代码综合后工具会自动帮你生成一个优化的加法器电路可能是行波进位也可能是超前进位取决于综合策略。2.3 完整Verilog代码与注释下面我给出一个4位加法器的完整代码包含门级描述和行为级两种写法你们可以对照学习。先看行为级这是最常写的module adder4 ( input wire [3:0] a, // 加数1 input wire [3:0] b, // 加数2 input wire ci, // 进位输入 output wire [3:0] sum, // 和的低4位 output wire co // 进位输出 ); assign {co, sum} a b ci; // 拼接进位输出与和 endmodule再看门级实现用全加器级联。先写一个1位全加器模块再在顶层例化4次// 1位全加器 module fulladder ( input wire a, b, cin, output wire sum, cout ); wire s1; assign s1 a ^ b; assign sum s1 ^ cin; assign cout (a b) | (cin s1); endmodule // 4位行波进位加法器 module adder4_rca ( input wire [3:0] a, b, input wire ci, output wire [3:0] sum, output wire co ); wire c1, c2, c3; // 中间进位 fulladder fa0 ( .a(a[0]), .b(b[0]), .cin(ci), .sum(sum[0]), .cout(c1) ); fulladder fa1 ( .a(a[1]), .b(b[1]), .cin(c1), .sum(sum[1]), .cout(c2) ); fulladder fa2 ( .a(a[2]), .b(b[2]), .cin(c2), .sum(sum[2]), .cout(c3) ); fulladder fa3 ( .a(a[3]), .b(b[3]), .cin(c3), .sum(sum[3]), .cout(co) ); endmodule门级实现的价值在于你能在仿真波形里肉眼看到进位c1、c2、c3是如何像多米诺骨牌一样逐位传递的。如果你用的是Vivado综合后打开Schematic视图能直接看到四个全加器串联的结构这一步强烈建议做对理解硬件很有帮助。3. 仿真验证与上板3.1 用ModelSim或Vivado自带仿真搭建testbench写好设计代码后下一步就是验证。很多同学拿到板子就直接烧结果LED乱闪又不知道哪里出了问题。正确的流程是先仿真。仿真工具可以用ModelSim或者直接用Vivado/Quartus自带的仿真器。我以Vivado为例新建工程后添加上面写的adder4模块然后创建一个testbench文件也叫激励文件timescale 1ns/1ps module tb_adder4; reg [3:0] a; reg [3:0] b; reg ci; wire [3:0] sum; wire co; adder4 dut ( .a(a), .b(b), .ci(ci), .sum(sum), .co(co) ); initial begin a 4d0; b 4d0; ci 1b0; #10; // 测试10 0 0 a 4d0; b 4d0; ci 1b0; #10; // 测试25 3 8, 无进位 a 4d5; b 4d3; ci 1b0; #10; // 测试315 1 16, 进位为1和的低4位为0 a 4d15; b 4d1; ci 1b0; #10; // 测试47 4 1 12 a 4d7; b 4d4; ci 1b1; #10; $display(a%d b%d ci%d sum%d co%d, a, b, ci, sum, co); $finish; end initial begin $monitor(time%0t a%b b%b ci%b sum%b co%b, $time, a, b, ci, sum, co); end endmodule这个testbench里用了initial块它在仿真开始后按顺序执行。#10表示延时10个时间单位用$monitor可以实时打印所有信号变化方便在控制台核对。运行仿真后在波形窗口里添加a、b、ci、sum、co这几个信号就能看到每一组输入下的输出。3.2 波形分析和常见仿真陷阱仿真波形看着简单但有几个坑是大家很容易踩的。第一个坑是没有把testbench加入仿真。在Vivado里你要在Sources面板里把tb_adder4设为仿真顶层不是把adder4设为顶层。很多同学在Simulation里点了Run后发现波形窗口空白多半是没设置对。第二个坑是信号显示为X态。X表示不定态常见原因是变量没有初始化。比如testbench里的reg变量默认值是X如果没在initial块里赋值就直接#10仿真出来的sum就是X。所有参与激励的reg变量在仿真开始前必须赋值这和我们写C语言时的“变量要初始化”一个道理。第三个坑是波形窗口里信号看不全。因为有些信号被综合优化掉了比如中间进位c1、c2、c3在综合后仿真里可能不存在。如果你是在行为级仿真也叫前仿真里这些内部net是能看到的。建议做功能仿真时用RTL仿真不要用综合后仿真否则你会被一堆缩写弄得头大。第四个坑更隐蔽测试用例覆盖不全。我见过很多同学只测了“35”和“72”以为仿真过了就没问题了结果上板后按到151进位co直接崩溃。加法器最重要的边界测试就是所有位全为1时再加1比如4位加法器的151、15151这些极端情况一定都要测到。3.3 上板验证时如何观察结果LED/数码管仿真通过后终于可以烧录到板子上。实验板的操作方式是把a和b对应的4位拨码开关拨到某个值按下输入按键模拟cin或者固定接地然后观察LED显示sum和co。在我的板上8个LED中前4个显示sum第5个显示co。具体管脚分配要参考你的板卡原理图在Vivado里新建约束文件.xdc把a端口绑定到拨码开关sum和co绑定到LED。这里有个小建议如果板子有数码管不妨把结果用两个数码管显示成十进制数。虽然实验只要求二进制LED但数码管能直观看到“13619”比二进制直观得多。做法是把sum和co再转成BCD码用两个数码管扫描显示。这个扩展会用到七段译码器和动态扫描技术刚好是很多FPGA课程下一个实验的内容提前练练不吃亏。引脚约束的语法很简单比如set_property PACKAGE_PIN J15 [get_ports {a[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {a[0]}]每个信号都要有对应的PACKAGE_PIN和IOSTANDARD具体引脚编号以你的板卡原理图为准。这里最容易犯的错误是引脚看错明明约束了a[0]对应的按键板子却映射到了另一个按键。我当年烧完波形怎么拨开关都没反应最后发现是原理图上的网络名和xdc里的引脚对不上折腾了整整一晚上。所以拿到板卡的第一步永远是看原理图确认外设连接到FPGA的哪个Bank、哪个Pin别想当然。4. 常见问题与调试心得4.1 仿真结果正确但上板不亮怎么回事如果你仿真波形完美但一烧到板子上LED死活不亮先别急着怀疑人生。按照优先级排查这三件事。第一检查管脚约束是否生效。在Vivado里打开Implementation后的Device视图看看你约束的引脚是否真的连接到了LED和开关。有时你约束了引脚但忘了保存约束文件工具会警告“忽略未约束的I/O”但很多同学不看警告直接生成bitstream结果所有端口都被工具自动分配到了任意位置。第二检查输入信号的消抖问题。拨码开关本质是机械触点按下去或拨动时会有几毫秒的抖动信号在0和1之间来回跳。你仿真时用的是理想的阶跃信号但真实电路里抖动会让加法器连续计算多次LED看起来就是乱跳或偶尔不变化。解决方法是给输入加个简单的消抖电路或者用按键而非拨码开关时配合一个计数器做消抖。第三检查时钟是否被使用。我们这次实验的sum输出是组合逻辑不依赖时钟所以上板时如果不加时钟也能看到结果只要输入稳定输出就稳定。但如果你在扩展里加了寄存器或者用数码管动态扫描那必须给设计提供正确的时钟信号。初学者最常犯的错就是忘记给板子的时钟引脚添加约束导致时序逻辑完全没反应。注意实验中如果使用always (posedge clk)这种写法就必须把clk约束到板载晶振的引脚上。4.2 进位链延迟与组合逻辑竞争实验做完后我建议你问问自己一个问题为什么4位行波进位加法器不能跑很高的时钟频率这就要说到进位链延迟了。每个全加器从输入到产生进位输出大约经过两个门延迟。4位加法器的进位要依次经过4个全加器总延迟是串联的。时钟周期必须大于这个延迟否则结果还没稳定下来下一拍就到了就会采样到错误数据。这在你后面学到时序约束和静态时序分析时会反复遇到。还有一个有趣的坑组合逻辑的输出会存在毛刺。比如两个输入从“35”变成“44”各位信号变化的时刻可能不完全相同导致sum先短暂变成无效值再稳定到8。如果你的LED直接接在组合逻辑输出上你肉眼可能看不见但用示波器测会发现毛刺。如果后续电路里有寄存器采样就可能在毛刺瞬间采到错误值。解决方法是让输出经过一级寄存器打拍把组合逻辑的结果在时钟沿到来前锁存住这样毛刺就不会传播到下一级。这个知识点看似超纲但考试面试很喜欢问。很多公司招聘FPGA工程师必然会问“组合逻辑和时序逻辑的区别”以及“如何消除毛刺”。从这个实验里能顺便弄明白这些问题性价比很高。4.3 初次使用FPGA开发板的3个建议如果你是完全没用过FPGA板卡的小白第一次上板前请记住下面三点都是我用眼泪换来的。第一先跑一遍板卡自带的DEMO。不管是黑金、正点原子还是Altera的板子出厂都会内置几个演示工程比如流水灯、数码管秒表。先下载官方demo确认硬件和下载线没问题再开始自己的设计。我见过有同学第一次烧录就把下载器驱动装错了折腾了三个小时最后才发现是驱动没装对原厂demo直接能跑。第二养成编译后看警告的习惯。综合和实现阶段工具会输出很多警告比如“inferred latch”、“unused input”等。初学阶段看到warning不用慌但要花时间看懂每条警告在说什么。很多发诡异的bug根源就在这些被忽略的警告里。比如“signal sum is not used”可能就是你的端口连接错了sum压根没驱动到LED上。第三用拨码开关做输入时一定要加消抖吗我的答案是拨码开关可以不消抖但按键必须消抖。因为拨码开关拨到位后基本就稳定了抖动发生在拨的过程中如果你在拨完后再看LED看到的已经是稳定结果。但按键按下和松开时的抖动会持续几十毫秒如果你的电路检测下降沿可能会检测到多次。所以建议所有输入按键都加一个简单的20ms计数器消抖这个技巧后面做交通灯、电子琴时都能用上。关于这个加法器实验最后再分享一个我觉得特别有用的操作在完成基本要求后试着把加法器改成减法器。补码减法其实很简单a - b等价于a (~b 1)也就是把b按位取反再加1。你只需要在原加法器的b输入前加一排异或门再控制cin为1就能实现减法。这个改动看起来不大但能帮你把二进制补码、进位、借位这些概念一次性串起来。做完之后你会发现CPU里加减法其实就是这么回事整个数字系统就像一个精致的拼图而加法器就是那块最核心的起点。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。