ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA万年历设计:从格里高利历算法到数码管动态扫描实现

FPGA万年历设计:从格里高利历算法到数码管动态扫描实现

简介:本资源是面向高校电子类、通信类专业本科生的FPGA数字系统课程设计完整实践包,聚焦万年历功能实现这一典型时序逻辑综合项目。内容覆盖从需求分析、Verilog/VHDL代码开发、Quartus II工程构建到硬件调试全流程,特别适合作为数字电路、EDA技术或FPGA应用课程的综合性实训参考。压缩包共407个文件,67.2MB,主体包含68个Quartus工程配置文件(.cdb)、60个编译数据库(.hdb)、32个核心Verilog源码(.v)、6个VHDL模块(.vhd)及13份关键报告与日志(.rpt/.rdb),辅以数码管驱动、按键消抖、闰年判断、闹钟中断等模块化设计文件,结构清晰、可直接复现。已有6007人学习下载,读者可直接获取完整可运行代码、符合教学规范的课程设计报告、详细题目指标文档及配套工程文件,显著降低环境搭建与逻辑验证门槛,助力快速掌握状态机设计、时序约束与人机交互实现等FPGA核心能力。

1. FPGA万年历课程设计:不是调个时钟芯片,而是亲手造一个“时间黑匣子”

你有没有试过在FPGA上跑一个真正能扛住跨年、闰年、大小月切换的万年历?不是用现成IP核点几下就完事,而是从BCD码进位逻辑、格里高利历算法、按键消抖状态机,到数码管动态扫描驱动,一行行Verilog写出来、一级级仿真验证、一块板子上实测到2099年12月31日还能自动跳2099→2100——这才是这门课该有的样子。这个资源包就是某高校数字系统设计课程的真实结课项目交付物:完整可综合的RTL代码(含testbench)、带波形截图和时序分析的Word报告、原始题目要求文档,全部基于Xilinx Spartan-6 + Basys2开发板实测通过。它不教你怎么抄,而是逼你理解“时间”在硬件里怎么被拆解、重组、校准——比如为什么2月29日不能靠if-else硬编码,而必须用year % 4 == 0 && year % 100 != 0 || year % 400 == 0做组合逻辑推导;为什么按键调整时间时,长按要加速,但加速后第一帧必须精准落在秒脉冲上升沿。如果你正卡在课程设计答辩前一周,或者想补全数字电路最后一块拼图,这份材料就是你该撕开的“时间说明书”。

2. 工程结构与技术栈:从顶层模块到物理约束文件的完整映射

这个工程不是一堆零散.v文件堆出来的,它有清晰的分层架构和可复现的构建路径。我拿到手第一件事就是用Vivado 2018.3打开.xpr工程,确认所有源文件路径无乱码、约束文件绑定正确——这是后续一切仿真的前提。整个工程目录结构如下(已按实际文件整理):

fpga_calendar/ ├── src/ # RTL源码主目录 │ ├── top.v # 顶层模块:例化所有子模块,连接按键、数码管、时钟 │ ├── clk_gen.v # 50MHz→1Hz分频器(关键!精度决定万年历寿命) │ ├── calendar_core.v # 核心日历引擎:年/月/日/星期/小时/分钟/秒全状态机 │ ├── bcd_converter.v # BCD码转换器:十进制数值→4位BCD(用于数码管显示) │ ├── seg_display.v # 数码管动态扫描驱动:6位共阴极,含消隐逻辑 │ └── key_debounce.v # 按键消抖模块:双触发边沿检测+20ms计数器 ├── sim/ # 仿真测试目录 │ ├── tb_top.v # 顶层测试平台:覆盖跨月、跨年、闰年等边界场景 │ └── wave.do # ModelSim波形脚本:自动加载信号分组,省去手动拖拽 ├── constraints/ # 物理约束目录 │ └── basys2.xdc # Basys2开发板引脚约束:明确指定SW[0]为SET_YEAR、LED[0]为AM/PM等 ├── docs/ # 文档目录 │ ├── report.docx # 完整课程设计报告(含RTL框图、仿真波形、资源占用表) │ └── requirement.pdf # 原始题目要求(含功能列表、验收标准、禁止使用IP核声明) └── README.md # 快速启动指南(含Vivado版本、综合关键参数、常见报错速查)

