24 小时计时、数码管扫描、校时、整点报时和跑表都塞进顶层 always,后面会很难改。
数字系统课程设计里,数字钟是一个很容易被低估的题目。
看起来只是“时分秒显示”,写起来很快会变成一团:分频、秒计数、分计数、时计数、按键调时、数码管扫描、整点报时、跑表、模式切换,全都想往顶层塞。
如果一开始就写一个巨大 always,前几天可能很快,后面调试会很痛苦。因为每个功能都在改同一段状态逻辑,改一次报时,可能影响计数;改一次校时,可能影响显示;跑表暂停时,顶层又多几个分支。
所以这次回头整理这个数字钟项目时,我最大的感受是:别急着把功能堆完,先把每个模块该管什么拆清楚。
顶层只负责连接,不负责所有逻辑
一个比较清楚的数字钟顶层,应该像系统接线图,别把所有功能都塞进去。
我会先把模块拆成几类:
| 模块 | 负责的事 |
|---|---|
clock_divider | 从系统时钟产生 1 Hz 或调试 tick |
time_counter_24h | 24 小时时分秒计数和时分调校 |
stopwatch | 跑表启动、暂停、清零 |
hourly_chime | 整点按点数报时 |
bin_to_bcd2 | 二进制转两位十进制 |
seg7_decoder | 七段数码管译码 |
seg_scan_6 | 6 位数码管动态扫描 |
modular_clock_top | 统一连接、模式选择和板级接口 |
这个拆法不复杂,但它能防止顶层变成“所有东西都知道所有东西”。
顶层可以决定当前显示时间还是跑表,可以把小时、分钟、秒送去 BCD 转换,也可以把数码管扫描接到板级引脚。不过它不该自己同时负责计数、调时、报时次数和段码译码。
接口比模块名更重要
模块化不是把代码切成几个文件就结束了。
最有用的是接口稳定:
clk / rst_ntick_1hzenablecarryhour / minute / secondadjust_hour / adjust_minutedisplay_digitschime这些信号一旦清楚,模块之间的关系就会自然很多。计数模块输出当前时间,显示模块只关心要显示的数字,报时模块只关心整点脉冲和当前小时,跑表模块只关心自己的启停和清零。
课程项目里最常见的混乱,不一定是写不出 Verilog,而是每个模块都偷偷依赖别的模块内部状态。这样一来,仿真时很难单独测,报告里也很难解释。
调试频率要做成参数
数字钟如果只按真实 1 Hz 跑,硬件验证会很慢。
比较稳的做法,是把顶层参数写出来:
modular_clock_top #( .CLK_FREQ_HZ(100_000_000), .TICK_HZ(1)) u_top (...);正常运行时用 1 Hz,调试时可以把 TICK_HZ 提高到 10 Hz 或 100 Hz。这样能快速观察秒进位、分进位、小时回绕、跑表暂停和整点报时。
这个参数看起来很小,但它会明显影响调试体验。课程设计最怕每次验证都等一分钟,等到最后只想“差不多能亮就交”。把调试频率做成参数,才有耐心把范围条件看完。
整点报时别写成一堆特殊分支
整点报时是这个项目里最容易把逻辑写乱的地方。
需求听起来简单:几点报几下。实现时却要考虑 24 小时制转 12 小时制、整点触发只触发一次、报时次数计数、报完停止、0 点按 12 下还是 0 下。
我更愿意把它单独做成 hourly_chime 模块。它只接收:
clk_1hzrst_nsec_low5hour_24hour_pulse内部再维护:
activetarget_countchime_count这样顶层只需要给它一个整点脉冲,报时模块自己决定目标次数和停止条件。哪怕后面把蜂鸣器换成 LED,或者把报时节奏改成更快的调试节奏,也不会污染主计时模块。
这也是课程设计里很值得练的一点:复杂逻辑别急着塞进现有模块,先问它是不是有独立状态、独立输入和独立输出。如果有,就应该单独拆出来。
数码管显示是另一条流水线
6 位数码管显示经常被当成附属功能,但它其实是另一条流水线:
hour / minute / second-> bin_to_bcd2-> display mux-> seg7_decoder-> seg_scan_6-> an / seg显示模块不用关心时间怎么进位,也不用关心跑表怎么暂停。它只需要拿到当前要显示的六位数字,然后按扫描频率轮流点亮。
这样拆的好处是,显示问题和计数问题能分开排查。数码管闪烁、鬼影、位选反了、段码反了,是显示链路的问题;秒不进位、小时不回绕,是计数链路的问题。两类问题别混在一个 always 里互相遮挡。
Block Design 可以当说明图,别替代可综合顶层
这次工程里还整理了一个 Vivado Block Design,用来展示模块关系。
我觉得它很适合当报告里的结构图:打开之后能看到模块之间怎么连,读者不用从一长串例化里猜系统结构。
但它不该替代当前可综合顶层。上板综合的入口仍然应该是 modular_clock_top,Block Design 更像说明图和教学辅助。
这里也要分清楚:Block Design 适合解释结构,上板时还是要以 Verilog 顶层、约束、仿真和 bitstream 为准。报告图可以帮助解释,不能变成另一个和源码不同步的系统。
我会按这个顺序验收
模块化数字钟可以按功能层逐步验收:
- 先单测
clock_divider,确认 tick 周期。 - 再测
time_counter_24h,覆盖 59 秒进位、59 分进位、23:59:59 回到 00:00:00。 - 单独测调时输入,确认只改小时或分钟,不破坏秒计数。
- 单独测
hourly_chime,覆盖 0 点、12 点、13 点和 23 点。 - 单独测
stopwatch,覆盖启动、暂停、清零。 - 最后接显示扫描和板级约束。
这个顺序比“全部写完再上板看灯”慢一点,但可靠得多。
上板失败时,最怕不知道是哪一层错了。按模块验收以后,失败范围会小很多。
别把数字钟写成一个大 always
数字钟课程设计练的不是让数码管亮起来这么简单,还要把一个小系统拆成职责清楚的模块。
分频、计数、校时、报时、跑表和显示扫描,每一块都不难。难的是别让它们互相缠在一起。先拆模块,再写顶层;先定接口,再加功能;先能单测,再谈上板。
对 Verilog 这种课程来说,这比多加一个炫酷功能更有用。功能可以加,但系统不能越加越像一个没人敢改的大 always。
继续读