恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
FPGA数码管动态扫描驱动:从原理到实战,解决秒表显示难题
首页
资讯中心
/
FPGA数码管动态扫描驱动:从原理到实战,解决秒表显示难题
FPGA数码管动态扫描驱动:从原理到实战,解决秒表显示难题
发布时间:2026/8/27 3:38:44
1. 项目概述从秒表到数码管显示的工程化思考最近在整理FPGA的学习笔记翻到了之前做的一个秒表项目。这个项目本身不复杂但其中关于数码管显示驱动的部分却是一个非常好的、能串起多个FPGA基础概念的“麻雀虽小五脏俱全”的案例。很多人学FPGA一上来就搞复杂的算法或者通信协议结果在数码管、LED这种看似简单的外设上反而栽了跟头显示乱码、闪烁、亮度不均等问题层出不穷。今天我就把这个秒表电路的数码管显示部分单独拎出来掰开揉碎了讲清楚。这不仅仅是点亮几个数字那么简单它涉及到时钟分频、数据拆分、动态扫描、消隐处理等一系列FPGA设计中必须掌握的“内功”。这个“秒表电路之数码管显示部分”的核心任务是将一个代表时间的二进制数值比如一个32位的计数器值单位是毫秒实时、稳定、无闪烁地转换成人类可读的十进制数字并在多位七段数码管上显示出来。它就像一个数字世界的“翻译官”和“调度员”。你需要理解FPGA如何与这种共阳或共阴的数码管硬件打交道如何用有限的IO口驱动多位显示以及如何让这个显示过程既准确又不占用过多的逻辑资源。无论你是想做一个桌面电子钟、一个跑步计时器还是任何需要人机交互界面的设备这套显示驱动逻辑都是基石。接下来我会从设计思路、模块拆解、代码实现到调试心得完整地走一遍。2. 核心需求与方案选型解析2.1 秒表显示的功能性需求拆解首先我们要明确秒表需要显示什么。一个典型的秒表需要显示“分:秒:百分秒”或“分:秒:毫秒”这样的格式例如“01:23:45”表示1分23秒45百分秒。这决定了我们的显示模块需要处理的数据范围和格式。数据输入显示模块的输入通常是一个从零开始累加的计时值一般用一个足够宽的二进制计数器实现。例如如果我们以10毫秒为基本计时单位那么要显示最大99分59秒99需要的计数器位数需要能计到 (9960100 59*100 99) 599,999 个脉冲。一个20位的计数器最大1,048,576是足够的。更常见的做法是使用一个毫秒计数器范围更大。显示模块的输入就是这个二进制计数值。数据转换这是核心需求之一。FPGA内部是二进制世界而数码管显示的是十进制数字。因此我们必须将输入的二进制数BIN转换为十进制数的各个位BCD码这个过程称为“二进制转BCD”BIN to BCD。例如输入值123456十进制毫秒我们需要将其转换为“分”、“秒”、“百分秒”各自的十进制数字02分03秒45百分秒假设转换关系为分值/60000秒(值%60000)/1000百分秒(值%1000)/10这里仅为示例实际需根据计时单位调整。多位数码管驱动我们不可能为每一位数码管分配一组独立的7段控制线那会浪费大量IO资源。对于共阳数码管所有位的相同段如所有位的A段是连在一起的而每一位的公共阳极是独立的。驱动方式只能是“动态扫描”在极短的时间内比如每位数码管点亮1-5毫秒依次快速点亮每一位利用人眼的视觉暂留效应形成所有位同时点亮的视觉效果。2.2 硬件连接与驱动方案选择方案选型直接决定了代码结构和资源消耗。这里我们针对最常见的“共阳数码管”和“FPGA直接驱动”场景进行设计。硬件基础假设我们有6位7段共阳数码管。那么需要seg[6:0]7段信号线对应a, b, c, d, e, f, g段低电平有效即给0时该段LED点亮。dig[5:0]6位位选信号高电平有效即给1时选中该位数码管的公共阳极使其可能被点亮。驱动方案对比静态驱动每位数码管独立连接驱动芯片如74HC595FPGA通过串行通信发送数据。优点是编程简单显示稳定无闪烁。缺点是占用IO口或通信接口成本高。不适合IO资源紧张且位数多的场景。动态扫描驱动FPGA直接控制段选和位选。优点是极大节省IO口6位仅需7613个IO。缺点是软件逻辑稍复杂需要精确的时序控制否则会出现闪烁、亮度不均、重影等问题。我们的选择动态扫描驱动。这是FPGA学习中最经典、最必须掌握的IO扩展技能充分体现了用逻辑换资源的FPGA设计思想。扫描频率的考量扫描太快每位点亮时间太短亮度不足扫描太慢人眼会察觉到闪烁。通常扫描频率在60Hz以上即整个6位循环一遍的时间小于16.7ms。如果每位点亮2ms6位就是12ms扫描频率约83Hz是合适的。这需要一个专门的扫描时钟例如从系统主时钟分频得到500Hz的扫描时钟每位占时2ms。3. 模块化设计与关键子模块详解一个健壮的显示驱动模块应该进行分层设计。我习惯将其分为三个核心子模块数据转换模块bin2bcd、扫描控制模块scan_ctrl和段码译码模块seg_decoder。顶层模块负责将它们连接起来。3.1 数据转换模块二进制到BCD的“数学引擎”这个模块负责将输入的二进制计时值分解为对应数码管每一位要显示的十进制数字0-9。对于秒表我们需要做两次转换先整体转BCD再按时间格式截取。方案选择移位加3算法实现BIN到BCD的算法有很多如查找表、除法取余、移位加3算法等。在FPGA中移位加3算法因其纯粹的组合逻辑或流水线设计无需除法器资源消耗少且速度快成为最常用的选择。 其原理是从二进制数最高位开始逐位移入一个初始为0的BCD寄存器组。在每次移位前检查BCD寄存器中每个“十进制位”4位一组是否大于等于5如果是则将该位加3然后再进行移位。这个过程模拟了二进制数乘以2左移一位在十进制下的等效操作。对于秒表我们通常不需要将整个32位计数器值都转换成BCD而是先进行预计算。例如输入是毫秒数timer_ms[31:0]。计算分钟数minutes timer_ms / (60*1000);这可以通过乘以一个固定系数的倒数来实现或者用状态机做减法循环。在FPGA中对于固定的除数可以使用一个专用的除法模块IP核或优化的减法状态机。计算剩余秒毫秒remainder_ms timer_ms % (60*1000);计算秒数seconds remainder_ms / 1000;计算毫秒数milliseconds remainder_ms % 1000;将minutes(0-99),seconds(0-59),milliseconds(0-999)分别送入各自的bin2bcd模块得到各自的BCD码分钟可能需要2位BCD秒需要2位毫秒需要3位。注意除法操作在FPGA中比较消耗资源。在实际工程中如果系统时钟频率很高而计时值更新频率很低比如每秒更新一次显示我们可以选择在软件如CPU中完成这些计算然后将计算好的BCD结果直接送给显示模块。但在纯FPGA逻辑的秒表设计中我们通常用状态机实现减法循环来求商和余数虽然需要多个时钟周期但资源占用少。3.2 扫描控制模块精准的“时间片轮转调度器”这个模块是动态扫描的节拍器它产生两个核心信号位选信号dig_sel和当前显示位的数据索引data_index。扫描时钟生成通常从系统主时钟如50MHz分频得到一个几百Hz的扫描时钟scan_clk。例如50MHz / 100000 500Hz。每位显示时间2ms。// 简单的分频器示例 reg [16:0] scan_cnt; always (posedge clk_50m or posedge rst) begin if (rst) scan_cnt 0; else scan_cnt scan_cnt 1; end assign scan_clk scan_cnt[16]; // 50M / 2^17 ≈ 381Hz 周期约2.62ms位选计数器一个在scan_clk驱动下循环计数的计数器计数值从0到5对应6位数码管。reg [2:0] dig_index; // 0-5 always (posedge scan_clk or posedge rst) begin if (rst) dig_index 0; else if (dig_index 3‘d5) dig_index 0; else dig_index dig_index 1; end位选信号译码根据dig_index生成独热码格式的dig_sel信号。assign dig_sel (6b000001 dig_index); // 独热码当前位为1数据索引映射根据dig_index从总的数据缓冲区存放着6位BCD码中选择出当前需要显示的那一位数字。例如dig_index0对应最高位分钟十位dig_index5对应最低位毫秒个位。3.3 段码译码模块与消隐处理点亮正确的笔画这个模块功能简单但至关重要将4位的BCD码0-9转换为7段数码管各段的控制信号。基本译码这是一个查找表LUT可以用case语句实现。always (*) begin case(bcd_data) 4‘h0: seg 7’b1000000; // 0, 段a-g 共阳低有效 4‘h1: seg 7’b1111001; // 1 4‘h2: seg 7’b0100100; // 2 // ... 3-9 4‘h9: seg 7’b0000100; // 9 default: seg 7‘b1111111; // 全灭 endcase end关键技巧消隐处理这是避免“鬼影”的关键在切换位选信号的瞬间如果段选数据没有同步变化上一个位的段选信号可能会短暂地“泄露”到下一个位导致重影。解决方法在扫描时钟驱动下先关闭所有位选dig_sel全部置0然后更新段选信号seg为新位对应的数据最后再打开新位的位选。这个过程可以在一个扫描时钟周期内完成但需要精细的时序控制。一个更简单实用的方法是在段码译码器的输入前加入一个选择器当需要消隐时不输入有效的BCD数据而是输入一个特定的“全灭”码。 我们可以在扫描控制模块中产生一个非常短暂的消隐信号blank它在位选切换前后有效。在段码译码模块中wire [3:0] data_to_decode (blank) ? 4‘hf : current_bcd_data; // 假设4’hf译码为全灭 seg_decoder u_decoder(.bcd_data(data_to_decode), .seg(seg_out));这样在切换位选的瞬间段选信号输出的是全灭状态完美消除了鬼影。4. Verilog代码实现与仿真分析让我们把上述模块组合起来形成一个顶层的segment_display模块。4.1 顶层模块集成module segment_display #( parameter CLK_FREQ 50_000_000 // 输入时钟频率 50MHz )( input wire clk, // 系统主时钟 input wire rst_n, // 低电平复位 input wire [31:0] timer_value_ms, // 计时值单位毫秒 output reg [6:0] seg, // 7段控制信号 (a-g) output reg [5:0] dig // 6位位选信号 ); // 内部信号声明 wire scan_clk; // 扫描时钟约500Hz wire [3:0] bcd_data [5:0]; // 6位BCD码数据0-分钟十位1-分钟个位2-秒十位... wire [2:0] dig_index; wire blank; // 实例化分频模块 clk_div #( .DIV_RATIO(CLK_FREQ / 500) // 分频比产生500Hz扫描时钟 ) u_clk_div ( .clk_in(clk), .rst_n(rst_n), .clk_out(scan_clk) ); // 实例化数据预处理模块将timer_value_ms转换为6位BCD time_to_bcd u_time_conv ( .clk(clk), .rst_n(rst_n), .timer_ms(timer_value_ms), .bcd0(bcd_data[0]), // 分十位 .bcd1(bcd_data[1]), // 分个位 .bcd2(bcd_data[2]), // 秒十位 .bcd3(bcd_data[3]), // 秒个位 .bcd4(bcd_data[4]), // 百分秒十位 .bcd5(bcd_data[5]) // 百分秒个位 ); // 实例化扫描控制模块 scan_controller u_scan_ctrl ( .scan_clk(scan_clk), .rst_n(rst_n), .dig_index(dig_index), .blank(blank) ); // 根据当前位索引选择要显示的BCD数据 reg [3:0] current_bcd; always (*) begin case(dig_index) 3‘d0: current_bcd bcd_data[0]; 3’d1: current_bcd bcd_data[1]; 3‘d2: current_bcd bcd_data[2]; 3’d3: current_bcd bcd_data[3]; 3‘d4: current_bcd bcd_data[4]; 3’d5: current_bcd bcd_data[5]; default: current_bcd 4‘hf; endcase end // 段码译码并加入消隐控制 wire [6:0] seg_raw; seg_decoder u_seg_dec ( .bcd_data(blank ? 4‘hf : current_bcd), .seg(seg_raw) ); // 位选信号译码独热码 always (*) begin dig 6‘b000000; if (rst_n) begin // 复位期间全部关闭 case(dig_index) 3’d0: dig 6‘b000001; 3’d1: dig 6‘b000010; 3’d2: dig 6‘b000100; 3’d3: dig 6‘b001000; 3’d4: dig 6‘b010000; 3’d5: dig 6‘b100000; default: dig 6’b000000; endcase end end // 输出寄存器消除毛刺 always (posedge scan_clk or negedge rst_n) begin if (!rst_n) begin seg 7‘b1111111; // 共阳全灭 end else begin seg seg_raw; end end endmodule4.2 关键子模块代码片段扫描控制模块scan_controller:module scan_controller( input wire scan_clk, input wire rst_n, output reg [2:0] dig_index, output reg blank ); // 位索引计数器 always (posedge scan_clk or negedge rst_n) begin if (!rst_n) begin dig_index 3‘d0; end else begin if (dig_index 3’d5) dig_index 3‘d0; else dig_index dig_index 1; end end // 消隐信号生成在位选切换前后各一个scan_clk周期的高电平 // 这是一个简化的实现实际可能需要更精细的时序 reg [2:0] dig_index_dly; always (posedge scan_clk or negedge rst_n) begin if (!rst_n) begin dig_index_dly 0; end else begin dig_index_dly dig_index; end end assign blank (dig_index ! dig_index_dly); // 当索引变化时产生一个时钟周期的消隐脉冲 endmodule4.3 Testbench仿真与结果分析编写测试平台验证功能是否正确。重点观察当timer_value_ms变化时输出的bcd_data是否正确。dig信号是否按0-1-2-3-4-5-0...的顺序循环且每次只有一位为高。seg信号是否随着dig的变化而同步变化且在blank有效时是否为全灭码。是否存在明显的“鬼影”可以通过检查相邻位切换时seg和dig同时有效的瞬间来判断。仿真中可以给timer_value_ms一个固定值如123456毫秒观察转换后的BCD输出是否为02,03,45对应的各位。同时检查扫描周期是否符合预期6位 * 扫描时钟周期。5. 硬件调试中的常见问题与实战技巧代码在仿真中完美不代表上板就能亮。以下是我在多次项目中踩坑后总结的“避坑指南”。5.1 问题一数码管闪烁或亮度不均现象所有数码管看起来在快速闪烁或者有的位亮有的位暗。排查与解决扫描频率过低这是最常见原因。如果扫描频率低于50Hz人眼就能察觉到闪烁。用示波器测量scan_clk的频率确保在60-200Hz范围内。计算一下如果系统时钟是50MHz分频系数DIV_RATIO应为50e6 / (扫描频率 * 位数)。例如目标扫描频率100Hz6位数码管则每位频率600Hz分频系数≈83333。scan_clk的频率应是50e6 / 83333 ≈ 600Hz。位选信号驱动能力不足FPGA的IO口驱动电流有限通常几mA到几十mA。一位数码管全亮时8个LED7段小数点的电流可能超过单个IO的驱动能力。这会导致该位整体变暗甚至损坏IO口。解决方案必须使用三极管或专用驱动芯片如ULN2003用于共阳管驱动位选74HC573等来增强驱动能力。这是硬件设计问题代码无法解决。务必在原理图设计阶段就考虑好。消隐时间不足或过长消隐时间太短鬼影消除不彻底消隐时间太长会降低有效点亮时间导致亮度下降。通常消隐时间设置为扫描时钟的1/10到1/5个周期即可。5.2 问题二显示乱码或某些段不亮现象显示的数字不是预期的或者某个段在所有位上都不亮/常亮。排查与解决段码表错误共阳和共阴的段码表是相反的确认你的数码管是共阳还是共阴。我的代码示例是共阳、低有效。如果你的硬件是共阴那么段码需要取反位选信号的有效电平也可能相反。这是最容易出错的地方拿到数码管的第一时间用万用表二极管档测量确认类型。硬件连接错误检查FPGA引脚分配文件.xdc或.qsf确认seg[6:0]和dig[5:0]是否与原理图一一对应。经常会发生seg的顺序a-g与PCB布线顺序不一致的情况。译码逻辑错误检查seg_decoder模块的case语句确认0-9的段码是否正确。一个技巧是写一个简单的测试程序让数码管循环显示0-9看显示是否正确。电流限制电阻每个段都需要串联一个限流电阻通常330Ω-1kΩ。如果电阻值过大会导致段亮度不足如果某个电阻虚焊或损坏对应的段就不会亮。5.3 问题三显示内容更新迟钝或有拖影现象秒表计时变化时显示不是立即更新或者变化时能看到残留的上一帧图像。排查与解决数据更新与扫描不同步如果timer_value_ms在扫描显示的过程中更新可能会导致一位显示新数据下一位显示旧数据造成显示错乱。解决方法是在扫描控制模块中在每一帧扫描开始时例如dig_index回到0时从输入寄存器中锁存一次完整的显示数据bcd_data。确保一帧内6位显示完数据是稳定的。// 在scan_controller中增加帧同步信号 wire frame_start (dig_index 3‘d0); // 在顶层用frame_start锁存数据 always (posedge scan_clk) begin if (frame_start) bcd_data_latched bcd_data_from_converter; end // 显示使用bcd_data_latched消隐逻辑缺陷拖影鬼影的根本原因是消隐没做好。确保你的消隐信号blank在位选信号变化之前就有效并且持续到新位选稳定之后。用示波器双通道同时测量一个dig信号和对应的seg信号观察切换瞬间seg是否先变为全灭。5.4 高级优化与扩展技巧亮度调节PWM调光动态扫描的亮度可以通过控制每位点亮时间占空比来调节。例如在scan_clk周期内只用80%的时间打开位选剩下20%关闭整体亮度就会降低。这可以通过在dig输出前增加一个PWM比较器来实现。显示消隐与休眠当秒表未启动或需要省电时可以关闭扫描时钟或直接将所有dig置为0seg置为全灭码让数码管完全熄灭。显示小数点与冒号如果数码管带小数点DP段将其视为第8段seg[7]。在数据预处理时就需要决定哪一位需要点亮小数点。例如在秒和百分秒之间的冒号通常由两位数码管中间的两个独立LED实现需要单独分配两个IO口控制。资源优化如果FPGA资源紧张bin2bcd模块可以用多周期状态机实现而不是纯组合逻辑。扫描控制也可以用更简单的环形移位寄存器实现。调试数码管显示示波器或逻辑分析仪是必不可少的。重点观察三组信号的关系扫描时钟scan_clk、位选信号dig、段选信号seg。确保它们的时序关系符合设计预期。最后所有外设驱动都一样理解硬件原理是写出正确代码的前提。在动手写Verilog之前花点时间读懂数码管的数据手册和开发板的原理图能帮你省掉一大半的调试时间。