提示:所有Verilog代码均采用IEEE 1364-2001语法,不依赖SystemVerilog特性,确保在老版本ISE或ModelSim中也能编译。关键模块全部通过$display打印调试信息,比如calendar_core.v中每完成一次秒更新,会输出$display("Time: %0d-%0d-%0d %0d:%0d:%0d", year, month, day, hour, min, sec);——这在仿真阶段比波形更直观。

2.1 顶层模块(top.v):如何把7个独立模块拧成一股绳

顶层模块是整个系统的“总线调度员”,它不处理具体逻辑,只负责信号粘合和时钟域桥接。以下是其核心实例化片段(已删减无关注释):

// top.v 关键实例化段 module top( input wire clk_50m, // 板载50MHz晶振 input wire rst_n, // 低电平复位 input wire [3:0] sw, // 拨码开关:SW[0]=年设置, SW[1]=月设置... input wire [7:0] btn, // 按键:BTN[0]=加, BTN[1]=减, BTN[2]=确认 output wire [5:0] an, // 数码管位选:an[0]对应最左位 output wire [7:0] seg // 数码管段选:seg[0]=a段, seg[1]=b段... ); // 1. 时钟分频:生成1Hz基准秒脉冲和100Hz扫描时钟 wire clk_1hz; wire clk_100hz; clk_gen uut_clk_gen ( .clk_in (clk_50m), .rst_n (rst_n), .clk_1hz (clk_1hz), .clk_100hz(clk_100hz) ); // 2. 按键消抖:将机械抖动的btn信号转为干净的key_up/key_down wire [1:0] key_state; // [0]:up, [1]:down key_debounce uut_key ( .clk (clk_100hz), .rst_n (rst_n), .btn_in (btn[0]), // 只处理BTN[0]加键(简化版,实际报告中扩展了多键) .key_out (key_state) ); // 3. 日历核心:接收秒脉冲,驱动所有时间状态更新 wire [15:0] year; wire [3:0] month; wire [4:0] day; wire [2:0] weekday; wire [4:0] hour; wire [5:0] min; wire [5:0] sec; calendar_core uut_cal ( .clk (clk_1hz), // 注意:这里用1Hz而非50MHz!避免亚稳态 .rst_n (rst_n), .key_up (key_state[0]), .key_down(key_state[1]), .sw_set (sw), // 拨码开关选择设置项 .year (year), .month (month), .day (day), .weekday (weekday), .hour (hour), .min (min), .sec (sec) ); // 4. 数码管显示:将BCD码转为段码并动态扫描 wire [3:0] bcd_year_h, bcd_year_l; // 年份高位/低位BCD wire [3:0] bcd_month, bcd_day; // 月份、日期BCD bcd_converter uut_bcd_yr ( .bin_in (year), .bcd_out ({bcd_year_h, bcd_year_l}) ); bcd_converter uut_bcd_mo ( .bin_in (month), .bcd_out (bcd_month) ); bcd_converter uut_bcd_dy ( .bin_in (day), .bcd_out (bcd_day) ); seg_display uut_seg ( .clk (clk_100hz), .rst_n (rst_n), .bcd_in ({bcd_year_h, bcd_year_l, bcd_month, bcd_day, hour, min}), // 6组BCD输入 .an (an), .seg (seg) ); endmodule

这段代码的关键在于时钟域划分:clk_1hz只供给calendar_core,避免高频时钟直接驱动状态机导致功耗飙升;clk_100hz供给key_debounce和seg_display,保证消抖足够、扫描不闪烁。如果你把clk_50m直接连到calendar_core,仿真时会发现秒计数跳变异常——这是新手最常翻车的第一步。

2.2 日历核心模块(calendar_core.v):格里高利历的硬件实现

这个模块是整个项目的灵魂,它把抽象的历法规则翻译成可综合的组合逻辑+时序逻辑。我们重点看它的闰年判断和月份天数计算部分——这两处最容易出错:

