
简介一份基于FPGA的闹钟系统课程设计资料包面向数字逻辑与EDA课程设计学生及FPGA入门开发者解决带闹钟功能的24小时计时器设计与验证需求。工程基于Vivado环境使用Verilog硬件描述语言编写包含时间显示、按键消抖、时间与闹钟设置、蜂鸣器到点提示等功能模块并提供了约束文件与配置比特流能够直接在开发板上演示完整闹钟流程。资源包共包含122个文件其中有16个Verilog源文件以及综合实现后的.dcp结果、最终.bit配置、仿真报告、自动运行脚本等覆盖工程设计全流程压缩包整体仅1.48MB结构紧凑、层次清晰便于学习与复用。目前已有546人浏览学习适合需要快速完成课程设计或复习FPGA基础流程的读者。通过该工程可以掌握自顶向下模块划分、按键输入处理、数码管动态扫描及蜂鸣器控制等典型技巧并可直接在Vivado中打开工程、运行综合实现并生成比特流用于答辩演示与功能扩展。1. 为什么基于FPGA的闹钟系统比单片机方案更值得做很多人听到“基于FPGA的闹钟系统设计”第一反应是杀鸡用牛刀——51单片机加个DS1302跑蜂鸣器不香吗但如果把这个题目拆开看你会发现它其实是一个非常好的数字逻辑训练载体计时、进位、BCD转换、动态扫描、按键消抖、状态机调度这些恰好覆盖了数字电路设计里的核心模块。项目标题里的“闹钟系统”不是目的重点是“用FPGA的方式”实现一个有时序、有交互、有输出外设的完整系统。相比单片机里按顺序执行指令FPGA的所有模块都在并行运行秒计数和数码管扫描互不干扰这种并行思维才是做这个题目真正想让你建立的。适合正在学习Verilog或者VHDL、想从仿真走向上板的工程师也适合准备竞赛作品、想快速做一个能演示的硬件系统的人。2. 基于FPGA的闹钟时钟源选型RTC芯片与计数器逻辑2.1 RTC芯片接线与I2C读写时序做基于FPGA的闹钟系统第一个要决策的是时间从哪里来。常见做法有两种一是用实时时钟芯片比如DS3231或DS1302二是用FPGA内部计数分频。RTC芯片的典型优势是断电后靠纽扣电池继续走时而且出厂精度较高DS3231在0到40摄氏度范围内年误差大约在±2分钟左右不需要每次上电都重新校时。缺点是FPGA需要模拟I2C或者SPI时序去读写寄存器和RAM这部分逻辑本身就要写不少代码。我一般会优先推荐DS3231因为它自带温度补偿晶振省去了外部32.768kHz晶振匹配的问题。接线上只需要SDA、SCL两根线外加I2C地址0x68。在Verilog里实现I2C主机时关键是SCL高电平期间SDA不能变化START和STOP除外这是I2C总线最基本也最容易写错的地方。下面是一个简化版的写寄存器状态机片段localparam IDLE 3d0; localparam START 3d1; localparam ADDR_PH 3d2; // 发送器件地址写位 localparam REG_ADDR 3d3; // 发送寄存器地址 localparam DATA_PH 3d4; // 发送数据字节 localparam STOP 3d5; reg [7:0] byte_buf; reg sda_out; reg scl_out; always (posedge clk) begin case (state) START: begin sda_out 1b0; // 在SCL高电平期间拉低SDA产生起始条件 scl_out 1b1; // 确保从机检测到高→低的跳变 state ADDR_PH; end ADDR_PH: begin // 每个bit在SCL低电平期间更新SDA高电平期间由从机采样 if (bit_cnt 8) begin sda_out byte_buf[7 - bit_cnt]; if (scl_rise) bit_cnt bit_cnt 1b1; end end endcase end这里的核心思路是每个SCL上升沿前SDA必须已经稳定所以要先在SCL为低时把数据放到SDA上再拉高SCL。很多第一次写I2C的人上来就对着时序图逐周期模拟结果不是SDA毛刺就是从机无应答。更实际的做法是先做一个bit级状态机再在上面套字节级状态机最后才是寄存器读写状态机三层各自独立出问题也好定位。2.2 用FPGA内部计数器生成时钟分频、校时与进位如果不想引入外部芯片用FPGA内部的时钟分频来生成秒脉冲也是一条完全可走的路。开发板上的晶振常见为50MHz要得到1Hz的秒脉冲就是对50_000_000做分频。分频系数是50M/2再减1因为要翻转一次才产生半个周期。需要注意计数器位宽至少要25位2^25 33M够用用32位更稳妥。reg [31:0] clk_cnt; reg tick_1hz; always (posedge clk_n or negedge rst_n) begin if (!rst_n) begin clk_cnt 32d0; tick_1hz 1b0; end else if (clk_cnt 32d24_999_999) begin clk_cnt 32d0; tick_1hz ~tick_1hz; // 翻转即产生秒脉冲 end else begin clk_cnt clk_cnt 1b1; end end上面这段代码产生的不是单个时钟周期的脉冲而是50%占空比的方波。如果后面接闹钟判定逻辑最好把tick_1hz再打一拍用边沿检测来捕获“刚进入下一秒”而不是用电平直接触发计数。校时功能也在这个层级做在秒计数基础上加一个key_adust信号每按一次秒值加一或者做成按住连续加速。一个容易被忽略的坑是分频计数器的比较值改变时要重新复位否则可能有半个周期的相位差。2.3 时钟源选择的3个关键参数对比对比维度RTC芯片方案内部计数分频方案断电续航支持纽扣电池供电走时掉电丢失重新校时精度高温补晶振±2ppm受晶振频率温度漂移影响逻辑复杂度需实现I2C读写分频逻辑简单但校时需要更多按键处理适用场景完整产品雏形教学实验、基本原理验证选型时不必纠结哪个更高级真实需求决定方案。如果这个基于FPGA的闹钟系统要连续跑几天做演示RTC会让现场更有说服力如果只是课堂验收内部计数已经完全够用。我个人建议两种都做默认用内部计数同时预留I2C接口代码里留一个sel_rtc信号切换。3. FPGA计时核心BCD计数、进位逻辑与动态数码管扫描显示3.1 秒分时的BCD计数器设计要点闹钟显示时间需要用十进制而二进制计数器到9之后要立刻回0并进位。十进制显示最直观的实现是BCD计数也就是每一位独立用4bit表示0到9。秒分时三层结构完全一样低4位是BCD个位高4位是BCD十位。以秒计数为例秒个位从0计到9在9的下一秒回0且产生进位秒十位从0计到5在5的下一秒回0且向分钟进位。reg [3:0] sec_low; reg [3:0] sec_high; reg carry_to_min; always (posedge clk_n or negedge rst_n) begin if (!rst_n) begin sec_low 4d0; sec_high 4d0; carry_to_min 1b0; end else if (clk_n) begin if (sec_low 4d9) begin sec_low 4d0; if (sec_high 4d5) begin sec_high 4d0; carry_to_min 1b1; // 一分钟过去产生分钟进位脉冲 end else begin sec_high sec_high 1b1; carry_to_min 1b0; end end else begin sec_low sec_low 1b1; carry_to_min 1b0; end end end需要注意这里的clk_n是秒脉冲不是全局时钟。把秒脉冲当使能用而不是时钟沿才符合异步设计的规范。如果直接把秒脉冲接到另一个计数器时钟脚会产生跨时钟域的亚稳态风险。code里carry_to_min本质上就是秒计数器回绕时刻的一个脉冲信号分钟计数器只需要把它当使能信号即可。3.2 数码管动态扫描刷新与消影时序四个数码管如果每个都单独接8根段选线占用FPGA引脚太多。动态扫描的做法是所有数码管共用段选线用位选信号轮流点亮只要扫描频率足够快人眼就看到四个都在亮。常用扫描频率是1kHz到2kHz之间也就是每个数码管每隔4ms刷新一次。频率太低会明显闪烁太高则切换太快数码管还没来得及稳定就被切走亮度下降。reg [1:0] scan_cnt; reg [3:0] bcd_data; reg [2:0] seg_sel; always (posedge clk or negedge rst_n) begin if (!rst_n) begin scan_cnt 2d0; seg_sel 3b111; end else begin scan_cnt scan_cnt 1b1; case (scan_cnt) 2d0: begin seg_sel 3b110; // 选中千位数码管 bcd_data sec_low; end 2d1: begin seg_sel 3b101; bcd_data sec_high; end 2d2: begin seg_sel 3b011; bcd_data min_low; end 2d3: begin seg_sel 3b111; bcd_data min_high; end endcase end end这里有一个所有做动态扫描都会踩的坑位选切换瞬间上一时刻的段码还没消失就会被下一时刻的段码覆盖结果看起来是低亮度或者有拖影。解决方式有两种一种先关闭所有位选送3b111再切换段码另一种是寄存器输出后打一拍再送位选。我更推荐前者逻辑直观且不会引入额外延迟。要审慎的是“消影”不能只做一次每一次位选切换前都必须执行否则隔一段时间就会出现一次微弱的幻影。3.3 进位脉冲的跨时钟域处理与毛刺过滤秒到分、分到时的进位本质上是一个脉冲信号它由秒脉冲域产生要被分钟计数器的同一个全局时钟捕获。如果全局时钟仍然是板级50MHz而秒脉冲是慢速使能信号那么只要保证信号有效时间超过一个时钟周期就不会丢脉冲。这里推荐的做法是把carry_to_min打两拍再做上升沿检测得到一个单周期的使能信号。reg carry_d1; reg carry_d2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin carry_d1 1b0; carry_d2 1b0; end else begin carry_d1 carry_to_min; carry_d2 carry_d1; end end wire carry_pulse carry_d1 ~carry_d2;第二个实际问题是按键产生的进位信号会有抖动毛刺。硬件上的机械按键在按下和释放的几十毫秒内电压会反复跳变如果不做消抖校时的时候按一下可能跳几秒。常见的过滤方法是20ms左右连续采样只有连续多个周期电平相同才确认状态变化。4. 按键消抖、闹铃判定与蜂鸣器驱动4.1 按键消抖与单脉冲生成的实现步骤基于FPGA的闹钟系统通常至少需要两个按键一个用于切换设置模式另一个用于加减数值。如果按键信号直接进计数器那么一次物理按下会产生多个跳变沿数字单脉冲生成电路也救不了它。准确的做法是先消抖再检测边沿最后输出一个单周期脉冲。第一步用全局时钟对按键信号打两拍消除亚稳态。第二步以1kHz的频率采样按键电平连续20个采样点相同才认为电平稳定实际消抖约20ms。第三步对稳定后的信号做上升沿检测产生一个时钟周期宽度的脉冲。第四步把脉冲接到状态机里控制当前选中的时间字段加一。这个流程每步单独验证不易出错。上板调的时候最常见的现象是按键偶尔失灵多半是消抖时间窗太短或者采样频率和消抖周期计算有误。reg [4:0] stable_cnt; reg key_stable; always (posedge clk_1k or negedge rst_n) begin if (!rst_n) begin stable_cnt 5d0; key_stable 1b1; end else begin if (key_in_raw key_stable) begin stable_cnt 5d0; // 电平未变化计数器清零 end else begin stable_cnt stable_cnt 1b1; if (stable_cnt 5d19) key_stable key_in_raw; end end end上面stable_cnt到19正好20次采样按1kHz就是20ms。注意key_in_raw进来之前一定要先做两级同步寄存否则直接用异步输入的按键信号做比较在电平跳变沿附近可能采样到亚稳态导致stable_cnt不稳定。4.2 闹钟到时判定逻辑与重复响铃周期到点判定在数码管显示基础上做就行把设定的闹钟小时、分钟与当前时间进行比较两者相等并且当前秒为0时触发蜂鸣器。这里需要防止一个问题——如果闹钟信号保持蜂鸣器会一直响所以一般要加一个“响铃持续时间”计数器比如响30秒后自动停止或者按任意键停止。一个更实用的设计是重复响铃蜂鸣器响3秒停2秒再响3秒直到手动关闭或持续一分钟。代码里用两个计数器tone_on_cnt和tone_off_cnt在设置模式下还能切换响铃模式常见的有单次响、重复响和SOS节奏响。SOS模式的实质是三种不同长度的间隔序列嘀嘀嘀0.2s x3—嗒嗒嗒0.6s x3—嘀嘀嘀0.2s x3每组之间有1秒静默。这比单纯方波驱动多一点细节在验收演示时也更容易让人看出设计不是流水线拼凑。4.3 蜂鸣器PWM驱动参数及分频设置蜂鸣器分有源和无源两类。有源蜂鸣器自带振荡电路给高电平就发声但声音频率固定无法通过调节频率改变音调。无源蜂鸣器需要外部提供PWM方波频率通常在2kHz到5kHz之间听起来最明显FPGA做这个功率绰绰有余关键是分频精度。localparam DIV_CNT 50000000 / 4000 / 2 - 1; // 50MHz产生4kHz方波 reg [13:0] pwm_cnt; reg beep_out; always (posedge clk or negedge rst_n) begin if (!rst_n) begin pwm_cnt 14d0; beep_out 1b0; end else if (beep_en) begin if (pwm_cnt DIV_CNT) begin pwm_cnt 14d0; beep_out ~beep_out; end else begin pwm_cnt pwm_cnt 1b1; end end else begin pwm_cnt 14d0; beep_out 1b0; end end上面这段代码节奏上就是一个简单的分频器比普通分频多了一个beep_en使能。注意DIV_CNT的计算方式是半周期对应的时钟数因为电平每翻转一次才是一个半周期。蜂鸣器驱动最后要加一个三极管或达林顿管FPGA引脚灌出来的电流不足以直接推动蜂鸣器否则声音会很轻。5. 上板验证行为仿真、引脚约束与调试技巧做一个基于FPGA的闹钟系统仿真能发现逻辑错误但真正的坑往往在上板后。先写一个testbench把时钟、复位、按键都模拟出来重点检查进位和闹钟触发边界module tb_alarm_clock; reg clk; reg rst_n; reg key_set; reg key_add; wire beep; alarm_clock dut ( .clk(clk), .rst_n(rst_n), .key_set(key_set), .key_add(key_add), .beep(beep) ); initial begin clk 0; rst_n 0; #100 rst_n 1; // 模拟校时按set进入调节按add加到设定值 #1000 key_set 1; #1000000 key_set 0; #2000 key_add 1; #1000000 key_add 0; end always #10 clk ~clk; // 50MHz一个周期20ns endmodule仿真的关键检查点有三个一是秒针到59跳0时分钟是否加1二是小时从23跳0时是否回绕到00三是闹钟设定时间到达时beep是否按预定的响铃节奏动作。这三个点错了就去查进位脉冲宽度和计数器使能逻辑对了再上板。板级验证时先不要接蜂鸣器用逻辑分析仪或者示波器看FPGA的引脚输出。我用过的最省钱的办法是用开发板自带的LED把闹钟触发信号临时拉到LED上如果LED定时亮灭就说明触发逻辑正常再接蜂鸣器不迟。引脚约束上最容易出错的是把按位分配的引脚顺序写错导致小数码管显示错位用XDC或者QSF文件里注释掉的功能对照检查还能省不少事。最后一个技巧是分频计数的阈值不要写死在代码里。调试时把1Hz秒脉冲临时改成1kHz也就是说分频系数改成50M/2k/2-1五分钟的验证变成几秒钟校时、闹钟到达全都能现场快速走一遍。确认逻辑无误再把分频系数改回来这能大幅缩短每次上板验证的等待时间。本文还有配套的精品资源点击获取