前言
使用FPGA开发简易数字钟项目旨在通过实现时钟信号生成、秒分小时计时、以及数字显示等功能,帮助我们掌握FPGA的编程和电路设计。具体来说,我们将设计FPGA以产生稳定的时钟信号,创建计时模块处理时间进位,控制显示器展示当前时间,并设计用户接口来设置和调整时间。这个项目不仅加深对FPGA操作的理解,还提供了将理论应用于实际电子设计的机会。
正文
一、 简易数字钟
1.项目需求
利用六位八段共阳极数码管显示时间,显示时间的格式为:小时:分钟:秒;小时,分钟,秒各占用两个数码管,时间的进制为24小时进制,那么在数码管上看到时间最大值为:23:59:59。
2.技术介绍
本工程要使用到数码管,如何用FPGA驱动数码管:数码管驱动电路讲解
需要6个八段数码,下面介绍一位数码管怎么驱动:
1.静态驱动方式
led静态显示时,其公共端接地(共阴极)或接电源(共阳极),各段选线分别与I/O口接线相连。 要显示字符,直接在I/O线发送相应的字段码。
优点: 静态显示结构简单,显示方便,要显示某个字符直接在IO线上发送相应的字段码
缺点: 一根数码管需要8根IO线,数码管比较多时候,非常占用IO线。
2.动态驱动方式
动态驱动方式是将所有的数码管的段选线并接在一起,用一个IO接口控制,公共端并不是直接接地(共阴极)或者电源(共阳极),而是通过相应的IO接口控制。如下图
六位数码管动态驱动方式连接图
每个数码管的公共端与FPGA一个IO相连,第一步使最右边一个数码管的公共端为1(位选端),其余数码管公共端为0(位选端),同时在(a,b,c,d,e,f,g,dp)(段选端)端上发送右边第一个数码管的字段码,这时候只有右边的第一个数码管显示,其余不显示;
第二步使右边第二个数码管的公共端为1(位选端),其余的公共端为0(位选端),同时在(a,b,c,d,e,f,g,dp)(段选端)端上发送右边第二个数码管的字段码,这时候,只有右边第二个数码管显示,其余不显示;
以此类推,直到最后一个,这样子4个数码管轮流显示相应的信息,一遍显示完毕,隔一段时间,又这样循环显示。
从计算机角度,每个数码管隔一段时间才显示一次,但是由于人的视觉暂留效应,只要隔离时间足够短,循环的周期足够长,美妙达到24次以上,看起来数码管就一直稳定显示了,这就是动态显示原理。这里我们为节省IO将六个位选端口用三个位选信号的译码表示:
位选信号 | 位选端口 |
其他 | 0(000000) |
001 | 1(100000) |
010 | 2(010000) |
011 | 3(001000) |
100 | 4(000100) |
101 | 5(000010) |
110 | 6(000001) |
数码管显示码表:(网上只找到共阴极数码管表,共阳极只需要取反就行:1变0,0变1)
3.数字钟驱动原理
秒数据包含秒十位数据sec1[3:0],秒个位数据sec0[3:0],秒个位数据变化范围在:0到9,秒十位数据变化范围在:0到5;通过定义一个1秒钟的延迟计数器cnt,当其计数到最大值时秒个位数据加一;秒十位数据加一的条件为:秒个位数据等于9且延迟计数器计数到最大值。
分钟数据包含分钟十位数据min1[3:0](0到5)和分钟个位数据min0[3:0](0到9),分钟个位变化一次的条件为:秒个位数据为9且秒十位数据为5且延迟计数器等于最大值,分钟十位数据变化一次的条件为:分钟个数数据等于9且秒个位数据为9且秒十位数据为5且延迟计数器等于最大值;
小时数据包含小时十位数据hour1[3:0](0到2),小时个位数据hour0[3:0](0到9),小时个位数据变化一次的条件为:需要先判断小时十位数据,如果小时十位数据小于2,那么小时个位数据的变化范围就在0到9,反之,小时个位数据的变化范围就在0到3;个位数据变化一次的条件为:分钟十位数据等于5且分钟个数数据等于9且秒个位数据为9且秒十位数据为5且延迟计数器等于最大值;
小时十位数据变化的条件为:如果小时十位数据小于2,那么其变化一次的条件为:小时个位数据等于9且分钟十位数据等于5且分钟个数数据等于9且秒个位数据为9且秒十位数据为5且延迟计数器等于最大值;如果小时十位数据等于2,那么其变化一次的条件为:小时个位数据等3且分钟十位数据等于5且分钟个数数据等于9且秒个位数据为9且秒十位数据为5且延迟计数器等于最大值;
3.顶层架构
顶层框架:整体分两大模块数字时钟控制模块与显示模块
数字时钟控制模块
通过one_sec对基础50MHZ时钟分频出1hz时钟,作为sec0_ctrl(秒个位)驱动时钟,该模块的进位信号作为sec1_ctrl(秒十位)的驱动时钟......直到驱动时钟十位hour1_ctrl,数字时钟设计完毕。
4.端口描述
clk | 基础时钟(50MHZ) |
rst_n | 复位按键(低电平有效) |
[2:0] sel | 位选信号 |
[7:0] seg | 段选信号 |
二、代码验证
one_sec模块:产生1s信号,时钟分频
/*一秒钟控制模块*/
module one_sec(
input clk,
input rst_n,
output flag//1s时间输出
);
reg [25:0] cnt;//1S
parameter MAX = 26'd50_000_000;
//50Mhz,一个周期20ns
//记50M次后,1s的时间到
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
cnt <= 26'd0;
else
if(cnt < MAX - 1)
cnt <= cnt + 26'd1;
else
cnt <= 26'd0;//记50M次后
end
assign flag = (cnt == MAX - 1)?1'b1:1'b0;//组合逻辑描述,计时1s时间到
endmodule
sec0_ctrl模块:秒个位模块,产生秒个位数码管数据,秒十位驱动信号
/*秒个位数据控制模块*/
module sec0_ctrl(
input clk,
input rst_n,
input flag,
output reg [3:0] sec0,//秒个位数据信号
output flag_10sec//进位信号
);
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
sec0 <= 4'd0;
else
if(flag == 1)
if(sec0 == 9)//最大记9
sec0 <= 4'd0;
else
sec0 <= sec0 + 4'd1;
else
sec0 <= sec0;
end
assign flag_10sec = ((sec0 == 9)&&(flag == 1))?1'b1:1'b0;
//计数器记到9,下一个秒信号到来时,产生秒十位信号
endmodule
sec1_ctrl模块:秒十位控制模块,产生秒十位数码管信号,分钟个位驱动信号
/*秒十位控制模块*/
module sec1_ctrl(
input clk,
input rst_n,
input flag,
output reg [3:0] sec1,//秒十位数据信号
output flag_min//分钟个位使能信号
);
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
sec1 <= 4'd0;
else
if(flag == 1)
if(sec1 < 5)//最大5
sec1 <= sec1 + 4'd1;
else
sec1 <= 4'd0;
else
sec1 <= sec1;
end
assign flag_min = ((sec1 == 5)&&(flag == 1))?1'b1:1'b0;
//秒十位记到5,下一个秒个位进位信号到来时,产生分钟个位使能信号
endmodule
min0_ctrl模块:产生分钟个位数据信号,分钟十位使能信号
/*分钟个位控制模块*/
module min0_ctrl(
input clk,
input rst_n,
input flag,
output reg [3:0] min0,//分钟个位数据信号
output flag_10min//分钟十位使能信号
);
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
min0 <= 4'd0;
else
if(flag == 1)
if(min0 < 9)//最大记9
min0 <= min0 + 4'd1;
else
min0 <= 4'd0;
else
min0 <= min0;
end
assign flag_10min = ((min0 == 9)&&(flag == 1))?1'b1:1'b0;
//分钟个位记到9,下一个秒十位进位信号到来,产生分钟十位使能信号
endmodule
min1_ctrl模块:产生秒十位数据信号,小时个位使能信号
/*分钟十位控制模块*/
module min1_ctrl(
input clk,
input rst_n,
input flag,
output reg [3:0] min1,//秒十位数据信号
output flag_hour//小时个位使能信号
);
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
min1 <= 4'd0;
else
if(flag == 1)
if(min1 < 5)//最大记到5
min1 <= min1 + 4'd1;
else
min1 <= 4'd0;
else
min1 <= min1;
end
assign flag_hour = ((min1 == 5)&&(flag == 1))?1'b1:1'b0;
//分钟十位记到5,下一个分钟个位进位信号到来,产生小时个位使能信号
endmodule
hour0_ctrl模块:产生小时个位数据信号,小时十位使能信号
/*小时个位控制模块*/
module hour0_ctrl(
input clk,
input rst_n,
input flag,
input [3:0] hour1,
output reg [3:0] hour0,//小时个位数据信号
output reg flag_10hour//小时十位使能信号
);
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
hour0 <= 4'd0;
else
if(hour1 < 2)//当小时十位信号小于2时,个位最大记到9
begin
if(flag == 1)
if(hour0 < 9)
hour0 <= hour0 + 4'd1;
else
hour0 <= 4'd0;
else
hour0 <= hour0;
end
else//当小时十位信号等于2时,个位最大记到3
begin
if(flag == 1)
if(hour0 < 3)
hour0 <= hour0 + 4'd1;
else
hour0 <= 4'd0;
else
hour0 <= hour0;
end
end
always @(*) begin
//进位信号控制,根据小时十位的不同数据,产生不同的进位信号条件
if(rst_n == 0)
flag_10hour <= 1'b0;
else
if(flag == 1)
begin
case(hour1)
4'd0: begin
if(hour0 == 9)//十位信号为0,个位逢9进一
flag_10hour <= 1'b1;
else
flag_10hour <= 1'b0;
end
4'd1: begin
if(hour0 == 9)//十位信号为1,个位逢9进一
flag_10hour <= 1'b1;
else
flag_10hour <= 1'b0;
end
4'd2: begin
if(hour0 == 3)//十位信号为2,个位逢3进一
flag_10hour <= 1'b1;
else
flag_10hour <= 1'b0;
end
default : flag_10hour <= 1'b0;
endcase
end
else
flag_10hour <= 1'b0;
end
endmodule
hour1_ctrl模块:产生小时十位数据信号
/*小时十位控制模块*/
module hour1_ctrl(
input clk,
input rst_n,
input flag,
output reg [3:0] hour1//小时十位数据信号
);
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
hour1 <= 4'd0;
else
if(flag == 1)
if(hour1 < 2)//24小时制,最大记到2
hour1 <= hour1 + 4'd1;
else
hour1 <= 4'd0;
else
hour1 <= hour1;
end
endmodule
time_ctrl模块:时钟数据产生模块,连接时钟各个模块
module time_ctrl(
input clk,
input rst_n,
output [23:0] data_out
);
//时钟数据产生模块
//连接时钟各个模块
wire flag;
wire flag_10sec;
wire [3:0] sec0;
wire flag_min;
wire [3:0] sec1;
wire flag_10min;
wire [3:0] min0;
wire flag_hour;
wire [3:0] min1;
wire flag_10hour;
wire [3:0] hour1;
wire [3:0] hour0;
one_sec /*#(.MAX(5))*/one_sec_inst(
.clk(clk),
.rst_n(rst_n),
.flag(flag)
);
sec0_ctrl sec0_ctrl_inst(
.clk(clk),
.rst_n(rst_n),
.flag(flag),
.sec0(sec0),
.flag_10sec(flag_10sec)
);
sec1_ctrl sec1_ctrl_inst(
.clk(clk),
.rst_n(rst_n),
.flag(flag_10sec),
.sec1(sec1),
.flag_min(flag_min)
);
min0_ctrl min0_ctrl_inst(
.clk(clk),
.rst_n(rst_n),
.flag(flag_min),
.min0(min0),
.flag_10min(flag_10min)
);
min1_ctrl min1_ctrl_inst(
.clk(clk),
.rst_n(rst_n),
.flag(flag_10min),
.min1(min1),
.flag_hour(flag_hour)
);
hour0_ctrl hour0_ctrl_inst(
.clk(clk),
.rst_n(rst_n),
.flag(flag_hour),
.hour1(hour1),
.hour0(hour0),
.flag_10hour(flag_10hour)
);
hour1_ctrl hour1_ctrl_inst(
.clk(clk),
.rst_n(rst_n),
.flag(flag_10hour),
.hour1(hour1)
);
assign data_out = {hour1,hour0,min1,min0,sec1,sec0};//合并数据
endmodule
seg_driver模块:数码管驱动,产生数码管位选和段选信号
module seg_driver(
input clk,
input rst_n,
input [23:0] data_in,//接收数字时钟信号
output reg [2:0] sel,//位选信号
output reg [7:0] seg//段选信号
);
reg [18:0] cnt;//1MS
parameter MAX = 19'd50_000;
//20ns记50000次
//产生1ms的延时
//位选信号1ms变换一次
reg [3:0] temp;
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
cnt <= 19'd0;
else
if(cnt < MAX - 1)//产生1ms的延时
cnt <= cnt + 19'd1;
else
cnt <= 19'd0;
end
//数码管位选信号控制逻辑
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
sel <= 3'd0;
else
if(cnt == MAX - 1)//位选信号6位,1ms的延时到变化一次
if(sel < 5)
sel <= sel + 3'd1;
else
sel <= 3'd0;
else
sel <= sel;
end
always @(*) begin
if(rst_n == 0)
temp <= 4'd0;
else
case(sel)//位选信号到将数据输出到对应输出位上
3'd0 : temp <= data_in[23:20];
3'd1 : temp <= data_in[19:16];
3'd2 : temp <= data_in[15:12];
3'd3 : temp <= data_in[11:8];
3'd4 : temp <= data_in[7:4];
3'd5 : temp <= data_in[3:0];
default : temp <= 4'd0;
endcase
end
always @(*) begin
if(rst_n == 0)
seg <= 8'hff;
else
case(temp)//段选信号,对应的数码管信号
4'd0 : seg <= 8'b1100_0000;//0
4'd1 : seg <= 8'b1111_1001;//1
4'd2 : seg <= 8'b1010_0100;//2
4'd3 : seg <= 8'b1011_0000;//3
4'd4 : seg <= 8'b1001_1001;//4
4'd5 : seg <= 8'b1001_0010;//5
4'd6 : seg <= 8'b1000_0010;//6
4'd7 : seg <= 8'b1111_1000;//7
4'd8 : seg <= 8'b1000_0000;//8
4'd9 : seg <= 8'b1001_0000;//9
4'd10 : seg <= 8'b1000_1000;//A
4'd11 : seg <= 8'b1000_0011;//b
4'd12 : seg <= 8'b1100_0110;//C
4'd13 : seg <= 8'b1010_0001;//d
4'd14 : seg <= 8'b1000_0110;//E
4'd15 : seg <= 8'b1000_1110;//F
default : seg <= 8'hff;
endcase
end
endmodule
easy_digital_clock模块:工程顶层模块进行连线
module easy_digital_clock(
input clk,
input rst_n,
output [2:0] sel,
output [7:0] seg
);
wire [23:0] data_out;//连线
time_ctrl time_ctrl_inst(
.clk(clk),
.rst_n(rst_n),
.data_out(data_out)
);
seg_driver seg_driver_inst(
.clk(clk),
.rst_n(rst_n),
.data_in(data_out),
.sel(sel),
.seg(seg)
);
endmodule
仿真文件
`timescale 1ns/1ps
module easy_digital_clock_tb;
reg clk;
reg rst_n;
wire [2:0] sel;
wire [7:0] seg;
easy_digital_clock easy_digital_clock_inst(
.clk(clk),
.rst_n(rst_n),
.sel(sel),
.seg(seg)
);
initial clk = 1;
always #10 clk = ~clk;
initial begin
rst_n = 0;
#200
rst_n = 1;
#20000000//100*60*60*24
$stop;
end
endmodule
三、仿真验证
工程模块较多,这里直接验证数字钟产生的数据是否正确:
观察小时信号的数据,再十位为2时个位最大记到3,为1时最大记到9,
观察黄线标记处,该时刻之前为02:59:59,后一秒时间为003:00:00,满足数字钟设计要求,
此处数字钟信号为23:59:59到00:00:00的跳转,即一天结束。