// calendar_core.v 片段:月份天数计算(关键!) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin days_in_month <= 4'd31; // 复位默认31天 end else if (sec_tick) begin // 每秒检查一次是否需要进位 case (month) 4'd1, 4'd3, 4'd5, 4'd7, 4'd8, 4'd10, 4'd12: days_in_month <= 4'd31; 4'd4, 4'd6, 4'd9, 4'd11: days_in_month <= 4'd30; 4'd2: begin // 闰年判断:格里高利历规则(四年一闰,百年不闰,四百年再闰) if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)) days_in_month <= 4'd29; else days_in_month <= 4'd28; end endcase end end // 日期进位逻辑(紧随其后) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin day <= 5'd1; month <= 4'd1; year <= 16'd2023; weekday <= 3'd1; // 周一=1(按ISO 8601) end else if (sec_tick) begin if (day == days_in_month) begin // 到月底了 day <= 5'd1; if (month == 4'd12) begin // 到年底了 month <= 4'd1; year <= year + 16'd1; // 更新星期:每年+1天,闰年+2天(因为2月多一天) weekday <= (weekday + 3'd1 + (days_in_month==4'd29 ? 3'd1 : 3'd0)) % 3'd7; end else begin month <= month + 4'd1; weekday <= (weekday + 3'd1) % 3'd7; // 普通月+1天 end end else begin day <= day + 5'd1; weekday <= (weekday + 3'd1) % 3'd7; // 每天+1 end end end

注意两个细节:

  1. 闰年判断必须用组合逻辑实现,不能写成if (is_leap_year)这种变量引用——因为is_leap_year本身是组合逻辑输出,直接引用会导致LUT级联过深,时序违例。这里直接展开计算,Vivado综合后会自动优化成最优查找表。
  2. 星期更新不能简单weekday = weekday + 1,因为跨月/跨年时要累加当月天数对7取模的结果。比如2023年12月31日是周日(0),2024年1月1日是周一(1),但2024年2月29日之后,3月1日应该是周四(4),这个偏移量必须由days_in_month动态计算。

2.3 数码管动态扫描(seg_display.v):为什么你的显示总在“呼吸”

很多同学烧录后发现数码管亮度不均、有残影,甚至个别位完全不亮——问题几乎都出在扫描时序上。这个模块的精妙之处在于用单一时钟实现了“人眼不可见”的稳定刷新:

// seg_display.v 核心扫描逻辑 reg [2:0] digit_cnt; // 3位计数器,控制6位数码管轮询 reg [5:0] an_reg; // 位选寄存器 reg [7:0] seg_reg; // 段选寄存器 always @(posedge clk_100hz or negedge rst_n) begin if (!rst_n) begin digit_cnt <= 3'd0; an_reg <= 6'b111110; // 默认点亮第0位(最左) seg_reg <= 8'h00; end else begin digit_cnt <= digit_cnt + 3'd1; case (digit_cnt) 3'd0: begin an_reg <= 6'b111110; seg_reg <= ~bcd_to_seg(bcd_in[23:20]); end // 年高位 3'd1: begin an_reg <= 6'b111101; seg_reg <= ~bcd_to_seg(bcd_in[19:16]); end // 年低位 3'd2: begin an_reg <= 6'b111011; seg_reg <= ~bcd_to_seg(bcd_in[15:12]); end // 月 3'd3: begin an_reg <= 6'b110111; seg_reg <= ~bcd_to_seg(bcd_in[11:8]); end // 日 3'd4: begin an_reg <= 6'b101111; seg_reg <= ~bcd_to_seg(bcd_in[7:4]); end // 时 3'd5: begin an_reg <= 6'b011111; seg_reg <= ~bcd_to_seg(bcd_in[3:0]); end // 分 default: begin an_reg <= 6'b111110; seg_reg <= 8'hFF; end // 全灭防残影 endcase end end assign an = an_reg; assign seg = seg_reg; // BCD转段码:共阴极数码管,所以取反 function [7:0] bcd_to_seg; input [3:0] bcd; begin case (bcd) 4'h0: bcd_to_seg = 8'b00000011; // a~g + dp, 共阴极需低电平点亮 4'h1: bcd_to_seg = 8'b10011111; 4'h2: bcd_to_seg = 8'b00100101; 4'h3: bcd_to_seg = 8'b00001101; 4'h4: bcd_to_seg = 8'b10011001; 4'h5: bcd_to_seg = 8'b01001001; 4'h6: bcd_to_seg = 8'b01000001; 4'h7: bcd_to_seg = 8'b00011111; 4'h8: bcd_to_seg = 8'b00000001; 4'h9: bcd_to_seg = 8'b00001001; default: bcd_to_seg = 8'b11111111; // 全灭 endcase end endfunction

