2019/01/08,第一个判断是否有按键按下的操作好像有问题,有空在修改!
红色为修改部分:
问题描述:
当三个独立按键的某一个被按下后,相应的LED被点亮;再次按下后,LED熄灭,按键控制LED亮灭
下面是LED灯的原理图:
可见,LED是低电平亮,高电平灭。
事实上,控制LED等的亮灭很简单,不是问题,对应的代码段如下:
reg d1;
reg d2;
reg d3;
always @ (posedge clk or negedge rst_n)
if (!rst_n) begin
d1 <= 1'b0;
d2 <= 1'b0;
d3 <= 1'b0;
end
else begin //某个按键值变化时,LED将做亮灭翻转
if ( led_ctrl[0] ) d1 <= ~d1;
if ( led_ctrl[1] ) d2 <= ~d2;
if ( led_ctrl[2] ) d3 <= ~d3;
end
assign led_d3 = d1 ? 1'b1 : 1'b0; //LED翻转输出
assign led_d2 = d2 ? 1'b1 : 1'b0;
assign led_d1 = d3 ? 1'b1 : 1'b0;
我来做出解释:
首先,系统复位时候,先把寄存器变量d1、d2、d3清零,由下面这三天语句可知,这三个寄存器变量清零后,对应的led灯就是低电平,也就是亮。
assign led_d3 = d1 ? 1'b1 : 1'b0; //LED翻转输出
assign led_d2 = d2 ? 1'b1 : 1'b0;
assign led_d1 = d3 ? 1'b1 : 1'b0;
然后如果某个按键按下,对应的led_ctrl[?]就会变成高电平,此时对应的d?就会翻转电平值,也就是说原来led?是低电平,按键按下后,d?就会翻转为高电平,此时,对应的led灯自然也会由亮变灭了。继续按下按键,led的亮灭又会翻转。
(这些分析都是本人通过代码以及实验结果证明的,确实如此。)
不得不说,如果仅仅是上面的分析那么简单,就省事了,同时这个实验也没啥意思了。
事实的情况我根据理解做如下的描述:
按键按下时候,按键值有一段时间的低电平,我们必须在低电平这段时间内的某一个瞬间采样按键值来控制led灯的亮灭。
可能不能采样对是一个值得研究的问题。
先给出按键的大概电路图;
对这个电路图的一些解释:
独立按键一般有2组管脚,这2组管脚在按键未被按下时是断开的,在按键被按下时则是导通的。
基于此原理,我们一般会把按键的一个管脚接地,另一个管脚上拉到VCC,并且也连接到GPIO。这样,在按键未被按下时,GPIO的连接状态为上拉到VCC,则键值为1;按键被按下时,GPIO虽然还是上拉到VCC,但同时被导通的另一个管脚拉到地了,所以它的键值实际上是0。
再给出按键的波形图:
如下图是按键按下时候的波形图,一个是理想情况下的波形,另一个是事实情况的模拟图:
可见,按下以及释放的瞬间都有一个抖动。
在按键按下或者释放的时候都会出现一个不稳定的抖动时间,如果不处理好这个抖动时间,我们就无法正确采集到正确有效的按键值,所以我们的设计中必须有效消除按键抖动。
好了,按键消抖的问题摆在面前了,我们怎么来消除抖动呢?这是一个有意思的问题。
看了网上好多解释,通过画图来解释这个消除抖动的原理,可是总是越看越糊涂,感觉这种不伦不类的图已经对我的理解产生一些误导了,于是找到了特权同学的视频看了一遍,说实话,看的似乎明白了,但是不是太清晰,学习这东西必须花时间,自己动手动脑分析才能够理清楚。
我就通过分析代码的方式来理解这个消抖的过程。
`timescale 1ns / 1ps
//说明:当三个独立按键的某一个被按下后,相应的LED被点亮;
// 再次按下后,LED熄灭,按键控制LED亮灭
module sw_debounce(
clk,rst_n,
sw1_n,sw2_n,sw3_n,
led_d1,led_d2,led_d3
);
input clk; //主时钟信号,25MHz
input rst_n; //复位信号,低有效
input sw1_n,sw2_n,sw3_n; //三个独立按键,低表示按下
output led_d1,led_d2,led_d3; //发光二极管,分别由按键控制
//---------------------------------------------------------------------------
reg key_rst;
always @(posedge clk or negedge rst_n)
if (!rst_n) key_rst <= 1'b1;
else key_rst <= sw3_n&sw2_n&sw1_n;
reg key_rst_r; //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中
always @ ( posedge clk or negedge rst_n )
if (!rst_n) key_rst_r <= 1'b1;
else key_rst_r <= key_rst;
//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期
wire key_an = key_rst_r & (~key_rst);
/*
key_rst 1 1 1 0 0 1
~key_rst 0 0 0 1 1 0
key_rst_r 1 1 1 0 0 1
key_an 0 0 1 0 0
*/
//---------------------------------------------------------------------------
reg[19:0] cnt; //计数寄存器
always @ (posedge clk or negedge rst_n)
if (!rst_n) cnt <= 20'd0; //异步复位
else if(key_an) cnt <=20'd0;
else cnt <= cnt + 1'b1;
reg[2:0] low_sw;
always @(posedge clk or negedge rst_n)
if (!rst_n) low_sw <= 3'b111;
else if (cnt == 20'hfffff) //满20ms,将按键值锁存到寄存器low_sw中 cnt == 20'hfffff
low_sw <= {sw3_n,sw2_n,sw1_n};
//---------------------------------------------------------------------------
reg [2:0] low_sw_r; //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中
always @ ( posedge clk or negedge rst_n )
if (!rst_n) low_sw_r <= 3'b111;
else low_sw_r <= low_sw;
/*
low_sw 111 111 111 110 110 110
~low_sw 000 000 000 001 001 001
low_sw_r 111 111 111 110 110 110
led_ctrl 000 000 000 001 000 000
*/
//当寄存器low_sw由1变为0时,led_ctrl的值变为高,维持一个时钟周期
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);
reg d1;
reg d2;
reg d3;
always @ (posedge clk or negedge rst_n)
if (!rst_n) begin
d1 <= 1'b0;
d2 <= 1'b0;
d3 <= 1'b0;
end
else begin //某个按键值变化时,LED将做亮灭翻转
if ( led_ctrl[0] ) d1 <= ~d1;
if ( led_ctrl[1] ) d2 <= ~d2;
if ( led_ctrl[2] ) d3 <= ~d3;
end
assign led_d3 = d1 ? 1'b1 : 1'b0; //LED翻转输出
assign led_d2 = d2 ? 1'b1 : 1'b0;
assign led_d1 = d3 ? 1'b1 : 1'b0;
endmodule
首先点名输入输出:
input clk; //主时钟信号,25MHz
input rst_n; //复位信号,低有效
input sw1_n,sw2_n,sw3_n; //三个独立按键,低表示按下
output led_d1,led_d2,led_d3; //发光二极管,分别由按键控制
这里需要提出一点的是,输入输出在开头都声明好,至于中间需要用到的一些网线变量(wire)以及寄存器变量(reg),我们在用到的位置声明,不失为一种便于理解的代码格式。
reg key_rst;
always @(posedge clk or negedge rst_n)
if (!rst_n) key_rst <= 1'b1;
else key_rst <= sw3_n&sw2_n&sw1_n;reg key_rst_r; //每个时钟周期的上升沿将key_rst信号锁存到key_rst_r中
always @ ( posedge clk or negedge rst_n )
if (!rst_n) key_rst_r <= 1'b1;
else key_rst_r <= key_rst;
//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期
wire key_an = key_rst_r & (~key_rst);
/*
key_rst 1 1 1 0 0 1
~key_rst 0 0 0 1 1 0
key_rst_r 1 1 1 0 0 1
key_an 0 0 1 0 0
*/
//---------------------------------------------------------------------------
这里定义了一个1位的寄存器变量(key_rst),用来存放三个按键状态值的与值,由于按键值在没有按下的时候,就是拉高的状态,所以当复位信号有效时,就把key_rst赋值为1,也就是每一位都是高电平。
没有复位的时候,就在每个时钟的上升沿到来时,把三个按键值的与状态赋值给key_rst;
这两句的解释对应的代码为:
reg key_rst;
always @(posedge clk or negedge rst_n)
if (!rst_n) key_rst <= 1'b1;
else key_rst <= sw3_n&sw2_n&sw1_n};
紧接着,我们讲下面这一小块代码:
reg key_rst_r;
always @ ( posedge clk or negedge rst_n )
if (!rst_n) key_rst_r <= 1'b1;
else key_rst_r <= key_rst;
又定义了一个1位的寄存器变量key_rst_r,这个寄存器变量在复位信号有效时,也都赋值为1;
否则,在每个时钟上升沿都会将key_rst的值赋值给key_rst_r;
这就意味这什么呢?
意味着这两个变量之间增加了一个触发器或寄存器,key_rst_r延时一拍(延迟了一个时钟周期)。
也就是相当于:
key_rst 1 1 1 0 0 1
key_rst_r 1 1 1 0 0 1
为什么要这么处理呢?
继续看下去,就会知道这有多么的巧妙?
//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期
wire key_an = key_rst_r & (~key_rst);
我不太喜欢这么写这句语法,更喜欢如下写法:
wire key_an;
assign key_an = key_rst_r & (~key_rst);
定了一个线网型变量key_an,它的值是上面的寄存器key_rst取反后和key_rst_r的与。
为什么相与呢?
key_rst 1 1 1 0 0 1
~key_rst 0 0 0 1 1 0
key_rst_r 1 1 1 0 0 1
key_an 0 0 1 0 0
这样处理的目的是边沿检测,也就是key_rst当中如果某一位由1变为0之后,这种处理就会在跳变时产生一个高电平脉冲,持续一个时钟周期,如果这样看还不算直观的话,我就勉为其难,画个图给你看看:
看到了吗?检测到了key_rst的一个下降沿,这个下降沿到来的时候key_an就会出现一个高电平脉冲,持续一个时钟周期。
(如果有小伙伴想知道我这个波形图如何画的,就得看我的这篇博文喽:对如何使用WaveDrom画波形图的研究(案例分解分析),也就是一个工具,可以画波形图,用来写文章使用,我只开发了一点功能,其他功能有兴趣的可以自己开发哦。)
接着对这段代码分解分析:
reg[19:0] cnt; //计数寄存器
always @ (posedge clk or negedge rst_n)
if (!rst_n) cnt <= 20'd0; //异步复位
else if(key_an) cnt <=20'd0;
else cnt <= cnt + 1'b1;
reg[2:0] low_sw;always @(posedge clk or negedge rst_n)
if (!rst_n) low_sw <= 3'b111;
else if (cnt == 20'hfffff) //满20ms,将按键值锁存到寄存器low_sw中 cnt == 20'hfffff
low_sw <= {sw3_n,sw2_n,sw1_n};
//---------------------------------------------------------------------------
reg [2:0] low_sw_r; //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中always @ ( posedge clk or negedge rst_n )
if (!rst_n) low_sw_r <= 3'b111;
else low_sw_r <= low_sw;
/*
low_sw 111 111 111 110 110 110
~low_sw 000 000 000 001 001 001
low_sw_r 111 111 111 110 110 110led_ctrl 000 000 000 001 000 000
*/
//当寄存器low_sw由1变为0时,led_ctrl的值变为高,维持一个时钟周期
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);
这里说明一下,就是按键抖动的时间,公认的大约是20ms左右,而按键按下的那段时间,至少也是几百ms。
reg[19:0] cnt; //计数寄存器
always @ (posedge clk or negedge rst_n)
if (!rst_n) cnt <= 20'd0; //异步复位
else if(key_an) cnt <=20'd0;
else cnt <= cnt + 1'b1;
又定义了一个20位计时计数器寄存器变量cnt,最大计数1048575次,大约1000_000次。
时钟频率是50MHz,那么时钟周期为20ns,那么计数1000_000次,大概是20ms。
好了,继续分析上面一小段代码,系统复位时,计数器清零,如果检测到按键值跳变(有可能是抖动引起),也就是key_an有高电平脉冲,计数器也清零,否则就一直计数。
reg[2:0] low_sw;
always @(posedge clk or negedge rst_n)
if (!rst_n) low_sw <= 3'b111;
else if (cnt == 20'hfffff) //满20ms,将按键值锁存到寄存器low_sw中 cnt == 20'hfffff
low_sw <= {sw3_n,sw2_n,sw1_n};
这里定义了一个3位的寄存器变量low_sw,如果系统复位,则low_sw赋值为111,否则如果计数器计数到最大,也就是大概计数到20ms时,将按键值赋值给low_sw;
上面的意思就是20ms锁存一次按键值。
reg [2:0] low_sw_r; //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中
always @ ( posedge clk or negedge rst_n )
if (!rst_n) low_sw_r <= 3'b111;
else low_sw_r <= low_sw;
再次定义一个寄存器变量low_sw_r,作用是存放延迟一拍的low_sw。
复位时候,low_sw_r赋值为111;
否则将low_sw的值赋值为low_sw_r,这不就相当于延迟了一拍吗?
意义呢?
继续看呗:
//当寄存器low_sw由1变为0时,led_ctrl的值变为高,维持一个时钟周期
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);
这和上面的分析一样,也是一个边沿检测的做法;
同样做出形象解释:
low_sw | 1 | 1 | 1 | 1 | 0 | 1 |
~low_sw | 0 | 0 | 0 | 0 | 1 | 0 |
low_sw_r | 1 | 1 | 1 | |||
led_ctrl | 0 | 1 | 0 |
这说明在led_ctrl的第二位出现了一个高电平值,说明了它代表的按键按下了。
low_sw 111 111 111 110 110 110 //这一行的意思是某一个按键按下时的情况
~low_sw 000 000 000 001 001 001 //取反
low_sw_r 111 111 111 110 110 110 //延迟一拍
led_ctrl 000 000 001 000 000 000 //相与,在相应的位置出现一个高电平脉冲
上面这段代码讲完了,之后就到了控制led灯亮灭的模块了,这部分内容我在开头就讲了,所以不再重复。接下来,我就是要总览全局的时刻了,从总体来认识下,第一次边沿检测的意义在哪里?计数20ms锁存一下键值的意义?第二次边沿检测的意义?
第一次边沿检测,检测到了按键状态的跳变,如果有跳变,则key_an出现一个高电平脉冲,这个跳变是什么引起的呢?有可能是抖动引起的。
如果是抖动引起的话,那么就到达解释计数器的意义了。
从代码可以看出,如果key_an出现高电平,则计数器清零,重新计数,由于抖动也只有20ms左右而已,在抖动器件,计数器肯定计数不到20ms就会被清零,直到抖动期过了,才能计数到20ms,这样锁定按键值,得到的才是非抖动的按键值,根据按键值的状态控制led灯的亮灭就可以了。
岂不完美!
这样就到达了滤除抖动的效果。
至于第二次边沿检测,当然就是检验按键是否按下,如果某个按键按下了,就会出现一个高电平脉冲,led_ctrl的某一位是高电平,并且持续一个周期的时间,通过此来控制led灯的亮灭。
感觉如果仔细看的话,应该会很明白了,很开心,知识是需要动手动脑才能够理解的。
这种代码的设计形式是最简单的,也很有效,实践证明,是可以的。