从加法器到数码管:用Verilog打造一个“会算数”的FPGA小系统
你有没有想过,计算机是怎么做加法的?
不是打开计算器App那种“做”,而是从最底层的晶体管和逻辑门开始,一步一步把0和1变成我们看得懂的数字。今天,我们就来亲手实现这个过程——在一块FPGA上,用Verilog写一个4位全加器,并让它驱动七段数码管,实时显示计算结果。
这不只是“点亮LED”那么简单。这是一个完整的数字系统雏形:有输入、有运算、有输出。它像一台微型计算机,虽然只能算加法,但麻雀虽小,五脏俱全。
为什么是“4位全加器”?
在数字电路的世界里,加法器是算术逻辑单元(ALU)的灵魂。无论是手机还是超级计算机,它们最基础的加减乘除,都是从一个个小小的加法器堆出来的。
而“4位”是个很巧妙的选择:
- 够简单:适合初学者理解;
- 够实用:能表示0~15,足够展示基本功能;
- 易扩展:掌握了4位,8位、16位自然水到渠成。
更重要的是,当我们把两个4位二进制数相加时,结果最大为1111 + 1111 = 11110(即十进制30),这意味着我们需要处理进位问题——而这正是全加器存在的意义。
半加器 vs 全加器:差一个“进位”而已?
半加器只能加两个一位数(A 和 B),输出和(Sum)与进位(Cout)。但它不支持来自低位的进位输入,所以无法级联。
全加器则多了一个输入 Cin(Carry In),让它可以“接力”工作。四个全加器串起来,就能组成一个4位加法器。
它的核心公式其实很简单:
Sum = A ⊕ B ⊕ Cin Cout = (A & B) | (Cin & (A ^ B))别被符号吓到,这就是异或和与或操作。关键在于:每一位的输出不仅取决于当前位的两个输入,还依赖前一位的进位。这种“连锁反应”就是所谓的“串行进位”(Ripple Carry)。
虽然现代CPU早已不用这种方式(太慢了!),但在教学和小规模设计中,它是理解硬件并行性的绝佳入口。
搭建你的第一个4位加法器模块
我们采用模块化设计思路:先做一个单个全加器,再把它复用四次。
// 单个全加器 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代码清晰得就像教科书一样。接下来是重头戏——把四个这样的模块连起来:
module adder_4bit( input [3:0] A, input [3:0] B, input Cin, output [3:0] Sum, output Cout ); wire [3:0] carry; full_adder fa0 (.A(A[0]), .B(B[0]), .Cin(Cin), .Sum(Sum[0]), .Cout(carry[0])); full_adder fa1 (.A(A[1]), .B(B[1]), .Cin(carry[0]), .Sum(Sum[1]), .Cout(carry[1])); full_adder fa2 (.A(A[2]), .B(B[2]), .Cin(carry[1]), .Sum(Sum[2]), .Cout(carry[2])); full_adder fa3 (.A(A[3]), .B(B[3]), .Cin(carry[2]), .Sum(Sum[3]), .Cout(Cout)); endmodule注意这里的carry[3:0]是内部信号,用来传递进位。最低位使用外部输入 Cin(通常接0),最高位产生最终进位 Cout。
🛠️调试小贴士:如果你发现高位加法出错,比如
7+8=0,那大概率是进位线没接对。建议在仿真时重点观察carry[0]到carry[2]的变化是否符合预期。
让结果“看得见”:七段数码管怎么控制?
再强大的计算,如果没人看得到,也就失去了意义。这时候就需要七段数码管登场了。
它由 a~g 七个LED段组成,通过不同组合点亮来显示数字0~9。常见的有两种类型:
| 类型 | 公共端连接 | 点亮方式 |
|---|---|---|
| 共阴极 | 接地(GND) | 给段送高电平点亮 |
| 共阳极 | 接电源(VCC) | 给段送低电平点亮 |
大多数FPGA开发板使用的是共阳极数码管,这点非常重要!因为这意味着我们的译码器输出必须是低电平有效。
如何把二进制结果变成“亮灯模式”?
假设加法结果是4'd5,我们要让数码管显示“5”。查一下真值表:
| 段 | a | b | c | d | e | f | g |
|---|---|---|---|---|---|---|---|
| 值 | 1 | 0 | 1 | 1 | 0 | 1 | 1 |
但这只是逻辑状态。对于共阳极来说,要让某段亮,就得输出0;不亮就输出1。所以我们实际输出应该是:
a=0, b=1, c=0, d=0, e=1, f=0, g=0 → 7'b0100100为了简化管理,我们可以建立一张“编码表”:
module seg_decoder( input [3:0] bcd, output reg [6:0] seg ); always @(*) begin case(bcd) 4'd0: seg = 7'b1000000; // 共阳极:a~f亮,g灭 → 输出0对应亮 4'd1: seg = 7'b1111001; 4'd2: seg = 7'b0100100; 4'd3: seg = 7'b0110000; 4'd4: seg = 7'b0011001; 4'd5: seg = 7'b0010010; 4'd6: seg = 7'b0000010; 4'd7: seg = 7'b1111000; 4'd8: seg = 7'b0000000; 4'd9: seg = 7'b0010000; default: seg = 7'b1111111; // 全灭 endcase end endmodule⚠️ 注意:上面的编码是针对共阳极数码管的。如果你的开发板是共阴极,请将所有值取反。
这里用了always @(*)来描述组合逻辑,综合工具会自动将其映射为查找表(LUT)。虽然也可以用assign+? :实现,但case更直观、易维护。
把它们连起来:构建完整系统
现在我们有两个核心模块:
adder_4bit:负责计算seg_decoder:负责显示
只需要一个顶层模块把它们串联起来即可:
module top_module( input [3:0] sw_a, // 开关输入A input [3:0] sw_b, // 开关输入B output [6:0] seg_out // 连接到数码管a~g ); wire [3:0] sum_result; wire carry_out; // 实例化加法器 adder_4bit u_adder ( .A(sw_a), .B(sw_b), .Cin(1'b0), .Sum(sum_result), .Cout(carry_out) ); // 实例化译码器 seg_decoder u_decoder ( .bcd(sum_result), .seg(seg_out) ); endmodule就这么简单?没错!
当然,现实不会总是这么理想。比如当sw_a = 4'd9,sw_b = 4'd7时,结果是16'd16,超出了单个数码管的显示范围(0~9)。这时你会看到什么?可能是乱码,也可能是熄灭。
怎么办?
方案一:只显示低4位,忽略溢出
这是最简单的做法,适合教学演示。用户自己意识到“哦,超过9就不准了”。
方案二:双数码管动态扫描
如果你想显示“16”,那就需要两个数码管。我们可以引入位选信号(sel[1:0]),并通过快速切换(>50Hz)实现视觉暂留效果。
但这已经属于进阶内容了,涉及状态机和定时控制。本文暂不展开,但你可以把它作为下一步挑战目标。
实际部署中的那些“坑”
纸上谈兵容易,真正在板子上跑通才是硬道理。以下是几个常见陷阱:
❌ 误区1:直接拿FPGA引脚接数码管,烧了IO口
FPGA的IO驱动能力有限(一般每脚不超过16mA)。而每个LED段的工作电流可能在5~20mA之间。如果没有限流电阻,轻则亮度异常,重则损坏芯片。
✅ 正确做法:每个段都串联一个220Ω~1kΩ的限流电阻。推荐470Ω,既能保证亮度又安全。
❌ 误区2:忘了开发板是共阳还是共阴
很多同学写完代码烧进去,发现“该亮的不亮,不该亮的全亮”。原因往往就是搞错了数码管类型。
✅ 解决办法:查阅开发板原理图,确认公共端接法。或者用万用表测通断判断。
❌ 误区3:按键抖动导致输入错误
如果用按键代替拨码开关作为输入,可能会遇到“按一次变多次”的问题。这是因为机械开关存在“抖动”现象。
✅ 解决方案:加入消抖电路,可以用延时检测,也可以用状态机实现软件消抖。
这个项目教会了我们什么?
表面上看,这只是个“小学生项目”——会加法、会显示。但实际上,它涵盖了数字系统设计的核心思想:
- 模块化设计:把复杂系统拆解为可复用的小模块;
- 数据通路构建:从原始输入到最终输出,形成闭环;
- 软硬协同验证:代码逻辑必须与物理世界匹配;
- 边界条件处理:溢出、无效输入、电平兼容等问题不可忽视;
- 调试思维养成:学会分段隔离问题,是工程师的基本功。
更重要的是,当你拨动开关,看到数码管跳出了正确的数字时,那种“我造了个会思考的东西”的成就感,是任何理论课都无法替代的。
下一步你可以做什么?
别停下!这个项目只是一个起点。试试这些升级方向:
- ✅ 改成8位加法器,看看资源占用变化;
- ✅ 加一个减法模式(用补码实现);
- ✅ 用两个数码管显示完整结果(0~30);
- ✅ 添加时钟,实现自动累加(计数器模式);
- ✅ 引入按键控制清零或启动;
- ✅ 用Verilog状态机实现简易计算器。
甚至有一天,你可以用类似的方法,搭建一个能运行简单程序的CPU。
如果你也在学习FPGA,不妨今晚就动手试一试。找块开发板,敲几行代码,连几根线,然后——让你的第一个“数字大脑”开始工作吧。
💬 你在实现过程中踩过哪些坑?欢迎留言分享你的调试经历!