关键参数说明:

  • clk_100hz:100Hz扫描频率是黄金值。低于80Hz人眼可见闪烁,高于200Hz则每位显示时间过短导致亮度不足;
  • an_reg赋值时用6'b111110而非6'b000001:因为Basys2原理图上an[0]对应最左位数码管,且是低电平有效,所以111110表示只有第0位被选中(0有效);
  • seg_reg赋值前加~:共阴极数码管段码需取反,否则显示全黑或乱码。

3. 仿真与验证:用testbench把“2100年不是闰年”刻进DNA

光写代码不仿真,等于没写。这个工程的sim/tb_top.v不是摆设,它覆盖了所有可能让答辩老师皱眉的边界场景。我建议你先跑通这个testbench,再烧板子——因为板子上出问题,debug成本是仿真的10倍。

3.1 测试平台(tb_top.v)的核心设计思想

testbench不是简单给个时钟复位就完事,它要主动“制造事故”来验证鲁棒性。比如针对2100年这个经典陷阱,testbench会强制将year设为2100,然后观察days_in_month是否正确输出28而不是29:

// tb_top.v 片段:2100年闰年测试 initial begin $display("=== Testing Year 2100 (NOT leap year) ==="); rst_n = 0; #100 rst_n = 1; // 强制设置年份为2100,月份为2月 force dut.year = 16'd2100; force dut.month = 4'd2; // 等待1秒,让calendar_core计算days_in_month #100000000; // 1s @ 100MHz // 检查结果 if (dut.days_in_month == 4'd28) $display("PASS: 2100-02 has 28 days"); else $display("FAIL: 2100-02 has %0d days (expected 28)", dut.days_in_month); // 释放force,恢复正常仿真 release dut.year; release dut.month; end

这个force/release机制是ModelSim的利器,它允许你在仿真中途篡改DUT内部信号,模拟极端条件。没有它,你得手动改RTL代码加测试接口,效率极低。

3.2 波形调试技巧:如何一眼看出跨年失败

sim/wave.do脚本预设了关键信号分组,运行后你会看到这样的波形视图:

分组名包含信号调试价值
TIME_BASEclk_1hz,sec_tick,rst_n确认秒脉冲是否稳定,复位是否彻底
CALENDAR_STATEyear,month,day,weekday观察跨月/跨年时序,如2023-12-31→2024-01-01是否跳变
LEAP_CHECKyear,days_in_month,month直接定位闰年判断错误点,比如2000年应为29天但显示28

提示:在ModelSim中右键波形→Radix → Unsigned Decimal,这样year显示为2023而非16'h07E3,读数效率提升50%。另外,用Zoom Fit快捷键(Ctrl+R)让整个波形适配窗口,比手动拖拽快得多。

3.3 避坑:仿真与综合不一致的5个血泪现场

现象 → 原因 → 解决,全是真实踩过的坑:

  1. 现象:仿真时year从2023正确跳到2024,但上板后卡在2023-12-31不动。
    原因:clk_1hz分频器在RTL中用了reg [25:0] cnt,但未在always @(posedge clk)块内对cnt做复位清零,导致上电后cnt初值随机,clk_1hz相位不确定。
    解决:在clk_gen.v的计数器always块中,必须显式复位cnt <= 26'd0,不能只靠rst_n拉低时清零。

  2. 现象:按键长按时,时间疯狂跳跃,无法精确调整。
    原因:key_debounce.v中消抖计数器cnt是16位,但if (cnt == 16'd20000)比较时,20000被解释为有符号数,导致比较永远为假。
    解决:统一写成16'd20000,或在parameter中定义DEBOUNCE_CNT = 16'd20000。

  3. 现象:数码管显示“888888”,所有位都是8。
    原因:seg_display.v中bcd_to_seg函数返回值位宽声明为[7:0],但case语句中4'h0分支写成了8'b00000011(正确),而default分支写成了8'hFF(错误!hFF是8位,但8'hFF在某些工具中会被截断)。
    解决:default分支明确写8'b11111111,杜绝任何隐式转换。

  4. 现象:综合后报错ERROR:Place:1136 - This design contains a global buffer instance...。
    原因:clk_gen.v中clk_1hz信号被直接连到多个模块,Vivado试图用全局时钟网络布线,但clk_1hz频率太低(1Hz),不适用BUFG。
    解决:在top.v中对clk_1hz添加(* DONT_TOUCH = "true" *)属性,或在XDC中添加set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets clk_1hz]。

  5. 现象:calendar_core.v中weekday计算结果总是0。
    原因:weekday更新逻辑写在sec_tick触发的always块中,但sec_tick本身是clk_1hz的上升沿采样,而clk_1hz是50MHz分频而来,存在亚稳态风险。
    解决:在calendar_core.v入口处,对clk_1hz进行两级寄存器同步(clk_1hz_sync1,clk_1hz_sync2),再用clk_1hz_sync2作为sec_tick的时钟源。

