从加法器到数码管:用Verilog点亮第一个数字电路
你有没有试过,在FPGA开发板上拨动几个开关,然后眼前那个小小的七段数码管突然亮起一个数字——那一刻,仿佛是你亲手让机器“看懂”了计算?
这正是很多工程师第一次接触硬件描述语言(HDL)时最激动的瞬间。而实现它的核心,往往就是一个看似简单的项目:用Verilog写一个4位全加器,并把结果通过七段数码管显示出来。
别小看这个“入门级”项目。它不仅是学习Verilog的敲门砖,更是一次完整的数字系统设计实战——从组合逻辑构建、信号译码,到物理输出驱动,整个流程一气呵成。今天我们就来一步步拆解这个经典设计,让你真正理解每行代码背后发生了什么。
加法器不是“算数”,是“搭积木”
在软件里,a + b一行代码搞定;但在硬件世界里,每个比特都要“亲手”连接起来。我们先从最基础的单元开始:1位全加器。
全加器的本质:三个输入,两个输出
想象你要加两位二进制数A和B,还要考虑来自低位的进位Cin。这一位的结果有两个:
- 当前位的和:Sum = A ⊕ B ⊕ Cin
- 向高位的进位:Cout = (A & B) | (Cin & (A ^ B))
这两个公式就是布尔代数对“加法”的翻译。把它封装成模块,就成了我们的基本积木块:
module full_adder ( input a, cin, b, output sum, cout ); assign sum = a ^ b ^ cin; assign cout = (a & b) | (cin & (a ^ b)); endmodule这段代码没有时钟、没有状态,纯粹靠门电路实时响应输入变化——典型的组合逻辑。你可以把它理解为一块已经焊好的IC芯片,插上去就能用。
把四个“1位”拼成“4位”:串行进位链
现在我们要把四个这样的“积木”连起来,形成能处理4'b1011 + 4'b0110这种运算的完整加法器。
关键在于进位传递:第0位的Cout接到第1位的Cin,依次类推。这种结构叫Ripple Carry Adder(波纹进位加法器),虽然速度受限于进位传播延迟,但胜在结构清晰、易于理解。
来看顶层模块怎么“搭”:
module adder_4bit ( input [3:0] a, b, input cin, output [3:0] sum, output cout ); wire c1, c2, c3; full_adder fa0 (.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(c1)); full_adder fa1 (.a(a[1]), .b(b[1]), .cin(c1), .sum(sum[1]), .cout(c2)); full_adder fa2 (.a(a[2]), .b(b[2]), .cin(c2), .sum(sum[2]), .cout(c3)); full_adder fa3 (.a(a[3]), .b(b[3]), .cin(c3), .sum(sum[3]), .cout(cout)); endmodule注意这里的连接方式:
- 使用.name(signal)的命名端口映射,避免引脚接错;
- 内部进位线c1~c3是wire类型,自动由综合工具布线;
- 最终输出的cout可用于判断是否溢出(比如两数相加超过15)。
小贴士:如果你发现仿真时输出有毛刺(glitch),那很可能是因为进位信号逐级传递过程中出现了短暂的中间状态。这是组合逻辑的典型问题,解决办法之一是在输出端加一级寄存器打拍同步。
数码管不是“显示器”,是“灯光秀控制器”
有了结果sum[3:0],接下来的问题是如何让人眼看得懂?这时候就得请出老朋友——七段数码管。
数码管的工作原理:点亮哪几段,就显示哪个数字
共阴极数码管的每个LED段(a~g)对应一个控制引脚。只要给某段送高电平,它就会亮。比如要显示“0”,就要点亮 a、b、c、d、e、f 段,g 不亮。
所以,我们需要一个“翻译官”:把4位二进制数转成7个控制信号。这就是BCD-to-7Segment 译码器。
module seg_7_decoder ( input [3:0] bin, output reg [6:0] seg // [6]=a, [5]=b, ..., [0]=g ); always @(*) begin case (bin) 4'h0: seg = 7'b1111110; 4'h1: seg = 7'b0110000; 4'h2: seg = 7'b1101101; 4'h3: seg = 7'b1111001; 4'h4: seg = 7'b0110011; 4'h5: seg = 7'b1011011; 4'h6: seg = 7'b1011111; 4'h7: seg = 7'b1110000; 4'h8: seg = 7'b1111111; 4'h9: seg = 7'b1111011; 4'ha: seg = 7'b1110111; 4'hb: seg = 7'b0011111; 4'hc: seg = 7'b1001110; 4'hd: seg = 7'b0111101; 4'he: seg = 7'b1001111; 4'hf: seg = 7'b1000111; default: seg = 7'b0000000; endcase end endmodule几点关键说明:
-always @(*)表示这是一个纯组合逻辑块,输入变,输出立刻响应;
- 输出seg[6:0]对应的是 a~g 段,顺序不能错;
- 如果你的开发板用的是共阳极数码管,记得取反:~seg才能正确点亮。
整体系统搭建:从拨码开关到灯光亮起
现在三个核心部件都有了:
1. 输入源:拨码开关提供a[3:0]和b[3:0]
2. 运算核心:adder_4bit计算和
3. 显示驱动:seg_7_decoder控制数码管
把它们串起来,形成完整的数据流:
module top_level ( input [3:0] sw_a, sw_b, // 来自拨码开关 output [6:0] seg, // 数码管段控 output an // 位选,通常接固定低电平(单管) ); wire [3:0] sum; wire cout; // 实例化4位加法器 adder_4bit u_adder ( .a(sw_a), .b(sw_b), .cin(1'b0), // 初始无进位 .sum(sum), .cout(cout) ); // 实例化数码管译码器 seg_7_decoder u_seg ( .bin(sum), .seg(seg) ); // an通常是位选信号,单管常接地或拉低 assign an = 1'b0; endmodule⚠️常见坑点提醒:
- FPGA引脚约束必须准确!确保seg[6:0]真正接到数码管的 a~g 段。
- 有些开发板数码管是动态扫描的,需要额外控制an(位选)信号。如果是多位数码管,还需加入扫描逻辑。
- 若显示乱码,优先检查seg输出极性是否与硬件匹配(共阴/共阳)。
调试技巧与工程思维升级
当你第一次烧录程序却发现数码管不亮或者显示错误时,别慌。以下是几个实用调试思路:
✅ 1. 分模块验证
先单独测试seg_7_decoder:强制输入4'h0,看是否输出7'b1111110,对应“0”的形状。
再测adder_4bit:用测试平台(testbench)模拟几种情况:
initial begin a = 4'b0011; b = 4'b0101; cin = 0; // 应得 6 #10; a = 4'b1111; b = 4'b0001; // 应得 0, cout=1 end✅ 2. 防止组合逻辑毛刺
由于加法器和译码器都是组合逻辑,输入变化瞬间可能出现短暂错误输出。建议在关键路径加入寄存器锁存:
reg [3:0] sum_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) sum_r <= 4'd0; else sum_r <= sum; // 打一拍稳定输出 end这样即使前端有毛刺,也不会传到显示端。
✅ 3. IO资源优化
如果IO紧张,可以考虑:
- 多个数码管采用动态扫描,复用段码线;
- 或者只显示0~9,超出部分统一显示“E”表示溢出。
为什么这个项目值得每一个初学者动手做一遍?
因为它不只是“写代码”,而是完成了一次从抽象逻辑到物理世界的跨越:
| 层级 | 内容 |
|---|---|
| 算法层 | 二进制加法规则 |
| 逻辑层 | 全加器结构、布尔表达式 |
| 实现层 | Verilog模块化设计 |
| 物理层 | 引脚绑定、电平匹配、限流电阻 |
| 交互层 | 用户输入 → 实时反馈 |
这个闭环训练了真正的工程能力:你会开始思考“我的信号真的传过去了么?”、“为什么明明算对了却显示不对?”——这些才是成为合格FPGA工程师的关键历练。
结尾彩蛋:还能怎么玩?
掌握了基础之后,不妨试试这些扩展玩法:
- 把cin接到按键,实现带进位的手动加法;
- 增加第二个数码管,同时显示低位结果和进位(如 “15+1=0↑1”);
- 加入减法功能,通过控制信号切换运算模式;
- 用状态机轮流显示多个运算结果,做个简易计算器雏形。
每一次小小的改动,都在加深你对“硬件并行性”、“时序控制”、“人机接口”的理解。
所以,别等了——打开你的EDA工具,新建一个工程,写下第一行module吧。当那个“8”字第一次在数码管上亮起的时候,你就已经踏上了数字系统设计的大道。
有问题?欢迎在评论区贴出你的代码和现象,我们一起debug!