从点亮第一段开始:手把手教你用Quartus驱动七段数码管

你还记得第一次看到FPGA开发板上的数码管亮起时的心情吗?也许只是显示了一个“0”,但那一刻,你写的代码真正变成了看得见、摸得着的硬件行为。这种从逻辑到现实的跨越,正是数字电路最迷人的地方。

今天,我们就从最基础也最关键的实验—— 七段数码管驱动 讲起。别看它简单,背后却藏着组合逻辑设计、电平控制、动态扫描以及时序协调等一整套工程思维。通过Altera(现Intel)的Quartus II平台,我们将一步步把4位二进制数变成清晰可读的数字显示。


数码管不是“智能屏”:它只认高低电平

很多初学者以为数码管能“理解”数字,其实不然。它本质上就是七个LED排成“日”字形,每个段(a~g)独立受控。你要让它显示“3”,就得手动点亮 a、b、c、d、g 这五段。

这就引出了两个关键问题:

  1. 怎么决定哪几段该亮?
  2. 亮灭由什么电平触发?

答案取决于你的数码管类型:

  • 共阴极 :所有LED负极接地,只要给某一段加高电平(1),就亮;
  • 共阳极 :所有LED正极接电源,必须给某一段送低电平(0),才亮。

✅ 小贴士:开发板上常用的是共阴极,但一定要查手册确认!一旦搞反,你会看到“全灭”或“鬼影”。