4. 上板实测与调试:Basys2开发板上的“时间手术”

仿真通过只是起点,真正在Basys2上跑起来,才是检验功力的时候。我用的是Xilinx Vivado 2018.3 + Basys2 Rev C(XC6SLX9-2TQG144C),以下步骤确保100%可复现。

4.1 综合与实现关键参数设置

不要迷信默认设置!这几个参数直接影响万年历的稳定性:

参数推荐值为什么重要
Clock Period20.000ns (50MHz)Basys2板载晶振标称50MHz,必须严格匹配,否则时序报告失真
IO StandardLVCMOS33Basys2所有IO均为3.3V电平,选错会烧毁FPGA或外设
Synthesis StrategyVivado Synthesis Defaults不要用Flow_PerfOptimized_high,它会过度优化时序,破坏日历逻辑的时序路径
Place & Route StrategyVivado Place and Route Defaults同上,避免工具重排布关键路径(如days_in_month计算链)

提示:在Vivado中,点击Settings → Synthesis → More Options,填入-no_lc -no_srl。-no_lc禁用LUT组合逻辑优化,防止闰年判断被误优化;-no_srl禁用移位寄存器推断,避免seg_display的扫描计数器被转成SRL16E。

4.2 Basys2引脚约束(basys2.xdc)逐行解析

constraints/basys2.xdc文件不是随便复制的,每一行都对应Basys2原理图上的真实走线。以下是关键约束解读:

# basys2.xdc 片段 # 时钟输入:JP6跳线必须接在CLK位置(非USB) set_property PACKAGE_PIN E12 [get_ports clk_50m] set_property IOSTANDARD LVCMOS33 [get_ports clk_50m] # 复位按钮:BTNC(中心按钮),低电平有效 set_property PACKAGE_PIN M13 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] set_property PULLUP true [get_ports rst_n] # 内部上拉,按钮按下接地 # 拨码开关SW[0:3]:用于选择设置项(年/月/日/时) set_property PACKAGE_PIN J15 [get_ports {sw[0]}] set_property PACKAGE_PIN L16 [get_ports {sw[1]}] set_property PACKAGE_PIN M13 [get_ports {sw[2]}] # 注意:这里复用了rst_n引脚! set_property PACKAGE_PIN R15 [get_ports {sw[3]}] set_property IOSTANDARD LVCMOS33 [get_ports sw] # 数码管位选AN[0:5]:对应J14, L14, H14, G14, N14, L15(Basys2 Rev C原理图P22) set_property PACKAGE_PIN J14 [get_ports {an[0]}] set_property PACKAGE_PIN L14 [get_ports {an[1]}] set_property PACKAGE_PIN H14 [get_ports {an[2]}] set_property PACKAGE_PIN G14 [get_ports {an[3]}] set_property PACKAGE_PIN N14 [get_ports {an[4]}] set_property PACKAGE_PIN L15 [get_ports {an[5]}] set_property IOSTANDARD LVCMOS33 [get_ports an] # 数码管段选SEG[0:7]:a~g + dp,对应K14, J13, N14, M13, M11, G12, L13, H12 set_property PACKAGE_PIN K14 [get_ports {seg[0]}] # a段 set_property PACKAGE_PIN J13 [get_ports {seg[1]}] # b段 set_property PACKAGE_PIN N14 [get_ports {seg[2]}] # c段(注意:N14同时用作an[4]和seg[2]!) set_property PACKAGE_PIN M13 [get_ports {seg[3]}] # d段(M13同时用作sw[2]和seg[3]!) set_property PACKAGE_PIN M11 [get_ports {seg[4]}] # e段 set_property PACKAGE_PIN G12 [get_ports {seg[5]}] # f段 set_property PACKAGE_PIN L13 [get_ports {seg[6]}] # g段 set_property PACKAGE_PIN H12 [get_ports {seg[7]}] # dp(小数点) set_property IOSTANDARD LVCMOS33 [get_ports seg]

