1. FPGA数字秒表设计概述
数字秒表作为基础计时设备,在体育赛事、实验室测量和工业控制等领域有着广泛应用。基于FPGA的实现方案相比传统单片机方案具有显著优势:计时精度可达1ms甚至更高,响应速度更快(硬件并行执行),且可通过修改Verilog代码灵活调整功能。
这个项目使用Xilinx Artix-7系列FPGA开发板(如Basys3)作为硬件平台,主要实现以下核心功能:
- 基本计时功能:启动/暂停/复位控制
- 高精度计时:1ms分辨率
- 计次功能:记录分段计时时间
- 六位数码管显示:显示格式为"分:秒:毫秒"
2. 系统架构设计
2.1 整体模块划分
系统采用自顶向下的设计方法,主要分为以下功能模块:
- 时钟分频模块:将50MHz系统时钟分频为1kHz计时时钟
- 计时控制模块:处理按键输入,生成控制信号
- 核心计时模块:实现毫秒、秒、分钟的计数和BCD转换
- 显示驱动模块:数码管动态扫描显示
- 计次存储模块:记录和切换多个计时结果
各模块通过标准接口连接,顶层模块负责协调数据流和控制信号。这种模块化设计便于单独调试和功能扩展。
2.2 关键接口定义
-
时钟与复位:
- clk_50m:50MHz系统时钟输入
- rst_n:低电平有效的异步复位信号
-
用户输入:
- key_start:启动/暂停按键
- key_reset:复位按键
- key_lap:计次按键
-
显示输出:
- seg[7:0]:数码管段选信号(共阳)
- sel[5:0]:数码管位选信号(低有效)
3. 时钟分频模块实现
3.1 分频原理与计算
FPGA开发板通常提供50MHz的系统时钟,而我们需要1kHz(1ms周期)的计时时钟。分频系数计算如下:
code复制分频系数 = 系统时钟频率 / 目标频率 = 50,000,000 / 1,000 = 50,000
由于计数器从0开始计数,实际计数值应为50,000-1=49,999。当计数器达到49,999时产生时钟翻转。
3.2 Verilog实现代码
verilog复制module clk_div(
input clk_50m, // 50MHz系统时钟
input rst_n, // 异步复位(低有效)
output reg clk_1k // 1kHz输出时钟
);
reg [15:0] cnt; // 分频计数器(需16位)
parameter CNT_MAX = 16'd49999; // 分频系数-1
always @(posedge clk_50m or negedge rst_n) begin
if (!rst_n) begin
cnt <= 16'd0;
clk_1k <= 1'b0;
end else if (cnt == CNT_MAX) begin
cnt <= 16'd0;
clk_1k <= ~clk_1k; // 时钟翻转
end else begin
cnt <= cnt + 1'b1;
end
end
endmodule
3.3 实际调试经验
-
占空比调整:上述代码产生50%占空比的1kHz时钟。若需要单周期脉冲,可修改为:
verilog复制always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) clk_1k <= 1'b0; else clk_1k <= (cnt == 16'd0); // 仅在第0个周期输出高电平 end -
资源优化:对于Artix-7 FPGA,16位计数器会使用约16个LUT。如果资源紧张,可改用DSP48E1硬核实现大位宽计数器。
4. 计时核心模块设计
4.1 计时逻辑实现
计时模块需要处理三个时间单位:毫秒(0-999)、秒(0-59)和分钟(0-59)。采用三级级联计数器结构:
verilog复制module timer(
input clk_1k, // 1kHz计时时钟
input rst_n, // 异步复位
input start, // 启动信号
input stop, // 停止信号
input reset, // 复位信号
output [23:0] time_bcd // BCD码输出:{分(8位),秒(8位),毫秒(8位)}
);
reg [9:0] ms_cnt; // 毫秒计数(0-999)
reg [5:0] sec_cnt; // 秒计数(0-59)
reg [5:0] min_cnt; // 分计数(0-59)
reg running; // 运行状态标志
// 状态控制逻辑
always @(posedge clk_1k or negedge rst_n) begin
if (!rst_n) begin
running <= 1'b0;
ms_cnt <= 10'd0;
sec_cnt <= 6'd0;
min_cnt <= 6'd0;
end else if (reset) begin
running <= 1'b0;
ms_cnt <= 10'd0;
sec_cnt <= 6'd0;
min_cnt <= 6'd0;
end else if (start) begin
running <= 1'b1;
end else if (stop) begin
running <= 1'b0;
end
// 计时逻辑(仅当running=1时计数)
if (running) begin
if (ms_cnt < 10'd999) begin
ms_cnt <= ms_cnt + 1'b1;
end else begin
ms_cnt <= 10'd0;
if (sec_cnt < 6'd59) begin
sec_cnt <= sec_cnt + 1'b1;
} else begin
sec_cnt <= 6'd0;
if (min_cnt < 6'd59) {
min_cnt <= min_cnt + 1'b1;
} else begin
min_cnt <= 6'd0; // 59分59秒999毫秒后归零
end
end
end
end
end
// 二进制转BCD码
assign time_bcd[23:20] = min_cnt / 10; // 分的十位
assign time_bcd[19:16] = min_cnt % 10; // 分的个位
assign time_bcd[15:12] = sec_cnt / 10; // 秒的十位
assign time_bcd[11:8] = sec_cnt % 10; // 秒的个位
assign time_bcd[7:4] = ms_cnt / 100; // 毫秒的百位
assign time_bcd[3:0] = (ms_cnt % 100) / 10; // 毫秒的十位
endmodule
4.2 BCD码转换技巧
二进制到BCD码的转换有多种实现方式:
- 除法取余法(如上代码):适合位数少的情况,但会综合出多个除法器,消耗较多LUT资源
- 移位加3算法:适合多位转换,只需组合逻辑
- 查找表法:使用ROM存储转换结果,适合FPGA实现
对于毫秒的十位计算,采用(ms_cnt % 100)/10而非(ms_cnt/10)%10,可以节省一个除法器。
5. 按键控制与消抖设计
5.1 机械按键的抖动问题
机械按键在按下和释放时会产生5-20ms的抖动,导致FPGA误判为多次按键。解决方案包括:
- 硬件消抖:RC低通滤波电路
- 软件消抖:延时检测(更灵活,本设计采用)
5.2 Verilog消抖实现
verilog复制module debounce(
input clk_50m, // 50MHz时钟
input button_in, // 原始按键输入
output reg button_out // 消抖后输出
);
reg [19:0] cnt; // 20位计数器(50MHz下1ms=50,000周期)
parameter DEBOUNCE_TIME = 20'd999_999; // 20ms消抖时间
always @(posedge clk_50m) begin
if (button_in != button_out) begin // 检测到变化
if (cnt == DEBOUNCE_TIME) begin
button_out <= button_in; // 稳定后更新输出
cnt <= 20'd0;
end else begin
cnt <= cnt + 1'b1;
end
end else begin
cnt <= 20'd0; // 状态稳定时清零计数器
end
end
endmodule
5.3 状态机控制逻辑
按键控制采用有限状态机(FSM)实现,状态转移图如下:
code复制IDLE --start--> RUNNING
RUNNING --stop--> PAUSED
PAUSED --start--> RUNNING
ANY --reset--> IDLE
对应Verilog实现:
verilog复制module control_fsm(
input clk_50m,
input rst_n,
input key_start_debounced,
input key_stop_debounced,
input key_reset_debounced,
output reg start,
output reg stop,
output reg reset
);
typedef enum {IDLE, RUNNING, PAUSED} state_t;
state_t current_state;
always @(posedge clk_50m or negedge rst_n) begin
if (!rst_n) begin
current_state <= IDLE;
{start, stop, reset} <= 3'b000;
end else begin
// 默认输出
start <= 1'b0;
stop <= 1'b0;
reset <= 1'b0;
case (current_state)
IDLE: begin
if (key_start_debounced) begin
current_state <= RUNNING;
start <= 1'b1;
end
if (key_reset_debounced) reset <= 1'b1;
end
RUNNING: begin
if (key_stop_debounced) begin
current_state <= PAUSED;
stop <= 1'b1;
end
if (key_reset_debounced) begin
current_state <= IDLE;
reset <= 1'b1;
end
end
PAUSED: begin
if (key_start_debounced) begin
current_state <= RUNNING;
start <= 1'b1;
end
if (key_reset_debounced) begin
current_state <= IDLE;
reset <= 1'b1;
end
end
endcase
end
end
endmodule
6. 数码管显示驱动
6.1 动态扫描原理
六位数码管采用动态扫描方式驱动,利用人眼视觉暂留效应(约20ms)。扫描频率计算:
code复制扫描频率 ≥ 6位数×60Hz = 360Hz
实际选择1kHz(每位数码管点亮约167μs)
6.2 Verilog实现
verilog复制module display(
input clk_50m,
input rst_n,
input [23:0] time_bcd, // BCD码时间输入
output reg [7:0] seg, // 段选信号
output reg [5:0] sel // 位选信号(低有效)
);
reg [2:0] scan_cnt; // 扫描计数器(0-5)
reg [3:0] digit_data; // 当前显示数字
reg [19:0] refresh_cnt; // 刷新计数器
// 数码管段选编码(共阳)
parameter [7:0] SEG_TABLE [0:15] = '{
8'b11000000, // 0
8'b11111001, // 1
8'b10100100, // 2
8'b10110000, // 3
8'b10011001, // 4
8'b10010010, // 5
8'b10000010, // 6
8'b11111000, // 7
8'b10000000, // 8
8'b10010000, // 9
8'b10001000, // A
8'b10000011, // b
8'b11000110, // C
8'b10100001, // d
8'b10000110, // E
8'b10001110 // F
};
// 1kHz扫描时钟生成
always @(posedge clk_50m or negedge rst_n) begin
if (!rst_n) refresh_cnt <= 20'd0;
else if (refresh_cnt == 20'd49999) refresh_cnt <= 20'd0;
else refresh_cnt <= refresh_cnt + 1'b1;
end
// 扫描计数器
always @(posedge clk_50m or negedge rst_n) begin
if (!rst_n) scan_cnt <= 3'd0;
else if (refresh_cnt == 20'd49999) begin
if (scan_cnt == 3'd5) scan_cnt <= 3'd0;
else scan_cnt <= scan_cnt + 1'b1;
end
end
// 位选和数据显示
always @(*) begin
case (scan_cnt)
3'd0: begin sel = 6'b111110; digit_data = time_bcd[23:20]; end // 分十位
3'd1: begin sel = 6'b111101; digit_data = time_bcd[19:16]; end // 分个位
3'd2: begin sel = 6'b111011; digit_data = time_bcd[15:12]; end // 秒十位
3'd3: begin sel = 6'b110111; digit_data = time_bcd[11:8]; end // 秒个位
3'd4: begin sel = 6'b101111; digit_data = time_bcd[7:4]; end // 毫秒百位
3'd5: begin sel = 6'b011111; digit_data = time_bcd[3:0]; end // 毫秒十位
default: begin sel = 6'b111111; digit_data = 4'd0; end
endcase
// 添加小数点(秒与毫秒之间的冒号)
if (scan_cnt == 3'd3) seg = SEG_TABLE[digit_data] & 8'b01111111;
else seg = SEG_TABLE[digit_data];
end
endmodule
6.3 显示优化技巧
- 亮度均衡:不同位数的点亮时间可微调,补偿因扫描顺序导致的亮度差异
- 闪烁提示:通过周期性地关闭位选信号,实现计时开始/暂停的视觉反馈
- 省电模式:长时间不操作时降低扫描频率
7. 计次功能实现
7.1 设计思路
计次功能需要:
- 存储当前时间到寄存器堆
- 提供多个存储位置(如8次)
- 支持前后浏览存储的计次时间
7.2 Verilog实现
verilog复制module lap_time(
input clk_50m,
input rst_n,
input latch_en, // 计次信号
input [23:0] curr_time, // 当前时间
output [23:0] disp_time // 显示时间
);
reg [23:0] lap_reg [0:7]; // 8组计次寄存器
reg [2:0] lap_ptr; // 当前显示计次位置
reg [2:0] lap_count; // 有效计次数
// 计次存储逻辑
always @(posedge clk_50m or negedge rst_n) begin
if (!rst_n) begin
lap_count <= 3'd0;
lap_ptr <= 3'd0;
end else if (latch_en) begin
if (lap_count < 3'd7) begin
lap_reg[lap_count] <= curr_time;
lap_count <= lap_count + 1'b1;
lap_ptr <= lap_count + 1'b1;
end else begin
// 循环覆盖最早记录
lap_reg[lap_ptr] <= curr_time;
lap_ptr <= lap_ptr + 1'b1;
if (lap_ptr == 3'd7) lap_ptr <= 3'd0;
end
end
end
// 显示选择逻辑
assign disp_time = (lap_count == 3'd0) ? curr_time : lap_reg[lap_ptr];
endmodule
8. 系统集成与调试
8.1 顶层模块设计
verilog复制module top_digital_stopwatch(
input clk_50m,
input rst_n,
input key_start,
input key_stop,
input key_reset,
input key_lap,
output [7:0] seg,
output [5:0] sel
);
// 内部信号声明
wire clk_1k;
wire start, stop, reset, latch_en;
wire [23:0] curr_time, disp_time;
// 模块实例化
clk_div u_clk_div(.clk_50m(clk_50m), .rst_n(rst_n), .clk_1k(clk_1k));
debounce u_debounce_start(.clk_50m(clk_50m), .button_in(key_start), .button_out(key_start_db));
debounce u_debounce_stop(.clk_50m(clk_50m), .button_in(key_stop), .button_out(key_stop_db));
debounce u_debounce_reset(.clk_50m(clk_50m), .button_in(key_reset), .button_out(key_reset_db));
debounce u_debounce_lap(.clk_50m(clk_50m), .button_in(key_lap), .button_out(key_lap_db));
control_fsm u_control_fsm(
.clk_50m(clk_50m),
.rst_n(rst_n),
.key_start_debounced(key_start_db),
.key_stop_debounced(key_stop_db),
.key_reset_debounced(key_reset_db),
.start(start),
.stop(stop),
.reset(reset)
);
timer u_timer(
.clk_1k(clk_1k),
.rst_n(rst_n),
.start(start),
.stop(stop),
.reset(reset),
.time_bcd(curr_time)
);
lap_time u_lap_time(
.clk_50m(clk_50m),
.rst_n(rst_n),
.latch_en(key_lap_db),
.curr_time(curr_time),
.disp_time(disp_time)
);
display u_display(
.clk_50m(clk_50m),
.rst_n(rst_n),
.time_bcd(disp_time),
.seg(seg),
.sel(sel)
);
endmodule
8.2 常见问题与解决方案
-
显示闪烁或不稳定:
- 检查数码管扫描频率是否足够高(建议≥300Hz)
- 确认位选信号切换时没有竞争冒险
- 测量各段驱动电流是否足够(通常2-10mA)
-
计时不准确:
- 用示波器测量1kHz时钟信号的实际频率
- 检查分频计数器是否溢出归零
- 确认没有信号路径上的时序违规
-
按键响应异常:
- 调整消抖时间(15-25ms为宜)
- 检查按键接线上拉电阻(通常4.7kΩ)
- 验证状态机转换逻辑
9. 功能扩展思路
- 串口通信:添加UART模块,将计时数据发送到PC
- RTC同步:集成DS1302等实时时钟芯片,实现时钟同步
- 无线控制:通过蓝牙或WiFi模块远程控制秒表
- 数据统计:计算多次计次的平均值、标准差等
- 触摸控制:替换机械按键为电容触摸传感器
10. 工程实践建议
- 版本控制:使用Git管理Verilog代码,特别是团队协作时
- 参数化设计:用
parameter定义常量(如分频系数、计数值等) - 仿真验证:编写testbench验证各模块功能
- 约束文件:正确编写XDC约束文件,包括时钟定义和管脚分配
- 功耗优化:在非活跃状态关闭不必要的模块时钟
这个FPGA数字秒表设计涵盖了从时钟分频、计时逻辑到显示驱动的完整开发流程,体现了硬件描述语言的特点和FPGA并行处理的优势。通过模块化设计和状态机控制,系统具有良好的可扩展性和可维护性。
