前段时间刚刚开始初步学习FPGA相关知识,在学习了一段时间后,利用前面所学知识,写了一个数字时钟,顺便在这里写下总结,方便理解。
(本人小白一名,有错欢迎指出,欢迎探讨)
(我使用的开发板是Cyclone IV的EP4CE6F17C8,如有想测试实现效果的同学,可以把后面3-1到3-5对应代码 建文件,修改一下开发板型号以及引脚位置,进行测试。)
我使用的开发板拥有6个数码管(由位选信号控制每一位的亮灭),每个数码管都有8个小段(由段选信号控制对应小段显示,得到目标数字,其中7个控制数码管小段,还有1个控制小数点),由于时钟显示时,每个位置数字都变化不同,而我们的段选信号是控制6个数码管的8个小段同时呈现什么效果(意思就是6个数码管上显示的数字是一样的),要想实现每个位置数字看起来不一样,我们就需要利用视觉暂留现象,显示时候只能让其中一个位显示,其他位不显示,让数码管位选信号切换的足够快,也就是说数码管对应位由亮到灭需要的时候很短,人眼就无法分清此时此刻数码管状态,使人感觉数码管一直都亮着,且亮着的那一位是正确的数字,这样就能达到看起来,不同位置数字不同且都亮着的效果(也就是我们需要的数字时钟效果)。
有三个子模块和一个总的处理模块,其中三个子模块分别是key_debounce(消抖模块),count(计数时钟沿,计数1s模块),count2(计数20ms,控制位选信号跳转时间)。总模块count_clk负责处理输入的信号,有6个计数器,分别计数秒,分,时的个位,十位,还有对位选信号的处理和译码部分。
这里我使用的计数方法是6个计数器分别计数时,分,秒的个位和十位,计数满了则使进位符置1,给到下一位,这个方法不需要进行取余取整操作,使用触发器资源较多,但节省组合逻辑资源。
module count(
input wire clk,
input wire rst_n,
input wire ok,
output reg flag1
);
reg[25:0] cnt;
//parameter MAX=24'd31250;//飞驰版54秒24小时 (上板时也可以使用这个,可以看到数码管显示很快)
parameter MAX=26'd50_000_000;//1s (上板时使用这个)
//parameter MAX=20'd1000_000;//测试 (modelsim时使用这个)
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
cnt<=26'd0;
flag1<=1'd0;
end
else if(cnt==MAX-1'd1)
begin
cnt<=26'd0;
flag1<=1'd1;
end
else if(ok)
begin
cnt<=cnt;
flag1<=1'd0;
end
else
begin
cnt<=cnt+1'd1;
flag1<=1'd0;
end
end
endmodule
module count2(
input wire clk,
input wire rst_n,
output reg box
);
reg[19:0] cnt;
parameter MAX=20'd1_000_0;//20ms
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
cnt<=20'd0;
box<=1'd0;
end
else if(cnt==MAX-1'd1)
begin
cnt<=20'd0;
box<=1'd1;
end
else
begin
cnt<=cnt+1'd1;
box<=1'd0;
end
end
endmodule
module key_debounce(
input clk,
input rst_n,
input wire [2:0] key,
output reg ok,
output reg flag9,//改变暂停时数字
output reg flag10//控制数码管调整数字的位置
);
parameter MAX_TEN=21'd1000_000;//20ms
reg [20:0] delay_cnt;
reg [20:0] delay_cnt2;
reg [20:0] delay_cnt3;
reg value_key;
reg value_key2;
reg value_key3;
reg[0:0] flag8;
//reg[0:0] flag9;
//消抖模块
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
value_key<=1'd1;
delay_cnt<=MAX_TEN;
end
else
begin
value_key<=key[0];//取得一瞬间key值
if(value_key==!key[0])//如果抖动
begin
delay_cnt<=MAX_TEN;
end
else if(delay_cnt>1'd0&&value_key==1'd0)//当没有抖动时候,开始计数
begin
delay_cnt<=delay_cnt-1'd1;
end
else
begin
delay_cnt<=21'd0;
end
end
end
//消抖符号的处理
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
flag8<=1'd0;
end
else if(delay_cnt==21'd1&&value_key==1'd0)
begin
flag8<=1'd1;
end
else
begin
flag8<=1'd0;
end
end
//暂停标识符的转化
always@(posedge flag8)
begin
if(flag8 && !value_key)//按下消除抖动后才置1(没有松开)
begin
ok<=~ok;
end
end
//消抖模块(按键三的消抖)
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
value_key2<=1'd1;
delay_cnt2<=MAX_TEN;
end
else
begin
value_key2<=key[1];//取得一瞬间key值(寄存器存储key值)
if(value_key2==!key[1])//如果抖动
begin
delay_cnt2<=MAX_TEN;
end
else if(delay_cnt2>1'd0&&value_key2==1'd0)//当没有抖动时候,开始计数
begin
delay_cnt2<=delay_cnt2-1'd1;
end
else
begin
delay_cnt2<=21'd0;
end
end
end
//消抖符号的处理
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
flag9<=1'd0;
end
else if(delay_cnt2==21'd1&&value_key2==1'd0)
begin
flag9<=1'd1;//表示按键按下了一次并未松开,且抖动已经消除了
end
else
begin
flag9<=1'd0;
end
end
//消抖模块(按键四的消抖)
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
value_key3<=1'd1;
delay_cnt3<=MAX_TEN;//这里其实可以去掉,就不用给后面加东西了
end
else
begin
value_key3<=key[2];//取得一瞬间key值
if(value_key3==!key[2])//如果抖动
begin
delay_cnt3<=MAX_TEN;
end
else if(delay_cnt3>1'd0&&value_key3==1'd0)//当没有抖动时候,开始计数
begin
delay_cnt3<=delay_cnt3-1'd1;
end
else
begin
delay_cnt3<=21'd0;
end
end
end
//消抖符号的处理
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
flag10<=1'd0;
end
else if(delay_cnt3==21'd1&&value_key3==1'd0)//注意(当复位后,置了MAX值,所以会导致计数一次(此时没有抖动),会造成flag10跟flag9置1一次,进而导致暂停时候复位,会让调数的位置默认加一和数字默认加一)
begin
flag10<=1'd1;//表示按键按下了一次并松开,且抖动已经消除了
end
else
begin
flag10<=1'd0;
end
end
//加一标识符的转变
//always@(posedge flag9)
//begin
// if(flag9 && !value_key2)
// begin
// ok2<=~ok2;
// end
//end
endmodule
module count_clk(
input wire clk,
input wire rst_n,
input wire flag1,//计数满1s
//input wire key,//为1时,表示无效,没被按下
input wire box,//控制位选跳转(流水灯)
input wire flag9,
input wire flag10,
input wire ok,
output reg[5:0] sel,
output reg[7:0] seg
);
reg [2:0] sum=3'd0;//计数控制调哪个位置的数字
reg [4:0] s_a;//秒的个位
reg [3:0] s_b;//秒的十位
reg [4:0] m_a;//分的个位
reg [3:0] m_b;//分的十位
reg [4:0] t_a;//时的个位
reg [2:0] t_b;//时的十位
reg [0:0] flag2 = 0;//秒的个位进位标识符
reg [0:0] flag3 = 0;//秒的十位进位标识符
reg [0:0] flag4 = 0;//分的个位进位标识符
reg [0:0] flag5 = 0;//分的十位进位标识符
reg [0:0] flag6 = 0;//时的个位进位标识符
reg [0:0] flag7 = 0;//时的十位进位标识符
reg [0:0] clear =0;//计数满了清零符
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
sum<=3'd0;
else if(flag10)
begin
if(sum<3'd5)
begin
sum<=sum+1'd1;
end
else
begin
sum<=3'd0;
end
end
else
sum<=sum;
end
//秒的个位计数(暂停采用的止住flag进位,而不是采用禁用另一个计数模块的clk) (已经改成clk暂停了)
always@(posedge clk or negedge rst_n )
begin
if(!rst_n)
begin
s_a<=5'd0;
flag2<=1'd0;
end
else if(flag1)
begin
if(s_a<5'd9)
begin
s_a<=s_a+1'd1;
flag2<=1'd0;
end
else
begin
s_a<=5'd0;
flag2<=1'd1;
end
end
else if(flag9)
begin
if(ok==1'd1&&sum==3'd0)
begin
if(s_a<5'd9)
begin
s_a<=s_a+1'd1;
end
else
begin
s_a<=5'd0;
end
end
end
else
begin
s_a<=s_a;
flag2<=1'd0;//因为改成clk触发了,触发频率变大,如果不是设置置零,则会在下一个always语句中,多次触发flag2为1 的情况,疯狂计数(最开始写法为flagx触发时候,没有考虑到这种情况,现在改为clk,则需要考虑到不进位时clk上升沿来了触发的情况)
end
end
//秒的十位计数
always@(posedge clk or negedge rst_n )
begin
if(!rst_n)
begin
s_b<=3'd0;
flag3<=1'd0;
end
else if(flag2)
begin
if(s_b<3'd5)
begin
s_b<=s_b+1'd1;
flag3<=1'd0;
end
else
begin
s_b<=3'd0;
flag3<=1'd1;
end
end
else if(flag9)
begin
if(ok==1'd1&&sum==3'd1)
begin
if(s_b<3'd5)
begin
s_b<=s_b+1'd1;
end
else
begin
s_b<=3'd0;
end
end
end
else
begin
flag3<=1'd0;
s_b<=s_b;
end
end
//分的个位计数
always@(posedge clk or negedge rst_n )
begin
if(!rst_n)
begin
m_a<=4'd0;
flag4<=1'd0;
end
else if(flag3)
begin
if(m_a<4'd9)
begin
m_a<=m_a+1'd1;
flag4<=1'd0;
end
else
begin
m_a<=4'd0;
flag4<=1'd1;
end
end
else if(flag9)
begin
if(ok==1'd1&&sum==3'd2)
begin
if(m_a<4'd9)
begin
m_a<=m_a+1'd1;
end
else
begin
m_a<=4'd0;
end
end
end
else
begin
m_a<=m_a;
flag4<=1'd0;
end
end
//分的十位计数
always@(posedge clk or negedge rst_n )
begin
if(!rst_n)
begin
m_b<=3'd0;
flag5<=1'd0;
end
else if(flag4)
begin
if(m_b<3'd5)
begin
m_b<=m_b+1'd1;
flag5<=1'd0;
end
else
begin
m_b<=3'd0;
flag5<=1'd1;
end
end
else if(flag9)
begin
if(ok==1'd1&&sum==3'd3)
begin
if(m_b<3'd5)
begin
m_b<=m_b+1'd1;
end
else
begin
m_b<=3'd0;
end
end
end
else
begin
m_b<=m_b;
flag5<=1'd0;
end
end
//时的个位计数
always@(posedge clk or negedge rst_n )
begin
if(!rst_n)
begin
t_a<=4'd0;
flag6<=1'd0;
end
else if(flag5)
begin
if(t_a==4'd9)
begin
t_a<=4'd0;
flag6<=1'd1;
end
else if(t_a==4'd3&&t_b==2'd2)
begin
t_a<=4'd0;
clear<=1'd1;//传递给下一位使其判断清零的符号。
flag6<=1'd1;
end
else
begin
t_a<=t_a+1'd1;
flag6<=1'd0;
clear<=1'd0;
end
end
else if(flag9)
begin
if(ok==1'd1&&sum==3'd4)
begin
if(t_a<4'd9&&t_b!=2'd2)
begin
t_a<=t_a+1'd1;
end
else if(t_b==2'd2)
begin
if(t_a<4'd3)
t_a<=t_a+1'd1;
else
t_a<=4'd0;
end
else
begin
t_a<=4'd0;
end
end
end
else
begin
t_a<=t_a;
flag6<=1'd0;
end
end
//时的十位计数(已设置满23:59:59自动清零)
always@(posedge clk or negedge rst_n )//?不能加第二个(or posedge clear)加了会导致个位跟十位显示相同,小数点都相同
begin
if(!rst_n)
begin
t_b<=2'd0;
end
else if(flag6)//flag6表示进位,也可以代表清零。
begin
if(clear)
begin
t_b<=2'd0;
end
else
t_b<=t_b+1'd1;
end
else if(flag9)
begin
if(ok==1'd1&&sum==3'd5)
begin
if(t_b<2'd2)
begin
t_b<=t_b+1'd1;
end
else
begin
t_b<=2'd0;
end
end
end
else
begin
t_b<=t_b;
end
end
//位选流水灯(实现快速闪烁每一个数码管形成残影,造成时钟现象)
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
sel<=6'b111_110;
end
else if(box)
begin
sel<={sel[4:0],sel[5]};
end
else
begin
sel<=sel;
end
end
//译码模块
always@(sel)
begin
if(sel==6'b111110)
begin
case(s_a)
4'd0:seg=8'b11000000;
4'd1:seg=8'b11111001;
4'd2:seg=8'b10100100;
4'd3:seg=8'b10110000;
4'd4:seg=8'b10011001;
4'd5:seg=8'b10010010;
4'd6:seg=8'b10000010;
4'd7:seg=8'b11111000;
4'd8:seg=8'b10000000;
4'd9:seg=8'b10010000;
default:;
endcase
end
else if(sel==6'b111101)
begin
case(s_b)
3'd0:seg=8'b11000000;
3'd1:seg=8'b11111001;
3'd2:seg=8'b10100100;
3'd3:seg=8'b10110000;
3'd4:seg=8'b10011001;
3'd5:seg=8'b10010010;
default:;
endcase
end
else if(sel==6'b111011)
begin
case(m_a)
4'd0:seg=8'b01000000;
4'd1:seg=8'b01111001;
4'd2:seg=8'b00100100;
4'd3:seg=8'b00110000;
4'd4:seg=8'b00011001;
4'd5:seg=8'b00010010;
4'd6:seg=8'b00000010;
4'd7:seg=8'b01111000;
4'd8:seg=8'b00000000;
4'd9:seg=8'b00010000;
default:;
endcase
end
else if(sel==6'b110111)
begin
case(m_b)
3'd0:seg=8'b11000000;
3'd1:seg=8'b11111001;
3'd2:seg=8'b10100100;
3'd3:seg=8'b10110000;
3'd4:seg=8'b10011001;
3'd5:seg=8'b10010010;
default:;
endcase
end
else if(sel==6'b101111)
begin
case(t_a)
4'd0:seg=8'b01000000;
4'd1:seg=8'b01111001;
4'd2:seg=8'b00100100;
4'd3:seg=8'b00110000;
4'd4:seg=8'b00011001;
4'd5:seg=8'b00010010;
4'd6:seg=8'b00000010;
4'd7:seg=8'b01111000;
4'd8:seg=8'b00000000;
4'd9:seg=8'b00010000;
default:;
endcase
end
else if(sel==6'b011111)
begin
case(t_b)
2'd0:seg=8'b11000000;
2'd1:seg=8'b11111001;
2'd2:seg=8'b10100100;
default:;
endcase
end
end
endmodule
module top_count_clk(
input clk,
input rst_n,
input [2:0] key,
output [5:0] sel,
output [7:0] seg
);
wire flag1;
wire box;
wire ok;
wire flag9;
wire flag10;
count u_count(
.clk(clk),
.rst_n(rst_n),
.flag1(flag1),
.ok(ok)
);
count2 u_count2(
.clk(clk),
.rst_n(rst_n),
.box(box)
);
count_clk u_count_clk(
.clk(clk),
.rst_n(rst_n),
.sel(sel),
.seg(seg),
.flag1(flag1),
.box(box),//位选信号例化
.flag9(flag9),
.flag10(flag10),
.ok(ok)
);
key_debounce u_key_debounce(
.clk(clk),
.rst_n(rst_n),
.key(key),
.ok(ok),
.flag9(flag9),
.flag10(flag10)
);
endmodule
`timescale 1ns/1ps
module top_count_clk_tb();
reg clk;
reg rst_n;
wire [5:0] sel;
wire [7:0] seg;
parameter CYCLE=20;
always#(CYCLE/2) clk=~clk;
initial begin
clk=1'd0;
rst_n=1'd0;
#(CYCLE);
rst_n=1'd1;
//#(CYCLE*20'd1000_000*17'd86400); //对应上面1s计数模块中的仿真参数
#(CYCLE*17'd31250*17'd86400); //对应1s计数中的飞驰人生版本parameter
$stop;
end
top_count_clk u_top_count_clk(
.clk(clk),
.rst_n(rst_n),
.sel(sel),
.seg(seg)
);
endmodule
上板使用说明:这个实验设置了四个按键,分别是key0(复位按键),key1(暂停按键),key2控制我们进行调数的位数(也就是让我们选择调数字时钟的哪一位),key3则是直接的控制我们调的位的数(按一下加一)。
注:调数功能只能在暂停状态下进行,可在暂停时候进行复位功能。
做实验过程中遇到的问题:
1. 在做好时钟后,想要加入调数功能,结果发现多一个按键需要加入always语句中,因为不能在多个地方给同一个参数赋值,所以这个地方卡了很久,最后只能把posedge flag改为了时钟上升沿触发,但因此在调试过程中,忽略了一个东西,clk上升沿触发的频率比flag触发的次数多非常多,最初的flag一层一层触发,没出现这个问题,是因为对应flag5置零(在上一个flag4来之前),不会进入always语句,影响flag5进位符号对应的位置显示,所以我在每一个always语句后面加了一个else来置零对应flag(这样每次clk触发进入,就能让flag发挥用处后及时置零,不会持续让下一位进位)。
2. 遇到的第二个问题是,消抖模块没写完整,忽略了复位情况下,把cnt置MAX了,所以会默认触发一次flag9和flag10(不知道为什么暂停消抖部分没受影响),进而导致了暂停时候复位,会看到00:00:10的效果,解决方法也很简单,要么把复位的cnt置MAX删掉,要么后面加上判断,使之不能把复位判断成一次按键按下,这样就能解决这个问题了)
后来我看了下:重新梳理一遍,当按下第二个键,再次取消暂停时,其他键也抖动了,且按下时,cnt又置MAX了,
结果时钟沿到来,没判定第一个if语句,直接第二个语句,value_key 直接取了key的瞬时值
又因为按下暂停时,其他键很可能也抖动了,所以就会导致某位计数(?),直到计数为21’d1,然后看下一个图:
此时clk来了又判断key的瞬时值,就阴差阳错把flag9置1了,flag10同理,结果就导致了我暂停时候复位了,在取消暂停,结果数码管显示的00:00:10。
(注:代码截图是已经改过了的代码,错误已经纠正,前面出现问题的时候忘记截图了,时间久了想不起来具体怎么改的,下次写复位注意多考虑清楚时序关系!)
3. 有机会可以加个谱子,加个到时间自动放音乐。