一个简单的基于FPGA的数字钟,语言用的是VerilogHDL,可以实现以下功能:
1. 数码管显示0-59(秒表)
2. 数码管显示:时-分-秒
3. 数码管显示时分秒并且可以设置时间(小时和分钟)
4. 在3的基础上,当分钟为59时,秒数从56-59依次对应不同LED亮
5. 在4的基础上,55秒、57秒、59秒警报,且59秒高音。
开发板上有四个LED灯(脚管输出高点亮)、一个长的数码管(有六个单个的,型号是LG3661BH)、四个按键(按下是低电平)
硬件 :altera公司的Cyclone IV --EP4CE6F17C8N
软件: Quartus Prime Lite Edition 17.1
操作系统:Windows10
按键消抖模块主要实现原理:采用两级触发器储存两个时钟节拍下连续读到的管教电平信息,通过比较两级触发器的数值来确定按键按下的状态是否稳定,稳定的话(两级触发器的值相同),启动定时器开始定时,期间如果值不稳定的话定时器会清零,若是定时器计数到指定的值后,输出键值。其具体代码如下
//按键消抖模块
module Btn_debounce(
input Btn_Clk, //输入时钟
input Btn_in, //键值输入
input Btn_nRst, //复位
output reg Btn_out //输出
);
parameter BTN_FRE = 50; //50MHZ
parameter DLY_TIME = 20; //20ms延时计数
parameter DLY_TIME_VAL = 1000*DLY_TIME*BTN_FRE;//计数值
reg[31:0] store_cnt;
reg[31:0] next_cnt;
//采用两级触发器来比较前后键值
reg DFF1;
reg DFF2;
wire IsReachTime; //稳定时间到达信号
wire IsKeyValSame; //触发器两次值相同信号
//获取当前按键状态
always@(posedge Btn_Clk or negedge Btn_nRst)
begin
if(1'b0 == Btn_nRst)
begin
DFF1 <= 1'b1;
DFF2 <= 1'b1;
store_cnt <= 1'b0 ;
end
else
begin
DFF1 <= Btn_in ; //获取当前键值
DFF2 <= DFF1;
store_cnt <= next_cnt;
end
end
//输出稳定键值
always@(posedge Btn_Clk or negedge Btn_nRst)
begin
if(1'b0 == Btn_nRst) Btn_out <= 1'b1 ;//复位输出值
//到达时间(按键以稳定一段时间了)输出键值
else if(store_cnt == DLY_TIME_VAL) Btn_out <= DFF2 ;
else Btn_out <= Btn_out ;
end
//键值稳定时间计数
assign IsKeyValSame = (DFF1^DFF2);//异或,不同为1相同为0
assign IsReachTime = ~(store_cnt == DLY_TIME_VAL); //
always@(IsKeyValSame or IsReachTime or store_cnt)
begin
if({IsKeyValSame,IsReachTime} == 2'b00) //时间到了并且稳定,可以输出
next_cnt <= store_cnt; //锁定计数值;
else if({IsKeyValSame,IsReachTime} == 2'b01) //计数时间没到
next_cnt <= store_cnt+1;
else
next_cnt <= 32'b0; //键值不稳定,计数值清零
end
endmodule
//共阳极数码管译码器
module seg_decoder(
input nEn,
input[3:0] bin_data, //4位二进制输入
output reg[6:0] seg_data //段码输出
);
always@(seg_data or nEn)
begin
if(nEn == 1'b1) seg_data <=7'b111_1111;
else
begin
case(bin_data)
4'd0:seg_data <= 7'b100_0000; //0
4'd1:seg_data <= 7'b111_1001;
4'd2:seg_data <= 7'b010_0100;
4'd3:seg_data <= 7'b011_0000;
4'd4:seg_data <= 7'b001_1001;
4'd5:seg_data <= 7'b001_0010;
4'd6:seg_data <= 7'b000_0010;
4'd7:seg_data <= 7'b111_1000;
4'd8:seg_data <= 7'b000_0000;
4'd9:seg_data <= 7'b001_0000;
4'ha:seg_data <= 7'b000_1000;
4'hb:seg_data <= 7'b000_0011;
4'hc:seg_data <= 7'b100_0110;
4'hd:seg_data <= 7'b010_0001;
4'he:seg_data <= 7'b000_0110;
4'hf:seg_data <= 7'b000_1110; //F
default:seg_data <= 7'b111_1111;
endcase
end
end
endmodule
module seg_scaner(
input Clk, //时钟信号
input nRst,
output[3:0] seg_cnt, //
output[5:0] seg_sel
);
//parameter SCAN_FRE = 10000000 ; //用于波形仿真测试
parameter SCAN_FRE = 10000 ;
parameter SYS_FRE = 50000000;
parameter SCAN_COUNT = SYS_FRE/SCAN_FRE*6-1; //6个数码管
reg[31:0] scan_counter;
reg[3:0] Tscan_sel;
reg[5:0] Tseg_sel;
assign seg_sel = Tseg_sel;
assign seg_cnt = Tscan_sel;
always@(posedge Clk or negedge nRst)
begin
if(1'b0 == nRst)
begin
scan_counter <= 32'd0;
Tscan_sel <= 4'd0;
end
else if(scan_counter >= SCAN_COUNT)
begin
scan_counter <= 32'd0;
if(Tscan_sel == 4'd5) Tscan_sel <= 4'd0;
else Tscan_sel <= Tscan_sel+4'd1;
end
else
begin
scan_counter <= scan_counter+32'd10;
end
end
//类似于译码器
always@(posedge Clk or negedge nRst)
begin
if(1'b0 == nRst) Tseg_sel <= 6'b111111; //共阳极,位选端
else
begin
case(Tscan_sel)
4'd0:Tseg_sel <= 6'b111110;
4'd1:Tseg_sel <= 6'b111101;
4'd2:Tseg_sel <= 6'b111011;
4'd3:Tseg_sel <= 6'b110111;
4'd4:Tseg_sel <= 6'b101111;
4'd5:Tseg_sel <= 6'b011111;
default:Tseg_sel <= 6'b111111;
endcase
end
end
endmodule
//计数器模块
module MyNcounter
#(parameter BIT=4,
parameter N = 10)
(
input CntClk,
input CntnRst,
output[BIT-1:0] CntDout, //数据输出
output CntCout //进位输出
);
reg[BIT-1:0] rDout;
reg rCout;
assign CntDout = rDout;
assign CntCout = rCout;
always@(posedge CntClk or negedge CntnRst)
begin
if(1'b0 == CntnRst)
begin
rDout <= {BIT {1'b0}};
rCout <= 1'b0;
end
else
begin
if(rDout < N-1)
begin
//rDout <= rDout+{BIT {1'b1}};
rDout <= rDout+1;
rCout <= 1'b0;
end
else
begin
rDout <= {BIT {1'b0}};
rCout <= 1'b1;
end
end
end
endmodule
//23点计数器
module My2BitNcounter
#(parameter BIT=8)
(
input CntClk,
input CntnRst,
output[BIT/2-1:0] CntHDout, //数据输出
output[BIT/2-1:0] CntLDout,
output CntCout //进位输出
);
reg[BIT/2-1:0] rHDout;
reg[BIT/2-1:0] rLDout;
reg rCout;
assign CntHDout = rHDout;
assign CntLDout = rLDout;
assign CntCout = rCout;
always@(posedge CntClk or negedge CntnRst)
begin
if(1'b0 == CntnRst)
begin
rHDout <= {BIT/2 {1'b0}};
rLDout <= {BIT/2 {1'b0}};
rCout <= 1'b0;
end
else
begin //23
if({rHDout,rLDout} < 8'b0010_0011) //正常
//if({rHDout,rLDout} <= 8'd23) //错误代码
begin
if(rLDout < 9)
begin
rLDout <= rLDout+1;
rCout <= 1'b0;
end
else
begin
rLDout <= {BIT/2 {1'b0}};
if(rHDout < 9)
begin
rHDout <= rHDout+1;
rCout <= 1'b0;
end
else
begin
rHDout <= {BIT/2 {1'b0}};
rCout <= 1'b0;
end
end
end
else
begin
rHDout <= {BIT/2 {1'b0}};
rLDout <= {BIT/2 {1'b0}};
rCout <= 1'b1;
end
end
end
endmodule
####6)分频器模块
//分频器独立小模块
module DigCtrlFre #(parameter N=8)
(
input PreClk,
input DivnRst,
input[N-1:0] PreNum,
output DivClk
);
reg[N-1:0] rCnt;
reg rDivClk;
reg[N-1:0] rPreNum;
assign DivClk = rDivClk;
always@(posedge PreClk or negedge DivnRst)
begin
if(1'b0 == DivnRst)
begin
rDivClk <= {N {1'b0}};
rPreNum <= {N {1'b0}};
rCnt <= {N {1'b0}};
end
else
begin
//防止从高分频系数到低分频系数变化时需等待1周期
if(rPreNum == PreNum)
begin
rCnt <= rCnt+1;
if(rCnt == PreNum)
begin
rDivClk <= (~rDivClk);
rCnt <= {N {1'b0}};
end
else
rDivClk <= rDivClk;
end
else
begin
rPreNum <= PreNum;
rCnt <= {N {1'b0}};
end
end
end
endmodule
/*======================================================================
实验内容:
1. 数码管显示0-59(秒表)
2. 数码管显示:时-分-秒
3. 数码管显示时分秒并且可以设置时间(小时和分钟)
4. 在3的基础上,当分钟为59时,秒数从56-59依次对应不同LED亮
5. 在4的基础上,55秒、57秒、59秒警报,且59秒高音
文件说明:
top1:实验内容1的顶层文件 seg_scaner: 6个数码管扫描显示文件
top2:实验内容2的顶层文件 seg_decoder: 数码管段码译码文件
top3:实验内容3的顶层文件 Btn_debounce:按键消抖模块文件
top4:实验内容4的顶层文件 MyNcounter: 任意进制计数器
top5:实验内容5的顶层文件 DigCtrlFre: 时钟分频模块
注意事项:
管脚分配:
CLK: E1
NRST: N13
SEG_SEL[0-5]: N9,P9,M10,N11,P11,M11
SEG_DATA[0-7]: R14,N16,P16,T15,P15,N12,N15,R16
KEYTSET: M15
KEYMADD: M16
KEYHADD: E16
LED_OUT[0-3]: E10,F9,C9,D9(高电平亮)
BUZ_OUT: C11(无源,低电平响)
代码方面:
按设置键后,小时和分钟个位会加一次(已解决)
=======================================================================*/
module top5(
input CLK,
input NRST,
input KEYTSET,
input KEYHADD, //小时加
input KEYMADD, //分钟加
output[5:0] SEG_SEL,
output[7:0] SEG_DATA,
output reg[3:0] LED_OUT,
output BUZ_OUT
);
reg[3:0] rBinData;
//reg rSegDecoderEn;
reg IsTimeSetState;
reg rBUZ_OUT;
wire[6:0] rSEG_DATA; //数码管译码器译码输出
wire wClk1HZ; //1HZ时钟线
wire wClk2kHZ;
wire wClk500HZ;
wire[3:0] wSegCnt;
//秒钟
wire[3:0] wSecCntGe;
wire wSecCntGeCy; //个位计数器进位输出
wire[3:0] wSecCntShi; //十位计数器数据输出
wire wSecCntShiCy; //十位计数器进位输出
//分钟
wire[3:0] wMinCntGe;
wire wMinCntGeCy; //个位计数器进位输出
wire[3:0] wMinCntShi; //十位计数器数据输出
wire wMinCntShiCy; //个位计数器进位输出
//小时
wire[3:0] wHorCntGe;
wire whorCntGeCy; //个位计数器进位输出
wire[3:0] wHorCntShi; //十位计数器数据输出
//按键消抖模块输出
wire wKeyBdSet;
wire wKeyBdHadd;
wire wKeyBdMadd;
reg rHorDrivClk; //小时驱动时钟
reg rMinDrivClk; //分钟驱动时钟
reg rSecDrivClk;
assign BUZ_OUT = rBUZ_OUT;
//显示时间格式为23.00.00(时 分 秒)
assign SEG_DATA = (wSegCnt%2==0)?{1'b1,rSEG_DATA}:{1'b0,rSEG_DATA};
//1HZ时间显示更新时钟
DigCtrlFre #(.N(32))
DigCtrlFre_m4(
.PreClk(CLK),
.DivnRst(NRST),
.PreNum(25000000),
//.PreNum(1), //用于波形仿真测试
.DivClk(wClk1HZ)
);
//2khz蜂鸣器驱动时钟
DigCtrlFre #(.N(32))
DigCtrlFre_m41(
.PreClk(CLK),
.DivnRst(NRST),
.PreNum(12500),
//.PreNum(1), //用于波形仿真测试
.DivClk(wClk2kHZ)
);
//500hz蜂鸣器驱动时钟
DigCtrlFre #(.N(32))
DigCtrlFre_m42(
.PreClk(CLK),
.DivnRst(NRST),
.PreNum(50000),
//.PreNum(1), //用于波形仿真测试
.DivClk(wClk500HZ)
);
//数码管扫描
seg_scaner seg_scaner_m4(
.Clk(CLK),
.nRst(NRST),
.seg_cnt(wSegCnt),
.seg_sel(SEG_SEL)
);
//位选
always@(wSegCnt)
begin
case(wSegCnt)
3'b000:rBinData <= wHorCntShi;
3'b001:rBinData <= wHorCntGe;
3'b010:rBinData <= wMinCntShi;
3'b011:rBinData <= wMinCntGe;
3'b100:rBinData <= wSecCntShi;
3'b101:rBinData <= wSecCntGe;
default:rBinData <= 1'b0;
endcase
end
//秒个位计数器
MyNcounter #(.BIT(4), //计数器位宽
.N(10) //计数器进制
)
MyNcounter_m40(
.CntClk(rSecDrivClk),
.CntnRst(NRST),
.CntDout(wSecCntGe),
.CntCout(wSecCntGeCy)
);
//秒十位计数器
MyNcounter #(.BIT(4), //计数器位宽
.N(6) //计数器进制
)
MyNcounter_m41(
.CntClk(wSecCntGeCy),
.CntnRst(NRST),
.CntDout(wSecCntShi),
.CntCout(wSecCntShiCy)
);
//分个位计数器
MyNcounter #(.BIT(4), //计数器位宽
.N(10) //计数器进制
)
MyNcounter_m42(
.CntClk(rMinDrivClk),
.CntnRst(NRST),
.CntDout(wMinCntGe),
.CntCout(wMinCntGeCy)
);
//分十位计数器
MyNcounter #(.BIT(4), //计数器位宽
.N(6) //计数器进制
)
MyNcounter_m43(
.CntClk(wMinCntGeCy),
.CntnRst(NRST),
.CntDout(wMinCntShi),
.CntCout(wMinCntShiCy)
);
//时个位计数器
//MyNcounter #(.BIT(4), //计数器位宽
// .N(10) //计数器进制
// )
// MyNcounter_m44(
// .CntClk(rHorDrivClk),
// .CntnRst(NRST),
// .CntDout(wHorCntGe),
// .CntCout(wHorCntGeCy)
//
// );
//
//
时十位计数器
//MyNcounter #(.BIT(4), //计数器位宽
// .N(3) //计数器进制
// )
// MyNcounter_m45(
// .CntClk(wHorCntGeCy),
// .CntnRst(NRST),
// .CntDout(wHorCntShi),
// .CntCout()
//
// );
//时分
My2BitNcounter #(.BIT(8))
My2BitNcounter_m4(
.CntClk(rHorDrivClk),
.CntnRst(NRST),
.CntHDout(wHorCntShi), //数据输出
.CntLDout(wHorCntGe),
.CntCout() //进位输出
);
//时间设置模块
always@(posedge wKeyBdSet or negedge NRST)
begin
if(1'b0 == NRST) IsTimeSetState <= 1'b0; //防止初次启动不计数
else
begin
if(wKeyBdSet)
begin
IsTimeSetState <= ~IsTimeSetState;
end
else IsTimeSetState <= IsTimeSetState;
end
end
always@(IsTimeSetState)
begin
//(1'b1 == IsTimeSetState)//问题代码
if((1'b1 == IsTimeSetState) && (wKeyBdHadd == 1'b0 || wKeyBdMadd == 1'b0)) //正常
begin
rHorDrivClk <= wKeyBdHadd;
rMinDrivClk <= wKeyBdMadd;
rSecDrivClk <= 1'b0;
end
else if(1'b0 == IsTimeSetState)
begin
rHorDrivClk <= wMinCntShiCy;
rMinDrivClk <= wSecCntShiCy;
rSecDrivClk <= wClk1HZ;
end
end
//LED驱动模块
always@(posedge wClk1HZ or negedge NRST)
begin
if(1'b0 == NRST) LED_OUT <= 4'b0000;
else
begin
//if((wMinCntGe == 9) && (wMinCntShi == 1)) //测试用
if((wMinCntGe == 9) && (wMinCntShi == 5))
begin
if(wSecCntShi == 5)
begin
if(wSecCntGe == 6) LED_OUT <= 4'b0001;
else LED_OUT <= (LED_OUT<<1);
end
else LED_OUT <= 4'b0000;
end
else LED_OUT <= 4'b0000;
end
end
//蜂鸣器驱动模块
always@(posedge CLK)
begin
if((wMinCntGe == 9) && (wMinCntShi == 5))
begin
if(wSecCntShi == 5)
begin
case(wSecCntGe)
4'd5:rBUZ_OUT <= wClk500HZ;
4'd7:rBUZ_OUT <= wClk500HZ; //低音
4'd9:rBUZ_OUT <= wClk2kHZ; //高音
default:rBUZ_OUT <= 1'b1;
endcase
end
else rBUZ_OUT <= 1'b1;
end
else rBUZ_OUT <= 1'b1;
end
//按键消抖模块
Btn_debounce Btn_debounce_m40(
.Btn_Clk(CLK),
.Btn_in(KEYTSET),
.Btn_nRst(NRST),
.Btn_out(wKeyBdSet)
);
Btn_debounce Btn_debounce_m41(
.Btn_Clk(CLK),
.Btn_in(KEYHADD),
.Btn_nRst(NRST),
.Btn_out(wKeyBdHadd)
);
Btn_debounce Btn_debounce_m42(
.Btn_Clk(CLK),
.Btn_in(KEYMADD),
.Btn_nRst(NRST),
.Btn_out(wKeyBdMadd)
);
//数码管译码器
seg_decoder seg_decoder_m4(
.nEn(1'b0),
.bin_data(rBinData),
.seg_data(rSEG_DATA)
);
endmodule
设计具体实现思路是:通过分频器模块将50Mhz的输入时钟分别分为:
wClk1HZ(1HZ)给时分秒计数器
wClk2kHZ(2KHZ)驱动蜂鸣器发出高音
wClk500HZ(500HZ)驱动蜂鸣器发出低音
然后通过wClk1HZ驱动四个计数器级联实现分和秒的功能,使用两位24进制计数器实现时的功能。利用快速扫描和视觉残留效果实现6个数码管的同时显示(seg_scaner模块),在通过多路选择器(顶层文件中的case语句)实现不同数码管显示不同的数值。
对于时间设置的功能,设置按键按下,将分钟个位和小时个位的驱动时钟源换为按键按下抬起过程中产生的上升沿驱动,当设置按键再次按下时,时钟源再次切换为经过分频器分频后的1HZ时钟,时间加按键失效。
LED和蜂鸣器功能实现的方法差不多,通过判断秒数个位来进行不同的操作,LED模块通过移位实现不同灯的点亮,蜂鸣器硬件上采用的是有源蜂鸣器(接上高电平就叫),通过切换不同频率的驱动时钟源,实现不同声调的声音输出。
因为刚开始学FPGA 和verilogHDL语言,所以代码在资源节约利用和规范性上有所欠缺,但是基本的功能都已实现;按照惯例,每次新学一个东西总是想发个博客记录一下。各位参考着看,若是有些可以改进之处也欢迎大家讨论。