用Verilog在FPGA上复刻一个复古数字钟:从分频到报时的完整实现
用Verilog在FPGA上复刻一个复古数字钟:从分频到报时的完整实现
复古电子产品总能勾起人们对技术发展历程的怀念。在数字技术高度发达的今天,用现代FPGA技术重现经典数字钟的体验,不仅是一次技术实践,更是一场穿越时空的对话。本文将带你从零开始,用Verilog语言在FPGA上构建一个完整的数字钟系统,涵盖时钟分频、BCD码计数、数码管显示和整点报时等核心功能模块。
1. 复古数字钟的核心设计理念
复古数字钟的魅力在于其简洁的数字逻辑实现方式。与现代智能设备不同,这类产品通常采用纯硬件逻辑实现所有功能,不依赖任何软件或操作系统。这种设计理念决定了我们需要用最基本的数字电路模块来构建整个系统。
复古设计的三个关键特征:
- 纯同步数字电路实现
- 有限状态机控制逻辑
- 硬件直接驱动显示器件
在FPGA上复刻这种设计时,我们需要特别注意保持原始设计的"数字味道",避免引入过于现代的解决方案。例如,使用简单的分频器而非PLL来生成1Hz时钟信号,采用BCD计数器而非二进制计数器等。
2. 时钟分频:从50MHz到1Hz
任何数字钟的核心都是一个精确的1Hz时钟信号。在FPGA项目中,我们通常需要将板载的高频时钟(如50MHz)分频到这个低频。
2.1 基本分频器设计
最简单的分频器实现方式是使用计数器:
reg [25:0] counter; // 足够计数50,000,000次 reg clk_1Hz; always @(posedge clk_50MHz) begin if (counter == 26'd49_999_999) begin counter <= 0; clk_1Hz <= ~clk_1Hz; end else begin counter <= counter + 1; end end这种实现虽然简单,但在实际应用中可能会遇到两个问题:
- 计数器位宽较大,消耗较多逻辑资源
- 1Hz信号的占空比可能不精确
2.2 改进型分频方案
更专业的实现会采用两级分频:
// 第一级:50MHz -> 1kHz reg [15:0] counter1; reg clk_1kHz; always @(posedge clk_50MHz) begin if (counter1 == 16'd24_999) begin counter1 <= 0; clk_1kHz <= ~clk_1kHz; end else begin counter1 <= counter1 + 1; end end // 第二级:1kHz -> 1Hz reg [9:0] counter2; reg clk_1Hz; always @(posedge clk_1kHz) begin if (counter2 == 10'd499) begin counter2 <= 0; clk_1Hz <= ~clk_1Hz; end else begin counter2 <= counter2 + 1; end end这种分级设计不仅减少了最大计数器的位宽,还便于生成其他中间频率信号,如用于数码管扫描的1kHz时钟。
3. 时间计数逻辑的实现
数字钟的时间计数系统由秒、分、时三个计数器级联构成,每个计数器都采用BCD编码。
3.1 BCD计数器设计
BCD计数器与普通二进制计数器的区别在于,它需要在计到9后归零并产生进位,而不是在15(对于4位计数器)时归零。
reg [3:0] seconds_units; // 秒个位 reg [2:0] seconds_tens; // 秒十位 reg [3:0] minutes_units; // 分个位 reg [2:0] minutes_tens; // 分十位 reg [3:0] hours_units; // 时个位 reg [1:0] hours_tens; // 时十位 always @(posedge clk_1Hz or posedge reset) begin if (reset) begin // 复位所有计数器 seconds_units <= 0; seconds_tens <= 0; minutes_units <= 0; minutes_tens <= 0; hours_units <= 0; hours_tens <= 0; end else begin // 秒计数逻辑 if (seconds_units == 4'd9) begin seconds_units <= 0; if (seconds_tens == 3'd5) begin seconds_tens <= 0; // 触发分钟进位 end else begin seconds_tens <= seconds_tens + 1; end end else begin seconds_units <= seconds_units + 1; end // 分钟计数逻辑(类似结构) // 小时计数逻辑(需要考虑24小时制) end end3.2 时间调整功能
复古数字钟通常提供两个按钮来调整时间:一个调整小时,一个调整分钟。实现时需要注意按键消抖:
// 按键消抖模块 module debounce ( input clk, input button_in, output reg button_out ); reg [19:0] counter; always @(posedge clk) begin if (button_in != button_out) begin if (counter == 20'd999_999) begin button_out <= button_in; counter <= 0; end else begin counter <= counter + 1; end end else begin counter <= 0; end end endmodule // 在顶层模块中实例化消抖模块 debounce hour_adj_deb ( .clk(clk_1kHz), .button_in(hour_button), .button_out(hour_button_debounced) ); debounce min_adj_deb ( .clk(clk_1kHz), .button_in(min_button), .button_out(min_button_debounced) );4. 显示系统设计
复古数字钟通常使用七段数码管显示时间。在FPGA实现中,我们需要处理数码管的动态扫描和段码生成。
4.1 数码管动态扫描
为了减少引脚使用,多个数码管通常采用动态扫描方式驱动:
reg [2:0] scan_counter; reg [7:0] digit_select; always @(posedge clk_1kHz) begin scan_counter <= scan_counter + 1; case (scan_counter) 3'd0: digit_select <= 8'b11111110; // 第一个数码管 3'd1: digit_select <= 8'b11111101; // 第二个数码管 // ... 其他数码管选择 endcase end4.2 七段译码器
将BCD数字转换为七段显示码:
function [6:0] seg7; input [3:0] digit; begin case (digit) 4'd0: seg7 = 7'b0111111; 4'd1: seg7 = 7'b0000110; 4'd2: seg7 = 7'b1011011; 4'd3: seg7 = 7'b1001111; 4'd4: seg7 = 7'b1100110; 4'd5: seg7 = 7'b1101101; 4'd6: seg7 = 7'b1111101; 4'd7: seg7 = 7'b0000111; 4'd8: seg7 = 7'b1111111; 4'd9: seg7 = 7'b1101111; default: seg7 = 7'b0000000; endcase end endfunction5. 整点报时功能
复古数字钟的整点报时通常采用LED闪烁或简单的蜂鸣音。我们可以设计一个状态机来控制报时过程。
5.1 报时状态机
localparam NORMAL = 2'b00; localparam COUNTDOWN = 2'b01; localparam ALARM = 2'b10; reg [1:0] state; reg [3:0] countdown_counter; reg alarm_output; always @(posedge clk_1Hz) begin case (state) NORMAL: begin if (minutes_tens == 3'd5 && minutes_units == 4'd9 && seconds_tens == 3'd5) begin state <= COUNTDOWN; countdown_counter <= 4'd5; end end COUNTDOWN: begin if (countdown_counter > 0) begin countdown_counter <= countdown_counter - 1; alarm_output <= ~alarm_output; // 闪烁 end else begin state <= ALARM; end end ALARM: begin if (seconds_tens == 3'd0 && seconds_units == 4'd0) begin state <= NORMAL; alarm_output <= 0; end else begin alarm_output <= ~alarm_output; // 继续闪烁 end end endcase end5.2 蜂鸣器驱动
如果需要声音报时,可以添加简单的蜂鸣器驱动:
reg [15:0] tone_counter; reg buzzer; always @(posedge clk_1kHz) begin if (alarm_output) begin if (tone_counter == 16'd500) begin tone_counter <= 0; buzzer <= ~buzzer; // 产生1kHz方波 end else begin tone_counter <= tone_counter + 1; end end else begin buzzer <= 0; end end6. 系统集成与调试
将各个模块集成到顶层模块中时,需要注意信号命名的一致性和时钟域的划分。在Quartus Prime中编译时,可能会遇到以下典型问题:
- 时序约束问题:对于跨时钟域的信号,需要添加适当的约束
- 资源利用率过高:优化计数器位宽和状态机编码
- 按键响应不灵敏:调整消抖计数器参数
调试时可以分阶段进行:
- 首先验证时钟分频器是否产生准确的1Hz信号
- 然后测试BCD计数器是否正确计数
- 接着验证显示系统是否能正确显示时间
- 最后测试整点报时功能
7. 复古风格的增强设计
为了进一步增强复古感,可以考虑以下设计元素:
- LED辉光效果模拟:通过PWM调节LED亮度,模拟老式LED的发光特性
- 数码管渐暗效果:在切换数字时添加短暂的渐暗过渡
- 机械开关音效:用蜂鸣器模拟调整时间时的"咔嗒"声
这些增强功能虽然不影响核心计时功能,但能大大提升产品的复古氛围和用户体验。