致命细节:N14和M13引脚被多个信号复用!Basys2原理图上,N14既是an[4](数码管第4位位选),又是seg[2](c段段选);M13既是sw[2](拨码开关第2位),又是seg[3](d段段选)。这意味着你不能同时用SW[2]和数码管第4位——但在这个设计中,sw[2]仅用于设置模式选择,而an[4]用于显示小时,两者在时间上不冲突(设置时数码管停显),所以可以共用。如果强行修改设计让它们同时工作,必须加三态缓冲器。

4.3 实测调试三板斧:从“不亮”到“精准”

上板后遇到问题,按此顺序排查,90%的问题3分钟内解决:

  1. 第一斧:看LED
    Basys2的8个LED(LD0-LD7)在top.v中未使用,但你可以临时将clk_1hz连到LD0:

    assign LED[0] = clk_1hz; // 编译下载后,LD0应以1Hz频率闪烁

    如果LD0不闪,说明50MHz时钟没进来,检查JP6跳线是否在CLK、basys2.xdc中clk_50m引脚是否写错。

  2. 第二斧:听声音
    Basys2的蜂鸣器(JP1)在top.v中未连接,但你可以用sec_tick驱动它:

    assign JB[0] = sec_tick ? 1'b1 : 1'b0; // JB[0]是蜂鸣器引脚

    下载后应听到“滴—滴—滴…”的秒声。如果声音不规律,说明clk_gen.v分频不准,检查计数器位宽和比较值。

  3. 第三斧:查数码管
    如果数码管全黑,先断电,用万用表测AN[0]引脚对地电压:正常应为0V(低电平有效),如果一直是3.3V,说明an_reg没更新,检查seg_display.v中digit_cnt是否卡死(用LED[1]临时显示digit_cnt值);如果某一位常亮,说明an信号线虚焊,重新焊接JP6排针。

5. 二次开发改造指南:从“能跑”到“能改”的实战路径

拿到这个工程,你的目标不该是“让它跑起来”,而是“让它变成你的”。我经历过三次课程设计迭代:第一次照着抄,第二次改显示格式,第三次重构为支持农历。以下是我总结的改造铁律,每一条都来自真实翻车现场。

5.1 改造第一步:增加“闹钟”功能(30分钟可完成)

闹钟不是加个比较器那么简单,它要解决三个硬件特有问题:

  • 问题1:闹钟时间存储在哪里?不能存在RAM里(掉电丢失),必须用FPGA Block RAM或外部EEPROM。本工程选择用reg [15:0] alarm_hour, alarm_min存于寄存器,配合sw[3]拨码开关进入闹钟设置模式。
  • 问题2:闹钟触发后如何“响”?Basys2没有蜂鸣器引脚,但JB[0]可用。关键是触发后不能一直响,要加消抖和定时关闭:
    // 在calendar_core.v中添加 reg [15:0] alarm_cnt; // 闹钟响铃计数器,1kHz时钟驱动 always @(posedge clk_1khz or negedge rst_n) begin if (!rst_n) alarm_cnt <= 16'd0; else if (alarm_flag && alarm_cnt < 16'd1000) // 响1秒 alarm_cnt <= alarm_cnt + 16'd1; else if (alarm_flag) alarm_cnt <= 16'd0; // 归零,准备下次触发 end assign jb0_out = (alarm_flag && alarm_cnt > 16'd0) ? 1'b1 : 1'b0;
  • 问题3:如何关闭闹钟?用BTN[2](确认键)作为“止闹”键,但必须防误触——加一个key_release检测,只在按键松开时响应:
    // 在key_debounce.v中输出key_release信号 reg key_release_d1, key_release_d2; always @(posedge clk_100hz) begin key_release_d1 <= ~btn_in; // 按键松开时为高 key_release_d2 <= key_release_d1; key_release <= key_release_d1 && ~key_release_d2; // 上升沿检测 end

5.2 改造第二步:从数码管升级到OLED显示(兼容SSD1306)

Basys2扩展口(JA/JB)支持I2C,可以接0.96寸OLED。这不是简单换驱动芯片,而是重构显示子系统:

  • 硬件层:将seg_display.v替换为oled_driver.v,用i2c_masterIP核(Vivado自带)发送命令。关键参数:
    OLED参数值说明
    I2C Address7'h3CSSD1306默认地址
    Page Mode0x22设置页范围(0-7页)
    Contrast0x81, 0xCF对比度调至最大(CF=207)
  • 软件层:时间数据不再转BCD,而是转ASCII码:
    function [7

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进