ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

VHDL秒表设计实战:时钟使能、BCD计数与数码管动态扫描

VHDL秒表设计实战:时钟使能、BCD计数与数码管动态扫描 简介这份PDF是一份基于VHDL语言的数字秒表EDA课程设计文档面向电子信息工程、集成电路等专业的本科学生与EDA初学者适合课程设计、毕业设计选题及Quartus II入门实践参考。文档围绕0秒至59分59.99秒的计时范围完整给出自顶向下的设计方案时基分频模块、控制状态机、六十进制与一百进制计数计时模块、七段译码显示与扫描电路并设计了复位与启停开关计时达到59分59秒后蜂鸣器报警六声。正文包含VHDL语言与可编程逻辑器件FPGA的基础介绍、Quartus II 6.0开发流程、系统组成框图与各模块源程序节选可帮助读者理解时钟脉冲分频、状态控制与数码管动态显示的实现思路。资源包为单一PDF文件共1个文件约447KB篇幅紧凑便于打印或对照阅读。目前已有293人学习适合需要一份可参照的秒表设计结构与接口划分说明的读者。1. 从一块六位共阳数码管说起VHDL 秒表在 EDA 里要解决什么最常见的开场是这样实验室发下一块 FPGA 开发板板载 50MHz 晶振、六位共阳数码管、几个独立按键任务是用 VHDL 做一个秒表显示分、秒、百分秒按一下启动、再按一下暂停、第三个键清零。这个标题里的 EDA 不是画板子那条链路——嘉立创 EDA 处理的是原理图、封装、走线和星型接地VHDL 秒表走的 EDA 流程是设计输入、行为仿真、综合映射、时序约束、引脚分配、下载上板验证。两件事共享EDA三个字母工具链却完全不搭界第一节课就容易跑错方向。适合读这篇的人分三类正在做数字逻辑课程设计的学生准备电子类设计赛项里数码管计时题目的同学以及第一次用 Quartus 或 Vivado 碰 VHDL 的工程师。核心矛盾只有一个——把看起来只是数数的功能拆成几个互不打架的进程同时保证仿真里对、综合后仍然对、烧到板子上还是对。2. VHDL 秒表的分频、BCD 计数与七段译码三个模块怎么切把秒表整块写完再调试是新手最常见的自找麻烦。合理做法是先按功能切成三层时基产生、计数链、显示驱动。三层之间只通过使能脉冲和BCD 码两类信号通信任何一层出问题都能用仿真单独定位。下面按这个切法逐层展开每一层都能单独写 Testbench、单独综合。2.1 为什么用使能脉冲分频而不是把分频出来的信号当新时钟50MHz 晶振要变成 100Hz 的计时基准直觉做法是写一个计数器计到 250000 时把输出翻转然后拿这个输出当时钟去驱动后面的进程。这条路在仿真里往往能跑在板子上就是灾难分频出来的信号没有进入全局时钟网络走的是普通布线资源偏斜大、扇出差静态时序分析还会报一堆未约束路径。行业里通用的写法叫时钟使能clock enable全程只有一个真实时钟clk分频器只负责每隔 N 个周期拉高一次en所有下游进程都在rising_edge(clk)里判断if en 1 then。-- 时钟使能发生器50MHz 输入输出周期性单周期高脉冲 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity clk_en is generic ( CLK_FREQ : integer : 50_000_000; -- 板载晶振频率按实际板卡改 EN_FREQ : integer : 100 -- 输出使能频率100Hz 对应 10ms 基准 ); port ( clk : in std_logic; rst_n : in std_logic; en : out std_logic ); end entity; architecture rtl of clk_en is constant DIV : integer : CLK_FREQ / EN_FREQ; -- 分频比必须整除 signal cnt : integer range 0 to DIV - 1 : 0; begin process(clk, rst_n) begin if rst_n 0 then cnt 0; en 0; elsif rising_edge(clk) then if cnt DIV - 1 then cnt 0; en 1; -- 只高一个时钟周期下游当脉冲用 else cnt cnt 1; en 0; end if; end if; end process; end architecture;逻辑说明cnt从 0 数到DIV-1归零在归零那一拍把en拉高一个周期。参数说明CLK_FREQ和EN_FREQ用 generic 暴露出来仿真时把两个值同时缩小比如 1000 和 10就能在几微秒里看到完整的使能周期不用真的等 10ms。注意DIV是常数除法综合时会直接算成常量但前提是CLK_FREQ能被EN_FREQ整除不整除时 Quartus 和 Vivado 都会给出截断警告而这个截断正是走时越跑越偏的根源之一。2.2 四位 BCD 计数器的进位链与时分秒级联百分秒是个 0 到 99 的十进制计数需要两个 BCD 位秒和分是 0 到 59各需要一个 0 到 9 和一个 0 到 5。与其写六份几乎一样的代码不如先做一个通用的 BCD 计数器用最大计数值参数区分。-- 通用 BCD 计数器cen 使能时加 1到达 MAX_VAL 归零并输出进位 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity bcd_cnt is generic ( MAX_VAL : integer : 9 -- 个位填 9十位填 5百分秒高位填 9 ); port ( clk : in std_logic; rst_n : in std_logic; cen : in std_logic; -- 计数使能 q : out std_logic_vector(3 downto 0); cout : out std_logic -- 进位输出喂给下一级 cen ); end entity; architecture rtl of bcd_cnt is signal cnt : unsigned(3 downto 0) : (others 0); begin process(clk, rst_n) begin if rst_n 0 then cnt (others 0); elsif rising_edge(clk) then if cen 1 then if cnt MAX_VAL then cnt (others 0); -- 到顶回零 else cnt cnt 1; end if; end if; end if; end process; q std_logic_vector(cnt); cout 1 when (cen 1 and cnt MAX_VAL) else 0; end architecture;级联关系是100Hz 使能同时驱动百分秒低位和高位高位用低位进位当使能百分秒高位进位去驱动秒个位秒个位进位驱动秒十位如此串到分十位。这里有个容易忽略的细节所有级联都发生在同一个clk边沿上进位信号必须在组合逻辑里由当前值算出而不是计数到 9 后下一拍再拉高否则整条链会凭空慢一拍跑一分钟就差出肉眼可见的量。显示位模块MAX_VAL使能来源百分秒个位bcd_cnt9100Hz 使能百分秒十位bcd_cnt9百分秒个位进位秒个位bcd_cnt9百分秒十位进位秒十位bcd_cnt5秒个位进位分个位bcd_cnt9秒十位进位分十位bcd_cnt5分个位进位2.3 七段译码的共阳共阴极性与动态扫描译码本身是一个纯组合的 case 结构真正的坑在极性。共阳数码管是低电平点亮段码表里0应该写成11000000最高位是小数点共阴反过来0是00111111。用错极性的表现是数码管全亮或全灭或者显示成一段乱码。-- 七段译码共阳极性seg(7) 为小数点seg(6..0) 为 g..a library ieee; use ieee.std_logic_1164.all; entity seg7 is port ( bcd : in std_logic_vector(3 downto 0); seg : out std_logic_vector(7 downto 0) ); end entity; architecture rtl of seg7 is begin process(bcd) begin case bcd is when 0000 seg 11000000; -- 0 when 0001 seg 11111001; -- 1 when 0010 seg 10100100; -- 2 when 0011 seg 10110000; -- 3 when 0100 seg 10011001; -- 4 when 0101 seg 10010010; -- 5 when 0110 seg 10000010; -- 6 when 0111 seg 11111000; -- 7 when 1000 seg 10000000; -- 8 when 1001 seg 10010000; -- 9 when others seg 11111111; -- 熄灭避免综合出锁存器 end case; end process; end architecture;when others这一行不是可选项。去掉之后bcd出现 1010 到 1111 时输出无定义综合器会推断出锁存器行为仿真和上板结果就对不上了。2.3.1 六位数码管的动态扫描刷新率怎么定六位共用一组段线靠位选轮流点亮本质是分时复用。刷新频率太低会看到闪烁太高又会让位选线上的驱动电流来不及建立。经验区间是每位点亮 1ms 到 2ms也就是整屏 160Hz 到 1000Hz。50MHz 时钟下1ms 对应 50000 个周期用一个模 50000 的计数器产生扫描节拍每到一个节拍切下一位六位循环。需要提醒的是扫描计数器不要从 100Hz 使能那里取信号两者用途不同100Hz 管计时精度1kHz 左右管视觉刷新。混用一个信号要么数码管闪要么秒表走得不准。3. 从 Testbench 到下载VHDL 秒表在 Quartus/Vivado 里的完整流程代码写完全部塞进工程直接综合是最浪费时间的路径。正确的顺序是先给分频器和计数器写 Testbench仿真确认脉冲周期和进位时序再综合顶层然后补约束、分引脚、下载。这三步各自的失败现象完全不同分开做能把定位时间从半天压到十几分钟。3.1 写一个几秒钟就能跑到进位翻转的 Testbench分频器的默认分频比是五百万仿真跑一个完整周期要等很久。用 generic map 覆盖参数把分频比压到千分之一。-- 使能脉冲发生器的仿真激励把分频比缩小到 100几微秒内跑完一轮 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity tb_clk_en is generic ( CLK_PERIOD : time : 20 ns -- 50MHz ); end entity; architecture sim of tb_clk_en is signal clk : std_logic : 0; signal rst_n : std_logic : 0; signal en : std_logic; signal en_cnt : integer : 0; -- 统计使能脉冲个数 begin dut : entity work.clk_en generic map ( CLK_FREQ 1000, -- 仿真用缩小值 EN_FREQ 10 ) port map ( clk clk, rst_n rst_n, en en ); clk not clk after CLK_PERIOD / 2; -- 自由振荡时钟 process begin rst_n 0; wait for 100 ns; -- 保持复位观察计数器是否清零 rst_n 1; wait for 200 us; -- 跑够 20 个使能周期 assert false report simulation finished severity failure; end process; process(clk) begin if rising_edge(clk) then if en 1 then en_cnt en_cnt 1; -- 在波形里看这个数是否线性增长 end if; end if; end process; end architecture;逻辑说明clk用并发赋值自翻转dut通过 generic map 拿到缩小后的分频参数en_cnt是专门用来验证使能是否均匀的探针。参数说明CLK_PERIOD设成 20ns 对应 50MHz仿真时也可以继续缩小到 10ns波形更紧凑。看波形时重点确认三件事——复位期间en是否一直为低、复位释放后第一个使能脉冲是否在预期的第 100 个时钟出现、en_cnt是否严格线性增长。任何一条不满足后面的计数链都不用看了。跑仿真用命令行更省事# ModelSim 命令行模式跑完自动退出 vsim -c -do vlib work; vmap work work; vcom -93 src/*.vhd; vsim -c tb_clk_en; run -all; quit -f # Quartus 走一遍完整流程综合、布局布线、生成下载文件 quartus_sh --flow compile stopwatch_top # Vivado 批处理模式 vivado -mode batch -source build.tcl三条命令对应三个层次第一条只做仿真不碰综合第二条和第三条是完整实现流程耗时通常在几十秒到几分钟。3.2 .sdc 与 .qsf一个管时序一个管引脚很多人把这两个文件混为一谈。.sdc告诉工具时钟频率是多少、哪些路径可以不检查.qsf告诉工具顶层端口连到芯片的哪个物理引脚。少了.sdc工具按默认条件综合跑出来可能能工作但换一块板子或者换个温度就挂了少了.qsf引脚随机分配按键可能被绑到晶振脚上。# stopwatch.sdc —— 时钟定义与异步输入豁免 create_clock -name sys_clk -period 20.000 [get_ports {clk}] # 50MHz derive_pll_clocks derive_clock_uncertainty set_false_path -from [get_ports {rst_n}] # 复位是异步输入 set_false_path -from [get_ports {key_run}] # 按键经过同步器不做时序检查 set_false_path -from [get_ports {key_clr}]# stopwatch.qsf 片段 —— 引脚绑定具体编号必须查板卡原理图 set_location_assignment PIN_E1 -to clk set_location_assignment PIN_M1 -to rst_n set_location_assignment PIN_J15 -to key_run set_location_assignment PIN_K16 -to key_clr set_location_assignment PIN_A3 -to seg[0] # 段线连续分配便于走线 set_location_assignment PIN_B4 -to seg[1]参数说明-period 20.000的单位是纳秒写错成 2 或者 200 都会让工具按错误频率做时序分析前者报一堆不可能满足的违例后者放过了真实违例。引脚编号必须查自己板卡的手册抄别人的工程只会得到一块点不亮的板子。段线建议连续分配位选线也连续分配PCB 侧的走线会顺很多——这一点上用嘉立创 EDA 画转接板时同样适用段线和位选分组走线避免交叉。3.3 综合报告里值得盯的三个数字综合完直接看报告最后几行是不够的。至少要看三处资源占用里的寄存器数量和查找表数量是否和预期量级吻合六位计数器加扫描逻辑通常在几百个寄存器以内出现几千个说明某处写出了组合环路时序报告里的最差裕量slack是正还是负负数意味着当前时钟频率下电路不收敛以及警告列表里有没有inferred latch和unconnected port前者意味着 case 缺分支后者意味着顶层连错线。报告项正常范围异常时的典型原因寄存器数量与位宽同量级出现大数组说明误写了存储器最差 slack大于 0扫描逻辑组合路径过长需插寄存器inferred latch 警告0 条组合进程缺when others未连接端口警告0 条顶层 port map 漏信号4. 秒表跑起来不对按一次跳数、走时漂移、数码管闪烁的定位方法仿真通过不等于上板正确。板子上多出来的变量是按键的机械特性、时钟的真实抖动、以及引脚到外设的电气连接。下面这几种现象几乎每个做秒表的人都会碰到一次按现象反推原因比盲目改代码快得多。4.1 按键抖动按一次跳好几个数机械按键在按下和释放的几毫秒内会产生一串随机跳变直接送进计数进程一次按压就被识别成好几次。表现是百分秒一次跳十几甚至几十。解决办法是两级同步加计数窗口消抖。-- 按键消抖两级同步消除亚稳态再计满 CNT_MAX 个周期才认可电平变化 library ieee; use ieee.std_logic_1164.all; entity debounce is generic ( CNT_MAX : integer : 500_000 -- 50MHz 下约 10ms ); port ( clk : in std_logic; rst_n : in std_logic; key_in : in std_logic; key_out : out std_logic ); end entity; architecture rtl of debounce is signal sync0, sync1 : std_logic : 1; -- 上拉输入默认高 signal cnt : integer range 0 to CNT_MAX - 1 : 0; signal key_r : std_logic : 1; begin process(clk, rst_n) begin if rst_n 0 then sync0 1; sync1 1; elsif rising_edge(clk) then sync0 key_in; -- 第一级可能产生亚稳态 sync1 sync0; -- 第二级输出已稳定 end if; end process; process(clk, rst_n) begin if rst_n 0 then cnt 0; key_r 1; elsif rising_edge(clk) then if sync1 key_r then cnt 0; -- 电平没变计数清零 else cnt cnt 1; if cnt CNT_MAX - 1 then key_r sync1; -- 稳定满窗口才更新输出 cnt 0; end if; end if; end if; end process; key_out key_r; end architecture;CNT_MAX取 500000 对应 10ms覆盖绝大多数轻触开关的抖动时长如果按键手感偏硬或者线缆较长可以加到 20ms 对应 1000000。同步级数不要省只做一级同步时亚稳态虽然概率低但在上板运行几小时后偶发一次误触发这种问题几乎没法复现。4.2 走时漂移非整除分频与扫描进程争用跑十分钟比对一次标准时间发现慢了一两秒通常是两个原因之一。一是CLK_FREQ / EN_FREQ不能整除被截断掉的那部分余数每周期都在累积二是把显示扫描的计数逻辑和计时逻辑塞进了同一个进程扫描计数溢出时顺带影响了计时使能。排查方法很直接在仿真里把en的周期用wait until或者波形游标量一遍和理论值比较。50MHz 晶振本身也有几十 ppm 的误差但那是量级在秒/天的问题秒表跑十分钟差一秒一定是逻辑误差而不是晶振误差。现象大概率原因定位手段每按一次跳十几个数按键未消抖抓key_run输入边沿数跳变次数跑十分钟慢一两秒分频比不整除仿真量en周期与理论值对比数码管某一两位偏暗位选占空比不均检查扫描计数器的分频是否六等分显示乱码段码极性反了把段码表按位取反再试一次整屏闪烁扫描频率低于 100Hz降低扫描计数上限4.3 用片上逻辑分析仪抓内部波形改代码猜原因效率最低。Quartus 里的 SignalTap 和 Vivado 里的 ILA 都能把内部信号引到片上 RAM 采集不需要额外示波器。抓取时优先选四个信号clk、en、key_r、以及百分秒低位的 BCD 输出。触发条件设成key_r的下降沿就能看到按下按键那一刻使能是否被正确闸住。需要注意的是采样深度和采样时钟要匹配。采样时钟用 50MHz 主时钟深度设 1024 只能看到 20 微秒的窗口对 10ms 级的消抖窗口来说太短这时候要么把采样时钟换成使能分频后的慢时钟要么把深度开到 32K 以上同时接受占用更多块 RAM 的代价。5. 把秒表做到能交付基准校验、消抖窗口与状态机的取舍功能能跑通只是及格线。让秒表值得交出去剩下的是几件具体的小事。5.1 计时基准确认与校准上板之后不要凭感觉说差不多准。用手机秒表或者实验室的频率计做一次十分钟比对启动 VHDL 秒表和一个参考计时器同时清零跑满十分钟读差。差值为零或者只有一个百分秒单位说明分频链正确差出整秒回头查分频常数和级联时序。如果板子上的晶振精度不够可以在分频常数上做补偿实测十分钟慢 0.3 秒意味着分频比需要缩小约 0.05%把DIV从 500000 改成 499750 再测一次。这种补偿只对固定温度和固定板子有效属于工程折中不是通用方案。5.2 启动、暂停、清零要不要做成状态机三键逻辑用几个 if-else 拼起来也能工作但按键组合一多就会互相干扰——比如暂停状态下按清零应该归零并保持停止运行状态下按清零应该归零并继续跑。这种带分支的时序控制写成三状态机清晰得多。-- 运行控制IDLE 待机 / RUN 计时 / HOLD 暂停 type state_t is (IDLE, RUN, HOLD); process(clk, rst_n) begin if rst_n 0 then state IDLE; elsif rising_edge(clk) then case state is when IDLE if run_pulse 1 then state RUN; end if; -- 首次启动 when RUN if run_pulse 1 then state HOLD; end if; -- 暂停 when HOLD if run_pulse 1 then state RUN; end if; -- 继续 when others state IDLE; end case; end if; end process; -- 只有 RUN 状态才放行使能脉冲计数链拿到的是被闸门后的信号 cen_gated en when state RUN else 0;参数说明run_pulse是消抖后的单周期脉冲不是电平所以在 RUN 和 HOLD 之间切换时需要在脉冲产生逻辑里做边沿检测否则按住按键会来回跳状态。清零键独立处理直接作用在计数链的同步复位上不参与状态转移这样任何时候按清零都能生效逻辑也简单。5.3 一个容易被忽略的资源细节cen_gated en when state RUN else 0这行组合逻辑会把使能信号扇出到六个计数器和状态机里。在资源紧张的器件上可以改成在每个计数器内部判断状态代价是状态信号多扇出几路。哪种更好取决于布局布线后的 slack先按上面的写法综合一次看时序报告再决定要不要拆别提前优化。交付前最后做一件事把CNT_MAX、DIV、MAX_VAL这些魔数集中到顶层的一个常量包package里工程里所有文件都从包引用。改板子换晶振时只动一处比全局搜索替换安全得多。本文还有配套的精品资源点击获取
返回列表