ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

VHDL秒表全流程:从时钟分频、按键消抖到数码管动态扫描

2026/9/17 16:24:45 拓冰建站 浏览量
VHDL秒表全流程:从时钟分频、按键消抖到数码管动态扫描 简介这份PDF是一份EDA课程设计报告主题为基于VHDL语言的数字秒表设计面向电子信息工程、通信工程等专业的本科生与初学FPGA/CPLD开发的读者可用于课程设计写作参考、VHDL入门练习或Quartus II工具上手。报告以自顶向下的设计思路组织涵盖VHDL语言基础与程序结构、Quartus II 6.0开发环境及完整设计流程、FPGA等可编程逻辑器件简介并给出时基分频、控制状态机、计时与显示等模块的源程序与系统原理框图要求实现0秒至59分59.99秒计时、复位与启停开关控制计时满59分59秒后蜂鸣器响6声。压缩包内为1个PDF文件约447KB篇幅完整、结构清晰便于直接查阅与模仿仿写。目前已有293人学习下载适合需要快速搭建秒表设计框架、理清模块划分与仿真下载步骤的读者参考。1. 一块 CPLD 加几根数码管VHDL 秒表能做到什么很多人第一次接触 EDA 课程设计拿到的题目就是用 VHDL 做一个秒表。听上去简单——计时、显示、按键控制三件事。但真把代码写进 Quartus 或 Vivado 里综合才会发现坑全藏在时序里50 MHz 的板级时钟怎么变成稳定的 1/100 秒基准机械按键抖动能吃掉多少个计数脉冲六位数码管动态扫描到第几位时人眼才不觉得闪。这个题目的价值不在秒表本身而在于它把数字系统设计里最核心的几件事压缩到了一个能在一周内做完的规模时钟分频、同步状态机、按键消抖、BCD 计数、扫描显示、引脚约束。做完一遍你对 EDA 工具链从 RTL 到比特流的每一步都会有实感。适合刚学完 VHDL 语法、想找一个完整项目练手的在校生也适合需要重新捡起 HDL 的老工程师——毕竟 Verilog 和 VHDL 之间的思维切换用秒表来热身刚刚好。2. VHDL 秒表的三根支柱分频、消抖与 BCD 计数链2.1 为什么不能直接拿 50 MHz 当计时基准最直觉的写法是用clkevent and clk1直接对 50 MHz 计数计到 50_000_000 就进位 1 秒。逻辑上没错但这样写出来的计数器位宽要到 26 位以上而且每一位都要参与比较综合出来的组合逻辑延迟会拖累 Fmax。更常见的做法是分两级先把系统时钟分频出一个 kHz 级的使能脉冲后面的计时逻辑全部用这个使能信号驱动而不是真的去生成一个新的低频时钟。用使能脉冲而不是分频时钟好处是整块设计只有一个时钟域静态时序分析只针对一条时钟路径省掉了跨时钟域同步的麻烦。下面是一段可直接综合的分频代码输出的是单周期宽度的使能信号。-- 50 MHz - 1 kHz 使能脉冲占空比 1/50000 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity clk_en is port ( clk : in std_logic; -- 50 MHz 板级时钟 rst_n : in std_logic; -- 低电平异步复位 en_1k : out std_logic -- 1 kHz 单周期使能 ); end entity; architecture rtl of clk_en is constant CNT_MAX : integer : 50_000; -- 50 MHz / 1 kHz signal cnt : integer range 0 to CNT_MAX-1 : 0; begin process(clk, rst_n) begin if rst_n 0 then cnt 0; en_1k 0; elsif rising_edge(clk) then if cnt CNT_MAX-1 then cnt 0; en_1k 1; -- 只拉高一个周期 else cnt cnt 1; en_1k 0; end if; end if; end process; end architecture;CNT_MAX是唯一需要按板子改的参数。如果开发板是 24 MHz 晶振把它改成 24_000要 1/100 秒的基准就把分频比再降一个数量级用 1 kHz 使能去驱动一个 0~99 的计数器即可。注意en_1k是使能而不是时钟下游模块必须在rising_edge(clk)里判断if en_1k 1 then不要写成rising_edge(en_1k)。2.2 按键消抖20 ms 阈值是怎么定出来的机械按键在按下和弹起瞬间会产生 5~10 ms 的抖动毛刺。如果直接把这根线接到状态机上一次按键会被识别成几十次跳变秒表要么瞬间清零要么疯狂启停。消抖思路有硬件 RC 和软件计数两种VHDL 里通常用后者连续采样到同一电平超过一定时间才认为按键状态真正改变。阈值取多少要算一下。人手指最快连续点击间隔大约 50 ms抖动窗口一般不超过 10 ms。取 20 ms 作为判定门限既能滤掉抖动又不会让响应变迟钝。在 1 kHz 使能下20 ms 就是 20 个使能脉冲。-- 20 ms 按键消抖cnt_max 20 * (50 MHz / 1 kHz) architecture rtl of debounce is constant CNT_MAX : integer : 20; -- 20 个 1 kHz 脉冲 signal cnt : integer range 0 to CNT_MAX : 0; signal key_sync : std_logic_vector(1 downto 0); signal key_stable : std_logic : 1; -- 默认高电平按键常开 begin -- 双触发器同步避免亚稳态 process(clk, rst_n) begin if rst_n 0 then key_sync (others 1); elsif rising_edge(clk) then key_sync key_sync(0) key_in; end if; end process; process(clk, rst_n) begin if rst_n 0 then cnt 0; key_stable 1; elsif rising_edge(clk) then if en_1k 1 then if key_sync(1) / key_stable then if cnt CNT_MAX then key_stable key_sync(1); -- 稳定 20 ms 才更新 cnt 0; else cnt cnt 1; end if; else cnt 0; -- 中途跳变重新计数 end if; end if; end if; end process; end architecture;这里有两个容易被忽略的点。一是key_sync双触发器同步链机械按键接在芯片引脚上属于异步输入不做同步在时序分析里会被标成亚稳态风险。二是cnt在电平稳定时清零而不是只在边沿清零否则持续按住键会让计数器溢出。2.3 BCD 计数链从 1/100 秒到分钟的进位显示需要的是十进制数字直接用二进制计数器再转 BCD 会多一层组合逻辑。常见的做法是每一位都用一个 0~9 的计数器计到 9 时归零并向高位产生进位。秒表通常需要 6 位分十位、分个位、秒十位、秒个位、1/100 秒十位、1/100 秒个位。计数位计数范围进位条件对应显示位1/100 秒个位0~9计到 9最右数码管1/100 秒十位0~9计到 9右二数码管秒个位0~9计到 9右三数码管秒十位0~5计到 5右四数码管分个位0~9计到 9右五数码管分十位0~5计到 5最左数码管秒和分的十位只能到 5这是秒表常见的 59 分 59 秒 99 封顶格式。实现上可以把 1/100 秒的两位合成一个 0~99 计数器再用它同时产生秒进位代码量会小一些但显示译码时需要做二进制到 BCD 的转换。哪种更划算取决于你用的 FPGA 片上资源LUT 紧张就合并寄存器紧张就分开。3. 秒表控制状态机与数码管动态扫描的 VHDL 落地3.1 三态控制器的状态定义与迁移条件秒表至少要有三种行为正常运行、暂停、清零。用 Moore 型状态机最稳输出只跟当前状态有关波形看着也干净。三个状态分别是 IDLE初始显示全零、RUN计数、HOLD冻结显示内部计数保留。start_pause 键在 RUN 和 HOLD 之间切换reset 键从任意状态回到 IDLE 并把所有 BCD 计数器归零。-- 秒表控制器三态 Moore 状态机 type state_t is (IDLE, RUN, HOLD); signal state, next_state : state_t; process(clk, rst_n) begin if rst_n 0 then state IDLE; elsif rising_edge(clk) then if en_1k 1 then state next_state; end if; end if; end process; process(state, key_start, key_rst) begin next_state state; case state is when IDLE cnt_en 0; if key_start 0 then -- 按下启动 next_state RUN; end if; when RUN cnt_en 1; if key_start 0 then next_state HOLD; end if; when HOLD cnt_en 0; if key_start 0 then next_state RUN; end if; when others next_state IDLE; end case; end process;cnt_en是给 BCD 计数链的计数使能只有在 RUN 状态下才为高。这里要特别注意按键检测的边沿问题key_start是消抖后的电平信号如果直接用电平判断按住不放会在一个状态里反复触发迁移。稳妥的做法是再对消抖输出做一次边沿检测用key_start的下降沿作为按下事件。否则从 RUN 切到 HOLD 后按键还没松开下一次en_1k又会把它切回 RUN。3.2 数码管动态扫描的刷新率与位选译码六位数码管如果每位都单独驱动需要 6×7 42 根 IO大多数课程板都不够用。动态扫描把所有段的公共端并联用 6 根位选线轮流点亮利用人眼视觉暂留形成同时亮的错觉。刷新率不能太低低于 50 Hz 会明显闪烁也不能太高太高则每位点亮时间太短、亮度不足。常见取值是每位点亮 1 ms六位一轮 6 ms刷新率约 166 Hz。扫描频率每位点亮时间六位一轮周期视觉效果1 kHz166 us1 ms亮度偏低但无闪500 Hz333 us2 ms亮度与刷新平衡点200 Hz833 us5 ms亮度足部分人可见微闪100 Hz1.67 ms10 ms亮度高边缘可见闪烁我一般取 500 Hz 到 1 kHz 之间具体看数码管是共阴还是共阳、限流电阻多大。位选译码用一个 3 位计数器从 0 数到 5配合 3-8 译码器选通对应位。-- 动态扫描1 kHz 使能下依次选通 6 位 signal scan_cnt : integer range 0 to 5 : 0; signal seg_data : std_logic_vector(6 downto 0); process(clk, rst_n) begin if rst_n 0 then scan_cnt 0; elsif rising_edge(clk) then if en_1k 1 then if scan_cnt 5 then scan_cnt 0; else scan_cnt scan_cnt 1; end if; end if; end if; end process; -- 根据扫描位置选择对应的 BCD 数字送译码 with scan_cnt select seg_data seg_dec(bcd_5) when 0, -- 分十位 seg_dec(bcd_4) when 1, seg_dec(bcd_3) when 2, seg_dec(bcd_2) when 3, seg_dec(bcd_1) when 4, seg_dec(bcd_0) when others;seg_dec是一个纯组合函数把 0~9 的 BCD 码翻译成七段码。共阴和共阳的段码表正好相反写的时候先翻一下板子的原理图。很多板子段码线是 a 到 g 的顺序但也有 g 到 a 反着接的综合前先在约束文件里对好顺序再烧录比事后飞线省事。4. 从 ModelSim 波形到板级引脚VHDL 秒表的仿真与综合验证4.1 Testbench 怎么写才能看出计数进位写完 RTL 直接上板是最费时间的做法因为一旦显示不对你没法区分是计数逻辑错了还是引脚接错了。标准流程是先跑仿真用 ModelSim 或 Vivado 自带的仿真器都能做。Testbench 不需要真实时钟的频率可以把CNT_MAX参数改小比如改成 50让仿真在几百个周期内就跑完一秒。-- 简化版 testbench把分频比调小加快仿真 library ieee; use ieee.std_logic_1164.all; entity tb_stopwatch is end entity; architecture sim of tb_stopwatch is signal clk : std_logic : 0; signal rst_n : std_logic : 0; signal key_s : std_logic : 1; signal seg : std_logic_vector(6 downto 0); signal sel : std_logic_vector(5 downto 0); constant T : time : 20 ns; -- 50 MHz begin clk not clk after T/2; -- 自由振荡时钟 dut: entity work.stopwatch_top port map ( clk clk, rst_n rst_n, key_start key_s, seg seg, sel sel ); stim: process begin wait for 100 ns; rst_n 1; wait for 1 us; key_s 0; -- 模拟按下启动 wait for 1 ms; key_s 1; wait for 500 ms; -- 观察计数进位 key_s 0; -- 暂停 wait for 1 ms; key_s 1; wait; end process; end architecture;仿真时重点看三件事cnt_en是否只在 RUN 状态下为高进位信号是否在 9 到 0 的瞬间产生一个单周期脉冲扫描计数器是否稳定在 0~5 循环。如果进位脉冲宽度超过一个时钟周期说明是组合逻辑直接赋值而不是在时钟沿里更新的这种写法在实际板上会造成高位多计。4.2 引脚约束与综合报告里该看什么仿真过了接下来是综合和引脚分配。Quartus 用 QSF 文件Vivado 用 XDC 文件本质都是把 RTL 里的端口名映射到芯片的物理引脚编号。# Quartus QSF 片段时钟、复位、按键、段选、位选 set_location_assignment PIN_E1 -to clk set_location_assignment PIN_M1 -to rst_n set_location_assignment PIN_J15 -to key_start set_location_assignment PIN_A3 -to seg[0] # ... seg[1] 到 seg[6] 依原理图补齐 set_location_assignment PIN_C14 -to sel[0] # ... sel[1] 到 sel[5] 依原理图补齐引脚分配完成后不要急着烧录先看综合报告里的几项数字Logic Elements 占用率、寄存器数量、最高时钟频率 Fmax。一个秒表设计在 Cyclone IV 这类器件上通常占几百个 LEFmax 能跑到 150 MHz 以上。如果 Fmax 只有几十兆多半是某处组合逻辑太长常见的是七段译码用了多层 if-elsif 嵌套改成 case 或者查表会明显改善。4.3 从 RTL 到 PCBEDA 工具链的衔接与焊盘对应VHDL 秒表最终要落到一块实际的板子上这就涉及原理图和 PCB。用嘉立创 EDA 或者立创 EDA 画原理图时FPGA/CPLD 芯片的引脚编号必须和 QSF/XDC 里的分配一一对应否则综合报告全绿板子上却只有半个数码管亮。这里有个高频踩坑点原理图符号的引脚号和你约束文件里写的引脚号如果对不上连线逻辑上没问题但物理上错位排查起来很费时间。绘制完原理图后用工具自带的设计规则检查把网络和 PCB 联动一遍确认每个网络都真正连到了封装焊盘上能省掉大量飞线调试。在 PCB 布局上秒表这类低速数字电路不复杂但数码管的段选线往往要走十几厘米段电流在几百微安到几毫安之间走线过细会造成亮度不均。位选线因为要轮流通过较大电流线宽可以比段选线粗一些。晶振尽量靠近 FPGA 的时钟输入引脚走线包地处理避免时钟毛刺被计数逻辑当成有效沿。5. 精度校准与资源取舍让 VHDL 秒表跑得更稳的几个技巧5.1 1/100 秒误差的两个来源秒表做出来之后很多人会发现跑十几分钟就比手机慢一两秒。误差主要来自两块一是晶振本身的频率偏差普通无源晶振的精度在 ±20 ppm 到 ±50 ppm 之间跑一小时累积误差在 0.1 秒量级二是分频器取整带来的系统性偏差如果你用的晶振频率不能被 100 整除每个 1/100 秒都会丢掉或漏掉几个时钟周期。对第二类误差可以在分频时做补偿。比如晶振是 24 MHz要得到 1/100 秒需要 240_000 个周期这个数整除得刚刚好没有误差。但如果晶振是 25 MHz240_000 除不尽这时可以用小数分频的思路大部分周期按 240_000 计每隔若干次补一个周期把平均频率拉回来。-- 25 MHz 晶振下用 250_000 分频得到 100 Hz -- 每 100 次分频中有 96 次按 250_000 计4 次按 249_999 计 constant BASE : integer : 250_000; signal acc : integer range 0 to 99 : 0; -- 补偿计数器 signal limit : integer range 249_998 to 250_000 : BASE;这种做法的本质是用时间平均换取瞬时精度对秒表这种长时间累积的应用很合适。如果对短期一致性要求高比如要做反应测试那还是选频率能被整除的晶振更省心。5.2 资源紧张时先砍哪里课程板上 FPGA 资源往往很有限如果综合报资源不足优化顺序建议是这样先看七段译码是不是每个位都实例化了一份改成一份译码器加输出多路选择能省掉不少 LUT再看 BCD 计数器能不能合并把 1/100 秒的两位合成一个 0~99 的二进制计数器再转 BCD 输出最后才考虑降低扫描位数比如只显示到 1/10 秒用五位数码管。还有一个容易被忽略的资源浪费复位信号的扇出。如果每个寄存器都单独接一个异步复位布线资源会被吃掉一大块。改成同步复位或者复位信号加一级缓冲综合工具更容易做时序收敛Fmax 通常能提升 10% 以上。提示每次改完 RTL 都要重新跑一次仿真尤其是涉及计数进位和状态迁移的改动。板上调试一分钟仿真里可能只要一秒。5.3 用 SignalTap 抓一次真实计数波形仿真环境再干净板子上仍然会有仿真里看不到的东西比如按键抖动波形和实际时钟质量。Quartus 的 SignalTap II 或者 Vivado 的 ILA 可以把片上信号引出来观察对于秒表这种低频设计非常划算。抓取时设置触发条件为进位信号上升沿采样深度设到 1024 就够看十几个进位周期了。如果发现进位脉冲宽度不均匀问题一定出在分频使能的生成逻辑上而不是计数链本身。把 SignalTap 抓到的波形和 ModelSim 里的仿真波形对照一遍两边一致了这个 VHDL 秒表才算真正做完。本文还有配套的精品资源点击获取