前言提醒
今天的这些内容也只是我自己自学的,有些东西可能理解不是很深刻,还有些东西是直接使用的之前的代码,有解释不清楚的地方还望海涵。
内容介绍
也就是综合之前学的东西,尝试这做了这个小项目,我通过开发板上的四个按钮,对数码管进行操作,其中第一个按钮让数字加一,第二个加十,第三个加一百,第四个加一千;其中,数码管的第二位需要展示出小数点,六位则是展示出负号,今天的这个还算比较简单,更为复杂的加减运算和符号显示今天暂时没有写上去,如果明天做了再分享
原理简述
首先我来根据我自己的理解解释一下这个功能需要的一些技术
按键消抖
附:按键消抖原理看上去容易理解,但是从代码层面上去写还是要花一番功夫,我暂时没有完全独立写出按键消抖代码的能力,所以这里也只是给出了一点理解;不要想着一下子就可以学会,这个不分内容还是比较困难的,主要还是要思考和多加练习。
首先就是按键部分,之前我们使用的按键都是按下触发,但这里我们需要的是:按下按键,但只触发一次,这里提到一个很重要的东西,叫做按键消抖,具体的内容可以在网上自行查看,这里给出我的理解。
由于按键电路是由高低电平来决定的,在按下按钮的过程中,按键信号由高电平转移至低电平(1->0),但是实际情况下,由于按键在按下过程中会有接触不良的部分,虽然在宏观时间上看不出来,但在微观时间上观测,就会显示出一种短时间内高低电平的快速转换,有可能会造成一次按下但触发多次的情况,如果呈现在波形图上,就会展示出一种类似于抖动的状态,而且很不巧,机器能够识别这种微观层面上的电平变化(人类真的好弱啊)。
图我就直接盗了,表达大致意思就行,不想自己画(嘿嘿嘿)~
所以呢,我们希望一次按下只被检测到一次,也就说我们在微观层面上的连续下降沿(1- >0的过程)我们要想办法让机器认为是同一次下降沿。
图我也厚颜无耻的盗了哈哈哈
这里我们的解决方案是使用延迟检测,基本原理是:当我们检测到第一个下降沿的时候,就让机器歇一会,把这次的信号存起来,等待一个在图片和理论上很长,但是宏观层面几乎是一瞬间的时间,等到按键的低电平稳定后,再向需要的位置发送这次信号。(大致是这么个意思,能懂当然最好,不懂我也解释不了更详细的了,比毕竟能力有限,这些也只是帮助理解的方式。)
理解了大致做法,这里也就给出一个解决方案,把这个微观时间控制在20毫秒(20*10-6s),这个时间对于机器来说足够了(另外我也不相信有人能在20毫秒以内连续按下同一个按键两次(乐)),通过多个寄存器对时钟上升沿的检测,将一个短时间内的多次按键下降沿装换成一个上升沿脉冲(其实也可以下降沿,但是上升沿使用起来比较方便,也比较容易读,例:if(key)和if(!key)比较,前者一看就是按下某个按键然后触发,而加上非(!)读起来容易混淆),然后再由这个上升沿去对其他的数据进行操作。
注:这个代码我也没吃透,但是当真是好用的代码,如果你心里没有负担直接用,其实也无可厚非,我无话可说;但若如果你认真吃透,然后自己写出来用自己的,那算你厉害(哈哈哈)。
key_debounce.v代码如下
/**************按键消抖**************/
module key_debounce(
input wire clk,
input wire rst_n,
input wire [3:0] key,
output wire [3:0] key_out
);
localparam MAX_20ms = 20'd100_0000;
reg [19:0] cnt_20ms;
reg start;
reg [3:0] key_r0;
reg [3:0] key_r1;
wire nedge;//下降沿信号
reg [3:0] key_r;
/****************20ms设置*******************/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_20ms <= MAX_20ms;
end
else if (start) begin
if (cnt_20ms == 1'd1) begin
cnt_20ms <= 20'd0;
end
else begin
cnt_20ms <= cnt_20ms - 1'd1;
end
end
else begin
cnt_20ms <= cnt_20ms;
end
end
/*-----------------------------------------*/
/*********************下降沿检测****************/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_r0 <= 4'b1111;
key_r1 <= 4'b1111;
end
else begin
key_r0 <= key; //一拍,同步时钟域
key_r1 <= key_r0; //一拍,检测按键下降沿
end
end
/*--------------------------------------------*/
/************************key_r*****************/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_r <= 4'd0000;
end
else if (cnt_20ms == 1'd1) begin
key_r <= ~key_r1;
end
else begin
key_r <= 4'd0000;
end
end
/*-------------------------------------------*/
/**********************约束start*************/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
start <= 1'd0;
end
else if (nedge) begin
start <= 1'd1;
end
else if (cnt_20ms == 1'd1) begin
start <= 1'd0;
end
else begin
start <= start;
end
end
/*-----------------------------------------*/
assign nedge = (~key_r0[0] && key_r1[0]) || (~key_r0[1] && key_r1[1]) || (~key_r0[2] && key_r1[2]) || (~key_r0[3] && key_r1[3]);//检测到下降沿
assign key_out = key_r;
endmodule
以上代码实现原理解析介绍
突然回归,对以上代码带着新的理解回来了
assign nedge = (~key_r0[0] && key_r1[0]) || (~key_r0[1] && key_r1[1]) || (~key_r0[2] && key_r1[2]) || (~key_r0[3] && key_r1[3]);//检测到下降沿
首先,这段代码的核心在于最后那一排的对nedge,由于在前面定义了一个key_r0和key_r1,这两个寄存器虽然都是由key本身直接复制,但是由于使用了阻塞赋值(<=),所以他们之间有一个一拍(20us)的差值,所以按下key前, ~ key_r0 && key_r1的值(0 && 1)是0,当按下key的时候,key会由高电平转移到低电平(1->0),此时key_r0会首先变成低电平,但是key_r1会保持20us的高电平,此时 ~ key_r0 && key_r1的值(1 && 1)则是1,再下一个20us后,r1也会变成低电平,这个值又重新归零,也就是说,这个地方产生了一个20us的高电平脉冲。而当key重新回到高电平(0->1)的过程中,r0变成0,不会产生任何反应,nedge依然是0.
/**********************约束start*************/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
start <= 1'd0;
end
else if (nedge) begin
start <= 1'd1;
end
else if (cnt_20ms == 1'd1) begin
start <= 1'd0;
end
else begin
start <= start;
end
end
/*-----------------------------------------*/
现在是最有意思的地方。上述原理的过程中,我们知道,按键如果不去对他操作,很有可能产生多个下降沿信号。此处同理,在按下一次按键的过程中,抖动会让nedge产生多个脉冲信号。但是,在最后一个模块中,我们只是当nedge产生脉冲信号的时候,让start信号变成高电平,却并没有在nedge归零的时候让start信号变回低电平,在我们设置的20ms内,start信号一直保持着高电平,直到第一个20ms结束。在这20ms内,产生的脉冲信号除了第一个以外都在做无用功。(他们也想让start变成高电平,但是start本身就是高电平,而start高电平会使时钟计数,这些脉冲不能产生新的计数,所以是无用功。)
也就是说,上边的原理图其实和我们写的代码有一点点出入,不是抖动结束开始计20ms,而是开始抖动就在计数了。
/************************key_r*****************/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_r <= 4'd0000;
end
else if (cnt_20ms == 1'd1) begin
key_r <= ~key_r1;
end
else begin
key_r <= 4'd0000;
end
end
/*-------------------------------------------*/
20ms之后,按键已经趋于平稳,这个时候再把已经变成低电平的key信号或者任何与key相同的信号,如r0,r1,赋给输出信号。这里对信号做了一点微小的处理,仅当20ms到达的一瞬间把按键信号取反进行复制,而在这之前之后都置零,这样输出的处理后的信号就也是一个仅有20us的脉冲信号,便于我们的后序使用。
注:这里基本上是确定了一个按键按下的抖动时长在20ms以内,而20ms又在人的感官中是一个瞬间,也就是说,在处理按键消抖的过程中,我们选择消抖时间不一定非要一个固定的时间,只要这个时间足够让按键稳定,而人的感官中,这个时间又是一瞬间即可(又是微观宏观概念)。如25ms,30ms,50ms这种微量级别的时间都可以拿来使用,不用拘泥一格非要20ms。我们使用20ms的理由大概是因为这个数在计数的时候正好是十的倍数,毕竟一次计数20us嘛,看着舒服。
动态数码管操作
其实这部分操作并不是很难,在FPGA开发板上,如果只有段选,那虽然麻烦,但是还是比较简单的,静态数码管都可以解决大部分问题;但是如果位选和段选都有,那要使用动态数码管,还是需要对微观和宏观有削微的理解嘿嘿嘿。
大致解释:在有位选位选的FPGA开发板上,展示数字,则当段选是多少,所有被位选标记的数位都会被段选的位置触发,所以要想同时在所有的数位上展示不同的数字,数位不断变化,而数字在不同的数位上则显示不同的内容。
这里我选择的时间是2毫秒,以确保人的肉眼不会观察到数位的变化,也就是超级快速的流水灯原理,然后再在状态机里,让不同的数字去找到对应的数位,在肉眼上看到的也就是全部点亮,而且每个数位的内容就不一样了。
这个部分比较上按键还是比较好理解的,然后就是我的各种按键照过来操作一下,因为我当时有点蠢,没有把按键控制数字和数字控制数码管分开,所以就将就一下看吧。更多的理解就直接看代码里的注释吧
sel_control.v代码如下
module sel_control(
input wire clk ,
input wire rst_n ,
input wire [3:0] key_out ,//检测出入按键信号上升沿脉冲
output wire [5:0] sel ,//位选
output wire [7:0] seg //段选
);
parameter MAX_2ms = 17'd10_0000;
reg [05:0] sel_r;//位选
reg [07:0] seg_r;//段选
reg [16:0] cnt_2ms;//让位选器不断选位的计数器
reg [19:0] num_single;//某一位数字
reg [19:0] num_all;//整体数字
//对数码管数字进行储存,方便后序使用
localparam S0 = 7'b100_0000,
S1 = 7'b111_1001,
S2 = 7'b010_0100,
S3 = 7'b011_0000,
S4 = 7'b001_1001,
S5 = 7'b001_0010,
S6 = 7'b000_0010,
S7 = 7'b111_1000,
S8 = 7'b000_0000,
S9 = 7'b001_0000,
SF = 7'b011_1111;
//2毫秒计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_2ms <= 17'd0;
end
else if (cnt_2ms == MAX_2ms - 1'd1) begin
cnt_2ms <= 17'd0;
end
else begin
cnt_2ms <= cnt_2ms + 1'd1;
end
end
/***
每过2毫秒,位选器向前位移一位,由于人眼看不见2毫秒的变化(也可
能是因为数码管2毫秒还不能熄灭),使得不同位能够显示不同的数字
***/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sel_r <= 6'b011_111;
end
else if (cnt_2ms == MAX_2ms - 1'd1) begin
sel_r <= {sel_r[0],sel_r[5:1]};
end
else begin
sel_r <= sel_r;
end
end
/***
对不同按键进行操作
这里希望按键一使整体数据加一
按键二使整体数据加十
按键三使整体数据加一百
按键四使整体数据加一千
***/
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
num_all <= 20'd0;
end
else if (key_out[0]) begin
num_all <= num_all + 1'd1;
end
else if (key_out[1]) begin
num_all <= num_all + 4'd10;
end
else if (key_out[2]) begin
num_all <= num_all + 7'd100;
end
else if (key_out[3]) begin
num_all <= num_all + 10'd1000;
end
else begin
num_all <= num_all;
end
end
/***
将整体的数字转换成单个位数的数字,取到各个10进制数
***/
always @(*) begin
case (sel_r)
6'b011_111 : num_single = num_all % 10;//对整体数字取余,和num_single = num_all - num_all / 10 * 10效果一致,但是更加方便,也比较好读
6'b101_111 : num_single = (num_all / 10) % 10;//一下算式同上,这里没有做整理是方便比较对比
6'b110_111 : num_single = ((num_all / 10) / 10) % 10;
6'b111_011 : num_single = (((num_all / 10) / 10) / 10) % 10;
6'b111_101 : num_single = ((((num_all / 10) / 10) / 10) / 10) % 10;
6'b111_110 : num_single = ((((num_all / 10) / 10) / 10) / 10) / 10;//最后一位直接除取得结果即可,因为没有更高位数的数字了
default : num_single = 20'd0;
endcase
end
/***
使各个数字的数位拥有相应的数码管触发
这里额外加入了第二位后显示小数点
第六位从数字改为了负号,表示负数
***/
always @(*) begin
if (sel_r == 6'b101_111) begin
seg_r[7] = 1'b0;//显示小数点
case (num_single)
20'd0 : seg_r[6:0] = S0;
20'd1 : seg_r[6:0] = S1;
20'd2 : seg_r[6:0] = S2;
20'd3 : seg_r[6:0] = S3;
20'd4 : seg_r[6:0] = S4;
20'd5 : seg_r[6:0] = S5;
20'd6 : seg_r[6:0] = S6;
20'd7 : seg_r[6:0] = S7;
20'd8 : seg_r[6:0] = S8;
20'd9 : seg_r[6:0] = S9;
default : seg_r[6:0] = SF;
endcase
end
else if (sel_r == 6'b111_110) begin
seg_r = {1'b1,SF};//显示负号,且不显示小数点
end
else begin
seg_r[7] = 1'b1;//不显示小数点
case (num_single)
20'd0 : seg_r[6:0] = S0;
20'd1 : seg_r[6:0] = S1;
20'd2 : seg_r[6:0] = S2;
20'd3 : seg_r[6:0] = S3;
20'd4 : seg_r[6:0] = S4;
20'd5 : seg_r[6:0] = S5;
20'd6 : seg_r[6:0] = S6;
20'd7 : seg_r[6:0] = S7;
20'd8 : seg_r[6:0] = S8;
20'd9 : seg_r[6:0] = S9;
default : seg_r[6:0] = SF;
endcase
end
end
assign seg = seg_r;
assign sel = sel_r;
endmodule
顶层模块
顶层模块还是老朋友了,就是吧我们的信号全部当成是物理电路里面的电线就可以了,这里我当时也是脑子抽了,顶层模块的名字都是乱取的(捂脸)。
key_control.v代码如下
module key_control(
input wire clk ,
input wire rst_n ,
input wire [3:0] key ,
output wire [5:0] sel ,
output wire [7:0] seg
);
wire [3:0] key_out;
sel_control u_sel_control(
.clk (clk ),
.rst_n (rst_n ),
.key_out (key_out ),
.sel (sel ),
.seg (seg )
);
key_debounce u_key_debounce(
.clk (clk ),
.rst_n (rst_n ),
.key (key ),
.key_out (key_out )
);
endmodule
总结
今天的内容就到此为止吧,结果我就不放出来了,各位有兴趣可以自己试试,反正我成功了(绝对不是因为手机不在身边懒得录),嘿嘿嘿。
FPGA的学习还是比较有意思的,自己取做点小项目也是很有成就感的,有不懂的地方去搞懂也很有成就感的,继续努力吧。拜了个拜~