ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FPGA数字闹钟设计:分频、计数回卷与时钟使能避坑

FPGA数字闹钟设计:分频、计数回卷与时钟使能避坑 简介面向FPGA与EDA初学者、数字系统设计课程学习者及需要完成综合实验的读者这份资料围绕基于FPGA的闹钟系统展开重点解决用VHDL完成计时、校时与定时提醒等模块化设计的问题。包内共1个doc文件压缩包约2.71MB以完整设计文档为主体涵盖EDA与FPGA基础、VHDL开发流程、时间存储、计时、闹钟设置、比较器、显示与用户交互等模块划分并给出Quartus仿真综合、GW48开发板下载测试的实现思路。已有832人学习可作为课程设计、实验报告或项目原型的参考帮助读者理顺从需求分析、系统划分到VHDL建模与验证的路径理解分频、计数、比较等数字逻辑的组织方式并对照开发流程排查仿真与板级测试中的常见问题。1. 把 48MHz 晶振变成“时:分:秒”一条被反复分频的脉冲链把一块 FPGA 板子上的 48MHz 晶振变成数码管上一位一位往前爬的时间显示中间隔着的其实是一条被反复分频、逐级进位、层层回卷的脉冲链真正需要理解的东西比代码行数多得多。很多人第一次做基于 FPGA 的数字闹钟系统设计注意力全放在 VHDL 语法上结果仿真波形漂亮得不像话下载到板子却一动不动——问题往往不在语法而在“哪一级脉冲驱动哪一级触发器”这个最朴素的机制上。一个数字闹钟的全部复杂度说穿了就是把高频振荡逐级降频、把十进制位做成模 10 和模 6 的回卷、再在某个时刻和一组预设值做一次相等比较。搞懂这条链任何计数器逻辑在你眼里都会变得眼熟。2. 秒计数的十进制约束十位为什么写成「计到 6 立刻清零」秒模块是整个系统的节拍源它的输出不只是两个数字还有一个进位脉冲。这个脉冲的宽度、产生的时刻、以及它由哪种赋值方式产生直接决定后面分和时能不能正确接住。2.1 变量与信号赋值生效的时机决定了你判「当前值」还是「下一个值」VHDL 里最容易让人翻车的一对概念就是变量variable和信号signal。变量用:赋值在进程内立即生效下一行读到的就是新值信号用赋值要等进程挂起执行到进程末尾或遇到 wait之后才统一更新而且在同一个进程里对同一个信号赋值多次只有最后一次有效。这听起来像是语言细节但它直接决定你在写回卷判断时该填哪个数字。载体赋值符号生效时机同一进程内读到的是回卷判定应写变量:立即生效自增之后的新值下一个值如0110信号进程挂起后统一更新自增之前的旧值当前值如0101这条规则解释了一个很奇怪的现象同一份设计文档里秒模块判的是M20110分模块判的是M20101加M11001两处写法不一致却都能跑出正确结果。原因不是谁写错了而是前者用了变量、后者用了信号。抄代码的人如果只抄判断条件不抄载体就会得到一个十位显示 6 的秒表或者一个永远不进位的分钟。2.2 秒模块源码逐层拆解个位进位、十位回卷、进位脉冲先把原始结构完整摆出来再逐段说它为什么这么写LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY MIAO IS PORT( CLR, CLK : IN STD_LOGIC; S1, S2 : OUT STD_LOGIC_VECTOR(3 DOWNTO 0); -- 秒个位 / 秒十位 CNT : OUT STD_LOGIC -- 进位脉冲送给分模块 ); END ENTITY; ARCHITECTURE BEHAV OF MIAO IS BEGIN PROCESS(CLR, CLK) VARIABLE M1, M2 : STD_LOGIC_VECTOR(3 DOWNTO 0); -- 个位 / 十位 BEGIN IF CLR 1 THEN -- 异步复位优先级最高 M1 : 0000; M2 : 0000; CNT 0; ELSIF CLKEVENT AND CLK 1 THEN -- 上升沿 IF M1 1001 THEN M1 : M1 1; -- 个位 0~9 正常累加 ELSE M1 : 0000; M2 : M2 1; -- 个位满 10回零并向十位进位 END IF; IF M2 0110 THEN -- 十位撞上 6 才回卷 M2 : 0000; CNT 1; ELSE CNT 0; END IF; END IF; S1 M1; S2 M2; -- 变量搬到输出端口 END PROCESS; END ARCHITECTURE;第一段是复位。CLR写在时钟沿判断之前构成异步复位只要CLR为高M1、M2、CNT立刻归零不等时钟沿。这在开发板上很实用按下复位键立刻见效果代价是复位释放的那一刻如果正好压在时钟沿附近触发器可能进入亚稳态工程上通常还要加一个复位同步器。第二段是个位计数。M1 1001等价于“小于 9”成立就加一不成立就清零点亮十位进位。注意比较用的是无符号比较所以STD_LOGIC_UNSIGNED这个库不能省否则和都没有定义。第三段是十位回卷也是全篇最值得琢磨的地方。因为M2是变量上面的M2 : M2 1已经立即生效所以这里读到的M2是“自增之后”的值。十位从 5 进位到 6 的那一拍M2等于0110条件成立立刻被清回0000同时把CNT拉高。下一个时钟沿M2已经是 0条件不成立CNT又拉低。所以CNT是一个宽度正好一个时钟周期的单脉冲。这个宽度很关键。如果下游分模块用同一个CLK采样CNT一拍足够如果下游用的是另一个时钟比如某个分频出来的信号这个单周期脉冲很可能被整拍漏掉表现出来就是“分钟偶尔不走”。这是踩坑率极高的一处后面第 5 章还会展开。再看为什么整个设计坚持用 BCD 码而不是二进制计数。假设内部用 0 到 86399 的二进制计数结构上确实省触发器——17 位就够而 BCD 要 6 组 4 位共 24 位。但显示时得把二进制转成十进制取模和除法在 FPGA 上要么靠长除法流水线要么靠 DSP 硬核代价远高于多出来的 7 个触发器。BCD 把“十进制”编码进状态本身每一位只做一次“是否到 9”的比较逻辑浅、扇入小、综合结果容易预估。这是典型的设计取舍拿资源换逻辑深度。提示CNT用的是信号赋值判断用的是变量这两者混用在同一个进程里是刻意为之。把CNT改成变量会让它在进程内立即生效后续判断逻辑就会读到新值脉冲宽度随之改变。2.3 1Hz 从哪里来分频器的参数怎么算秒模块每来一个上升沿加一意味着它的CLK必须是一秒一个上升沿。板载晶振通常是几十兆赫兹中间必须有分频。最常见的写法是“计满半个周期翻转一次”保证 50% 占空比-- 48 MHz 分频到 1 Hz SIGNAL DIV_CNT : INTEGER RANGE 0 TO 24_000_000 : 0; SIGNAL CLK_1HZ : STD_LOGIC : 0; PROCESS(CLK_48M, CLR) BEGIN IF CLR 1 THEN DIV_CNT 0; CLK_1HZ 0; ELSIF RISING_EDGE(CLK_48M) THEN IF DIV_CNT 24_000_000 - 1 THEN -- 半周期计满 DIV_CNT 0; CLK_1HZ NOT CLK_1HZ; -- 翻转一个完整周期 2 秒? 见下 ELSE DIV_CNT DIV_CNT 1; END IF; END IF; END PROCESS;参数要算清楚48MHz 下要让输出一秒翻转两次即输出周期 1 秒半个周期是 0.5 秒需要计 24,000,000 个主时钟。所以计数值上限取 24,000,000 减一计满清零并翻转。INTEGER RANGE 0 TO 24_000_000综合后大约占 25 位触发器因为 2 的 25 次方是 33,554,432刚好装得下。如果板子是 50MHz这个上限就要改成 25,000,000改错一位整块表就走快或走慢而且仿真时看不出来——仿真里你给的时钟周期是多少它就按多少走只有上板才对得上真实晶振。3. 分与时的计数通道同一个计数器为什么需要两套寄存器分模块是全篇结构最复杂的一处。它不像秒模块那样只做一件事而是要同时承担“正常走时”和“被用户手动设置”两个职责这两件事的驱动源完全不同一个来自秒进位一个来自按键。3.1 两套寄存器的分工一套负责「设」一套负责「走」原始代码里出现了四个四位宽的寄存器M1、M2、Q1、Q2。这不是冗余而是把两条数据通路彻底分开Q1/Q2是设置通道的暂存M1/M2是计时通道的现行值。ENTITY FEN IS PORT( CLR, CLK, LOAD, EN : IN STD_LOGIC; -- EN 高电平时处于设置态 S1, S2 : OUT STD_LOGIC_VECTOR(3 DOWNTO 0); CNT : OUT STD_LOGIC ); END ENTITY; ARCHITECTURE BEHAV OF FEN IS SIGNAL M1, M2, Q1, Q2 : STD_LOGIC_VECTOR(3 DOWNTO 0); BEGIN -- 进程一设置通道由 LOAD 的上升沿推动 Q1/Q2 PROCESS(EN, CLR, LOAD) BEGIN IF CLR 1 THEN Q1 0000; Q2 0000; ELSIF EN 1 THEN IF LOADEVENT AND LOAD 1 THEN IF Q1 1001 THEN Q1 Q1 1; ELSE Q1 0000; Q2 Q2 1; END IF; IF Q1 1001 AND Q2 0101 THEN -- 59 之后回到 00 Q1 0000; Q2 0000; END IF; END IF; END IF; END PROCESS; -- 进程二计时通道正常走时或跟随设定值 PROCESS(CLR, LOAD, EN, CLK) BEGIN IF CLR 1 THEN M1 0000; M2 0000; ELSIF EN 1 THEN M1 Q1; M2 Q2; -- 设置态下现行值跟随设定值 ELSIF CLKEVENT AND CLK 1 THEN IF M1 1001 THEN M1 M1 1; ELSE M1 0000; M2 M2 1; END IF; IF M2 0101 AND M1 1001 THEN -- 判的是旧值 M2 0000; CNT 1; ELSE CNT 0; END IF; END IF; END PROCESS; END ARCHITECTURE;进程一的门控是EN 1也就是“只有处于设置态按键才有效”。按下SET_M键LOAD来一个上升沿Q1加一Q1到 9 再按一次Q1归零、Q2加一。在Q1已经是 9、Q2已经是 5 的状态下再按一次Q1先归零、Q2加到 6然后第二个条件用旧值判定成立Q2被重新赋成0000。于是 59 分之后按键会绕回 00 分不会出现 60 分这种非法状态。进程二的ELSIF EN 1分支意味着设置态下现行时间直接等于设定值。用户按一下键屏幕上跳一下所见即所设不需要“确认”这一层中间态。但这里的优先级也带来一个副作用——EN 1时时钟沿那一路被完全屏蔽分和时停止走动。设置花多久时间就冻结多久秒模块却还在自顾自地跑。退出设置后分的值突然被跳到新值秒却还是原来那个秒中间那段进位就丢了。这个瑕疵在演示里看不出来实际用起来会觉得“每次调完时间都慢了十几秒”。3.2 59 到 00 的进位信号延迟更新与后赋值覆盖怎么互相配合IF M2 0101 AND M1 1001这一行第一次读会觉得有问题上面的ELSE分支里明明已经写了M1 0000怎么下面还判M1 1001答案在于信号赋值的两条语义同时起了作用。第一条是延迟更新M1 0000之后进程内读M1读到的仍然是上一拍的值1001所以条件里的M1 1001成立判断的是“进位前”的状态这正是我们想要的。第二条是后赋值覆盖M2 M2 1让M2排进了更新队列紧接着M2 0000又排了一次进程挂起时只有最后一次生效所以M2最终是0000而不是 6。两条语义缺一不可。假如 VHDL 的信号是立即更新的那么M1在这一行已经是 0条件永远不成立59 分之后会跳成 60 分假如一个进程内对同一信号的多次赋值是“先到先得”M2就会变成 6。这两种假设都会导致同一个症状分钟显示越界。所以这一行不是随手写的它精确地踩在两个语言特性的交叉点上。和秒模块对照就更有意思了。秒模块用变量所以必须判“下一个值”0110分模块用信号所以必须判“当前值”0101。同一个功能两种载体两套判断数字全都正确。血泪经验就在这抄代码的时候判断条件和载体是一体的拆开抄必翻车。3.3 小时的模 24 边界十位和个位不能各自为政小时模块原始文档没有给出完整源码但结构上和分模块同构只是回卷条件从 60 变成 24。这里最容易写错的地方是很多人只处理个位的 9 到 0忘了十位最大值是 2 而不是 5。-- 小时计时通道M1 个位、M2 十位 IF M1 1001 THEN M1 M1 1; ELSE M1 0000; M2 M2 1; END IF; IF M2 0010 AND M1 0011 THEN -- 23 点的下一拍回到 00 M1 0000; M2 0000; ELSIF M2 0011 THEN -- 兜底十位不允许出现 3 M2 0000; END IF;第一个条件处理 23 到 00 的正常回卷用的是旧值判断和分模块一致。第二个ELSIF是兜底如果因为某种原因M2变成了 3比如复位不彻底、或者从设置通道灌进来一个非法值这里会把它拉回 0避免显示出一个不存在的“3x 点”。这种防御性写法在计数器里很常见代价只是一个额外比较器收益是系统不会卡在非法状态里出不来。4. 从按键到时间按下 SET_M 到底触发了什么按键是这套系统里唯一的异步输入也是最不听话的部分。它在物理上是弹簧片接触在电气上是一串抖动的方波在逻辑上却必须表现为“一次干净的动作”。4.1 三个按键拼出的隐式状态机原始设计没有画显式的状态机而是用几个使能信号拼出了一台隐式状态机SET_CLOCK控制“是否处于设置时间态”SET_DINGS控制“是否处于设置闹钟态”DISPLAY_EN控制“屏幕上看的是时间还是闹钟”。三根线组合出四种界面状态。最省资源的实现是用一位 toggle 寄存器-- SET_CLOCK 每按一次切换一次设置态 PROCESS(CLK_1K, CLR) BEGIN IF CLR 1 THEN EN_SET_TIME 0; ELSIF RISING_EDGE(CLK_1K) THEN IF SET_CLOCK_PULSE 1 THEN EN_SET_TIME NOT EN_SET_TIME; -- 奇数次进入偶数次退出 END IF; END IF; END PROCESS;一个 toggle 位的好处是逻辑极省一个触发器解决问题坏处是它对脉冲的完整性极度敏感。如果抖动让一次按键产生两个脉冲toggle 位翻转两次状态就错位了——用户按了一下屏幕没进设置再按一下才进去用起来像“按键不灵”。这就是为什么消抖不是可选项。DISPLAY_EN同理它只在非设置态下才允许切换显示内容否则用户正在调闹钟却被切到时间界面设了一半的值就丢了。原文把“显示切换”和“设置进入”分成两个键本身就是在回避这个冲突。4.2 消抖与两级同步异步按键进时序逻辑前必须做的两件事机械按键的抖动时间通常在 5 到 20 毫秒之间。做法是拿一个 1kHz 的采样时钟周期 1 毫秒连续 20 次采到相同的电平才认账-- 20 ms 消抖 两级同步 上升沿提取 PROCESS(CLK_1K, CLR) VARIABLE CNT_DB : INTEGER RANGE 0 TO 19 : 0; BEGIN IF CLR 1 THEN KEY_SYNC 0; KEY_STABLE 0; CNT_DB : 0; ELSIF RISING_EDGE(CLK_1K) THEN KEY_SYNC KEY_IN; -- 第一级把异步信号拉进本时钟域 IF KEY_SYNC / KEY_STABLE THEN -- 与稳定值不同开始计时 IF CNT_DB 19 THEN KEY_STABLE KEY_SYNC; -- 连续 20 ms 一致承认新电平 CNT_DB : 0; ELSE CNT_DB : CNT_DB 1; END IF; ELSE CNT_DB : 0; -- 中途抖回去重新计时 END IF; END IF; END PROCESS; KEY_PULSE KEY_STABLE AND NOT KEY_STABLE_D; -- 上升沿 - 单周期脉冲KEY_SYNC这一级同步不能省。按键相对采样时钟是随机相位的异步信号如果直接送进触发器在建立保持时间窗口内变化就会产生亚稳态输出既不是 0 也不是 1持续时间不确定。打一拍之后第二级采到的概率已经极小代价只是一个触发器。CNT_DB用变量而不是信号是因为它只在进程内做计数不需要对外可见用变量省掉一组触发器综合工具会把它推断成组合逻辑加寄存器行为更直观。4.3 闹钟比较与显示切换相等条件为什么要挂一个「秒00」的限定比较逻辑本身很简单小时的十位个位、分的十位个位全部相等就认为命中。但如果只比较时分这个相等条件在整个一分钟内都为真。ALARM_EQ 1 WHEN (H1 AH1) AND (H2 AH2) AND (M1 AM1) AND (M2 AM2) ELSE 0; PROCESS(CLK_1HZ, CLR) BEGIN IF CLR 1 THEN RING_EN 0; ELSIF RISING_EDGE(CLK_1HZ) THEN IF ALARM_EQ 1 AND S1 0000 AND S2 0000 AND ALARM_ON 1 THEN RING_EN 1; -- 只在整分那一拍锁存 ELSIF RING_TIMEOUT 1 OR RING_STOP 1 THEN RING_EN 0; -- 超时或手动关闭 END IF; END IF; END PROCESS;如果把这个比较结果直接接到蜂鸣器命中那一分钟里它会一直响如果只在时钟沿触发却不加锁存它会在那一分钟里的每个时钟沿反复触发永远停不下来。加“秒等于 00”的限定加锁存把它压缩成一个一次性的边沿事件再配一个超时计数器比如 60 秒自动关掉才是可用的行为。还有一个容易被忽略的细节RING_EN打开之后真正驱动蜂鸣器的不能是 1Hz 方波那听起来是一秒一响的“滴…滴…”不是闹铃。常见的做法是另外起一个 2kHz 左右的分频器做音源用RING_EN去门控它得到的是连续的“嘀——”声。门控写错成直接赋值蜂鸣器要么不响要么一直响这是第 5 章里的一条现场记录。显示通路的切换放在数据前面比放在段码后面省资源-- 4 路二选一放在扫描之前 DISP_H2 AH2 WHEN DISPLAY_EN 1 ELSE H2; DISP_H1 AH1 WHEN DISPLAY_EN 1 ELSE H1; DISP_M2 AM2 WHEN DISPLAY_EN 1 ELSE M2; DISP_M1 AM1 WHEN DISPLAY_EN 1 ELSE M1;一组四位选择器乘四次比在段码输出端做八路选择便宜得多。数码管本身是动态扫描的1kHz 节拍轮流点亮四位刷新率 250Hz人眼完全看不出闪烁。5. 避坑与排查仿真通过、下载到板子却翻车的五个现场仿真和上板之间隔着一整个物理世界布线延迟、时钟偏斜、按键抖动、器件电气特性全都不在仿真模型里。5.1 时钟与时序波形对、板子错的两类现场现象一仿真波形完全正确下载后数码管数字长时间不变化偶尔跳一下。原因基本可以锁定在分频出来的 1Hz 信号上。它是由一个普通触发器翻转产生的如果综合工具没有把它分配到全局时钟网络而是当普通数据走线那么时钟到达各个触发器的延迟就不一致时钟偏斜会把窄脉冲吃掉。1Hz 的周期有一秒偏斜几十纳秒听起来不算什么但边沿质量差、上升沿缓慢触发器采样就可能落在不确定区。解决思路是改结构而不是改参数不要用分频出来的信号当时钟改成一路主时钟加时钟使能脉冲。所有时序逻辑共用同一个时钟域1Hz 只是一个宽度一拍的使能信号跨模块传递天然安全综合工具也不需要为衍生时钟单独写约束。现象二综合报告里时序分析一片红或者提示时钟频率远低于预期。原因往往是分频器把 25 位比较器挂在了时钟路径上工具为了满足这个比较关系把大量逻辑塞进了时钟树。解决方法是把“计满清零”改成“计满翻转”把比较结果转成寄存器的使能条件同时给主时钟加一条明确的周期约束让工具按真实频率优化。5.2 输入与显示按键和数码管最容易出问题的两处现象三按一次SET_M分钟一下跳三四个数。原因就是抖动被当成了多次上升沿。消抖逻辑缺失或者消抖窗口太短比如只连续采 3 次弹簧片在前几毫秒的抖动全被计数了。按 4.2 的做法改成 20 毫秒窗口并且用CNT_DB中途翻转就清零的策略跳数问题立刻消失。现象四数码管某一位偏暗或者相邻位有重影。原因是位选信号和段码在同一个时钟沿更新但两者经过的组合逻辑深度不同到达数码管的时间有先后切换瞬间旧段码会在新位上亮一小会儿。解决方法是在位选之后打一拍再送段码或者把位选和段码都寄存输出让它们严格同沿更新。如果亮度不均还要检查扫描频率低于 50Hz 会看到闪高于几 kHz 则每位点亮时间太短、亮度被拉低。5.3 逻辑与资源进位脉冲和设置模式留下的坑现象五闹铃只在某一瞬间“嘀”一下或者设置完时间后整体走慢十几秒。前者的原因是把只有一拍宽的进位脉冲直接送去了蜂鸣器或者用另一个时钟域去采样它脉冲被整拍漏掉。进位脉冲只应该做进位音源必须另起一个高频分频器。后者是 3.1 节提到的设计副作用EN 1期间时钟通道被屏蔽秒模块却照跑退出设置时秒和分对不上。补救办法有两个——设置态下把秒模块也一起冻结或者在退出设置的那一拍统一清秒。选哪个取决于你要的是“设置期间时间暂停”还是“设置期间时间照走”前者实现简单后者更符合真实手表的行为。6. 分频时钟与时钟使能教学代码和工程代码的分岔口同一份闹钟用分频时钟直接当时钟能跑用主时钟加使能也能跑但从这里分出了两条路。维度分频信号直接当时钟主时钟 时钟使能时钟域数量秒、分、时、闹钟比较各一个全系统只有一个时序约束每个衍生时钟都要单独约束只约束主时钟跨域传递单拍脉冲跨域容易丢同一域内传递天然安全资源开销无需使能判断每级多一个使能条件换器件移植时钟树要重做改参数即可文档里那套写法在实验箱上能跑是因为实验箱的时钟资源宽裕、逻辑规模小、频率极低任何偏差都被一秒的周期淹没了。一旦系统变大、时钟变快、或者要移植到另一块板子上衍生时钟就会变成最难查的那类问题——它能仿真、能综合、能下载只是偶尔错一次像黑匣子一样。从那以后我每次写计数器都先问自己一句这个信号的边沿是被当作时钟用还是被当作数据用只要答案是后者就老老实实把它做成使能脉冲让整块板子只有一个时钟域。这个习惯没有让我少写过代码但确实让我少熬了很多夜。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表