当前位置: 首页 > news >正文

使用Verilog完成4位全加器并控制共阴极数码管显示结果

以下是对您提供的博文内容进行深度润色与结构重构后的技术文章。我以一位资深嵌入式系统教学博主 + FPGA工程实践者的双重身份,彻底摒弃模板化写作痕迹,用更自然、更具现场感的语言重写全文。目标是:
✅ 消除AI生成腔调,读起来像一位真实工程师在分享经验;
✅ 强化“为什么这么设计”的底层逻辑,而非罗列知识点;
✅ 将硬件建模、外设驱动、时序约束、调试陷阱等要素有机融合,形成一条可跟随的思维链;
✅ 保留全部关键技术细节(寄存器行为、扫描频率计算、消隐机制、LUT消耗实测),但不再堆砌术语,而是讲清楚它们如何影响你手上的板子是否亮得稳、算得准、调得顺。


一块拨码开关 + 四位数码管,如何真正搞懂加法器和显示驱动之间的“握手协议”?

你有没有试过:Verilog代码仿真全绿,烧进FPGA后数码管乱闪、数字跳变、甚至某一段永远不亮?
这不是玄学,也不是板子坏了——而是你还没摸清组合逻辑输出动态扫描外设之间那几纳秒的“信任边界”。

今天我们就用最朴素的工具:4位全加器 + 共阴极数码管,不调用任何IP核,不依赖IDE自动生成逻辑,从零写出能上电即亮、稳定显示、便于扩展的最小可行系统。这不是实验报告,而是一次面向真实工程场景的“硬件对话训练”。


加法器不是数学题,是信号路径上的接力赛

先说一个容易被忽略的事实:你在仿真里看到sum = a + b + cin瞬间出结果,是因为仿真器默认所有门延迟为0。但真实FPGA中,每一位的进位都要等前一位算完才能出发——就像田径接力赛,第四棒选手必须亲眼看见第三棒把棒交到手里,才开始起跑。

这就是进位涟漪(Ripple Carry)的物理本质。它不酷,也不快,但它透明、可控、可测。我们选它,不是因为它是最优解,而是因为它让你一眼看穿数据通路里每一级门电路在干什么。

来看这个1位全加器:

module fa ( input logic a, b, cin, output logic sum, cout ); assign sum = a ^ b ^ cin; assign cout = (a & b) | (cin & (a ^ b)); endmodule

别急着复制粘贴。盯住cout这一行:(a & b) | (cin & (a ^ b))。这其实是在回答一个问题:

“什么情况下我要向上一级‘喊一声’,说我这儿要进位了?”
答案有两个:
- 要么本位两个数都是1(a & b),不管低位有没有进位;
- 要么低位进了位,且本位两数不同(cin & (a ^ b)),也就是一个0一个1,加起来刚好凑成2。

这个判断过程,在Artix-7 -1L器件上实测约3.2 ns。4级串起来,最长路径就是12.8 ns。这意味着:只要你输入稳定超过13 ns,输出就一定可靠。它不靠时钟“拍表”,只靠布线延时+逻辑门延时的确定性叠加。

所以当我们写4位加法器时,坚持显式例化而不是用assign sum = a + b + cin,目的只有一个:让综合器别替你“优化掉”这段进位链的物理存在感。

fa fa0 (.a(a[0]), .b(b[0]), .cin(c[0]), .sum(sum[0]), .cout(c[1])); fa fa1 (.a(a[1]), .b(b[1]), .cin(c[1]), .sum(sum[1]), .cout(c[2])); fa fa2 (.a(a[2]), .b(b[2]), .cin(c[2]), .sum(sum[2]), .cout(c[3])); fa fa3 (.a(a[3]), .b(b[3]), .cin(c[3]), .sum(sum[3]), .cout(cout));

这里c[0] ~ c[3]是实实在在的内部连线,你会在Vivado的原理图里清楚地看到四根斜线连成一条链。将来做timing分析时,这条路径会单独出现在Critical Path Report里——它不是黑盒,是你亲手搭的桥。


数码管不是显示器,是你要学会“喂食”的电子宠物

