FPGA闹钟实现
这里并没有用开发板上的DS1302模块,而是直接分频产生的时钟
写这篇文章的目的也是为了记录一下学习过程
开发环境
-
FPGA开发板:蓝桥杯FPGA的A版
-
开发软件:VS Code , Quartus 18.0
实现目标
数码管
- 界面 1:时钟界面
- 初始会在数码管上显示 23-59-55(时-分-秒)。
- 界面 2:闹钟设置界面
- 初始为 00-00-00(时-分-秒)。
按键
-
S1:界面切换
- 按下后会在界面1(时钟界面)和界面2(闹钟设置界面)之间进行一次切换。
- 初始界面为时钟界面,按下后切换到闹钟设置界面,再按下切换回时钟界面。
-
S2:闹钟设置位切换
- 按下后会切换选择闹钟的时、分、秒。
- 选中的位会以500ms的间隔进行闪烁,表示当前设置的位。
-
S3:数字加
- 按下后会让选中的闹钟时、分、秒数字加1。
- 秒数最大为59,分数最大为59,时数最大为23,超过最大值时回到0。
-
S4:数字减
- 按下后会让选中的闹钟时、分、秒数字减1。
- 秒数最小为0,分数最小为0,时数最小为0,低于最小值时回到最大值(59秒、59分、23小时)。
-
注意: S2,S3,S4仅在数码管处于界面二时生效
其他功能
-
时钟显示
- 实时的时钟数据会更新在数码管上,并且每秒更新时间。
-
闹钟响铃
- 当当前时间与设置的闹钟时间一致时,LED会闪烁并发出提示。
- LED闪烁时间为8秒,然后停止闪烁。
代码部分
我这里文件的创建顺序为
- prj (Quartus产生的文件)
- src(我们编写的代码文件)
- driver (底层驱动文件)
- user (应用层驱动文件)
user文件
top.v
module top (
input clk,
input rst_n,
input [3:0] key_in,
output [7:0] led,
output [7:0] dula,
output [7:0] wela
);
reg [3:0] seg_num [7:0]; // 改为 reg 类型,因为要在 always 中赋值
wire [3:0] seg_mode; // `seg_mode` 信号
wire [7:0] seg_dp;
wire [3:0] led_mode; // `led_mode` 信号
wire [1:0] set_time_indix; // 设置时间指针信号
wire [5:0] real_time [2:0]; // 实际时间
wire [5:0] set_time [2:0]; // 闹钟时间
wire [5:0] set_time_temp [2:0]; // 闹钟设置时间
wire seg_star_flag;
// 在 always 块中赋值 seg_num 数组
always @(*) begin
case (seg_mode)
4'd0: begin
seg_num[7] = real_time[2] / 10;
seg_num[6] = real_time[2] % 10;
seg_num[5] = 4'd11;
seg_num[4] = real_time[1] / 10;
seg_num[3] = real_time[1] % 10;
seg_num[2] = 4'd11;
seg_num[1] = real_time[0] / 10;
seg_num[0] = real_time[0] % 10;
end
4'd1: begin
seg_num[7] = (set_time_indix == 2'd2 && seg_star_flag == 1) ? 10 : set_time_temp[2] / 10;
seg_num[6] = (set_time_indix == 2'd2 && seg_star_flag == 1) ? 10 : set_time_temp[2] % 10;
seg_num[5] = 4'd11;
seg_num[4] = (set_time_indix == 2'd1 && seg_star_flag == 1) ? 10 : set_time_temp[1] / 10;
seg_num[3] = (set_time_indix == 2'd1 && seg_star_flag == 1) ? 10 : set_time_temp[1] % 10;
seg_num[2] = 4'd11;
seg_num[1] = (set_time_indix == 2'd0 && seg_star_flag == 1) ? 10 : set_time_temp[0] / 10;
seg_num[0] = (set_time_indix == 2'd0 && seg_star_flag == 1) ? 10 : set_time_temp[0] % 10;
end
endcase
end
// 实例化按键处理模块
key_proc key_proc_inst (
.clk (clk),
.rst_n (rst_n),
.key_in(key_in),
.seg_mode (seg_mode),
.set_time_indix(set_time_indix),
.set_time_h(set_time[2]),
.set_time_d(set_time[1]),
.set_time_s(set_time[0]),
.set_time_temp_h(set_time_temp[2]),
.set_time_temp_d(set_time_temp[1]),
.set_time_temp_s(set_time_temp[0])
);
// 实例化 LED 处理模块
led_proc led_proc_inst (
.clk (clk),
.rst_n(rst_n),
.led_mode (led_mode),
.led (led)
);
// 实例化数码管处理模块
seg_proc u_seg_proc (
.clk(clk),
.rst_n(rst_n),
.seg_number_in({
seg_num[7], seg_num[6], seg_num[5], seg_num[4],
seg_num[3], seg_num[2], seg_num[1], seg_num[0]
}),
.seg_dp(seg_dp),
.dula(dula),
.wela(wela)
);
// 实例化信息处理模块
infor_deal u_infor_deal (
.clk(clk),
.rst_n(rst_n),
.seg_mode(seg_mode),
.seg_star_flag(seg_star_flag),
.led_mode(led_mode),
.real_time_h(real_time[2]),
.real_time_d(real_time[1]),
.real_time_s(real_time[0]),
.set_time_h(set_time[2]),
.set_time_d(set_time[1]),
.set_time_s(set_time[0]),
);
endmodule
seg_proc.v
module seg_proc (
input clk, // 50MHz 时钟信号
input rst_n, // 低电平复位信号
input [31:0] seg_number_in, // 输入的数字内容
input [7:0] seg_dp, // 每个位的小数点状态(1: 显示小数点,0: 不显示)
output [7:0] dula, // 段选信号
output [7:0] wela // 位选信号
);
wire clk_1k; // 1kHz 时钟信号
reg [2:0] bits; // 当前显示的数码管位索引(0 到 7)
reg [3:0] current_num; // 当前显示位的数字内容
reg current_dp; // 当前显示位的小数点状态
// 分频器实例化,将 50MHz 时钟分频至 1kHz
frequency_divider u_frequency_divider (
.clk (clk),
.rst_n (rst_n),
.clkbase(50_000_000),
.clkdiv (1_000),
.clkout (clk_1k)
);
// 根据当前的位选择信号 bits,选择对应的数码管输入数据和小数点状态
always @(*) begin
current_num = seg_number_in[(7-bits)*4+:4]; // 选择相应的数字
current_dp = seg_dp[7-bits]; // 获取小数点状态
end
// 数码管驱动模块实例化
seg_driver u_seg_driver (
.digit (current_num),
.rst_n (rst_n),
.current_dp (current_dp),
.position (bits),
.digit_sel (wela),
.segment_data(dula)
);
// 动态扫描逻辑,每 1ms 切换一个显示位
always @(posedge clk_1k or negedge rst_n) begin
if (!rst_n)
bits <= 3'd0; // 复位时从第 0 位开始显示
else
bits <= (bits == 3'd7) ? 3'd0 : bits + 3'd1; // 切换到下一个显示位
end
endmodule
key_proc.v
module key_proc (
input clk, // 系统时钟信号
input rst_n, // 低电平复位信号
input [3:0] key_in, // 4 个按键输入型号
output reg [3:0] seg_mode, // 模式输出,用于切换LED显示模式
output reg [1:0] set_time_indix,
output reg [5:0] set_time_h,
output reg [5:0] set_time_d,
output reg [5:0] set_time_s,
output reg [5:0] set_time_temp_h,
output reg [5:0] set_time_temp_d,
output reg [5:0] set_time_temp_s
);
wire [3:0] down; // 每个按键的按下检测信号
wire [3:0] up; // 每个按键的松开检测信号
wire clk_100; // 100Hz 时钟信号
frequency_divider u_frequency_divider (
.clk(clk),
.rst_n(rst_n),
.clkbase(50_000_000),
.clkdiv(100),
.clkout(clk_100)
);
// 实例化按键消抖模块 key_ctrl,用于按键消抖处理
key_ctrl key_ctrl_inst (
.clk_100(clk_100), // 系统时钟输入
.rst_n(rst_n), // 低电平复位信号
.key_in(key_in), // 按键输入信号
.down(down), // 下降沿检测输出
.up(up) // 上升沿检测输出
);
// 根据按键按下状态控制模式切换
always @(posedge clk_100 or negedge rst_n) begin
if (!rst_n) begin
seg_mode <= 4'd0; // 复位时模式设为初始模式 0
set_time_h <= 6'd0;
set_time_d <= 6'd0;
set_time_s <= 6'd0;
set_time_temp_h <= 6'd0;
set_time_temp_d <= 6'd0;
set_time_temp_s <= 6'd0;
end
else begin
// 界面切换
if (down[0]) begin
if (seg_mode == 4'd0) // 真实时间界面
seg_mode <= 4'd1; // 设置为闹钟时间界面
else begin // 闹钟时间界面
seg_mode <= 4'd0; // 设置为真实时间界面
//保存数据
set_time_h <= set_time_temp_h;
set_time_d <= set_time_temp_d;
set_time_s <= set_time_temp_s;
// 设置为秒
set_time_indix <= 2'd0;
end
end
// 设置闹钟指针
else if (down[1] && seg_mode == 4'd1) begin
if (set_time_indix == 2'd0) // 如果是秒
set_time_indix <= 2'd1; // 设置为分
else if (set_time_indix == 2'd1) // 如果是分
set_time_indix <= 2'd2; // 设置为时
else // 如果是时
set_time_indix <= 2'd0; // 设置为秒
end
// 加
else if (down[2] && seg_mode == 4'd1) begin
case (set_time_indix)
2'd0: begin
if (set_time_temp_s == 6'd59)
set_time_temp_s <= 6'd0; // 秒归零
else
set_time_temp_s <= set_time_temp_s + 6'd1; // 秒加1
end
2'd1: begin
if (set_time_temp_d == 6'd59)
set_time_temp_d <= 6'd0; // 分归零
else
set_time_temp_d <= set_time_temp_d + 6'd1; // 分加1
end
2'd2: begin
if (set_time_temp_h == 6'd23)
set_time_temp_h <= 6'd0; // 时归零
else
set_time_temp_h <= set_time_temp_h + 6'd1; // 时加1
end
endcase
end
// 减
else if (down[3] && seg_mode == 4'd1) begin
case (set_time_indix)
2'd0: begin
if (set_time_temp_s == 6'd0)
set_time_temp_s <= 6'd59; // 秒归59
else
set_time_temp_s <= set_time_temp_s - 6'd1; // 秒减1
end
2'd1: begin
if (set_time_temp_d == 6'd0)
set_time_temp_d <= 6'd59; // 分归59
else
set_time_temp_d <= set_time_temp_d - 6'd1; // 分减1
end
2'd2: begin
if (set_time_temp_h == 6'd0)
set_time_temp_h <= 6'd23; // 时归23
else
set_time_temp_h <= set_time_temp_h - 6'd1; // 时减1
end
endcase
end
end
end
endmodule
led_proc.v
module led_proc (
input clk, // 50MHz 时钟输入
input rst_n, // 低电平复位信号
input [3:0] led_mode, // 模式选择信号
output [7:0] led // LED 输入
);
reg [7:0] led_pattern; // LED 控制模式寄存器
wire clk_100; // 1Hz 分频时钟信号
// 实例化 frequency_divider 模块,将 50MHz 时钟分频到 1Hz
frequency_divider freq_diw_init (
.clk(clk),
.rst_n(rst_n),
.clkbase(50_000_000),
.clkdiv(100),
.clkout(clk_100)
);
// 根据 mode 模式和 1Hz 慢时钟信号 clk_1 控制 LED 模式
always @(*) begin
if (!rst_n) begin
led_pattern <= 8'b0000_0000; // 全灭
end
else begin
case (led_mode)
4'd0:
led_pattern <= 8'b0000_0000; // 全灭
4'd1:
led_pattern <= 8'b1111_1111; // 全亮
default:
led_pattern <= 8'b0000_0000; // 全灭
endcase
end
end
// 实例化 LED 显示模块,将 led_pattern 映射到实际 LED 输出
led_display led_display_init (
.led_pattern(led_pattern),
.led(led)
);
endmodule
infor_proc.v
module infor_deal(
input clk,
input rst_n,
output reg [5:0] real_time_h,
output reg [5:0] real_time_d,
output reg [5:0] real_time_s,
input [5:0] set_time_h,
input [5:0] set_time_d,
input [5:0] set_time_s,
output reg [3:0] led_mode,
output reg seg_star_flag,
input [3:0] seg_mode
);
wire led_star_clk_500ms_2Hz;
wire real_time_clk_1s_1Hz;
reg led_star_flag;
reg [2:0] led_star_cnt;
// 500ms时钟
frequency_divider infor_frequency_divider (
.clk(clk),
.rst_n(rst_n),
.clkbase(50_000_000),
.clkdiv(2),
.clkout(led_star_clk_500ms_2Hz)
);
// 1s时钟
frequency_divider real_time_frequency_divider (
.clk(clk),
.rst_n(rst_n),
.clkbase(50_000_000),
.clkdiv(1),
.clkout(real_time_clk_1s_1Hz)
);
// 闪烁控制
always @(posedge led_star_clk_500ms_2Hz or negedge rst_n) begin
if (!rst_n) begin
led_mode <= 4'd0;
seg_star_flag <= 0;
end
else begin
// LED闪烁控制
if (led_star_flag == 1) begin
if (led_mode == 4'd0) led_mode <= 4'd1; // 进入全亮
else led_mode <= 4'd0; // 进入全灭
end
else begin
led_mode <= 4'd0;
end
// 数码管闪烁控制
if (seg_star_flag == 1) seg_star_flag <= 0;
else seg_star_flag <= 1;
end
end
// 时间控制
always @(posedge real_time_clk_1s_1Hz or negedge rst_n) begin
if (!rst_n) begin
real_time_h <= 6'd23;
real_time_d <= 6'd59;
real_time_s <= 6'd55;
led_star_flag <= 0;
led_star_cnt <= 3'd0;
end
else begin
real_time_s <= real_time_s + 6'd1;
//秒
if (real_time_s == 6'd59) begin
real_time_s <= 6'd0;
real_time_d <= real_time_d + 6'd1;
end
if (real_time_d == 6'd59 && real_time_s == 6'd59) begin
real_time_d <= 6'd0;
real_time_h <= real_time_h + 6'd1;
end
if (real_time_h == 6'd23 && real_time_d == 6'd59 && real_time_s == 6'd59)
real_time_h <= 6'd0;
// 闹钟
if (real_time_s == set_time_s && real_time_d == set_time_d && real_time_h == set_time_h)
led_star_flag <= 1; // 闹钟响
if (led_star_flag == 1) begin
led_star_cnt <= led_star_cnt + 3'd1; // 计数5s
if (led_star_cnt == 3'd7) begin
led_star_cnt <= 3'd0;
led_star_flag <= 0; // 闹钟停止
end
end
end
end
endmodule
driver文件
frequency_divider.v
module frequency_divider (
input wire clk, // 输入时钟信号 50MHz
input wire rst_n, // 低电平复位信号
input wire [31:0] clkbase, // 基准时钟频率(单位:Hz),例如50MHz
input wire [31:0] clkdiv, // 期望输出频率(单位:Hz),例如1000代表1kHz
output reg clkout // 输出时钟信号
);
reg [31:0] counter; // 计数器
reg [31:0] limit; // 计数限制值
//计算每个分频周期的计数限制
always @(*) begin
if (clkdiv > 0) begin
limit = clkbase / (2 * clkdiv); // 计算分频周期
end
else begin
limit = 0; // 防止除以零
end
end
// 计数逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
counter <= 0; // 复位计数器
clkout <= 0; // 复位输出时钟
end
else begin
if (limit > 0) begin
if (counter >= limit - 1) begin
clkout <= ~clkout; // 翻转输出时钟
counter <= 0; // 计数器归零
end
else begin
counter <= counter + 1; // 计数器加一
end
end
end
end
endmodule
seg_driver.v
module seg_driver (
input [3:0] digit, // 输入的十进制数字
input rst_n, // 复位信号(低电平有效)
input current_dp, // 小数点使能信号
input [2:0] position, // 当前显示的位
output reg [7:0] digit_sel, // 位选信号
output reg [7:0] segment_data // 段选信号
);
// 段码定义(每个数字对应的七段显示编码)
localparam [7:0] SEG_0 = 8'b1100_0000; //0
localparam [7:0] SEG_1 = 8'b1111_1001; //1
localparam [7:0] SEG_2 = 8'b1010_0100; //2
localparam [7:0] SEG_3 = 8'b1011_0000; //3
localparam [7:0] SEG_4 = 8'b1001_1001; //4
localparam [7:0] SEG_5 = 8'b1001_0010; //5
localparam [7:0] SEG_6 = 8'b1000_0010; //6
localparam [7:0] SEG_7 = 8'b1111_1000; //7
localparam [7:0] SEG_8 = 8'b1000_0000; //8
localparam [7:0] SEG_9 = 8'b1001_0000; //9
localparam [7:0] SEG_10 = 8'b1111_1111; //灭
localparam [7:0] SEG_11 = 8'b1011_1111; //-
// 根据输入的数字和小数点状态生成段码
always @(*) begin
if (!rst_n) begin
segment_data <= 8'b1111_1111; // 复位时熄灭所有段码
digit_sel <= 8'b1111_1111; // 复位时关闭所有位选
end
else begin
// 根据输入的数字选择对应的段码
case (digit)
4'd0: segment_data <= SEG_0;
4'd1: segment_data <= SEG_1;
4'd2: segment_data <= SEG_2;
4'd3: segment_data <= SEG_3;
4'd4: segment_data <= SEG_4;
4'd5: segment_data <= SEG_5;
4'd6: segment_data <= SEG_6;
4'd7: segment_data <= SEG_7;
4'd8: segment_data <= SEG_8;
4'd9: segment_data <= SEG_9;
4'd10: segment_data <= SEG_10;
4'd11: segment_data <= SEG_11;
endcase
// 如果 decimal_en 为 1,则激活小数点(最高为小数点位)
if (current_dp) segment_data[7] = 1'b0;
// 根据当前位置 `position` 控制位选信号
digit_sel = ~(8'b0000_00001 << position);
end
end
endmodule
key_ctrl.v
module key_ctrl (
input clk_100, // 输入时钟信号(100Hz)
input rst_n, // 低电平复位信号
input [3:0] key_in, // 4个按键输入信号
output reg [3:0] down, // 按键按下检测信号(下降沿触发)
output reg [3:0] up // 按键释放检测信号(上升沿触发)
);
reg [3:0] new; // 当前按键状态
reg [3:0] old; // 上一个时钟周期的按键状态
//按键状态检测逻辑
always @(posedge clk_100 or negedge rst_n) begin
if (!rst_n) begin
// 复位时将所有状态设为未按下
new <= 4'b1111;
old <= 4'b1111;
down <= 4'b0000;
up <= 4'b0000;
end
else begin
old <= new; // 状态更新
new <= key_in; // 获取键码
down <= new & (new ^ old); // 检测下降沿
up <= ~new & (new ^ old); // 检测上升沿
end
end
endmodule
led_display.v
module led_display (
input [7:0] led_pattern,
output reg [7:0] led
);
always @(*) begin
led = ~led_pattern;
end
endmodule