所以,我们真正要做的,是一个“翻译器”——把输入的BCD码(比如 4'b0011 表示3)转换成对应的7位段选信号。这个过程,叫做 BCD-七段译码 。


写一个真正的译码器:不只是背表

虽然可以直接查真值表写输出,但我们更希望写出可综合、易维护的Verilog代码。下面这个模块,就是你在Quartus里可以编译运行的核心组件:

module bcd_to_7seg (
    input [3:0] bcd,
    output reg [6:0] seg
);

always @(*) begin
    case(bcd)
        4'd0:  seg = 7'b1111110;  // a-b-c-d-e-f
        4'd1:  seg = 7'b0110000;  //   b-c
        4'd2:  seg = 7'b1101101;  // a-b-d-e-g
        4'd3:  seg = 7'b1111001;  // a-b-c-d-g
        4'd4:  seg = 7'b0110011;  // b-c-f-g
        4'd5:  seg = 7'b1011011;  // a-c-d-f-g
        4'd6:  seg = 7'b1011111;  // a-c-d-e-f-g
        4'd7:  seg = 7'b1110000;  // a-b-c
        4'd8:  seg = 7'b1111111;  // a-b-c-d-e-f-g
        4'd9:  seg = 7'b1111011;  // a-b-c-f-g
        default: seg = 7'b0000000; // 其他情况熄灭
    endcase
end

endmodule

关键细节解析:

  • always @(*) 是组合逻辑的标准写法,确保无锁存器生成;
  • 输出 [6:0] 中, seg[6] 对应 a 段, seg[0] 是 g 段(按习惯定义);
  • 高电平有效 → 适用于 共阴极数码管 ;
  • default 分支防止未定义状态导致意外输出。

把这个文件加入Quartus工程后,你可以右键创建符号(Create Symbol Files),之后就能在原理图中调用它了。


多位显示怎么办?别让IO资源爆炸

假设你要做一个两位计数器,如果每个数码管都独占7根段线,总共需要 2×7 + 2 = 16 根IO。而FPGA的IO是宝贵的,尤其在小容量芯片上。

怎么办?用 动态扫描 ——利用人眼视觉暂留效应,快速轮换显示每一位。

动态扫描的本质:时间换空间

想象你在黑暗中快速挥动一支发光棒,看起来像一条连续的光带。同样地,如果我们每毫秒切换一次数码管:

  • 第1ms:显示十位数字,个位关闭;
  • 第2ms:显示个位数字,十位关闭;

只要刷新率高于50Hz(即每20ms内完成一轮),人眼就会认为两个数字是“同时”亮着的。

实现结构也很直观:

  • 所有数码管的 a~g 并联接到同一组IO口(共享段选);
  • 每个数码管的公共端(COM)单独控制(位选 DIG0、DIG1…);
  • FPGA内部用状态机或分频器控制切换节奏。

动态扫描控制器实战代码

下面是一个实用的双位动态扫描模块,支持外部输入两个BCD数据:

module scan_7seg (
    input clk,            // 主时钟(如50MHz)
    input [3:0] data0,    // 个位数据
    input [3:0] data1,    // 十位数据
    output reg [6:0] seg, // 段选输出(连接 a~g)
    output reg [1:0] dig  // 位选输出(低电平有效,DIG0=bit0, DIG1=bit1)
);

localparam SCAN_FREQ = 1000;         // 扫描频率:1kHz
localparam CNT_MAX = 50_000_000 / (2 * SCAN_FREQ) - 1;  // 50MHz分频

reg [15:0] counter;
reg sel;  // 当前选择哪一位

// 分频器:生成约1kHz的切换信号
always @(posedge clk) begin
    if (counter >= CNT_MAX) begin
        counter <= 0;
        sel <= ~sel;  // 每半周期翻转一次
    end else begin
        counter <= counter + 1;
    end
end

// 主控逻辑:根据sel选择输出哪个数字
always @(posedge clk) begin
    case(sel)
        1'b0: begin
            seg <= bcd_to_7seg(data0);  // 显示个位
            dig <= 2'b01;              // DIG0使能(低电平有效)
        end
        1'b1: begin
            seg <= bcd_to_7seg(data1);  // 显示十位
            dig <= 2'b10;              // DIG1使能
        end
    endcase
end

// 注意:这里调用的是前面定义的译码函数或实例化模块
// 若作为子模块使用,请先将其例化

endmodule

调试建议:

  • 初始阶段可用100Hz扫描频率测试,避免过快导致观察困难;
  • dig 输出为低电平有效,是因为多数开发板使用N-MOS驱动位选;
  • 如果出现“拖影”,说明扫描太慢或消隐不足,尝试提高频率至 >500Hz。

完整系统怎么搭?从计数器到显示

现在我们已经有了译码器和扫描器,怎么连起来做一个会自动递增的两位计数器呢?

顶层模块结构示意:

module top_counter_00_to_99 (
    input clk_50m,
    output [6:0] seg,
    output [1:0] dig
);

wire [3:0] count_tens;
wire [3:0] count_units;

// 四位计数器(个位)
counter_4bit u1 (.clk(clk_50m), .rst(0), .en(1), .q(count_units));

// 十位计数器:当个位满9且上升沿到来时进位
counter_4bit u2 (.clk(clk_50m), .rst(0), .en(count_units == 4'd9), .q(count_tens));

// 动态扫描控制器
scan_7seg u_scan (
    .clk(clk_50m),
    .data0(count_units),
    .data1(count_tens),
    .seg(seg),
    .dig(dig)
);

endmodule

其中 counter_4bit 是一个带使能端的基本计数器模块,每来一个时钟脉冲加1,到15后归零。


引脚分配与下载验证:最后一步不能错

在Quartus中完成编译后,进入 Pin Planner 进行物理引脚绑定。务必对照开发板原理图操作!

常见命名示例(以DE2-115或类似板为例):

信号 推荐引脚
seg[6] (a) PIN_AB28
seg[5] (b) PIN_AC28
… …
dig[0] PIN_G19
dig[1] PIN_H19

⚠️ 错误警示:若将段选与位选接反,可能导致多个数码管同时部分点亮,形成“重影”。

完成后生成 .sof 文件,通过USB-Blaster下载到FPGA。如果一切正常,你应该能看到数码管从 “00” 开始缓慢递增。


常见坑点与解决秘籍

问题现象 可能原因 解决方法
完全不亮 电源未供/位选无效 检查VCC/GND、dig是否拉低使能
显示乱码 译码逻辑与共阴/共阳不匹配 输出取反试试,或修改case值
只有一位亮 扫描逻辑卡死 查看sel是否翻转,仿真波形验证
闪烁明显 扫描频率太低 提高至 >100Hz,推荐500Hz~1kHz
出现“鬼影” 前一段未完全关闭 加入短暂消隐期,或优化驱动电路
某段亮度低 限流电阻过大 换用220Ω~470Ω标准阻值

工程思维升级:不只是点亮,更要设计

当你熟练掌握基础驱动后,不妨思考以下几个进阶方向:

  1. 参数化设计 :把位数做成参数 WIDTH ,支持任意位扩展;
  2. 支持小数点控制 :增加dp输出端,用于显示温度、电压等带小数的数据;
  3. 集成消隐功能 :在切换瞬间关闭所有段,减少串扰;
  4. 与Nios II联动 :作为软核处理器的输出终端;
  5. 加入按键接口 :实现可调时钟或计数暂停。

这些都不是遥不可及的功能,而是建立在你现在掌握的这套逻辑之上的自然延伸。


写在最后:每一个“简单”背后都有深度

七段数码管实验看似入门级,但它完整覆盖了现代数字系统开发的关键环节:

  • HDL建模 → 把数学逻辑转化为硬件行为;
  • 工具链操作 → 从Quartus到下载调试;
  • 硬件协同 → 考虑电平、驱动、时序匹配;
  • 系统集成 → 多模块协作完成整体功能。

这正是FPGA学习的魅力所在:你不需要一开始就做CPU或图像处理,只要能把一个“0”稳定清晰地显示出来,你就已经踏上了通往复杂系统的正确道路。

如果你正在做这个实验,或者刚刚踩过某个坑,欢迎留言分享你的调试经历。有时候,那一行不起眼的 default: seg = 7'b0000000; ,可能就是让你折腾半天的关键所在。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