简介面向学习 VHDL 与数字电路课程设计的学生这份文档围绕多功能数字万年历的完整实现展开可用于课程设计、实验报告撰写以及 FPGA 数码管显示练习。压缩包共 1 个 docx 文件约 775KB以实验报告形式组织涵盖基本要求与发挥要求包含设计思路、程序调试、程序分析、实验总结与 RTL 图等内容。文档按分频器、设置、控制、显示等模块说明年月日时分秒计数、四位数码管扫描输出、按键切换小时与分钟及月日年显示、秒闪烁 LED、设置与显示模式切换并给出按键消抖状态机、整点报警、闹钟、闰月检测和七段 LED 星期显示的实现要点。文中还附有关键 VHDL 代码分析便于对照理解各模块接口、计数进位关系与调试方法。目前已有 836 人学习适合需要参考万年历系统结构、状态机设计和实验报告写法的读者。1. 四位数码管里塞下六年这个万年历项目的真实难点四位数码管要显示年月日时分秒第一次听上去就像把六件衣服塞进一个行李箱。VHDL 数字时钟 万年历 这类题目在课程设计里出现频率极高可真落到板子上卡住人的从来不是“写一个计数器”而是六个计数器怎么互相进位、四位数码管怎么轮流刷新去骗过眼睛、按键怎么在人手抖动的几十毫秒里只被认成一次。这份资源用 50MHz 晶振做时基靠一条co逐级串联的分频链一路走到 1Hz再用两个状态机分别管界面切换和数码管扫描另外带整点报时、闹钟、闰年判断和七段独立 LED 星期显示。它适合刚学完 VHDL 语法、想找一份能真正上板跑通的完整例子的人也适合拿它当骨架改自己课设的人——因为里面该踩的坑基本都踩过一遍了。2. 模块划分与 RTL 骨架50MHz 进来七段码出去先看整体。这套设计按功能切成四块计数分频、设置、控制、显示。原文的 RTL 图里能看到晶振电路、分频器电路、计数链年月日时分秒、校时控制、显示控制、数码管驱动扫描输出这几大块并排。真正写代码时不会真画那么多层原理图而是把它们落成一组entity用顶层文件连线。搞清这层对应关系后面看每个模块才不会迷路。2.1 顶层端口怎么定先把板子上的资源数清楚顶层定端口是最容易被忽略的一步。板上有什么entity里就得有什么多一个少一个都综合不过去。这块板子的资源清单大致是一个 50MHz 时钟输入、四个独立按键key0 切界面、key1 选设置项、key2 加、key3 减、四位数码管8 位段码 4 位位选、7 个独立 LED 显示星期、1 个 LED 做秒闪、1 个蜂鸣器做整点和闹钟。LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY calendar_top IS PORT( clk : IN STD_LOGIC; -- 50MHz 板载晶振 key0 : IN STD_LOGIC; -- 界面切换时钟/月日/年/秒表/闹钟 key1 : IN STD_LOGIC; -- 当前界面内切换设置项时 or 分 key2 : IN STD_LOGIC; -- 加 key3 : IN STD_LOGIC; -- 减 dis : OUT STD_LOGIC_VECTOR(7 DOWNTO 0); -- 段码共阴/共阳按板子定 dis1en : OUT STD_LOGIC; -- 第 1 位数码管位选 dis2en : OUT STD_LOGIC; dis3en : OUT STD_LOGIC; dis4en : OUT STD_LOGIC; led_sec : OUT STD_LOGIC; -- 秒闪指示 led_week : OUT STD_LOGIC_VECTOR(6 DOWNTO 0); -- 星期一到星期日 beep : OUT STD_LOGIC -- 蜂鸣器整点报时 闹钟 ); END calendar_top;这段entity是整个项目的对外契约参数含义全在注释里clk是唯一时基四个按键的语义在顶层就固定下来后面的状态机全部围绕它们转。dis是段码总线位选单独拉出来四根因为四位数码管是动态扫描的同一时刻只有一位亮。注意led_week用 7 位而不是编码成 3 位二进制——直接一位对一天省掉译码逻辑代价是多占 4 个引脚板子上 LED 够就无所谓。常见做法是把顶层写成纯“连线层”里面只做元件例化和信号声明不放任何process。我一般会强制自己遵守这条顶层一旦出现if rising_edge(clk)说明有逻辑漏到了不该在的层后面改需求就得两头翻。2.2 分频链为什么每一级都要往下吐一个co整个设计的心脏是一条链50MHz → 1000Hz → 1Hz → 分 → 时 → 日 → 月 → 年。每一级模块都长一个样一个输入时钟、一个输入进位脉冲、一个输出计数值、一个输出进位脉冲co。这个co只在计满归零的那一个时钟周期拉高下一级看到它高就加一。-- 第一级50MHz 分频到 1000Hz供扫描和秒表用 ENTITY count IS PORT(clk : IN STD_LOGIC; co : OUT STD_LOGIC); END count; ARCHITECTURE behav OF count IS SIGNAL cnt : INTEGER RANGE 0 TO 49999; -- 50_000_000 / 1000 50000 BEGIN PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF cnt 49999 THEN cnt 0; co 1; -- 只高一个时钟周期 ELSE cnt cnt 1; co 0; END IF; END IF; END PROCESS; END behav;cnt的上界是 49999不是 50000因为计数器从 0 开始数数满 50000 个时钟正好是 0~49999。co拉高的宽度严格等于一个clk周期宽度是 20ns它只作为下一级的“使能”用绝不能拿去当时钟——这是后面避坑章要重点说的一条。原文里这个信号的声明写成了RANGE 0 TO 2而比较值却是 49999综合工具要么报越界要么悄悄扩位行为不确定这种低级错误在抄改过程中特别容易留下来。参数怎么改想换成 25MHz 的板子把 49999 改成 24999想要 1kHz 之外的扫描基准改这个数就行。第二级 1000Hz → 1Hz 就是原文的second模块里面temp数到 1000 归零这一段逻辑是对的。2.3 文件怎么组织一份能综合也能讲的工程结构课设代码最常见的状态是所有entity塞在一个.vhd里编译能过但谁接手都得从头读。这份资源里也能看到这个痕迹——控制模块和显示模块共享同一个 vhd 文件。真要继续改我建议按下面这个方式拆开Quartus 和 Vivado 都能直接吃文件内容说明calendar_top.vhd顶层例化与连线不含任何时序逻辑clkdiv.vhd50MHz→1000Hz、1000Hz→1Hz可参数化分频系数cnt_sec.vhdcnt_min.vhdcnt_hour.vhd秒/分/时计数都带加减设置通道cnt_day.vhdcnt_month.vhdcnt_year.vhd日/月/年计数日月之间有天数传递debounce.vhd按键消抖状态机一次按下只出一个脉冲display.vhd界面状态机 扫描状态机 段码表项目里最厚的一块alarm.vhd闹钟比较与蜂鸣器驱动可独立仿真拆文件不是洁癖是为了单独仿真。分频链和计数链可以脱离显示模块跑波形显示模块可以拿固定数值灌进去看段码两者分开调出问题时定位范围直接砍一半。3. 计数链拆解秒分时日月年六级的进位与置数六个计数模块长得几乎一样差异只在两处计数上限不一样以及“这个月到底有几天”这个信息要从月模块传给日模块。把这两处理顺剩下就是复制粘贴改数字。3.1 秒模块既出秒又出秒闪秒模块干三件事把 1000Hz 再分频到 1Hz、维护 0~59 的秒计数、给板上的 LED 输出一个 0.5Hz 的方波做“秒闪”。注意秒闪不是每秒闪一下而是亮半秒灭半秒人眼看上去才是“一秒一跳”。ENTITY cnt_sec IS PORT( clk : IN STD_LOGIC; -- 1000Hz sec : OUT INTEGER RANGE 0 TO 59; miao : OUT STD_LOGIC; -- 秒闪方波0.5Hz co : OUT STD_LOGIC -- 每分钟一个脉冲 ); END cnt_sec; ARCHITECTURE behav OF cnt_sec IS SIGNAL sect : INTEGER RANGE 0 TO 59 : 0; SIGNAL temp : INTEGER RANGE 0 TO 999 : 0; BEGIN PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF temp 999 THEN temp 0; IF sect 59 THEN sect 0; co 1; -- 59→0 时向分钟进位 ELSE sect sect 1; co 0; END IF; ELSE temp temp 1; co 0; END IF; END IF; END PROCESS; -- 秒闪temp 前半周期亮后半周期灭 PROCESS(temp) BEGIN IF temp 500 THEN miao 1; ELSE miao 0; END IF; END PROCESS; sec sect; END behav;temp的范围是 0~999比较值写 999 而不是 1000和前面分频器同理。co在sect从 59 回 0 的那个周期拉高宽度一个 1000Hz 周期也就是 1ms够下一级采样。miao用组合逻辑从temp直接译出不额外占一个计数器。这里有个细节值得说miao的占空比取决于temp的阈值写 500 就是 50%。想要“亮 0.2 秒灭 0.8 秒”这种更明显的闪法把 500 改成 200 就行不用动别的地方。原文用的是if temp500 then miao1逻辑一致只是temp的边界声明成0 to 1000而比较值只到 1000 判等等于多浪费一个状态综合后会多一个比较器问题不大但不干净。3.2 分和时一条计数通道一条设置通道分和时的结构完全对称都是一个“正常进位”分支加一个“按键加减”分支靠if/elsif的优先级把两者排开。这个优先级顺序很关键进位优先于按键按键优先于保持。ENTITY cnt_min IS PORT( clk, seco : IN STD_LOGIC; key_add, key_sub : IN STD_LOGIC; -- 已消抖低电平有效 cent : OUT INTEGER RANGE 0 TO 59; co : OUT STD_LOGIC ); END cnt_min; ARCHITECTURE behav OF cnt_min IS SIGNAL centt : INTEGER RANGE 0 TO 59 : 0; BEGIN PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF seco 1 THEN -- 优先级 1秒进位 IF centt 59 THEN centt 0; co 1; ELSE centt centt 1; co 0; END IF; ELSIF key_add 0 THEN -- 优先级 2按键加 IF centt 59 THEN centt 0; ELSE centt centt 1; END IF; co 0; ELSIF key_sub 0 THEN -- 优先级 3按键减 IF centt 0 THEN centt 59; ELSE centt centt - 1; END IF; co 0; ELSE co 0; END IF; END IF; END PROCESS; cent centt; END behav;这段代码的骨架可以原样搬到小时模块只改两处centt的范围改成0 TO 23归零判断从 59 改成 23。原文小时模块里hourt声明成了0 to 59虽然功能上因为判断写的是 23 所以不会跑飞但声明范围和行为不符属于典型的“能跑但不对”审查代码时这种地方必须挑出来。按键加减放在rising_edge(clk)里面意味着什么意味着按住不放时每个时钟周期加一50MHz 下就是天文数字根本没法用。正确做法是消抖模块输出的key_add本身就是一个只持续一个时钟周期的脉冲接到这里才能“按一次加一”。如果消抖模块输出的是电平就得在这里再做一个边沿检测。这个配合关系是整个项目里最容易断的一环我在避坑章会再提。3.3 日与月把“这个月有几天”传下去日和月是整条链里唯一有反向依赖的地方——日模块要知道当月是 28、29、30 还是 31 天而这个信息由月模块产生。原文的做法是月模块输出一个yue信号给日模块当上限日模块再输出co给月模块进位。这条回环在 RTL 图里看着自然写成代码却要小心组合环。-- 月模块计数 天数译码 闰年标志接入 ENTITY cnt_month IS PORT( clk, dayo : IN STD_LOGIC; key_add, key_sub : IN STD_LOGIC; run : IN STD_LOGIC; -- 闰年标志1 表示闰年 month : OUT INTEGER RANGE 1 TO 12; days : OUT INTEGER RANGE 28 TO 31; -- 当月天数 co : OUT STD_LOGIC ); END cnt_month; ARCHITECTURE behav OF cnt_month IS SIGNAL montht : INTEGER RANGE 1 TO 12 : 1; BEGIN PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF dayo 1 THEN IF montht 12 THEN montht 1; co 1; ELSE montht montht 1; co 0; END IF; ELSIF key_add 0 THEN IF montht 12 THEN montht 1; ELSE montht montht 1; END IF; co 0; ELSIF key_sub 0 THEN IF montht 1 THEN montht 12; ELSE montht montht - 1; END IF; co 0; ELSE co 0; END IF; END IF; END PROCESS; -- 天数译码二月看 run小月 30其余 31 PROCESS(montht, run) BEGIN CASE montht IS WHEN 2 IF run 1 THEN days 29; ELSE days 28; END IF; WHEN 4 | 6 | 9 | 11 days 30; WHEN OTHERS days 31; END CASE; END PROCESS; month montht; END behav;注意原文用的是WHEN 3 yue30; WHEN 6 yue30; ...这种一列到底的写法逻辑对但容易漏。用WHEN 4 | 6 | 9 | 11 一条搞定少三行也少三个出错机会。days的范围锁死在 28~31综合工具能据此优化比较器位宽。日模块拿到days之后判断条件就从固定的 31 变成dayt days。这里必须警惕一个初值问题如果dayt声明成0 TO 31且初值为 0而显示模块直接把dayt送上数码管上电瞬间会看到“0 日”。所以日计数器的声明范围应该是1 TO 31或者在送出显示前做一次if dayt 0 then 1。原文的dayt声明是0 to 31这就是个需要修的隐患。3.4 年与闰年mod 4只是最低配的闰年判断年模块的输出有两样年份本身和一个run闰年标志。原文用(yeart mod 4) 0判断能过“年份能被 4 整除”这一关但严格来说整百年要能被 400 整除才是闰年1900 年不是闰年2000 年是。课程设计层面用mod 4通常被接受但如果这个设计要真用就得补全。-- 完整闰年判断能被 4 整除且不能被 100 整除 或 能被 400 整除 PROCESS(yeart) BEGIN IF (yeart MOD 4 0 AND yeart MOD 100 / 0) OR (yeart MOD 400 0) THEN run 1; ELSE run 0; END IF; END PROCESS;mod在综合时会被展开成一堆比较器和减法器资源消耗不算小。如果只做两位数年份0~99其实只有 0、4、8……96 这些值满足mod 4 0等价于判断低两位是不是 00写法是yeart(1 DOWNTO 0) 00综合出来几乎不占资源。这种“数学上等价、硬件上便宜”的替换是把课设代码往产品方向推时最该养成的习惯。还有个坑必须提原文年模块的yeart声明成0 TO 59而year端口声明是0 TO 99两个范围对不上。仿真时可能因为信号没被赋值而看不出来上板后年份一到 59 就回绕显示直接跳变。声明范围和端口范围必须一致这是 VHDL 里少有的“工具不一定报错但一定出事”的地方。4. 显示与按键四位数码管怎么装下六组信息显示模块是这份资源里最厚的一块也最值得逐段读。它同时干三件事用一个状态机决定当前看哪个界面、用另一个状态机做数码管动态扫描、把按键事件翻译成对各计数模块的加减控制信号。原文里闹钟和星期的逻辑也塞在这块所以文件显得臃肿。4.1 界面状态机五个界面轮流切key0每按一次界面往后走一格循环五格时钟时分→ 月日 → 年 → 秒表 → 闹钟。这个切换用两段式状态机写最稳一段管状态寄存一段管次态译码。TYPE state_type IS (s_time, s_date, s_year, s_stopwatch, s_alarm); SIGNAL pre_s, next_s : state_type : s_time; -- 第一段状态寄存key0 的下降沿触发 PROCESS(key0) BEGIN IF falling_edge(key0) THEN pre_s next_s; END IF; END PROCESS; -- 第二段次态译码 PROCESS(pre_s) BEGIN CASE pre_s IS WHEN s_time next_s s_date; WHEN s_date next_s s_year; WHEN s_year next_s s_stopwatch; WHEN s_stopwatch next_s s_alarm; WHEN s_alarm next_s s_time; END CASE; END PROCESS;两段式的价值在于状态寄存那段只碰时钟和复位次态译码那段是纯组合逻辑可以单独跑波形验证跳转顺序对不对。如果写成一段式if rising_edge then case pre_s is ... pre_s next一旦某个分支漏写综合工具会推出锁存器上板表现是“按键有时候不响应”很难查。key1管的是“当前界面里设置哪一项”。在时钟界面它切“时/分”在月日界面切“月/日”在年界面只有年一项所以不切。原文用一个set : std_logic_vector(1 downto 0)循环 00→01→10→00本质上是个三态循环10那一档留给“退出设置”。这个设计能用但状态含义不直观改成枚举(none, item_a, item_b)的可读性会好很多。4.2 扫描状态机与段码表靠刷新率骗过眼睛四位数码管同一时刻只能亮一位要显示四位就得快速轮流点亮。人眼临界闪烁频率大概 50Hz实践上取 1kHz 左右的扫描时钟最舒服——既看不出闪亮度也够。CONSTANT seg0 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 11000000; -- 0 CONSTANT seg1 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 11111001; -- 1 CONSTANT seg2 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 10100100; -- 2 CONSTANT seg3 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 10110000; -- 3 CONSTANT seg4 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 10011001; -- 4 CONSTANT seg5 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 10010010; -- 5 CONSTANT seg6 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 10000010; -- 6 CONSTANT seg7 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 11111000; -- 7 CONSTANT seg8 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 10000000; -- 8 CONSTANT seg9 : STD_LOGIC_VECTOR(7 DOWNTO 0) : 10010000; -- 9 TYPE scan_state IS (c1, c2, c3, c4); SIGNAL pre_c, next_c : scan_state : c1; -- 扫描状态推进clk 用 1000Hz 那一路 PROCESS(clk) BEGIN IF rising_edge(clk) THEN pre_c next_c; END IF; END PROCESS; PROCESS(pre_c) BEGIN CASE pre_c IS WHEN c1 next_c c2; WHEN c2 next_c c3; WHEN c3 next_c c4; WHEN c4 next_c c1; END CASE; END PROCESS;段码表是共阳极编码0对应亮段为低电平。这里必须核对板子如果数码管是共阴极整张表要按位取反否则上电全是乱码。这个“翻车”现场太常见了很多人调一晚上以为是扫描逻辑错其实是段码极性反了。扫描状态只决定“现在亮第几位”真正的数据选择在另一个组合进程里根据pre_c和pre_s一起决定这一位该显示什么。比如时钟界面下c1/c2显示小时c3/c4显示分钟月日界面下c1/c2显示月c3/c4显示日。这种“界面 × 位序”的二维选择用嵌套CASE最直白缺点是代码长好处是任何一屏显示什么一眼能查到。原文里有一批拆分十位个位的语句其中有hourhhourh/10;这种自己给自己赋值的写法明显是笔误应该是hourh hour / 10;、hourl hour MOD 10;。这类错误仿真时表现是高位恒为 0 或恒定值上板就是“小时只显示个位”必须在跑仿真前逐行核一遍。4.3 按键消抖一次按下只出一个脉冲机械按键按下和松开时会有几毫秒到几十毫秒的抖动直接接到计数器上按一次可能加好几下。消抖的做法很多状态机是其中行为最可控的一种。TYPE db_state IS (idle, press_debounce, held, release_debounce); SIGNAL pre_d, next_d : db_state : idle; SIGNAL cnt20ms : INTEGER RANGE 0 TO 999_999; -- 50MHz 下 20ms PROCESS(clk) BEGIN IF rising_edge(clk) THEN CASE pre_d IS WHEN idle IF key_in 0 THEN next_d press_debounce; cnt20ms 0; ELSE next_d idle; END IF; WHEN press_debounce IF cnt20ms 999_999 THEN IF key_in 0 THEN next_d held; ELSE next_d idle; END IF; ELSE cnt20ms cnt20ms 1; next_d press_debounce; END IF; WHEN held IF key_in 1 THEN next_d release_debounce; cnt20ms 0; ELSE next_d held; END IF; WHEN release_debounce IF cnt20ms 999_999 THEN IF key_in 1 THEN next_d idle; ELSE next_d held; END IF; ELSE cnt20ms cnt20ms 1; next_d release_debounce; END IF; END CASE; END IF; END PROCESS; -- 只在进入 held 的那一拍输出一个时钟周期的高脉冲 key_pulse 1 WHEN (pre_d press_debounce AND next_d held) ELSE 0;四个状态各管一段idle等按下press_debounce数满 20ms 确认还按着才算数held等松开release_debounce数满 20ms 确认真松开了才回idle。这样按住不放也只会输出一次脉冲天然做到“按一次加一”。cnt20ms的上界 999999 对应 50MHz 下 20ms板子时钟换成 25MHz 就改成 499999。key_pulse用组合逻辑从状态跳变里译出宽度是一个clk周期。这个信号就是 3.2 节里key_add该接的东西。如果直接把消抖后的电平接到计数器按住一秒能加上千万这是新手最容易忽略的时序配合问题。4.4 闹钟、整点报时与星期 LED闹钟本质是一个相等比较器当前时和分同时等于设定的闹钟值时拉高beep。整点报时则是“分等于 0 且秒小于 3”这类条件响几秒然后自动停。两者共用一个蜂鸣器输出用OR合并最省事。PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF (hour alarm_hour) AND (cent alarm_min) AND (sec 5) THEN beep 1; -- 闹钟响 5 秒 ELSIF (cent 0) AND (sec 2) THEN beep 1; -- 整点报时响 2 秒 ELSE beep 0; END IF; END IF; END PROCESS;星期用 7 个独立 LED 显示思路是记录一个“基准日 天数偏移”。设某年某月某日是星期一之后每过一个日进位计数器在 1~7 之间循环再把 1~7 译成 7 位独热码输出到 LED。原文的ledweekend初值写成0000010是独热码而不是二进制编码接线时务必对清楚哪一位对应星期一。闹钟设置界面下key2/key3的加减要作用到alarm_hour和alarm_min上而不是主时钟。这就是 4.1 节状态机的实际用处——所有按键动作都先看pre_s是哪个界面再决定把加减信号发给谁。原文把这套分发逻辑全写在显示模块里导致模块端口上挂了一长串centkeyadd/centkeysub/hourkeyadd/...。能工作但接口太胖改成“显示模块输出一个 4 位的按键事件编码 一个界面前缀”顶层会更干净。5. 避坑与排查几个会让波形看着对、板子跑不对的地方下面这几条不是理论推演是这类设计在上板调试阶段最常撞上的具体现象。每条按“现象 → 原因 → 解决”写遇到对应症状可以直接对号入座。现象数码管显示全乱或者某几位是黑块。原因段码极性搞反或者位选有效电平搞反。共阳数码管段码低电平点亮位选可能是高有效也可能是低有效取决于板上有没有加三极管驱动。 解决先写一个最简单的测试顶层只让四位数码管同时显示1234跑通再往里加数据。这一步花十分钟能省掉后面两小时的怀疑人生。现象时间走得不准一天差好几分钟。原因分频系数算错或者分频链里某一级用了错误的时钟源。比如第二级本该接 1000Hz却接回了 50MHz。 解决把每一级的clk输入和co输出都拉出来接到 LED 上逐级看频率。50MHz 分频到 1000Hz 用示波器测周期应该是 1ms差一个数量级就说明接错线。现象按键按一次加好几个数或者按住不动一直加。原因消抖输出的是一段电平而不是单周期脉冲或者振荡器在rising_edge里被当成使能每个时钟都采一次。 解决按 4.3 节改成状态机 单周期脉冲并在计数模块入口加上边沿检测做双重保险。我一般会在仿真里专门跑一个“按键按住 100ms”的用例波形上co只应该出现一次高电平。现象仿真波形完美综合后时序报告一堆警告上板功能时好时坏。原因把co这类组合译码出来的脉冲拿去当时钟用rising_edge(co)或者if/elsif链条有分支没覆盖全综合出了锁存器。 解决全设计只允许clk一个时钟沿co只作使能。所有组合process的第一句先给所有输出赋默认值再进CASE保证每条路径都有赋值。综合报告里出现latch inferred就必须改到没有。现象日期显示到月底不翻月或者二月直接跳到三月一日。原因日模块的上限没有跟着月模块的days变或者那个跨模块的yue信号接错了端口。原文里日模块有个yue输入、月模块有个yue输出名字一样但方向容易在顶层连反。 解决顶层连线时把信号名统一加上方向前缀比如days_from_month别让同名信号在两端飘。日模块的判断条件写成dayt days_from_month并且dayt的声明范围必须是1 TO 31。6. 二次开发把课设代码改成能真用的东西这份代码最大的价值不在功能在骨架——分频链、计数链、两个状态机、消抖这五样凑齐了剩下的都是往里填肉。下面几条是我拿它继续改时会优先动的三个地方。第一是把integer换成unsigned或std_logic_vector。integer在仿真里好用但综合后位宽由工具推跨平台移植容易出意外尤其是0 TO 99和0 TO 59混用时。统一改成固定位宽秒、分用 6 位时用 5 位日用 5 位月用 4 位年用 7 位顶层端口和内部信号就完全对齐了。第二是给每个计数模块加一个load通道替代现在“靠按键一路加减”的方式。设置时间时直接输入一个数值一次写入比按住加号等它从 0 数到 23 体验好得多IF load 1 THEN centt load_value; -- load_value 来自拨码开关或上位机 ELSIF seco 1 THEN ...这个改动会让设置模块的复杂度下降一个档次因为不用再区分“加”“减”两条路径界面状态机只需要发一个load脉冲。第三是把显示模块的数据选择部分抽成一张查找表。现在是一堆嵌套CASE加一个界面就要动好几处。换成“先算出 4 个待显示的十进制数字再统一扫描”的两级结构界面切换只影响前级扫描逻辑永远不变。改完之后加一个“倒计时”界面只需要十几行。改造点改动量收益integer→ 定宽向量全文件替换跨平台可移植资源可控加load直写通道每个计数模块 5 行设置时间从“按住等”变“一次写入”显示两级拆分重写display.vhd新增界面成本降到几十行闰年判断补 100/4001 个process从“课程设计能过”变“能真用”验证方法很简单每改一处先跑一次行为仿真把 59 秒→0 秒、23 时→0 时、12 月→1 月、闰年 2 月 28→29 这四个边界用例全过一遍再综合上板。这四个边界覆盖了这条链上 90% 的进位错误我后来做任何带日期的设计都先跑这四个波形跑不过就不往下写。最后说个血泪教训我第一次调这类设计时把co直接接到了下一级的clk上仿真完全正常——因为仿真器不 care 时钟质量——上板后时间忽快忽慢查了整整两天才在综合报告的时钟树警告里看到问题。从那以后我每次新起一个工程第一件事就是把所有rising_edge()里的信号名 grep 一遍只要不是顶层那个clk立刻改。这个习惯帮我省下的时间比任何调试技巧都多。希望帮到你。本文还有配套的精品资源点击获取