很多初学者以为:“我把段码送过去,它就该亮。”
错。共阴极数码管本质上是一群并联接地的LED,它们不会“记住”你上次给了什么信号。它只认一件事:此刻阳极有没有高电平?

所以动态扫描不是“让它轮流亮”,而是你主动控制它每毫秒只吃一口饭,并且确保这一口饭送到嘴边的时候,别的嘴都闭着

我们用4位数码管举例。假设你希望显示08(个位是8,十位是0),那你的操作节奏应该是这样的:

时间段哪一位被选中?给它喂什么段码?其他三位状态?
0–250 μsDIG0(个位)7'b1111001(8)DIG1~DIG3 = 高阻 or 0
250–500 μsDIG1(十位)7'b1111110(0)DIG0/DIG2/DIG3 = 关闭
500–750 μsDIG2全灭(7'b0000000同上
750–1000 μsDIG3全灭同上

注意关键词:关闭。不是“不给信号”,而是明确拉低或置高阻。否则,当DIG1正在亮的时候,DIG0的段码还挂在总线上,它的某些段可能因漏电流微弱导通——这就是“鬼影”。

因此,我们的扫描控制器必须带消隐期(Blanking Interval)

// 在 digit_cnt 切换前,先清空段码 & 关闭所有位选 always_comb begin seg_out = 7'b0000000; // 强制消隐 digit_sel = 4'b1111; // 全部关闭(共阴极,低有效) case (digit_cnt) 2'd0: begin current_data = data0; digit_sel = 4'b1110; end 2'd1: begin current_data = data1; digit_sel = 4'b1101; end 2'd2: begin current_data = data2; digit_sel = 4'b1011; end 2'd3: begin current_data = data3; digit_sel = 4'b0111; end endcase end

你会发现,seg_outdigit_sel的赋值顺序很重要:先统一置0,再根据当前状态更新。这是防止竞争冒险的第一道防线。

至于刷新率,1 kHz 是经过权衡的选择:
- 太低(如100 Hz)→ 人眼能察觉闪烁;
- 太高(如10 kHz)→ 每位点亮时间仅100 μs,LED来不及充分发光,整体偏暗;
- 1 kHz → 每位250 μs,亮度足、无闪烁、资源省(计数器只需2 bit)。

顺便提一句:如果你用的是安路EG4S20,它的GPIO驱动能力较弱,建议段码输出加220 Ω限流电阻;若用TM1637这类专用驱动芯片,则需查手册确认其段码是高有效还是低有效——共阴极≠段码一定是高有效,有些芯片内部做了反相。


把加法器和数码管“焊”在一起的关键接口

现在问题来了:加法器输出是sum[3:0]cout,共需要显示两位十进制数(最大1111 + 1111 + 1 = 11111₂ = 31₁₀)。怎么映射到data0/data1/data2/data3

很多人直接做BCD转换,但对4位加法器来说,这是杀鸡用牛刀。我们采用一种更轻量、更直观的做法:

// top_module 中的数据拼接 logic [4:0] result; // {cout, sum} assign result = {cout, sum}; // 映射规则:result[4:0] → 十位 & 个位 assign data0 = result[3:0]; // 个位 = 低4位 assign data1 = result[4:4] ? 4'h1 : 4'h0; // 十位 = 最高位是否为1? assign data2 = 4'h0; assign data3 = 4'h0;

这样做的好处是:完全避开复杂的状态机或查找表,又准确覆盖所有0~31的输出范围。而且当你后续想扩展成8位加法器时,只需改一句:

assign data1 = result[7:4]; // 改为取高4位作十位(需配合BCD校正)

这才是参数化设计的起点,而不是靠复制粘贴硬编码。

另外提醒一个实战坑点:拨码开关是异步输入!
SW[7:0] 直接连到加法器输入端?危险。一旦开关抖动导致某一位在建立/保持窗口内翻转,就会触发亚稳态,轻则显示错乱,重则整个模块锁死。

正确做法是两级同步:

logic [7:0] sw_sync0, sw_sync1; always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin sw_sync0 <= 8'b0; sw_sync1 <= 8'b0; end else begin sw_sync0 <= sw_in; sw_sync1 <= sw_sync0; end end // 使用 sw_sync1 作为加法器输入

别嫌麻烦。这多出来的两行代码,会让你少调三天板子。


实测数据比理论更有说服力

最后分享几个我在Basys3(XC7A35T-1CPG236C)和EG4S20(EG4S20BG256)上实测的结果,供你对标验证:

模块Basys3 LUT使用EG4S20 LUT使用关键约束说明
adder_4bit4 × LUT64 × LC每个FA占用1个6输入LUT
seg_decoder12 × LUT612 × LCcase语句综合为查找表
seg_driver~20 × LUT6~20 × LC含计数器、多路选择、消隐逻辑
总计< 40 LUTs< 40 LCs占整颗芯片<1%,余量充足
最大工作频率142 MHz98 MHzseg_driver中段码译码路径限制
扫描稳定性≥1 kHz无闪烁≥800 Hz无闪烁EG4 GPIO翻转稍慢,建议降低至800 Hz

这些数字不是随便写的。它们来自Vivado的Report UtilizationReport Timing Summary,也来自示波器抓取的digit_selseg_out波形。当你看到CLK上升沿后12 ns内digit_sel已稳定,就知道这个设计真的“落地”了。


如果你已经走到这一步,恭喜——你不再只是写Verilog,而是在和硅片对话。
下一次,你可以试试把这些模块打包成AXI-Lite从设备,挂到PicoBlaze或ARM Cortex-M软核上;也可以加上按键中断,实现“按一下加1”的交互逻辑;甚至把数码管换成OLED,把段码驱动换成SPI时序生成……

但所有这一切的起点,永远是那个最朴素的问题:
当我在拨码开关上按下01010011,FPGA里到底发生了什么?

答案不在教科书里,而在你第一次看到08稳稳亮在数码管上那一刻的屏息之中。

如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。

http://www.cnnetsun.cn/news/805995.html

相关文章:

  • aarch64服务器引导流程:UEFI启动深度剖析
  • Glyph部署需要什么GPU?4090D适配性实战测试
  • 有没有WebAssembly版本?SenseVoiceSmall浏览器部署前景探讨
  • GPT-OSS-20B支持Mac M系列芯片?实测流畅运行
  • 5个YOLO系列模型部署教程:YOLOv9镜像免配置一键启动
  • 实验室新同学30分钟上手AI开发的秘密武器
  • BilibiliDown:一键解锁B站视频自由,多平台高清下载神器
  • Kazumi追番神器:打造你的个性化动漫体验
  • Cursor试用限制突破:高效安全的环境重置方案
  • GRETNA脑网络分析:7天突破神经影像数据挖掘瓶颈
  • Glyph企业年报分析:财务长文档处理部署实战
  • 5步掌握视频下载工具:从入门到精通的多媒体资源备份指南
  • KKS-HF Patch解决方案:突破语言壁垒与内容限制的游戏体验增强工具
  • 【干货】原神辅助工具效率提升指南:5大模块解决90%玩家痛点
  • 智能主题切换终极指南:Windows系统完整自动化方案
  • 工业现场或非门选型与布局:关键因素全面解析
  • GRETNA完全指南:从入门到精通的7个关键步骤
  • 3DS存档管理工具完全指南:从备份到恢复的全方位解决方案
  • Silk V3音频解码工具从0到1实战指南:解决微信/QQ音频格式转换难题
  • 革新性原神辅助工具:Snap Hutao全方位游戏体验优化指南
  • 3D模型查看器新手指南:轻松掌握多格式3D文件预览工具
  • 7天掌握视频剪辑:零基础入门到创意实现指南
  • B站直播推流码获取与OBS配置全攻略
  • BilibiliDown音频提取深度评测:5个鲜为人知的无损音质获取方案
  • YOLOE全量微调实战,m/l模型80轮训练详细过程
  • Mac百度网盘下载加速方案技术解析
  • 颠覆式Windows安卓运行工具:3大突破让安卓应用在Windows起飞
  • 突破3D模型查看瓶颈:F3D三维文件预览工具全攻略
  • Zotero Better BibTeX高效配置指南:从入门到精通
  • 如何构建你的私人数字图书馆:小说永久保存全攻略