从“1+1”开始:在FPGA上亲手点亮一个全加器
你有没有想过,计算机是怎么做加法的?不是打开计算器敲几个数字那种——而是真正从硬件层面,让电流流动、信号翻转,完成一次二进制的“1+1=10”。今天,我们就从最基础的全加器(Full Adder)出发,用一块FPGA开发板,把教科书里的真值表变成眼前闪烁的LED灯。
这不仅是一个实验,更是一次对数字世界底层逻辑的“破壁之旅”。
为什么是全加器?
别看它结构简单,全加器可是现代所有算术运算的起点。无论是你的手机在处理图像,还是服务器在跑AI模型,背后都离不开成千上万个并行工作的加法单元。
而FPGA,正是我们亲手搭建这些硬件模块的最佳沙盒。它不像MCU那样按指令顺序执行,而是允许你“画”出电路本身——想让它怎么算,就怎么连。
更重要的是:你能看到结果。拨一下开关,灯亮了,你就知道,那一瞬间,硅片里真的发生了一次加法。
全加器的本质:三位输入,两位输出
先回到定义:
全加器是一个组合逻辑电路,接收三个输入:
- A:第一个操作数位
- B:第二个操作数位
- Cin:来自低位的进位
输出两个结果:
- Sum:当前位的和
- Cout:向高位的进位
它的核心逻辑来自于这张8行的真值表:
| A | B | Cin | Sum | Cout |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 0 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 1 | 0 | 0 | 1 |
| 0 | 0 | 1 | 1 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |
通过卡诺图化简或直接观察,可以得出两个关键公式:
- Sum = A ⊕ B ⊕ Cin
- Cout = (A & B) | (Cin & (A ^ B))
这两个表达式简洁到极致,却蕴含着整个二进制加法的核心机制。
- Sum 是异或链:只有奇数个1时才为1。
- Cout 是进位生成与传递:要么A和B同时为1(生成进位),要么有进位输入且A和B中有一个为1(传递进位)。
写代码?不,我们在“描述”硬件
很多人初学Verilog时会误以为它是编程语言,其实不然。HDL(硬件描述语言)的本质是描述物理连接关系。我们写的每一行,最终都会映射成实实在在的门电路。
下面是单比特全加器的实现:
module full_adder ( input A, input B, input Cin, output Sum, output Cout ); assign Sum = A ^ B ^ Cin; assign Cout = (A & B) | (Cin & (A ^ B)); endmodule就这么两行。没有循环,没有函数调用,也没有变量赋值——只有连续赋值assign,表示信号一旦变化,输出立刻响应。
这就是组合逻辑的魅力:零延迟(理想情况下)、完全并行、状态透明。
⚠️ 提醒:千万不要在组合逻辑中漏写分支!否则综合工具可能会推断出锁存器(latch),导致不可预测的行为。虽然这个例子很简单不会出错,但在复杂逻辑中务必警惕。
让它“活”起来:接上拨码开关和LED
光有逻辑还不够,我们要把它接到真实的硬件上。假设使用Xilinx Basys 3开发板(Artix-7芯片),常见的资源包括:
- 16个拨码开关(SW[15:0])
- 16个LED(LED[15:0])
现在我们设计一个顶层模块,把全加器和IO口连起来:
module top_full_adder ( input SW0, // 连接 A input SW1, // 连接 B input SW2, // 连接 Cin output LED0, // 显示 Sum output LED1 // 显示 Cout ); wire sum_wire, cout_wire; // 实例化子模块 full_adder uut ( .A(SW0), .B(SW1), .Cin(SW2), .Sum(sum_wire), .Cout(cout_wire) ); assign LED0 = sum_wire; assign LED1 = cout_wire; endmodule这里的关键在于实例化(instantiation)。uut就是你设计的“芯片”,而顶层模块就像PCB上的插座,负责把引脚焊接到正确的外设上。
引脚约束:连接虚拟与现实
FPGA开发中最容易被忽视却又至关重要的一环:约束文件(.xdc)。
没有它,工具不知道SW0对应哪个物理引脚,也不知道LED该接在哪里。以下是Basys 3常用的约束配置:
## Switches set_property PACKAGE_PIN V17 [get_ports SW0] # IO_L24N_T3_RS0_15 set_property PACKAGE_PIN V16 [get_ports SW1] # IO_L24P_T3_RS1_15 set_property PACKAGE_PIN W16 [get_ports SW2] # IO_L23N_T3_FOE_B_15 ## LEDs set_property PACKAGE_PIN U16 [get_ports LED0] # IO_L19N_T3_A02_14 set_property PACKAGE_PIN E19 [get_ports LED1] # IO_L16N_T2_A27_14 ## I/O Standard set_property IOSTANDARD LVCMOS33 [get_ports {SW*}] set_property IOSTANDARD LVCMOS33 [get_ports {LED*}]这些PACKAGE_PIN编号来自开发板原理图。每块板子都不一样,必须查手册确认。
✅ 小技巧:命名保持一致!比如端口叫
SW0,约束里也写[get_ports SW0],避免拼写错误导致映射失败。
开发流程走一遍:从代码到比特流
以Xilinx Vivado为例,完整流程如下:
新建工程
选择“RTL Project”,跳过添加源文件,后续手动导入。添加设计文件
把full_adder.v和top_full_adder.v加入工程。添加约束文件
创建新文件,类型选XDC,粘贴上面的引脚分配。综合(Synthesis)
点击“Run Synthesis”。如果语法无误,你会看到网表视图中出现两个LUT(查找表)。实现(Implementation)
工具将逻辑布局到实际FPGA资源中,进行布线优化。生成比特流(Generate Bitstream)
输出.bit文件,这是烧录到FPGA的“固件”。下载到板子
连接USB线,打开Hardware Manager,连接设备,下载比特流。
一切顺利的话,此刻你的FPGA已经变成了一个真正的“加法芯片”。
动手验证:试遍所有组合
接下来就是最有成就感的部分——动手测试!
| SW2(Cin) | SW1(B) | SW0(A) | 预期Sum | 预期Cout | 实际LED0/LED1 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 灭 / 灭 |
| 1 | 0 | 0 | 1 | 0 | 亮 / 灭 |
| 0 | 1 | 1 | 0 | 1 | 灭 / 亮 |
| 1 | 1 | 1 | 1 | 1 | 亮 / 亮 |
当你拨动开关,看到LED按照预期亮起时,那种“我造出了一个小东西”的感觉,比任何仿真波形都来得真实。
资源到底用了多少?
在Artix-7上综合后,这个全加器的实际资源消耗如下:
| 资源类型 | 数量 | 说明 |
|---|---|---|
| LUTs | 2 | Sum 和 Cout 各占一个4输入LUT |
| FFs | 0 | 纯组合逻辑,无需寄存器 |
| IOBs | 5 | 3个输入 + 2个输出 |
这意味着什么?一块中等规模的FPGA(如XC7A35T)有超过20,000个LUT,理论上可以放下上万个并行工作的全加器。
换句话说:你可以同时做一万次独立的加法运算——而这,正是FPGA在高性能计算、加密、AI推理等领域不可替代的原因。
它能做什么?不只是“1+1”
别小看这个小模块,它是构建复杂数字系统的基石。比如:
多位加法器
把多个全加器串起来,就成了行波进位加法器(RCA):
FA0: A[0]+B[0]+0 → S[0], C1 FA1: A[1]+B[1]+C1 → S[1], C2 ... FA3: A[3]+B[3]+C3 → S[3], Cout虽然结构简单,但进位像波浪一样逐级传递,速度受限。于是就有了超前进位加法器(CLA),提前计算进位,大幅提升性能。
ALU的基础单元
现代CPU中的算术逻辑单元(ALU)本质上就是一堆可配置的全加器网络。配合多路选择器和控制信号,不仅能加法,还能减法(补码)、比较、甚至逻辑运算。
流水线加速
如果你需要高速数据流处理(如视频帧叠加),可以把多个加法器组成流水线,在每个时钟周期吞吐一个新的结果,实现超高吞吐率。
初学者常踩的坑与应对秘籍
❌ 坑1:改了代码没重新生成比特流
很多新手修改Verilog后只点击“Generate Bitstream”,但忘了先重新运行Implementation。记住:
修改逻辑 → 必须重新综合 + 实现 + 生成比特流!
❌ 坑2:引脚锁定错误
某个LED一直不亮?大概率是XDC里写错了PIN名称。建议:
- 打开开发板官方PDF,对照原理图核对;
- 使用官方提供的模板XDC作为参考。
❌ 坑3:电源不足导致不稳定
某些开发板通过USB供电,若外接负载过大可能重启。确保使用稳定的5V电源适配器。
✅ 秘籍:善用ILA调试核
想看内部信号?Vivado提供Integrated Logic Analyzer(ILA),可实时抓取波形。哪怕只是一个中间wire,也能可视化观测。
只需在代码中标记要监测的信号,添加ILA IP核,重新综合即可。比示波器探针还方便。
更进一步:你能探索的方向
掌握了单个全加器,下一步就可以挑战更有意思的设计:
- 4位超前进位加法器:摆脱串行进位延迟,体验速度飞跃。
- 带进位链的多位加法器生成器:用参数化设计(parameterized module)一键生成任意位宽加法器。
- FPGA上的简易ALU:支持ADD/SUB/AND/OR/XOR等多种操作。
- 流水线加法器:提升吞吐率,理解时序与性能的权衡。
- 用HLS从C语言生成硬件:尝试Vitis HLS,用高级语言写出加法函数,自动生成RTL。
结语:每一次“1+1”,都在硅片上真实发生
我们从一个最简单的逻辑单元出发,完成了从理论→建模→部署→验证的完整闭环。这不是模拟,也不是仿真,而是真正在硅基半导体上运行的硬件电路。
当你拨动开关,看到LED亮起的那一刻,你应该意识到:
你不是在“运行程序”,而是在“构建机器”。
这种掌控底层硬件的感觉,是软件工程师难以体会的独特魅力。
所以,别停留在“Hello World”了。来试试“1+1”,让它在你的FPGA上真实发生吧。
如果你已经点亮了属于你的第一个全加器,欢迎在评论区晒出你的测试照片或者遇到的问题。我们一起,把数字世界的地基打得更牢。