1.实现说明
这个实验不需要外接,直接编译下载程序就可以了。
管脚说明
set_pin_assignment { clk_ref } { LOCATION = P128; IOSTANDARD = LVCMOS33; }
set_pin_assignment { i_rst } { LOCATION = P55; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment { key_in_0 } { LOCATION = P56; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment { led[0] } { LOCATION = P52; }
set_pin_assignment { led[1] } { LOCATION = P54; }
set_pin_assignment { led[2] } { LOCATION = P57; }
set_pin_assignment { led[3] } { LOCATION = P58; }
- clk_ref 时钟参考脚
- i_rst 是复位脚,也即板子上的按键SW2,
- key_in 是切换音乐的按键SW1,
- led[0:3] 是LED灯在教程(2)中介绍过。
本实验通过SW1切换流水灯的流转方向,视频效果如下:
2. 原理说明
- 使用 FPGA 开发板上的机械按键时,当按下去的时候,会有 5-10ms 的一个抖动,内部所获得的波形图就在那期间会有好几个上升沿,造成了干扰,所以需要消除抖动。
- 其实本文所讲述的按键消抖,其实是在按键按下后的 20ms 才读取此时按键的电平值,此时就避免了抖动的影响,如下图所示:
- 当下降沿到来时,开始计数 20ms ,然后计数完成后取电平值,这样就避免了按键抖动的影响,不会让 FPGA 认为你按下了很多次。
3.代码说明
与本实验相关的只有两个模块,一个是按键消抖,一个流水灯。
按键消抖模块
在点亮 LED 实验中我们知道了板子上按键的设计, 当按键未被按下时, 连接到 FPGA 管脚认为是高
电平; 当按键被按下时, 连接到 FPGA 管脚认为是低电平。
要消除按键的抖动, 我们需要去扫描按键, 也就是不断的去采集按键的状态。 软件消抖时我们一
般只考虑按键按下时的抖动, 而放弃对释放时抖动的消除。 用系统时钟(频率较高) 去采集按键状
态, 当检测到按下时用计数器延时 20ms, 再去检测按键状态, 如果这时仍为按下状态, 确认是一次按
下动作, 否侧的话认为无按键按下。 如何检测按键状态变化就需要用到脉冲边沿检测的方法。
检测按键按下时要用到脉冲边沿检测的方法, 捕捉信号的突变、 捕捉时钟的上升下降沿等经常会
用到这种方法。 简单地说就是用一个频率更高的时钟去触发要检测的信号, 用两个寄存器去储存相邻
两个时钟采集到的值, 然后进行异或运算, 如果不为零, 代表发生了上升沿或者下降沿。
在按键消抖的过程中, 同样运用了脉冲边沿检测。 用两个寄存器储存相邻时钟采集的值(例如
datapre,data) ,然后将 data 取反与前一个值相与(state=datapre&(~data) ) ,如果为 1, 则判断 有下降沿既按键按下由高到低; 否则无变化。 将一个信号由连续时钟采集, 相邻两个钟触发的值存入
两个寄存器。 理解 verilog 实现这个过程要充分了解其中的非阻塞赋值。
本例程在进行按键消抖时只考虑按键按下时的抖动,放弃对释放时抖动的消除。 按键消抖具体流程如下: 用
系统时钟(频率较高)去采集按键状态,当检测到按键按下时进行计数器延时, 延时 20ms 后再次检测
按键状态, 当按键仍为按下状态时, 认为此次为按下动作,否则认为无按键按下。 其中该例程将采用脉
冲边沿检测的方法进行按键状态变化的检测。
首先使用同步器:key_r0 不断地从 key 中获取新值,而 key_sec_pre
不断地保留 key_sec
原来的值,所以两个信号就有一个时钟周期差。通过将 key_r0 取反再相与,则可以检测到下降沿,如代码中:key_pulse = key_sec_pre & (~key_sec); //检测下降沿
检测按键按下时要用到脉冲边沿检测的方法,捕捉信号的突变、捕捉时钟的上升沿或下降沿等也经
常会用到这种方法。 该方法具体指用一个频率更高的时钟去触发要检测的信号,用两个寄存器去储存相
邻两个时钟采集到的值,然后进行异或运算,如果不为零,代表发生了上升沿或者下降沿。 具体边沿检
测时序如下图所示。
脉冲边沿检测具体实现方法即使用两个寄存器储存相邻时钟采集的值(例如将采集到的相邻值设
置为 key_sec_pre
, key_sec
), 然后将 key_sec
取反与前一个值 key_sec_pre
相与(即 key_pulse = key_sec_pre & (~key_sec);
) ,
如果 key_pulse
结果为 1, 便判断此时脉冲有下降沿即按键按下;否则判断脉冲无变化即按键没有按下。 获取到下降沿信号后,使能 delay_flag 信号,这里为什么要多此一举,有下降沿信号后直接让计数器开始工作不是更简单吗?通过标志 delay_flag 信号,可以让计数器在 delay_flag 为高电平的期间计数,如果只有个下降沿标志,那么计数器就会重新开始初始化好几次,就不能从最初的下降沿信号开始进行计数,下图中我只画了三个下降沿信号,真实情况不止三个,所以需要一个 delay_flag 信号,标志计数的区间。
然后计数器累加到 1000_000 个时钟周期,因为延迟 20 ms,一个单位的时钟周期时 20 ns,所以20ms /20 ns =1000000个时钟周期,也就是要计数 1000_000 次。
当计数器满了后,对此刻的同步信号进行取反,即可得到按键按下的信号,并传输给 LED 模块。press <= ~key_r0;
得到的 press 按键按下信号的波形图如下:
press 信号就是我们所需要的按键信号。
代码第一段进行复位
always@(posedge I_clk or posedge I_rst)begin
if (I_rst) begin
key_I_rst <= {N{1'b1}};
key_I_rst_pre <= {N{1'b1}};
end
else begin
key_I_rst <= key;
key_I_rst_pre <= key_I_rst;
end
end
第二产生20ms延时
always@(posedge I_clk or posedge I_rst)begin
if(I_rst)
cnt <= 25'd0;
else if(key_edge)
cnt <= 25'd0;
else
cnt <= cnt + 1'b1;
end
第三部分进行key_pulse按键脉冲的生成,去抖处理。
always@(posedge I_clk or posedge I_rst)begin
if (I_rst)
key_sec <= {N{1'b1}};
else if (cnt==25'd49_9999)
key_sec <= key;
end
always@(posedge I_clk or posedge I_rst)begin
if (I_rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);
always@(posedge I_clk or posedge I_rst)begin
if (I_rst)
key_sec <= {N{1'b1}};
else if (cnt==25'd49_9999)
key_sec <= key;
end
每隔20ms捕获一次按键,
always@(posedge I_clk or posedge I_rst)begin
if (I_rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
保存上一次的按键值
key_pulse = key_sec_pre & (~key_sec)
比较上一次按键状态与当前按键的装态是否有区别,检测按键电平是否有变化(即下降沿)。
流水灯模块
在 demo1中讲过了,没啥本质区别。
4.增强版去抖
前面的代码为了简单起见,没有做过多复位杂理,这里提供一个相对复杂的去抖模块,效果更好。
module key_debounce(
input wire clk , //50MHz时钟频率
input wire rst_n , //复位按键
input wire key , //按键输入口
output reg press //按键按下标志输出
);
//全局参数定义
parameter DELAY_TIME = 1000_000; //延时20ms也就是1000_000个时钟周期
//信号定义
reg key_r0 ; //同步 当前时钟周期输入状态
reg key_r1 ; //打拍 前一个时钟周期输入的状态
wire key_nedge ; //下降沿
reg [19:0] delay_cnt ; //计数20ms,需要20ms/20ns = 1000_000个时钟周期
reg delay_flag ; //按下的下降沿标志
//同步计数实现
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0) begin
key_r0 <= 1'b1;
key_r1 <= 1'b1;
end
else begin
key_r0 <= key;
key_r1 <= key_r0;
end
end
assign key_nedge = ~key_r0 & key_r1; //检测下降沿
//assign key_pedge = key_r0 & ~key_r1; //检测上升沿
//delay_cnt 计数器计满1000_000个时钟周期
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
delay_cnt <= 0;
end
else if(delay_flag) begin
//按下的下降沿标志出现,则执行
if(delay_cnt == DELAY_TIME - 1) begin
//如果计满1000_000个时钟周期,则归零处理
delay_cnt <= 0;
end
else begin
//没有计满,则不断累加
delay_cnt <= delay_cnt + 1;
end
end
end
//delay_flag
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
delay_flag <= 1'b0;
end
else if(key_nedge) begin
//下降沿出现后,设置按下标志位为 1
delay_flag <= 1'b1;
end
else if(delay_cnt == DELAY_TIME - 1) begin
/*
计数器计满1000_000个时钟周期后,
设置按下标志位为 0
表示按键按下结束,以备下次按键
*/
delay_flag <= 1'b0;
end
end
//press
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
press <= 1'b0;
end
else if(delay_cnt == DELAY_TIME - 1) begin
/*
计满1000_000个时钟周期后
获取当前的按压标志
此时 press = 1
*/
press <= ~key_r0;
end
else begin
/*
没有计满1000_000个时钟周期
则 press = 0
*/
press <= 1'b0;
end
end
endmodule
没有回复内容