1730 字
9 分钟
- 次浏览
写 Verilog 数字钟时,我把模块拆开了
读前导览

24 小时计时、数码管扫描、校时、整点报时和跑表都塞进顶层 always,后面会很难改。

正文
1730 字
阅读
9 分钟
结构
8 节
来源线索
GitLaughs/Verilogexp
Verilogexp:modular-clock-vivado Verilogexp:clock_5 Verilogexp:Vivado模块化工程实验说明

数字系统课程设计里,数字钟是一个很容易被低估的题目。

看起来只是“时分秒显示”,写起来很快会变成一团:分频、秒计数、分计数、时计数、按键调时、数码管扫描、整点报时、跑表、模式切换,全都想往顶层塞。

如果一开始就写一个巨大 always,前几天可能很快,后面调试会很痛苦。因为每个功能都在改同一段状态逻辑,改一次报时,可能影响计数;改一次校时,可能影响显示;跑表暂停时,顶层又多几个分支。

所以这次回头整理这个数字钟项目时,我最大的感受是:别急着把功能堆完,先把每个模块该管什么拆清楚。

顶层只负责连接,不负责所有逻辑#

一个比较清楚的数字钟顶层,应该像系统接线图,别把所有功能都塞进去。

我会先把模块拆成几类:

模块负责的事
clock_divider从系统时钟产生 1 Hz 或调试 tick
time_counter_24h24 小时时分秒计数和时分调校
stopwatch跑表启动、暂停、清零
hourly_chime整点按点数报时
bin_to_bcd2二进制转两位十进制
seg7_decoder七段数码管译码
seg_scan_66 位数码管动态扫描
modular_clock_top统一连接、模式选择和板级接口

这个拆法不复杂,但它能防止顶层变成“所有东西都知道所有东西”。

顶层可以决定当前显示时间还是跑表,可以把小时、分钟、秒送去 BCD 转换,也可以把数码管扫描接到板级引脚。不过它不该自己同时负责计数、调时、报时次数和段码译码。

接口比模块名更重要#

模块化不是把代码切成几个文件就结束了。

最有用的是接口稳定:

clk / rst_n
tick_1hz
enable
carry
hour / minute / second
adjust_hour / adjust_minute
display_digits
chime

这些信号一旦清楚,模块之间的关系就会自然很多。计数模块输出当前时间,显示模块只关心要显示的数字,报时模块只关心整点脉冲和当前小时,跑表模块只关心自己的启停和清零。

课程项目里最常见的混乱,不一定是写不出 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_1hz
rst_n
sec_low5
hour_24
hour_pulse

内部再维护:

active
target_count
chime_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 为准。报告图可以帮助解释,不能变成另一个和源码不同步的系统。

我会按这个顺序验收#

模块化数字钟可以按功能层逐步验收:

  1. 先单测 clock_divider,确认 tick 周期。
  2. 再测 time_counter_24h,覆盖 59 秒进位、59 分进位、23:59:59 回到 00:00:00。
  3. 单独测调时输入,确认只改小时或分钟,不破坏秒计数。
  4. 单独测 hourly_chime,覆盖 0 点、12 点、13 点和 23 点。
  5. 单独测 stopwatch,覆盖启动、暂停、清零。
  6. 最后接显示扫描和板级约束。

这个顺序比“全部写完再上板看灯”慢一点,但可靠得多。

上板失败时,最怕不知道是哪一层错了。按模块验收以后,失败范围会小很多。

别把数字钟写成一个大 always#

数字钟课程设计练的不是让数码管亮起来这么简单,还要把一个小系统拆成职责清楚的模块。

分频、计数、校时、报时、跑表和显示扫描,每一块都不难。难的是别让它们互相缠在一起。先拆模块,再写顶层;先定接口,再加功能;先能单测,再谈上板。

对 Verilog 这种课程来说,这比多加一个炫酷功能更有用。功能可以加,但系统不能越加越像一个没人敢改的大 always

写 Verilog 数字钟时,我把模块拆开了
https://blog.sunmmyapi.xyz/posts/modular-digital-clock-before-big-always/
作者
Sun
发布于
2026-05-29
许可协议
CC BY-NC-SA 4.0

继续读

相关内容