
简介这份VHDL多功能数字万年历课程设计文档面向电子信息、集成电路及相关专业学生与FPGA入门学习者围绕数字时钟与万年历的硬件描述语言实现提供从设计思路到程序分析的完整参考。压缩包内仅含1个docx文件约775KB以实验报告形式组织包含基本要求与发挥要求、系统模块划分、程序调试记录、代码分析及实验总结便于按章节理解整体方案。内容覆盖分频器、计数、设置、控制与显示模块并讨论数码管驱动与扫描输出、按键消抖状态机、显示控制状态机及RTL图发挥部分还涉及整点报警、闹钟、闰月检测和星期指示。已有836人学习适合需要完成VHDL课程设计、核对模块实现或查漏补缺的读者参考。1. 项目定位四位数码管怎么装下年月日时分秒拿到一块开发板用 VHDL 做一个数字时钟万年历是很多人绕不开的一关。真正的难点不在写计数器而在资源约束板载时钟 50MHz显示只有四位数码管加 7 个独立 LED按键一共四个。年月日时分秒十二位数字四位数码管一次最多摆四位这一关过不去代码写得再对也出不来效果。这份资源给出的解法很直接用 key0 在时钟显示、月日、年、秒表、闹钟五个界面之间轮转key1 在当前界面里切换要设置的是哪一项key2、key3 负责加减秒脉冲走板上独立 LED 做 1Hz 闪烁星期用 7 个 LED 点亮对应位置。整份设计是纯 VHDL 行为级描述七个分频计数模块加一个显示控制模块没有 IP 核没有厂商库依赖丢进任何支持 VHDL-93 的综合工具都能从头跑一遍。适合两类人一类是要在两周内交一份能演示的课程设计另一类是想把状态机、分频链、按键消抖这几件事在一个完整工程里串起来看的人。2. 架构拆解从 50MHz 晶振到秒脉冲的七级分频链整体结构一句话能说完一颗晶振一条分频链七个计数器一个显示调度器。但这条链上每一级的进位信号怎么传、传多宽、下游什么时候采决定了这套代码上板之后是走时准确还是每天慢几秒。先把链路画清楚再谈代码。2.1 七级分频50MHz 一路切到年月日链路是串行的。第一级 count 把 50MHz 切成 1kHz 的节拍第二级 second 在 1kHz 上累计 1000 次得到 1 秒同时输出一个 1Hz 方波给板上独立 LED 做秒闪秒进位往后是分 cent、时 hour、日 day、月 month、年 year每一级只认上一级给出的进位脉冲。library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity count is port( clk : in std_logic; -- 50MHz 板载晶振 co : out std_logic -- 每 50000 个周期给出一个单拍高电平 ); end count; architecture behav of count is -- 关键范围必须覆盖到 49999写小了综合会直接截位 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; -- 高电平只维持一个时钟周期 else cnt cnt 1; co 0; end if; end if; end process; end behav;这段是整个工程的地基。cnt的位宽由范围推导0 to 49999需要 16 位正好装得下 49999如果误写成0 to 2综合器按两位处理cnt 49999这个比较永远不成立后面所有计数器全部静止现象是数码管上电后一个数都不动。co之所以只在归零那一拍拉高是为了让下游能把它当使能脉冲用而不是当时钟用。第二级 second 拿到 1kHz 使能后累计 1000 次得到一秒architecture behav of second is signal sect : integer range 0 to 59 : 0; signal ms : integer range 0 to 999 : 0; -- 毫秒计数 begin process(clk) begin if rising_edge(clk) then if en1k 1 then -- 只在 1kHz 使能那一拍推进 if ms 999 then ms 0; if sect 59 then sect 0; co 1; -- 秒进位送给分钟模块 else sect sect 1; co 0; end if; else ms ms 1; end if; end if; end if; end process; -- 前 500ms 亮、后 500ms 灭得到标准 1Hz 方波 miao 1 when ms 500 else 0; sec sect; end behav;miao这根线直接接板上独立 LED不用另做分频用毫秒计数值的高低各半就能凑出占空比 50% 的秒闪。ms的上界是 999 而不是 1000是因为计数从 0 起算0 到 999 一共 1000 个状态正好一秒。2.2 进位脉冲的宽度决定了下游能不能采到这套结构里最容易翻车的地方不是分频系数算错而是进位脉冲的宽度和采样时机对不上。所有模块共用同一个clk进位信号co在时钟上升沿被写成1下一个上升沿又被写回0所以它只维持完整的一个时钟周期。下游模块在同一个rising_edge(clk)里判断if sec_en 1读到的是上一个沿之后稳定下来的值正好能采到。这条链能走通靠的就是「单拍脉冲 同一时钟域」这两个前提。一旦有人把上游的co直接当下游的clk用问题就来了-- 不推荐把进位当第二时钟 u_min : entity work.cent port map(clk c_sec, seco 1, ...);这样写仿真能过综合也能过但c_sec走的是普通布线资源而不是全局时钟网络几十级串联之后偏斜累积某些工艺角下分钟模块可能漏采一拍。我一般会把所有模块的时钟端口统一接到系统时钟进位信号只当使能u_min : entity work.cent port map(clk clk, sec_en c_sec, ...);多说一句分频系数。50MHz 切 1kHz 理论上是 50000 分频代码里判cnt 49999是从 0 数到 49999 共 50000 个状态这个写法没错错的是判cnt 50000而范围只到 49999比较恒假整条链又停摆。2.3 顶层只做连线一个实体一个文件工程按实体拆文件count.vhd、second.vhd、cent.vhd、hour.vhd、day.vhd、month.vhd、year.vhd、display.vhd再加一个顶层calendar_top.vhd。顶层不写任何逻辑只做端口映射和信号声明RTL 图上看就是七个小方块串成一条链最后汇进显示模块。architecture top of calendar_top is signal c_1k, c_sec, c_min, c_hour, c_day, c_mon : std_logic; signal s_sec, s_min : integer range 0 to 59; signal s_hour : integer range 0 to 23; signal s_day : integer range 1 to 31; signal s_month : integer range 1 to 12; signal s_year : integer range 0 to 99; signal s_yue : integer range 1 to 31; -- 当月天数month 反馈给 day signal s_run : std_logic; -- 闰年标志year 反馈给 month begin u_div : entity work.count port map(clk clk, co c_1k); u_sec : entity work.second port map(clk clk, en1k c_1k, sec s_sec, co c_sec, miao sec_led); u_min : entity work.cent port map(clk clk, sec_en c_sec, cent s_min, co c_min, ...); u_hour: entity work.hour port map(clk clk, min_en c_min, hour s_hour, co c_hour, ...); u_day : entity work.day port map(clk clk, hour_en c_hour, yue s_yue, day s_day, co c_day, ...); u_mon : entity work.month port map(clk clk, day_en c_day, run s_run, month s_month, yue s_yue, co c_mon, ...); u_year: entity work.year port map(clk clk, mon_en c_mon, year s_year, run s_run, ...); u_dis : entity work.display port map(clk clk, sec s_sec, cent s_min, hour s_hour, day s_day, month s_month, year s_year, key0 key(0), key1 key(1), key2 key(2), key3 key(3), ...); end top;注意s_yue和s_run是两根反向流动的信号日期模块需要知道这个月有几天月份模块需要知道今年是不是闰年。这两根线在 RTL 图上画出来像是回环综合器不会报错因为它们走的是纯组合路径不构成时序环。3. 计数器模块拆解分、时、日、月、年怎么各自回绕七个计数器看着重复实际上每级的回绕边界都不同分和秒是 59时是 23日是当月天数月是 12年是 99。真正需要想清楚的是按键加减和进位使能挤在同一个 process 里的时候谁优先。3.1 分和小时一个 process 里三条互斥路径分钟模块有三条驱动路径上一级的进位、key2 加、key3 减。三条路径互斥用 if-elsif 串起来优先级从高到低。process(clk) begin if rising_edge(clk) then if sec_en 1 then -- 路径一秒进位优先级最高 if mint 59 then mint 0; co 1; else mint mint 1; co 0; end if; elsif key_add 0 then -- 路径二按键加低有效 if mint 59 then mint 0; else mint mint 1; end if; co 0; -- 手动模式下必须清掉进位 elsif key_sub 0 then -- 路径三按键减 if mint 0 then mint 59; else mint mint - 1; end if; co 0; else co 0; -- 空闲时也要给 co 赋值避免锁存 end if; end if; end process;几个参数值得单独说。mint的范围是0 to 59回绕点写 59 而不是 60因为计数从 0 开始59 是最后一个合法值。co在每一条分支里都被显式赋值这一条很关键——如果只在进位分支里写co 1其余分支不写综合器会推断出一个锁存器上板表现为进位信号有时候莫名其妙一直高分钟飞快地跳。小时的逻辑和分钟几乎一样只有两处不同范围是0 to 23回绕点是 23 而不是 59。if min_en 1 then if hourt 23 then hourt 0; co 1; else hourt hourt 1; co 0; end if; elsif key_add 0 then if hourt 23 then hourt 0; else hourt hourt 1; end if; elsif key_sub 0 then if hourt 0 then hourt 23; else hourt hourt - 1; end if; end if;设置的时候按一下是加一小时按住不放会连续加这是电平触发的自然结果配合消抖模块的单拍脉冲就没问题如果消抖没做好按住一秒能加几百个小时最终停在哪个值完全随机。3.2 日与月用一根 yue 总线把天数反馈回去日期模块的难点在于每个月的天数不固定。这份设计的做法是让月份模块算出一个当月天数yue直接连到日期模块的输入端日期模块拿yue当回绕边界。日期模块的核心判断if hour_en 1 then if dayt yuet then -- 到了本月最后一天 dayt 1; -- 回到 1 号不是 0 号 co 1; else dayt dayt 1; co 0; end if; elsif key_add 0 then if dayt yuet then dayt 1; else dayt dayt 1; end if; elsif key_sub 0 then if dayt 1 then dayt yuet; else dayt dayt - 1; end if; end if;yuet是从yue端口引进来的信号范围1 to 31。这里有个初始化细节dayt上电后是 0第一天的判断dayt yuet不成立于是加 1 变成 1从第二天开始才正常。上电瞬间数码管上会闪过一个「0 日」虽然一闪而过但设置模式下停下来的时候会看到观感不好所以我把dayt的初值写成了 1。月份模块一边自增一边算天数process(clk) begin if rising_edge(clk) then if day_en 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; -- 天数查表纯组合 process(montht, run) begin case montht is when 2 if run 1 then yue 29; else yue 28; end if; when 4 | 6 | 9 | 11 yue 30; when others yue 31; end case; end process;run是闰年标志从年份模块引过来。二月的分支单独拎出来其他小月合并成4 | 6 | 9 | 11剩下的全是大月。这种写法比罗列十二个分支清晰综合出来的逻辑也更省。3.3 年模块与闰年判断年份的边界是两位十进制 0 到 99。回绕点写 99同时要输出一个run给月份模块用。process(clk) begin if rising_edge(clk) then if mon_en 1 then if yeart 99 then yeart 0; else yeart yeart 1; end if; elsif key_add 0 then if yeart 99 then yeart 0; else yeart yeart 1; end if; elsif key_sub 0 then if yeart 0 then yeart 99; else yeart yeart - 1; end if; end if; end if; end process; -- 能被 4 整除即为闰年 run 1 when (yeart mod 4) 0 else 0;yeart mod 4直接落在组合赋值上综合成两位取模逻辑资源开销很小。严格讲闰年规则里还有整百年不闰、四百年再闰的例外但在两位年份的语境下00 对应的是能被 400 整除的整百年按规则就该闰所以这个判断反而是对的。年份信号的原稿里有个隐患yeart声明成0 to 59回绕点也写的 59。这会导致年份显示到 59 之后回 0而不是到 99 才回。综合器会把范围外的高位截掉数值一旦超界就变成未定义行为。范围改到0 to 99之后一切都对上了。4. 显示与控制模块五界面状态机、段码扫描与按键消抖显示和控制压在同一个display.vhd里原因很实际按键的语义跟着界面走key1 在时:分界面切换的是时和分在月日界面切换的是月和日拆成两个文件反而要来回传状态。4.1 五个界面怎么用一个按键轮转界面用枚举类型定义五个状态key0 每按一次跳一页type state_type is (s0, s1, s2, s3, s4); signal pre_s : state_type : s0; process(clk) begin if rising_edge(clk) then if key0_pulse 1 then -- 消抖后的单拍脉冲 case pre_s is when s0 pre_s s1; -- 时:分 when s1 pre_s s2; -- 月:日 when s2 pre_s s3; -- 年 when s3 pre_s s4; -- 秒表 when others pre_s s0; -- 闹钟 end case; end if; end if; end process;关键点在key0_pulse。如果直接用if key0 0判断电平按住按键的每 20ns 都会跳一次页面松手的时机决定你最终停在哪一页。用消抖模块产生一个只维持一拍的脉冲按一次跳一步行为就确定下来了。每个界面要从十二个数字里挑四位用一个小组合进程做选择process(pre_s, s_hour, s_min, s_day, s_month, s_year) begin case pre_s is when s0 d1 s_hour / 10; d2 s_hour mod 10; d3 s_min / 10; d4 s_min mod 10; when s1 d1 s_month / 10; d2 s_month mod 10; d3 s_day / 10; d4 s_day mod 10; when s2 d1 0; d2 0; d3 s_year / 10; d4 s_year mod 10; when others d1 0; d2 0; d3 0; d4 0; end case; end process;/10和mod 10在整数类型上是综合器直接支持的运算十进制的十位和个位就靠这两句拆出来。这里有个血泪教训十位拆分必须从原始计数值算不能写成hourh hourh / 10;这种自引用那个表达式的意思是从hourh算出hourh综合器会优化成保持原值十位数字永远停在初始状态。设置模式用另一个两位向量控制key1 在 0、1、2 之间循环0 表示不设置1 和 2 分别对应当前界面的两个变量process(clk) begin if rising_edge(clk) then if key1_pulse 1 then if set 2 then set 0; else set set 1; end if; end if; end if; end process;set的值不进计数模块只在显示层决定哪两位闪烁或者输出加减使能。这样设计的好处是计数模块完全不知道设置模式的存在接口保持干净。4.2 段码表与四位动态扫描共阳数码管的段码表用常量数组写死0 到 9 十个值constant SEG0 : std_logic_vector(7 downto 0) : 11000000; constant SEG1 : std_logic_vector(7 downto 0) : 11111001; constant SEG2 : std_logic_vector(7 downto 0) : 10100100; constant SEG3 : std_logic_vector(7 downto 0) : 10110000; constant SEG4 : std_logic_vector(7 downto 0) : 10011001; constant SEG5 : std_logic_vector(7 downto 0) : 10010010; constant SEG6 : std_logic_vector(7 downto 0) : 10000010; constant SEG7 : std_logic_vector(7 downto 0) : 11111000; constant SEG8 : std_logic_vector(7 downto 0) : 10000000; constant SEG9 : std_logic_vector(7 downto 0) : 10010000;八位里高四位是段选低位分别是小数点和几个备用位共阳极是低电平点亮所以11000000表示 a、b、c、d、e、f 六段亮g 段和 dp 灭显示出来是 0。扫描输出用 1kHz 左右的频率轮转四位process(clk) begin if rising_edge(clk) then if scan_en 1 then -- 约 1ms 一次的扫描节拍 case scan_cnt is when 00 dis_sel 1110; dis seg_of(d1); when 01 dis_sel 1101; dis seg_of(d2); when 10 dis_sel 1011; dis seg_of(d3); when others dis_sel 0111; dis seg_of(d4); end case; scan_cnt scan_cnt 1; end if; end if; end process;dis_sel是四位位选低电平有效1110表示点亮最左边那一位。段码和位选在同一个时钟沿一起更新两者都是寄存器输出切换是同步的不会出现某一位用着上一位的段码这种串影。扫描频率定在 1kHz人眼完全看不出闪烁如果扫到 100Hz 以下视觉上就会明显抖。4.3 按键消抖状态机与单拍脉冲机械按键按下去的几十毫秒内电平会来回弹直接进计数器就是灾难。用四状态机过滤type kstate is (idle, debounce, press, release); signal k_pre, k_next : kstate : idle; process(clk) begin if rising_edge(clk) then case k_pre is when idle if key_in 0 then k_pre debounce; end if; when debounce if key_in 0 then k_pre press; else k_pre idle; end if; when press if key_in 1 then k_pre release; end if; when release k_pre idle; end case; end if; end process; -- 只在 press 状态输出一个周期的脉冲 key_pulse 1 when k_pre debounce and k_next press else 0;debounce状态再等一拍才确认用来跳过抖动最剧烈的那一段。press状态里按键还保持低电平但不产生新脉冲所以长按只会输出一次本文还有配套的精品资源点击获取