简介:面向电子、微电子与FPGA方向的学习者,这份VHDL多功能数字万年历课程设计文档可用于数字时钟、万年历类课设的方案参考、代码理解与报告撰写,尤其适合需要从需求分析走向模块实现的初学者。压缩包内含1个docx文件,约775KB,以实验报告为主体,按设计思路、模块划分、程序调试、程序分析和实验总结逐步展开。内容具体到50MHz输入时钟的分频处理、秒分时日月年的计数进位、四位数码管扫描显示、按键消抖与显示控制状态机,也涵盖整点报警、闹钟、闰月检测、星期LED显示以及校时和秒表等扩展要求,并借助RTL图说明计数、设置、控制、显示各模块之间的协作关系。已有836人学习,适合对照VHDL工程层次、调试记录与课设写作框架,快速定位关键设计并查漏补缺。
1. 先看全景:万年历这件「小事」到底由几层构成
用 VHDL 写一个数字时钟万年历,初看像只是把秒数不停加一,真正动手才会发现它是一条很长的链:晶振进来要分频,分频之后要级联计数,计数结果要拆成个位十位送去数码管扫描,扫描的同时还得让按键插进来改数,最后再挂上闹钟、整点报时和闰月判断。整份设计的知识地图大致是四条链——时钟链、显示链、交互链、附加功能链,四条链都跑在同一个 50MHz 时钟域里,靠一根根 co 进位线串起来。这套东西适合两类人:手里有开发板、四位数码管和几个独立按键,正在做课程设计的人;以及代码已经能跑、但被几十条警告、乱码、进位错位折腾过一轮,想回头把结构理顺的人。地图先给到这里,后面按链拆。
2. 时钟链:从 50MHz 晶振到秒闪 LED 的分频阶梯
时钟链是整份设计的地基。板载晶振给的是 50MHz,这个频率对人来说太快、对计数器来说又太细,所以第一件事永远是把它「数慢」。原文把这条链拆成了七个分频实体:一个通用计数分频器负责把 50MHz 降到 1000Hz 的控制节拍,后面秒、分、时、日、月、年各自一个实体,每级吃上一级的进位输出。梳理这条链的时候最好在纸上画一遍信号流向,从 clk 出发一路标到 ledweekend,任何一个环节断了,现象都是「数码管不动」或者「秒闪不闪」,光看代码很难定位。
2.1 分频不是除法,是数数:50MHz 到 1000Hz 的第一级
分频器在 VHDL 里的实现几乎没有第二种写法:声明一个计数器,每个上升沿加一,加到上限清零并吐一个周期的使能脉冲。50MHz 要得到 1Hz,上限是 50 000 000 − 1;要得到 1ms 的节拍也就是 1000Hz,上限是 50 000 / 1 − 1 = 49999。原文选的是后者的粒度,因为 1000Hz 既够驱动秒计数,也够给秒表功能做基准。
-- 第一级分频:50MHz -> 1000Hz(周期 1ms)的控制节拍 -- 计数上限 = 50_000_000 / 1000 - 1 = 49999 LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY count IS PORT( clk : IN STD_LOGIC; co : OUT STD_LOGIC); END count; ARCHITECTURE behav OF count IS -- 关键:范围必须能装下 49999,16 位整数最大 65535,刚好够 SIGNAL cnt : INTEGER RANGE 0 TO 49999 := 0; BEGIN PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF cnt = 49999 THEN cnt <= 0; co <= '1'; -- 每 50000 个时钟周期输出一个周期宽度的使能 ELSE cnt <= cnt + 1; co <= '0'; END IF; END IF; END PROCESS; END behav;这段代码有两个参数需要记住来回复习:一是上限值,改上限就是改输出频率,公式是 f_out = f_clk / (上限 + 1);二是 co 的宽度,它只维持一个 clk 周期,下游必须用同一个 clk 去采,换成别的时钟去采就会丢脉冲或者重复计数。原文里这个实体的计数信号声明成了INTEGER RANGE 0 TO 2,却拿去和 49999 比较,比较条件永远不成立,计数会一直加下去直到溢出回绕——这类「声明范围与比较值不匹配」的问题在综合阶段不一定报错,仿真波形上却一眼能看出来 co 根本没有脉冲。复习时把「范围声明」和「比较常量」当成一对绑定参数来核对,比逐行读代码效率高得多。
2.2 六个计数器怎么串:秒、分、时、日、月、年的进制表
第一级节拍出来之后,后面就是标准的级联计数。每一级都是同一个模板:上升沿触发,看到上一级的 co 为高就自己加一,自己加满就把 co 拉高一个周期,同时把值清零或回到起始值。把六级的进制、范围、进位来源整理成一张表,比在代码里翻来翻去靠谱得多。
| 模块 | 进制 | 有效计数范围 | 进位来源 | 是否支持手动增减 |
|---|---|---|---|---|
| 秒 second | 60 | 0 ~ 59 | 1000Hz 节拍(每 1000 个脉冲一秒) | 否,只输出秒闪信号 |
| 分 cent | 60 | 0 ~ 59 | 秒的 co | 是,centkeyadd / centkeysub |
| 时 hour | 24 | 0 ~ 23 | 分的 co | 是,hourkeyadd / hourkeysub |
| 日 day | 28 / 29 / 30 / 31 | 1 ~ yue | 时的 co | 是,daykeyadd / daykeysub |
| 月 month | 12 | 1 ~ 12 | 日的 co | 是,monthkeyadd / monthkeysub |
| 年 year | 100 | 0 ~ 99 | 月的 co | 是,yearkeyadd / yearkeysub |
这张表里有三处是重点,也是考试和答辩最爱问的地方。第一,时是 24 进制不是 23 进制,判断写成if hourt = 23 then hourt <= 0,因为 23 是最后一个有效值,加满之后回 0,不要写成减到 23。第二,日和月的起点是 1 而不是 0,dayt如果声明成0 to 31,上电复位后显示的就是 00 日,照片一拍就露馅。第三,日的上限不是常量,它由月模块吐出来的yue信号决定,所以日模块必须多一个yue : IN INTEGER RANGE 1 TO 31的端口,这是六個计数器里唯一一个「上限外挂」的模块,也是最容易在顶层例化时漏接的一根线。
秒模块还有一个独立输出miao,用temp < 500判断高低,也就是在 1000 个 1ms 节拍里前 500 个为高、后 500 个为低,占空比 50%、周期 1 秒,直接驱动开发板上的独立 LED 做秒闪。这段逻辑单独放在一个PROCESS(temp)里,属于纯组合输出,复习时要能一眼分清哪些进程是时序的、哪些是组合的。
2.3 进位线 co 的作用与隐患:一个周期宽度的握手
级联计数能跑起来,靠的全是 co 这根线。它的语义很明确:本级刚刚完成一次回绕,请下一级加一。宽度只有一个 clk 周期,所以下一级必须在同一个时钟域里、在同一个上升沿分支里去采它,写成IF rising_edge(clk) THEN IF 上一级co = '1' THEN ...。这种写法在仿真里几乎不会出问题,但在真实板子上有个隐藏前提:上一级的 co 和下一级的 clk 是同一根时钟,否则就是跨时钟域采样,脉宽只有一个周期的信号很容易被漏掉。
更容易踩的坑是 co 的分支覆盖不全。VHDL 里信号在进程内未赋值时会保持上一次的值,综合工具会因此推断出一个锁存结构,波形上表现为 co 一直挂在高电平,下一级就会疯跑。典型场景是加减设置的三个分支里只给计数逻辑赋了值,没有给 co 赋默认低电平,于是「手动加一」和「自动进位」混在一起,秒表一开就乱。
提示:每个时序进程开头先给所有输出赋一次默认值(co <= '0'),再进 IF 分支,能一次性消掉一大批「未赋值保持」类警告,这是血泪经验里性价比最高的一条。
时钟链梳理完,应该能回答三个问题:50MHz 到 1Hz 要数多少次、日的上限由谁决定、co 的宽度是多少。答不上来说明这条链还没通。
3. 显示与交互:四位数码管上的五个界面怎么组织
开发板只有四位数码管,而系统要显示秒、分、时、日、月、年六组数据,加上秒表、闹钟共五类界面,信息量和显示能力之间差了一大截。原文的解法是「分页 + 扫描」:分页解决信息装不下的问题,扫描解决四位数码管只能同时亮一位的问题。这两件事分别由界面状态机和扫描状态机负责,它们跑在不同的节拍上,这是理解显示模块的关键。
3.1 段码表与位选:先分清共阴还是共阳
数码管的每一位由 8 段(7 段笔画加小数点)组成,段码表就是把 0~9 翻译成 8 位二进制。原文用的是"11000000"表示 0,也就是段码为'0'时该段点亮,这是共阳数码管的接法。段码表搞反了,现象是所有数字都变成「反白」,笔画该亮的灭、该灭的亮,很直观但容易在换板子的时候翻车——换了块共阴的板子,整张表每一位都要取反。
-- 共阳数码管 0~9 段码表,最高位对应小数点 dp -- 段码位序约定:dp g f e d c b a,'0' 点亮 CONSTANT led0 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "11000000"; -- 0 CONSTANT led1 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "11111001"; -- 1 CONSTANT led2 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10100100"; -- 2 CONSTANT led3 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10110000"; -- 3 CONSTANT led4 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10011001"; -- 4 CONSTANT led5 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10010010"; -- 5 CONSTANT led6 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10000010"; -- 6 CONSTANT led7 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "11111000"; -- 7 CONSTANT led8 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10000000"; -- 8 CONSTANT led9 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10010000"; -- 9表里led0到led9是常量,用CASE或者查表的方式根据当前要显示的数字选一个送出去。四位数码管同一时刻只能亮一位,所以要配合四个位选使能信号dis1en ~ dis4en轮流拉低,扫描频率足够高(通常几百 Hz 以上)时人眼看到的就是四位同时亮。这里有个易忽略的细节:扫描频率太低会闪烁,太高则每位点亮时间太短、亮度不够,实际调试时先在 1kHz 附近试,再根据板子的限流电阻微调。
数值拆位是另一件必须做对的事。时分秒这类数据在内部是整数(0~59),要显示成两位十进制,得用mod 10取个位、/ 10取十位。原文里出现了centl <= cent mod 10; centh <= cent/10;这样正确的写法,也出现了hourh <= hourh/10;、dayl <= dayl mod 10;这样把右边的源写成了自己的自引用形式——组合逻辑里自己除以自己,综合出来要么是常量 0,要么直接报组合环。拆位这件事看着琐碎,但它是「显示乱码」这个现象的第一嫌疑人,排查顺序永远是:先看拆位对不对,再看段码表对不对,最后看位选时序。
3.2 界面状态机:key0 一次翻一页,五页循环
界面切换用的是一个五状态的状态机,状态定义为s0 ~ s4,分别对应时钟显示、月日显示、年显示、秒表显示、闹钟显示。触发条件是 key0 的下降沿,写法是IF falling_edge(key0) THEN pre_s <= next_s;,用一个「现态 / 次态」双信号的标准两段式结构。这种写法把状态转移和输出分开,比单进程写法可读性高,答辩时也好讲。
五个页面的分配不是随意的,它遵循「常用信息单独一页、低频信息合并一页」的原则:时钟页显示时和分,是最常看的一页;月日合并一页,因为月和日都是两位,正好占满四位;年单独一页,只占两位,另外两位空着或者显示世纪位;秒表页显示分秒;闹钟页显示闹钟的时和分。理解这个分配逻辑,比死记五个状态名有用——如果要求改成六页或者四页,你能自己推出来该怎么挪。
状态机的时钟源在这里是个值得停下来想一想的点。扫描状态机用的是rising_edge(clk),界面状态机用的却是falling_edge(key0),也就是把按键当成时钟在用。这样写在课程设计里很常见、也能跑,但本质上是一次异步输入当时钟,抖动会直接变成多次状态跳变,后面消抖模块能不能兜住,取决于消抖输出是不是已经变成了干净的单周期脉冲。复习时如果被问到「这个设计有什么可以改进的地方」,这就是标准答案之一:把按键同步到系统时钟域,用使能信号驱动状态转移,而不是用按键沿当时钟。
3.3 设置状态机:key1 选项目,key2 / key3 加减
设置模式由一个两位的set信号控制,set = "00"是显示模式,按一下 key1 进"01",再按进"10",再按回到"00"。原文的写法是if set = 2 then set <= "00" else set <= set + 1,配合set声明成STD_LOGIC_VECTOR(1 DOWNTO 0),等于在 00 / 01 / 10 三态之间循环。
设置的核心约束是「只允许在当前界面内设置」。在时钟页,key1 切的是「设时」还是「设分」;在月日页,切的是「设月」还是「设日」;在年页只有一项,key1 相当于空转。这个约束的实现方式是把set的值和当前界面状态pre_s一起做译码,产生八组加减控制信号centkeyadd / centkeysub / ... / yearkeysub,送到对应的计数模块。这些信号是显示模块的输出端口,一路连回分、时、日、月、年五个实体——这也是为什么显示模块的端口列表特别长,它其实是整个系统的控制中枢。
加减动作的执行位置在各自的分频模块内部,不在显示模块里。以分为例,进程里先是IF seco = '1'走自动进位,ELSIF centkeyadd = '0'走手动加一,ELSIF centkeysub = '0'走手动减一。这三个分支的优先级顺序很关键:自动进位排在最前,意味着即使你正按着加键,只要秒进位来了,它会先执行进位。实际使用中这不影响结果,但读代码时要能说清这个优先级是作者有意为之还是顺手写的。
注意:加减键用
'0'有效还是'1'有效,必须和开发板上按键的硬件接法一致。上拉接法按下为 0,下拉接法按下为 1,写反了现象是「不按一直在加」。
设置模块还有一层「防误触」的隐含设计:set = "00"时所有加减信号都应该是无效电平,否则显示模式下随便碰一下按键就把时间改了。检查这一条的方法很朴素——把 set 的译码逻辑单独列一张真值表,三行乘八个信号,二十四格逐个填,填完就知道有没有漏。
4. 两个状态机与一个比较器:消抖、扫描、闹钟的实现细节
显示与控制模块里塞了三个独立的东西:按键消抖状态机、数码管扫描状态机、闹钟与整点报时的比较逻辑。它们共享同一个 vhd 文件,但彼此之间的耦合很弱,复习时可以拆开单独看。这一章把它们分开讲,重点是搞清楚每个东西「为什么必须这么写」,而不是记住代码长什么样。
4.1 按键消抖:为什么用状态机而不是延时
机械按键按下和松开的瞬间会有几毫秒的抖动,直接当沿用会触发好几次。常见做法有两种:一种是计数延时,检测到变化后等 20ms 再看电平;另一种是状态机,原文选的是后者,效果是「不仅消抖,而且一次按下只输出一个时钟周期的脉冲」。
状态机消抖的思路是四个状态循环:空闲等待按下、确认按下、等待松开、确认松开。每进入一个状态都要连续采到若干个相同电平才迁移,抖动期间状态原地不动,等电平稳定了才往前走。这样输出的按键事件天然是单脉冲,下游状态机可以直接用,不需要再判断「这次是不是重复触发」。
两种做法对比一下更清楚:延时法实现简单,但延时期间 CPU 式地空等,而且松手时的抖动还得再消一次;状态机法多写十几个状态转移,换来的是输出干净、可复用、时序确定。课程设计里如果只做一两个按键,两种都行;按键一多,状态机法的优势就出来了——每个按键一个实例,接口完全一致。
4.2 扫描与界面:两个状态机的节拍为什么必须分开
扫描状态机的状态是c1 ~ c4,对应四位使能,每个rising_edge(clk)走一步,四位循环一次只需要四个时钟周期。界面状态机的状态是s0 ~ s4,只在 key0 的下降沿才动一次,可能几秒钟才动一下。这两个状态机的节拍差了六个数量级,放在同一个进程里写必然出问题:要么扫描被按键拖慢导致闪烁,要么按键被扫描节拍采样多次导致连翻好几页。
分开写之后,两者通过信号交互:扫描状态机根据当前界面状态决定这一位该显示什么数字。译码逻辑是一张「界面 × 位选」的二维表,五行四列二十格,每格填一个数字来源。这张表是显示模块里最值得抄下来的部分,因为它把「哪个页面显示哪几位」这件事一次性说清楚了,改界面只改这张表,不用动扫描时序。
-- 位选译码示意:根据界面状态 pre_s 与扫描步 pre_c 选送哪一位数字 -- 实际工程中用 CASE 嵌套实现,这里只列结构 PROCESS(pre_s, pre_c, cent, hour, day, month, year) BEGIN CASE pre_s IS WHEN s0 => -- 时钟页:高两位时,低两位分 CASE pre_c IS WHEN c1 => dis <= led_hour_h; -- 时十位 WHEN c2 => dis <= led_hour_l; -- 时个位 WHEN c3 => dis <= led_cent_h; -- 分十位 WHEN c4 => dis <= led_cent_l; -- 分个位 END CASE; WHEN s1 => -- 月日页:高两位月,低两位日 NULL; -- 结构同上,按表填 WHEN OTHERS => NULL; END CASE; END PROCESS;这段结构说明了一件事:扫描输出dis是组合逻辑的输出,它的输入既有扫描步进pre_c,也有界面状态pre_s,还有六组计数值。任何一个输入变化,dis都要重新算一次。这也是为什么段码必须做成常量——如果段码也用组合逻辑现算,这条路径的延迟会叠加,扫描频率一高就容易出毛刺。
4.3 闹钟与整点报时:比较器放在控制模块里
闹钟和整点报时在原文里都放在控制模块(也就是显示模块那个 vhd)内部,实现方式本质上是两个比较器。闹钟的做法是维护一组独立的闹钟值ahour / acent,每个节拍和当前的时分比较,相等就拉高beep;整点报时的做法更简单,分和秒同时为 0 时拉高 beep 若干个节拍。
蜂鸣器的驱动信号beep从显示模块输出,说明设计者把「什么时候响」这个决策权集中在了控制中枢,而不是分散到各个计数模块。这个选择有好处也有代价:好处是所有和时间比较相关的逻辑都在一处,改闹钟逻辑不用动六个计数器;代价是显示模块的端口列表进一步膨胀,而且闹钟值和显示值容易混淆。复习时要能分清hour / cent是当前时间,ahour / acent是闹钟时间,两者在端口列表里挨着,名字只差一个字母,例化时接反了现象就是「闹钟永远不响」或者「时间一显示成闹钟值」。
星期显示是另一块附加功能,用 7 个独立 LED 表示周一到周日。原文里ledweekend是一个 7 位向量,初值"0000010"表示周一。真正的难点不是驱动,而是「星期怎么算出来」——按日期推算星期需要知道起始基准日,这块在原文里没有展开,属于典型的发挥空间。复习时如果要做这一项,先想清楚基准日和模 7 的关系,再动手改状态机。
5. 易错点清单:闰月、位宽、自引用赋值这几处最容易翻车
代码能综合、板子能跑,不等于逻辑是对的。万年历这类设计的错误往往不表现为「完全不能工作」,而是表现为「某个月 31 号之后直接跳到 1 号」「年份到 60 就回零」「某一位永远显示 0」——这些现象藏得很深,要翻到对应模块才能定位。下面五条是从这份设计里梳理出来的高频问题,每条都按「错在哪、为什么容易漏、怎么补」写一遍。
5.1 计数与进位里的三处漏算
第一条:闰年判断只做了 year mod 4。原文的写法是if (yeart mod 4) = 0 then run <= '1',这是最简化的闰年规则,能覆盖绝大多数年份,但漏掉了「整百年不是闰年、四百年又是闰年」这两条。为什么容易漏:基本要求里根本没写闰月识别,这条是发挥要求,写的时候注意力全在「让它能跑」上,规则的完整性就放在了一边。怎么补:在 mod 4 的判断外面再套一层,先判断是不是整百年,是的话再看能不能被 400 整除。补的时候注意run信号的产生位置——它是年模块的输出,却要被月模块用来决定二月的天数,这根线跨了两个模块,例化时最容易漏接。
第二条:日计数器的起点写成了 0。原文里dayt声明为INTEGER RANGE 0 TO 31,而日期语义上是从 1 号开始的。为什么容易漏:0 和 1 只差一个数,仿真波形上如果没盯着看,上电那一秒显示 00 日也很正常,复位一下又正常了,很容易被当成复位毛刺忽略掉。怎么补:把声明改成1 TO 31,同时把比较条件从「等于 yue」改成明确的边界判断,并且确认yue的类型也是1 TO 31,两边类型不一致在 VHDL 里会直接报错,反而是好事。
第三条:年的计数范围装不下两位数。year端口声明成0 TO 99,但内部信号yeart声明成0 TO 59,这是从分钟模块复制过来忘了改。为什么容易漏:综合工具对整数范围的检查并不总是严格,超出范围赋值在仿真里可能只是回绕,波形上看起来「年跳到 60 又变回 0」,很像正常的 100 进制循环,只有对着端口声明逐字比对才能发现。怎么补:养成习惯,每写一个计数器实体,先把「端口范围、内部信号范围、比较常量」三个数字抄在一行上核对,三个值必须自洽。
5.2 显示与赋值里的两处错位
第四条:拆位语句写成了自引用。正确的拆位是hourh <= hour / 10,写法上左边是十位、右边是原始值除以 10;一旦手滑写成hourh <= hourh / 10,右边就成了自己的函数,组合进程里形成环,综合工具要么报循环、要么优化成常量。为什么容易漏:这类错误通常在复制粘贴时产生——从上一行hourl <= hour mod 10复制下来改成hourh,只改了左边忘了改右边。怎么补:拆位语句集中写在一个组合进程里,六组数据十二行并排放,写完之后横向扫一遍,看每一行的右边是不是都指向原始信号而不是自己。
第五条:co 在部分分支没有赋值。加减设置的进程里通常有三个分支(自动进位、手动加、手动减),如果只在自动进位分支里给co赋了值,另外两个分支里co会保持上一次的值。为什么容易漏:VHDL 的时序进程默认「未赋值保持」,这个语义在软件语言里不存在,写惯了 C 的人不会预期到;而且现象很怪——手动加了几次之后,下一级的进位突然连着触发好几次。怎么补:进程开头统一赋初值co <= '0',后面再按分支覆盖;或者把 co 的赋值从分支里提出来,用「本周期是否发生回绕」这个中间信号统一驱动。
提示:把仿真波形里的 co 单独拉出来看宽度,正常情况下它应该永远是一个 clk 周期宽。看到宽度不规则的脉冲,直接回到这个进程查分支覆盖。
这五条有个共同点:全都不是「写不出来」,而是「写出来了但边界差一点」。万年历这种设计,功能本身不难,难的是把六十进制、二十四进制、二十八到三十一天、四年一闰这些边界全都摆对,一处差一点,现象就是日期错乱。复习时与其重读一遍代码,不如把这五条当成检查表,对着自己的实现逐条核对。
6. 复习与查漏:按信号流从晶振走到数码管
复习这份设计,最没效率的方式是从第一行代码读到最后一行。有效的顺序是跟着信号走:从 clk 端口进,沿分频、进位、拆位、扫描、位选一路走到 dis 输出,每经过一个实体就停下来问三个问题——这个实体的输入从哪来、输出到哪去、它的边界值是多少。走完一遍大概二十分钟,比通读代码快得多,而且能立刻发现漏接的线。
6.1 五遍回读法
第一遍只看端口。把七个实体的 PORT 列表抄在一张纸上,用箭头连起来,凡是只出现一次的端口名就是可疑的——要么是顶层没接,要么是名字写错了。第二遍只看时钟。确认每个rising_edge里跟的是不是同一个 clk,凡是出现rising_edge(key)或者falling_edge(key)的地方都标红,作为待改进项记下来。第三遍只看边界值。把 59、23、12、31、99、49999 这几个数字圈出来,逐个核对它所在的比较语句和声明范围是否自洽。第四遍只看 co。把所有 co 的赋值语句列出来,检查每个分支是不是都有覆盖,脉冲宽度是不是都只有一个周期。第五遍才看显示。从段码表开始,到拆位、到位选译码,最后到界面状态机,确认五行四列那张表没有空格。
6.2 自测清单
| 检查项 | 正确判据 | 出错先看哪里 |
|---|---|---|
| 秒闪 LED | 周期 1 秒,占空比约 50% | 秒模块的 miao 组合进程 |
| 秒进位 | 59 之后回 0 并吐一个 co | second 实体的 co 分支 |
| 时进位 | 23 之后回 0 | hour 的比较常量写成 23 还是 24 |
| 日进位 | 跟着 yue 走,不是固定 31 | day 实体的 yue 端口有没有接上 |
| 二月天数 | 平年 28、闰年 29 | year 的 run 输出到 month 的 run 输入 |
| 年份范围 | 0 ~ 99 循环 | yeart 的范围声明 |
| 界面切换 | 五页循环,一次一页 | key0 的消抖输出是不是单脉冲 |
| 设置生效 | 只在当前页、只改对应项 | set 译码表有没有漏格 |
| 数码管 | 四位同时亮、不闪 | 扫描频率与位选使能极性 |
| 闹钟 | 时分同时相等才响 | ahour / acent 与 hour / cent 是否接反 |
这张表里最值得反复过的是「日进位」和「二月天数」两行,因为它们是唯二跨模块依赖的项——日的上限来自月,月的二月天数来自年,三根线串在一起,任何一根断了现象都是「日期不对」,但断的位置完全不同。查的时候不要从现象猜,直接从 year 的 run 端口开始顺着线走。
最后留一个我自己的教训。第一版做完的时候所有功能都能演示,我自以为没问题,直到把日期手动设到 2 月 28 日再让它自己走,才发现 29 号直接跳到了 3 月 1 日——闰年那根 run 线在顶层例化时名字拼错了一个字母,综合工具默默把它当成了一个悬空输入,默认值恰好让二月恒为 28 天。从那以后我每次例化完顶层,都强制走一遍「端口逐个连线核对 + 波形上看一眼跨模块信号」,再急也不跳过这一步。希望帮到你。
本文还有配套的精品资源,点击获取