09A-独立按键消抖实验01——小梅哥FPGA设计思想与验证方法视频教程配套文档
芯航线——普利斯队长精心奉献
实验目的: 1.复习状态机的设计思想并以此为基础实现按键消抖
2.单bit异步信号同步化以及边沿检测
3.在激励文件中学会使用随机数发生函数$random
4.仿真模型的概念
实验平台:芯航线FPGA核心板
实验原理:
按键在电子设计中使用的最多,从复位到控制设置均可以看到其身影。现在按键的功能也种类也越来越多,例如多向按键、自锁按键、薄膜按键等。普通按键其硬件示意图如图9-1所示。
图9-1 按键示意图
芯航线开发板所载的为两脚贴片按键,分别位于开发板正面的左下角以及右下角,原理图如图9-2所示。按键不按下时IO口为高电平,按键按下时则变为电平,因此系统即可通过检测IO的电平来判断按键的状态。
图9-2 按键原理图
从图9-1中可以看到按键存在一个反作用弹簧,因此当按下或者松开时均会产生额外的物理抖动,然而物理抖动便会产生电平的抖动,总的来说,按键从按下再到松开的过程,若检测其电平变化,即如图9-4所示。
图9-4 按键从按下到松开的电平变化
图9-4中产生的抖动次数以及间隔时间均是不可预期的,这就需要滤波来消除抖动过程中可能造成的影响。一般情况抖动的总时间会持续20ms以内。这种抖动,可以通过硬件电路或者逻辑设计的方式来消除,其中硬件电路消除的电路图之一如图9-5所示。
图9-5 利用RS触发器进行硬件消抖
图9-5中两个"与非"门构成一个RS触发器。此时即使B点的电压波形是抖动的,但经双稳态电路之后,其输出为正规的矩形波。当按键未按下时,输出为1;当键按下时,输出为0。
此外还可以通过利用电容器和电阻对噪声波形积分,吸收因触点跳动产生的噪声。也可以通过555定时器组成的单稳态触发器同样可以消除开关的抖动。这两种电路此处不再详述。
对于FPGA通常使用状态机来进行消抖设计,在图9-4中可看出若按照第08讲的状态机概念对其进行状态编码即存在以下状态:未按下时空闲状态IDLE、按下抖动滤除状态FILTER0、按下稳定状态DOWN以及释放抖动滤除状态FILTER1。若对其进行独热码编码分别为0001,0010,0100以及1000。其状态转换图如图9-6所示。因此其为一个米利型状态机,输出与输入相关。
图9-6 状态转换图
其状态转移条件如表9-1所示。
当前状态 |
下一状态 |
转移条件 |
IDLE |
FILTER0 |
nedge |
FILTER0 |
IDLE |
pedge |
FILTER0 |
DOWN |
Cnt_full |
DOWN |
FILTER1 |
Pedge |
FILTER1 |
DOWN |
nedge |
FILTER1 |
IDLE |
Cnt_full |
表9-1 状态转移条件
实验步骤:
新建一个以名为key_filter的工程保存在prj下,并在本工程目录的rtl文件夹下新建verilog file文件并以key_filter.v保存。
其模块接口如图9-7所示,即可得出如下的接口声明。
图9-7 按键消抖模块接口
input Clk; input Rst_n; input key_in;
output reg key_flag; output reg key_state; |
这里的按键输入信号key_in相对于FPGA内部信号来说是一个异步信号,这里就需要对其使用两级同步D触发器进行进行异步信号的同步化,同步后的信号为key_in_sb。
reg key_in_sa,key_in_sb; always@(posedge Clk or negedge Rst_n) if(!Rst_n)begin key_in_sa <= 1'b0; key_in_sb <= 1'b0; end else begin key_in_sa <= key_in; key_in_sb <= key_in_sa; end |
由实验原理中的状态转移表可以看出其转换条件中需要检测到下降沿以及上升沿,而边沿检测其原理就是利用两级寄存器寄存的不同值来进行比较判断,如图9-8所示。
图9-8 边沿检测原理图
其检测过程,可以假设data_in从0变1,也就是上升沿:
第一个时钟到来第一个寄存器regc的输出为0;
第二个时钟沿到来后第一个寄存器输出为1,第二个寄存器输出此时为0,这样对两个寄存器输出进行相关组合逻辑运算则可检测出,
同理data_in从1变为0,也就是下降沿:
第一个时钟到来第一个寄存器regc的输出为1;
第二个时钟沿到来后第一个寄存器输出为0,第二个寄存器输出此时为1。
本部分逻辑设计如下,这样就实现了当有上升沿时信号pedge就会产生一个时钟周期的高电平,当有下降沿时信号nedge也会产生一个时钟周期的高电平,没有上升沿或者下降沿变化时pedge以及nedge保持低电平状态。 这里使用的"!"是逻辑非运算,对0110逻辑非运算后是0000;而"~"是按位取反,对0110按位取反后是1001。
reg key_tmpa,key_tmpb; wire pedge,nedge; always@(posedge Clk or negedge Rst_n) if(!Rst_n)begin key_tmpa <= 1'b0; key_tmpb <= 1'b0; end else begin key_tmpa <= key_in_sb; key_tmpb <= key_tmpa; end
assign nedge = !key_tmpa & key_tmpb; assign pedge = key_tmpa & (!key_tmpb); |
还应有20ms计数器模块以及计数器使能模块,这里也可以合并成一个always模块。一般还是推荐一个always块只对一个信号进行操作。
reg [19:0]cnt; reg en_cnt; //使能计数寄存器 //计数使能模块 always@(posedge Clk or negedge Rst_n) if(!Rst_n) cnt <= 20'd0; else if(en_cnt) cnt <= cnt + 1'b1; else cnt <= 20'd0; //计数模块 always@(posedge Clk or negedge Rst_n) if(!Rst_n) cnt_full <= 1'b0; else if(cnt == 999_999) cnt_full <= 1'b1; else cnt_full <= 1'b0; |
现在开始状态机设计,首先用本地参数化定义来定义其状态机。
localparam IDEL = 4'b0001, FILTER0 = 4'b0010, DOWN = 4'b0100, FILTER1 = 4'b1000; |
由于状态以及判断条件较少,此处先用一段式状态机来进行描述。当复位时候将计数器清零,状态回到IDLE,key_flag与key_state也回到初始态。
reg [3:0]state; always@(posedge Clk or negedge Rst_n) if(!Rst_n)begin en_cnt <= 1'b0; state <= IDEL; key_flag <= 1'b0; key_state <= 1'b1; end else begin case(state) //。。。。。。。。 default: begin state <= IDEL; en_cnt <= 1'b0; key_flag <= 1'b0; key_state <= 1'b1; end
endcase end |
在未按下时空闲状态IDLE时,如果检测到下降沿则状态进入按下抖动滤除状态FILTER0并使能计数器,否则继续保持IDLE状态。
IDEL : begin key_flag <= 1'b0; if(nedge)begin state <= FILTER0; en_cnt <= 1'b1; end else state <= IDEL; end |
当在FILTER0状态时,如果20ms尚未计时结束就有上升沿到来,则认为此时还是按键按下抖动过程,状态回到IDLE并清0计数器。按下过程中当最后一次抖动后不会存在上升沿,计数器则可以一直计数,计数满后则将key_flag置1、key_state置0、状态进入按下稳定状态DOWN并将计数器清0。这样就可以通过判断key_flag && !key_state来确定按键的状态,为1则按下。
FILTER0: if(cnt_full)begin key_flag <= 1'b1; key_state <= 1'b0; en_cnt <= 1'b0; state <= DOWN; end else if(pedge)begin state <= IDEL; en_cnt <= 1'b0; end else state <= FILTER0; |
进入DOWM状态后将key_flag清0,如果检测到上升沿则进入释放抖动滤除状态FILTER1,否则保持当前态。
DOWN: begin key_flag <= 1'b0; if(pedge)begin state <= FILTER1; en_cnt <= 1'b1; end else state <= DOWN; end |
进入FILTER1状态后,如果20ms计数尚未结束就检测到下降沿,则认为此时还是按键释放抖动过程,状态回到DOWN并清0计数器。释放过程中当最后一次抖动后不会存在下降沿,计数器则可以一直计数,计数满后则将key_flag与key_state均置1、状态进入IDLE并将计数器清0等待下一次按键被按下。
FILTER1: if(cnt_full)begin key_flag <= 1'b1; key_state <= 1'b1; state <= IDEL; en_cnt <= 1'b0; end else if(nedge)begin en_cnt <= 1'b0; state <= DOWN; end else state <= FILTER1; |
这里如果改写为两段式,则如下所示,状态转移部分省略。
always@(posedge Clk or negedge Rst_n) if(!Rst_n) begin state <= IDLE;end else state <= nx_state;
always@(state or key_in or key_flag or key_state) begin en_cnt = 1'b0; key_flag = 1'b0; key_state = 1'b1; case(state) ……………. end |
进行分析和综合直至没有错误以及警告。
为了测试仿真编写测试激励文件,新建key_filter_tb.v文件保存到testbench文件夹下,本激励文件除产生正常的时钟以及复位信号外,还人为模拟了按键从按下到松手释放的过程,人为产生的抖动之一如下所示,复制几次并在合理范围内修改参数。再次进行分析和综合直至没有错误以及警告。
//模拟按下抖动20ms内 key_in = 0;#1000; key_in = 1;#2000; key_in = 0;#1400; key_in = 1;#2600; key_in = 0;#1300; key_in = 1;#200; //产生一个低电平大于20ms key_in = 0;#20_000_100; #50_000_000; //模拟释放抖动20ms内 key_in = 1;#2000; key_in = 0;#1000; key_in = 1;#2000; key_in = 0;#1400; key_in = 1;#2600; key_in = 0;#1300; key_in = 1;#200; //产生一个高电平大于20ms key_in = 1;#50_000_100; #30000; |
设置好仿真脚本后进行功能仿真,可以看到产生的按下抖动以及松开抖动如图9-9所示。
图9-9-1按下抖动
图9-9-2释放抖动
其局部仿真图如图9-10所示,可以看出当key_in从正常态到按下稳定后状态经历了0001、 0010、0100三个状态,从按下到释放经历了0100、1000、0001三个状态,且按下稳定以及释放稳定均会产生一个时钟周期的key_flag高电平信号,key_state也会正常变化。
图9-10按键消抖仿真波形图
从图9-9可以看出是人为的设计了一些抖动,不具有随机性且编写出来的文件太长,因此继续采用随机数发生函数来产生抖动。
$random这一系统函数可以产生一个有符号的32bit随机整数。一般的用法是$random%b,其中b>0。这样就会生成一个范围在(-b+1):(b-1)中的随机数。如果只想要正数的随机数即可采用{$random}%b来产生。在工程中需要产生在20ms的按下与松手抖动,道理上应该产生20_000_000以内随机数的抖动,这里为了节约仿真时间,只产生一个16位的随机数也就是0到65535。
在人为产生抖动时复制多次抖动后可以看出激励文件比较长,这里采用使用任务task,其语法如下:
task <任务名>; <端口及数据类型声明语句> <语句1> <语句2> <语句n> endtask |
任务的调用的语法如下:
<任务名>(端口1,端口2,...端口n); |
综合以上两点,编写出以下设计文件,这里实现了五十次的0-65535ns按下抖动然后key_in赋固定值0且延时50ms(大于20ms即为稳定),同时也实现了释放抖动后key_in赋固定值1且延时50ms。
reg [15:0]myrand;
task press_key; begin repeat(50) begin myrand = {$random}%65536; #myrand key_in = ~key_in; end key_in = 0; #50_000_000;
repeat(50) begin myrand = {$random}%65536; #myrand key_in = ~key_in; end key_in = 1; #50_000_000; end endtask |
这样在key_filter_tb.v中除了产生时钟以及复位信号后只需调用这个任务即可。
initial begin Rst_n = 1'b0; key_in = 1'b1; #(`clk_period*10) Rst_n = 1'b1; #30000; press_key; #10000;
press_key; #10000;
press_key; #10000; $stop; end |
编译无误后再次启动仿真,可以看到如图9-11所示的仿真波形图,按下稳定以及释放稳定均会产生一个时钟周期的key_flag高电平信号,key_state也会正常变化。这里放大产生的抖动过程可以看到每一个抖动时间均不一样,这样就成功模拟了随机抖动过程。
图9-11-1引入task后的仿真波形图
图9-11-2 随机抖动过程
这里再提出一个仿真模型的概念,新建key_module.v保存到testbench文件夹下并输入以下内容。
`timescale 1ns/1ns
module key_model(key);
output reg key;
reg [15:0]myrand;
initial begin key = 1'b1; press_key; #10000; press_key; #10000; press_key; $stop; end
task press_key; begin ******* end endtask
endmodule |
这样在key_filter_tb中只调用这个仿真模型以及产生复位以及时钟信号即可,更简化了激励文件。这样key_in就是一个内部信号了,需要将reg型改为wire型。
key_model key_model(.key(key_in)); |
这样整个激励文件的内部结构即为图9-12所示:
图9-12 激励文件内部结构
这里的仿真模型也是不可综合,需要在设置脚本时需要额外添加进来,如图9-13所示。
图9-13 将仿真模型加入激励文件
再次运行仿真,同样可以看到图9-14的仿真波形图。
图9-14 引入按键仿真模型后的仿真波形图
这样就完成了单个按键的消抖设计与验证,且复习了状态机相关设计思路以及仿真模型的概念与应用。