1. FPGA双通道秒表项目概述
在数字电路设计领域,FPGA因其并行处理能力和可重构特性,成为实现复杂时序逻辑的理想平台。这次我们要实现的是一个具有实用价值的双通道秒表系统,能够在数码管和1602液晶屏上同步显示计时信息,并通过按键实现模式切换。这个项目完美结合了FPGA的硬件设计思想和实际应用需求。
DE2-115开发板作为我们的硬件平台,提供了丰富的外设接口:4个7段数码管、1602字符液晶接口、多个用户按键以及50MHz的系统时钟。我们将充分利用这些资源,构建一个完整的秒表系统。这个项目的独特之处在于双显示通道的同步控制,以及通过状态机实现的多种操作模式。
提示:对于FPGA初学者来说,这个项目涵盖了多个关键知识点:时钟分频、状态机设计、外设驱动、按键消抖等,是提升Verilog编码能力的绝佳练习。
2. 系统架构设计
2.1 整体模块划分
系统采用层次化设计,主要包含以下功能模块:
- 时钟管理模块:将50MHz系统时钟分频为1Hz基准时钟
- 按键处理模块:实现按键消抖和模式切换逻辑
- 计时控制模块:核心状态机,管理计时、暂停、复位等操作
- 数码管驱动模块:动态扫描显示计时数值
- 1602液晶驱动模块:按照特定时序控制液晶屏显示
各模块通过清晰的接口信号互联,形成完整的系统数据流。这种模块化设计不仅便于调试,也增强了代码的可移植性。
2.2 关键信号定义
系统顶层模块需要定义以下重要信号:
verilog复制module top(
input clk_50M, // 50MHz系统时钟
input rst_n, // 复位信号(低有效)
input [3:0] key_in, // 按键输入
output [3:0] sel, // 数码管位选
output [6:0] seg, // 数码管段选
output lcd_rs, // 1602寄存器选择
output lcd_rw, // 1602读写选择
output lcd_en, // 1602使能信号
output [7:0] lcd_data // 1602数据线
);
3. 核心模块实现细节
3.1 时钟分频与计时逻辑
秒表的核心是精确的计时功能。我们需要将50MHz系统时钟分频为1Hz信号:
verilog复制module clk_div(
input clk,
input rst,
output reg clk_1s
);
reg [25:0] cnt;
always @(posedge clk or posedge rst) begin
if (rst) begin
cnt <= 0;
clk_1s <= 0;
end else begin
if (cnt == 26'd49_999_999) begin
cnt <= 0;
clk_1s <= ~clk_1s;
end else begin
cnt <= cnt + 1;
end
end
end
endmodule
计时模块根据1Hz时钟信号更新计时数值:
verilog复制module timer(
input clk_1s,
input rst,
input pause,
output reg [15:0] time_val
);
always @(posedge clk_1s or posedge rst) begin
if (rst) begin
time_val <= 0;
end else if (!pause) begin
time_val <= time_val + 1;
end
end
endmodule
3.2 1602液晶驱动实现
1602液晶模块的驱动是项目难点之一。我们需要严格按照时序要求初始化并控制显示:
verilog复制module lcd1602_controller(
input clk,
input rst,
input [15:0] time_val,
output reg lcd_rs,
output reg lcd_rw,
output reg lcd_en,
output reg [7:0] lcd_data
);
// 状态定义
typedef enum {
INIT,
FUNCTION_SET,
DISPLAY_ON,
CLEAR_DISPLAY,
ENTRY_MODE,
WRITE_TIME,
DELAY
} state_t;
state_t current_state;
reg [19:0] delay_cnt;
reg [3:0] init_step;
reg [7:0] ascii_time[6:0];
// 时间值转换为ASCII码
always @(*) begin
ascii_time[0] = "0" + (time_val / 600) % 6;
ascii_time[1] = "0" + (time_val / 60) % 10;
ascii_time[2] = ":";
ascii_time[3] = "0" + (time_val / 10) % 6;
ascii_time[4] = "0" + time_val % 10;
ascii_time[5] = " ";
ascii_time[6] = 0;
end
always @(posedge clk or posedge rst) begin
if (rst) begin
current_state <= INIT;
delay_cnt <= 0;
init_step <= 0;
end else begin
case (current_state)
INIT: begin
if (delay_cnt < 20'd500_000) begin
delay_cnt <= delay_cnt + 1;
end else begin
delay_cnt <= 0;
current_state <= FUNCTION_SET;
end
end
FUNCTION_SET: begin
lcd_rs <= 0;
lcd_rw <= 0;
lcd_en <= 1;
lcd_data <= 8'b00111000; // 8位, 2行, 5x7点阵
current_state <= DELAY;
end
// 其他状态类似实现...
WRITE_TIME: begin
lcd_rs <= 1;
lcd_rw <= 0;
lcd_en <= 1;
lcd_data <= ascii_time[init_step];
if (init_step < 7) begin
init_step <= init_step + 1;
end else begin
init_step <= 0;
end
current_state <= DELAY;
end
DELAY: begin
lcd_en <= 0;
if (delay_cnt < 20'd10_000) begin
delay_cnt <= delay_cnt + 1;
end else begin
delay_cnt <= 0;
current_state <= WRITE_TIME;
end
end
endcase
end
end
endmodule
3.3 数码管动态扫描
数码管显示采用动态扫描技术,通过快速切换位选信号实现多位数码管显示:
verilog复制module seg_display(
input clk,
input rst,
input [15:0] time_val,
output reg [3:0] an,
output reg [6:0] seg
);
reg [15:0] scan_cnt;
reg [3:0] digit;
always @(posedge clk or posedge rst) begin
if (rst) begin
scan_cnt <= 0;
end else begin
scan_cnt <= scan_cnt + 1;
end
end
always @(*) begin
case (scan_cnt[15:14])
2'b00: begin
an = 4'b1110;
digit = (time_val / 10) % 10; // 秒个位
end
2'b01: begin
an = 4'b1101;
digit = (time_val / 60) % 6; // 秒十位
end
2'b10: begin
an = 4'b1011;
digit = (time_val / 600) % 10; // 分钟
end
2'b11: begin
an = 4'b0111;
digit = 12; // 显示"-"
end
endcase
case (digit)
0: seg = 7'b0000001;
1: seg = 7'b1001111;
2: seg = 7'b0010010;
// 其他数字编码...
12: seg = 7'b0111111; // "-"
default: seg = 7'b1111111;
endcase
end
endmodule
4. 按键处理与模式切换
4.1 按键消抖实现
机械按键存在抖动问题,需要通过硬件或软件方式消除:
verilog复制module debounce(
input clk,
input btn_in,
output reg btn_out
);
reg [19:0] cnt;
reg btn_sync;
always @(posedge clk) begin
btn_sync <= btn_in;
if (btn_out != btn_sync) begin
if (cnt == 20'd999_999) begin
btn_out <= btn_sync;
cnt <= 0;
end else begin
cnt <= cnt + 1;
end
end else begin
cnt <= 0;
end
end
endmodule
4.2 模式控制状态机
秒表需要支持多种操作模式,通过状态机实现:
verilog复制module control_fsm(
input clk,
input rst,
input key_start,
input key_pause,
input key_reset,
output reg is_running,
output reg is_paused
);
typedef enum {
IDLE,
RUNNING,
PAUSED
} state_t;
state_t current_state;
always @(posedge clk or posedge rst) begin
if (rst) begin
current_state <= IDLE;
is_running <= 0;
is_paused <= 0;
end else begin
case (current_state)
IDLE: begin
if (key_start) begin
current_state <= RUNNING;
is_running <= 1;
is_paused <= 0;
end
end
RUNNING: begin
if (key_pause) begin
current_state <= PAUSED;
is_paused <= 1;
end else if (key_reset) begin
current_state <= IDLE;
is_running <= 0;
end
end
PAUSED: begin
if (key_start) begin
current_state <= RUNNING;
is_paused <= 0;
end else if (key_reset) begin
current_state <= IDLE;
is_running <= 0;
is_paused <= 0;
end
end
endcase
end
end
endmodule
5. 系统集成与调试
5.1 顶层模块连接
将各功能模块在顶层模块中实例化并连接:
verilog复制module top(
input clk_50M,
input rst_n,
input [3:0] key_in,
output [3:0] sel,
output [6:0] seg,
output lcd_rs,
output lcd_rw,
output lcd_en,
output [7:0] lcd_data
);
wire clk_1s;
wire [15:0] time_val;
wire key_start, key_pause, key_reset;
wire is_running, is_paused;
clk_div u_clk_div(
.clk(clk_50M),
.rst(~rst_n),
.clk_1s(clk_1s)
);
debounce u_debounce_start(
.clk(clk_50M),
.btn_in(key_in[0]),
.btn_out(key_start)
);
// 其他按键消抖实例化...
control_fsm u_control_fsm(
.clk(clk_50M),
.rst(~rst_n),
.key_start(key_start),
.key_pause(key_pause),
.key_reset(key_reset),
.is_running(is_running),
.is_paused(is_paused)
);
timer u_timer(
.clk_1s(clk_1s),
.rst(~rst_n || key_reset),
.pause(is_paused || !is_running),
.time_val(time_val)
);
seg_display u_seg_display(
.clk(clk_50M),
.rst(~rst_n),
.time_val(time_val),
.sel(sel),
.seg(seg)
);
lcd1602_controller u_lcd1602(
.clk(clk_50M),
.rst(~rst_n),
.time_val(time_val),
.lcd_rs(lcd_rs),
.lcd_rw(lcd_rw),
.lcd_en(lcd_en),
.lcd_data(lcd_data)
);
endmodule
5.2 常见问题与解决方案
在实际调试过程中,可能会遇到以下典型问题:
-
1602液晶显示乱码
- 检查初始化时序是否严格遵循数据手册要求
- 确保各状态之间的延时足够
- 验证RS、RW、EN信号的电平时序
-
数码管显示闪烁或亮度不均
- 调整扫描频率,通常在100Hz-1kHz之间
- 检查位选和段选信号的驱动能力
- 确保各数码管的点亮时间均衡
-
按键响应不灵敏或误触发
- 调整消抖时间参数,通常在10-20ms
- 检查按键硬件连接是否可靠
- 考虑增加按键长按检测功能
-
计时精度偏差
- 校准时钟分频系数
- 使用更精确的时钟源或PLL
- 考虑使用硬件定时器替代软件计数
经验分享:在调试1602液晶时,我发现初始化阶段的延时非常关键。数据手册要求的上电延时至少15ms,而实际应用中建议预留更长时间(如50ms)以确保稳定性。另外,每个指令之间的执行时间也需要严格把控,这是很多初学者容易忽视的地方。
6. 项目扩展与优化
6.1 功能扩展建议
- 多组计时记忆功能:增加存储多组计时结果的能力
- 分段计时功能:实现圈速记录等高级计时功能
- 串口通信接口:将计时数据上传到PC分析
- RTC时间基准:使用外部RTC模块提高计时精度
- 显示效果增强:增加倒计时、时间格式选择等功能
6.2 性能优化方向
- 时钟系统优化:使用PLL生成精确时钟
- 低功耗设计:在暂停状态下关闭不必要的外设
- 显示刷新优化:只更新变化的数据区域
- 代码重构:使用参数化设计增强可配置性
- 资源利用优化:合理使用FPGA的硬件资源
6.3 移植到其他平台
本项目代码可以方便地移植到其他FPGA平台,主要考虑以下几点:
- 时钟频率适配:根据目标平台调整时钟分频系数
- 引脚约束修改:更新外设连接的FPGA引脚定义
- 显示驱动适配:针对不同型号的数码管或液晶屏调整驱动参数
- 资源评估:确保目标器件有足够的逻辑资源和存储单元
在实际移植到Xilinx Artix-7平台时,我只需要修改时钟管理模块和引脚约束文件,核心功能代码可以完全重用,这充分体现了Verilog硬件描述语言的可移植性优势。