Verilog实现数字钟以及一个bug的讨论
我现在是FPGA新手入门,数字钟大概是新手最常做的一个入门系统项目吧,所以也想分享以下我的方案,以及求教一个我无法解决的bug。
测试采用Altera的EP4CE6F17C8芯片开发板。
分频模块
module clk_div(cr,clk_50M,clk_1k,clk_5,clk_1);
parameter N_1k=50_000; //1kHz分频常数
parameter N_5=1000_0000; //500Hz分频常数
parameter N_1=5000_0000; //1Hz分频常数
input cr,clk_50M;
output reg clk_5,clk_1k,clk_1;
reg [14:0]cnt_1k;
reg [24:0]cnt_5;
reg [24:0]cnt_1;
always @(posedge clk_50M or negedge cr)
begin
if(~cr) begin
clk_1<=0;
clk_1k<=0;
clk_5<=0;
cnt_1<=0;
cnt_1k<=0;
cnt_5<=0;
end
else begin
//1kHz输出
if(cnt_1k<(N_1k/2-1)) cnt_1k<=cnt_1k+1'b1;
else begin
cnt_1k<=0;
clk_1k<=~clk_1k;
end
//5Hz输出
if(cnt_5<(N_5/2-1)) cnt_5<=cnt_5+1'b1;
else begin
cnt_5<=0;
clk_5<=~clk_5;
end
//1Hz输出
if(cnt_1<(N_1/2-1)) cnt_1<=cnt_1+1'b1;
else begin
cnt_1<=0;
clk_1<=~clk_1;
end
end
end
endmodule
时钟模块
分、秒60进制采用10进制和6进制两个模块构成。
//模10计数器
module cnt10(cp,cr,en,q);
input cp,
cr,
en;
output reg[3:0]q;
always @(posedge cp or negedge cr) begin
if(~cr) q<=4'b0000; //cr置低清零
else if(en) begin
if(q>=9) q<=4'b0000;
else q<=q+1;
end
else q<=q;
end
endmodule
//模6计数器
module cnt6(cp,cr,en,q);
input cp,
cr,
en;
output reg[3:0]q;
always @(posedge cp or negedge cr) begin
if(~cr) q<=4'b0000; //cr置低清零
else if(en) begin
if(q>=5) q<=4'b0000;
else q<=q+1;
end
else q<=q;
end
endmodule
//模60计数器
module cnt60(cp,cr,en,q);
input cp,cr,en;
output wire[7:0]q; //输出8位BCD码
wire ENP; //内部进位信号
cnt10 u0(.cp(cp),.cr(cr),.en(en),.q(q[3:0])); //引用模10计数器模块
cnt6 u1(.cp(cp),.cr(cr),.en(ENP),.q(q[7:4]));
assign ENP=(q[3:0]==4'b1001);
endmodule
//模24计数器
module cnt24(cp,cr,en,q);
input cp,cr,en;
output reg[7:0]q;
always @(posedge cp or negedge cr)
begin
//个位模10计数,十位模3计数,终态23
if(~cr) q<=8'h00; //清零
else if(~en) q<=q; //保持
else if(q[7:4]==2&&q[3:0]>=3||q[7:4]>2||q[3:0]>9) q[7:0]<=8'h00; //错误处理
else if(q[7:4]==2&&q[3:0]<3) begin q[7:4]<=q[7:4]; q[3:0]<=q[3:0]+1; end //20~23计数
else if(q[3:0]==9) begin q[7:4]<=q[7:4]+1; q[3:0]<=4'b0000; end //十位进位,各位清零
else begin q[3:0]<=q[3:0]+1; q[7:4]<=q[7:4]; end //其他情况个位加一
end
endmodule
时钟顶层模块
module clock(
input clk_1,clk_5,cr,en,
input key_2, //调分信号
input key_3, //调时信号
output [7:0]hour, //时信号
output [7:0]min, //分信号
output [7:0]sec //秒信号
);
wire sec_min; //秒-分进位信号
wire min_hour; //分-时进位信号
//引用模块
cnt60 u_sec(.cp(clk_1),.cr(cr),.en(en),
.q(sec));
cnt60 u_min(.cp(~sec_min),.cr(cr),.en(en),
.q(min));
cnt24 u_hour(.cp(~min_hour),.cr(cr),.en(en),
.q(hour));
assign sec_min=key_2?(sec==8'h59):clk_5;
assign min_hour=key_3?({min,sec}==16'h5959):clk_5;
endmodule
这里的clk_5是校时用的5Hz信号。key_2和key_3分别用于调校分和时,松开时接收上一级的进位信号,按下后接5Hz方波信号校时。
注意这里用16进制格式显示的是23时59分处的进位情况,秒到达59后正常进位。
显示译码模块
//六位数显管,1ms频率扫描,使用1kHz频率
//此处为共阳极数码管,低电平有效
//片选信号低电平有效
module dig_display(cp,dig,sel,seg);
input cp;
input [23:0]dig; //输入24位BCD码
output reg [5:0]sel; //选择六个数显管
output reg [7:0]seg; //输出8位译码(h为dp)
reg [2:0]cnt;
reg [3:0]dig_r; //实际转换的4位BCD码
initial cnt<=3'd0;
//片选,1ms刷新
always @(posedge cp)
begin
if(cnt<6) cnt<=cnt+1;
else cnt<=3'd0;
case(cnt)
0:begin dig_r<=dig[3:0];sel<=6'b111110; end //Q6,右一
1:begin dig_r<=dig[7:4];sel<=6'b111101; end //Q5,右二
2:begin dig_r<=dig[11:8];sel<=6'b111011; end //Q4,右三
3:begin dig_r<=dig[15:12];sel<=6'b110111; end //Q3,左三
4:begin dig_r<=dig[19:16];sel<=6'b101111; end //Q2,左二
5:begin dig_r<=dig[23:20];sel<=6'b011111; end //Q1,左一
default:sel<=6'b111111; //默认下均不选中
endcase
end
//译码
always @(dig_r)
begin
case(dig_r)
4'h0: seg=8'b1100_0000; //abcdef
4'h1: seg=8'b1111_1001; //bc
4'h2: seg=8'b1010_0100; //abdeg
4'h3: seg=8'b1011_0000; //abcdg
4'h4: seg=8'b1001_1001; //bcfg
4'h5: seg=8'b1001_0010; //acdfg
4'h6: seg=8'b1000_0010; //acdefg
4'h7: seg=8'b1111_1000; //abc
4'h8: seg=8'b1000_0000; //all
4'h9: seg=8'b1001_0000; //abcdfg
default:seg=8'b0111_1111; //出现错误显示小数点
endcase
end
endmodule
按键消抖模块
仅就完成一个数字钟而言,按键消抖不重要。但使用5Hz信号校时时,如果不进行按键消抖总是会导致跳动,于是还是加上了。
我的按键消抖方案是参考的下面这个链接按键消抖方案中的方案三来的,几乎完美实现了按键消抖,详细大家自己去看,我只做一点简单解释。
- 带自锁的原版方案
//源码来自:https://blog.csdn.net/zhoutaopower/article/details/103180042
module keyscan_sl(
input clk,
input cr,
input key_in,
output reg key_out
);
//上升沿/下降沿检测模块
reg key_in_0; //按键抽样现态
reg key_in_1; //按键上次抽样状态
always @(posedge clk or negedge cr)
begin
if(~cr) begin
key_in_0<=1'b1;
key_in_1<=1'b1;
end
else begin
key_in_0<=key_in;
key_in_1<=key_in_0;
end
end
//下降沿和上升沿的检测方案,记下
wire key_posedge;
wire key_negedge;
assign key_posedge=key_in_0 & (~key_in_1); //上次检测为0,现态为1时key_posedge置1
assign key_negedge=key_in_1 & (~key_in_0);
//状态机
reg[2:0]state; //三个状态
parameter IDLE = 3'b001;
parameter SAMPLING = 3'b010;
parameter DOWN = 3'b100;
reg en_cnt; //计数使能
reg key_pressed;
always @(posedge clk or negedge cr)
begin
if(~cr) begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b0;
end
else begin
case(state)
IDLE:begin
if(key_negedge) begin
state<=SAMPLING;
en_cnt<=1'b1;
key_pressed<=1'b0;
end
else begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b0;
end
end
SAMPLING:begin
if(key_posedge) begin
state <=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b0;
end
else begin
if(cnt_full) begin
state<=DOWN;
en_cnt<=1'b0;
key_pressed<=1'b0;
end
else begin
state<=SAMPLING;
en_cnt<=1'b1;
key_pressed<=1'b0;
end
end
end
DOWN:begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b1;
end
default:begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b0;
end
endcase
end
end
//Counter
reg [19:0]cnt;
always @(posedge clk or negedge cr)
begin
if(~cr) cnt<=20'h00000;
else if(en_cnt) cnt<=cnt+1;
else cnt<=20'h00000;
end
//Counter_full logic
parameter _20ms=100_0000; //50M晶振T=20ns,计数1000000次即为20ms
reg cnt_full;
always @(posedge clk or negedge cr)
begin
if(~cr) cnt_full<=1'b0;
else if(cnt==_20ms) cnt_full<=1'b1;
else cnt_full<=1'b0;
end
//保持按键输出状态(自锁)
always @(posedge clk or negedge cr)
begin
if(~cr) key_out<=1'b1;
else if(key_pressed) key_out<=~key_out;
end
endmodule
以上基本是原版copy下来的,当计数器满(20ms)后认为按键真实按下,置低电平并且按键松开后仍保持状态,类似于电气自锁,我的“暂停”按键使用的便是这个方案。但校时按键一般是按下校时,松开结束校时,所以需要一个没有自锁的,我根据这个方案改进了一个方案,基本思路就是将上面的三个状态复制了一遍用于检测上升沿(比较冗长,与主题无关不感兴趣请跳过):
- 取消自锁的改进方案
//仅当判定按键松开(key_loose=1)时才进行下降沿检测,当判定按键按下(key_pressed)才进行上升沿检测
//根据以下源码改进:https://blog.csdn.net/zhoutaopower/article/details/103180042
module keyscan(
input clk,
input cr,
input key_in,
output reg key_out
);
//上升沿/下降沿检测模块
reg key_in_0; //按键抽样现态
reg key_in_1; //按键上次抽样状态
always @(posedge clk or negedge cr)
begin
if(~cr) begin
key_in_0<=1'b1;
key_in_1<=1'b1;
end
else begin
key_in_0<=key_in;
key_in_1<=key_in_0;
end
end
//下降沿和上升沿的检测方案,记下
wire key_posedge;
wire key_negedge;
assign key_posedge=key_in_0 & (~key_in_1); //上次检测为0,现态为1时key_posedge置1
assign key_negedge=key_in_1 & (~key_in_0);
//状态机
reg[2:0]state; //三个状态
parameter IDLE = 3'b001;
parameter SAMPLING = 3'b010;
parameter CONFIRM = 3'b100;
reg en_cnt; //计数使能
reg key_loose; //按键松开
reg key_pressed; //按键按下
always @(posedge clk or negedge cr)
begin
if(~cr) begin
state<=IDLE;
en_cnt<=1'b0;
key_loose<=1'b1;
key_pressed<=1'b0;
end
else begin
if(key_loose) begin //按键松开,检测下降沿
case(state)
IDLE:begin
if(key_negedge) begin
state<=SAMPLING;
en_cnt<=1'b1;
key_pressed<=1'b0;
key_loose<=1'b1;
end
else begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b0;
key_loose<=1'b1;
end
end
SAMPLING:begin
if(key_posedge) begin
state <=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b0;
key_loose<=1'b1;
end
else begin
if(cnt_full) begin
state<=CONFIRM;
en_cnt<=1'b0;
key_pressed<=1'b0;
key_loose<=1'b1;
end
else begin
state<=SAMPLING;
en_cnt<=1'b1;
key_pressed<=1'b0;
key_loose<=1'b1;
end
end
end
CONFIRM:begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b1;
key_loose<=1'b0;
end
default:begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b0;
key_loose<=1'b1;
end
endcase
end
else if(key_pressed) begin //按键按下,检测上升沿
case(state)
IDLE:begin
if(key_posedge) begin
state<=SAMPLING;
en_cnt<=1'b1;
key_pressed<=1'b1;
key_loose<=1'b0;
end
else begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b1;
key_loose<=1'b0;
end
end
SAMPLING:begin
if(key_negedge) begin
state <=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b1;
key_loose<=1'b0;
end
else begin
if(cnt_full) begin
state<=CONFIRM;
en_cnt<=1'b0;
key_pressed<=1'b1;
key_loose<=1'b0;
end
else begin
state<=SAMPLING;
en_cnt<=1'b1;
key_pressed<=1'b1;
key_loose<=1'b0;
end
end
end
CONFIRM:begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b0;
key_loose<=1'b1;
end
default:begin
state<=IDLE;
en_cnt<=1'b0;
key_pressed<=1'b1;
key_loose<=1'b0;
end
endcase
end
end
end
//Counter
reg [19:0]cnt;
always @(posedge clk or negedge cr)
begin
if(~cr) cnt<=20'h00000;
else if(en_cnt) cnt<=cnt+1;
else cnt<=20'h00000;
end
//Counter_full logic
parameter _20ms=100_0000; //50M晶振T=20ns,计数1000000次即为20ms
reg cnt_full;
always @(posedge clk or negedge cr)
begin
if(~cr) cnt_full<=1'b0;
else if(cnt==_20ms) cnt_full<=1'b1;
else cnt_full<=1'b0;
end
always @(posedge clk or negedge cr)
begin
if(~cr) key_out<=1'b0;
else if(key_pressed) key_out<=1'b1;
else if(key_loose) key_out<=1'b0;
end
endmodule
顶层模块
module display_clock(
input cp,cr,key_1,
input key_2,key_3,
output [5:0]sel,
output [7:0]seg
);
wire clk_1; //1Hz信号
wire clk_1k; //1kHz信号
wire clk_5; //5Hz调时专用信号
wire key_min,key_sec;
wire [7:0]hour;
wire [7:0]min;
wire [7:0]sec;
//引用模块
clk_div u0(.cr(cr),.clk_50M(cp),
.clk_1k(clk_1k),.clk_5(clk_5),.clk_1(clk_1));
clock u1(.clk_1(clk_1),.clk_5(clk_5),
.cr(cr),.en(key_en),
.key_2(key_min),.key_3(key_sec),
.hour(hour),.min(min),.sec(sec));
dig_display u2(.cp(clk_1k),.dig({hour,min,sec}),
.sel(sel),.seg(seg));
//key_2、key_3不带自锁按键消抖
keyscan u3(.clk(cp),.cr(cr),.key_in(key_2),.key_out(key_min));
keyscan u4(.clk(cp),.cr(cr),.key_in(key_3),.key_out(key_sec));
//key_en带自锁
keyscan_sl u5(.clk(cp),.cr(cr),.key_in(key_1),.key_out(key_en));
endmodule
RTL结构图:
- cp:50MHz信号输入
- cr:复位信号输入(Key0)
- key_1:计数使能信号输入(Key1)
- key_2:分校时信号(Key2)
- key_3:时校时信号(Key3)
以上,可以顺利在开发板上输出结果。但是!!存在一个令我感到诡异的
BUG
秒在59处可以正常进位,同时还会在49处进位,但正如上方仿真结果所示,整个clock的计数模块是没有问题的,并没有出现在49秒处异常进位的情况。如果排除clock模块的问题,其他模块感觉都不会影响计数系统。还是希望有大神可以发现问题出在哪里指点一下,谢谢!