在按键控制 LED中采用直接读取按键电平状态,然后根据电平状态控制LED。虽然直接读取按键电平状态然后执行相应处理程序的方法非常简单,但是这种方式可能存在误判问题,进而有可能导致程序功能异常,这是因为按键按下和松开时存在抖动问题,可能会使程序检测到错误的按下或松开操作。
 本章节将以按键控制蜂鸣器为例讲解按键消抖算法的实现。
蜂鸣器简介
蜂鸣器按照结构原理不同可分为压电式蜂鸣器和电磁式蜂鸣器。压电式蜂鸣器主要由多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱、外壳等组成;电磁式蜂鸣器由振荡器、电磁线圈、磁铁、振动膜片及外壳等组成。压电式蜂鸣器是利用压电效应原理工作的,当对其施加交变电压时它会产生机械振动发声;电磁式蜂鸣器是接通电源后,振荡器产生的音频信号电流通过电磁线圈,使电磁线圈产生磁场,振动膜片在电磁线圈和磁铁的相互作用下,周期性地振动发声。
 蜂鸣器按照驱动方式不同又可分为有源蜂鸣器和无源蜂鸣器,其主要区别为蜂鸣器内部是否含有震荡源。一般的有源蜂鸣器内部自带了震荡源,只要通电就会发声。而无源蜂鸣器由于不含内部震荡源,需要外接震荡信号才能发声。
 
按键消抖原理
常见的按键均为机械弹性开关,当按下或松开按键时,由于弹片的物理特性,不能立即闭合或断开,往往会在断开或闭合的过程中产生机械抖动,消除这种抖动的过程即称为按键消抖。
 按键消抖可分为硬件消抖和软件消抖;硬件消抖主要使用 RS 触发器或电容等方法在硬件电路上实现消抖抖,一般在按键较少时使用。软件消抖的原理为按键按下或松开后,由处理器延时 5ms 至 20ms再对按键状态进行采样并判断,如下图所示软件消抖的原理图。
 
软件消抖
在按键被按下和按键被释放时都会产生抖动,而按键的抖动在数字电路中的体现就是不断变化的高低电平,由此可以绘制按键消抖模块输入信号的波形如下图所示。
 
 抖动部分的 key 信号也有低电平的情况,但是因为机械抖动的原因很快又被拉高了,如果我们把每次 key 的低电平都检测到,相当于在极短的时间内按键被按下很多次,而不是我们实际操作的按下一次,所以我们需要把这段抖动的信号给滤除,只有按键值保持 20ms 及以上的稳定状态,我们才确认按键值有效,此时 key 值为高电平则按键被释放,key 值为低电平则按键被按下,所以消抖的过程就是滤除按键值保持时间小于 20ms 的值。
- 如何滤除保持时间小于20ms的值:从 20ms 开始倒计时,如果 20ms 的倒计时还没有完成按键值就再次产生变化,此时需要从头开始 20ms 倒计时,前一次导致按键值变化的操作视为无效操作,将该次变化视为按键抖动消除;如果计完 20ms 按键状态一直没有改变,说明此次按键被按下或者被释放是有效操作。
 - 如何判断按键变化:可以给按键值打两拍(一般外部输入信号我们都使用打拍处理消除亚稳态),然后比较按键两次的打拍值,第一次打拍值与第二次打拍值一致说明按键值没有变化,第一次打拍值与第二次打拍值不一致说明按键值产生了变化。

 
硬件设计
本次实验采用有源蜂鸣器,其原理图如下:
 
 按键原理图参考按键控制 LED章节
程序功能
用 KEY0 按键来控制蜂鸣器发声,初始状态为蜂鸣器鸣叫,按下按键后蜂鸣器停止鸣叫,再次按下开关,蜂鸣器重新鸣叫。
系统框图
系统分为两个模块,分别是按键消抖模块和蜂鸣器控制模块
 
编写代码
按键消抖代码
`timescale 1ns / 1ns	//仿真单位/仿真精度
module key_debounce #(
	parameter COUNT_WIDTH = 20,					//按键延时消抖计数器宽度
	parameter KEY_DELAY = 20'd100_0000,			//按键消抖延时
	parameter DEFAULT_LEVEL = 1'b1				//按键默认状态
)
(
	input sys_clk,								//时钟
	input sys_rst_n,							//复位
	input key_in,								//外部输入的按键值
	output reg key_filter						//消抖后的按键值
);
//延时消抖计数器,当按键按下时从KEY_DELAY开始递减计数,当其值为1时锁定按键状态
reg [COUNT_WIDTH-1:0] count;
//按键信号延迟一个时钟周期
//这里将key_debounce作为内部模块,只需要延迟一个时钟周期,用于捕获上升沿和下降沿
//例化时应将外部按键输入再延迟两个时钟周期,用于消除亚稳态(在FPGA中,对于外部输入信号均需要延迟两拍在使用,以消除亚稳态)
reg key_d1;
//按键信号延迟一个时钟周期
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		key_d1 <= DEFAULT_LEVEL;
	else
		key_d1 <= key_in;
end
//检测按键跳变,当按键跳变时重置计数器为KEY_DELAY
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		count <= 0;
	else if(key_d1 != key_in)
		count <= KEY_DELAY;			//按键跳变,重置计数器为KEY_DELAY
	else if(count > 0)
		count <= count - 1;			//递减计数,直到为0
	else
		count <= 0;
end
//在计数器值为1时(消抖延时结束)锁定按键状态
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		key_filter <= DEFAULT_LEVEL;
	else if(count == 1)
		key_filter <= key_in;		//计数器递减为1时锁定按键状态
end
endmodule
 
按键消抖仿真激励代码
`timescale 1ns / 1ns	//仿真单位/仿真精度
module tb_key_debounce();
reg sys_clk;					//时钟
reg sys_rst_n;					//复位
reg key_in;						//输入按键
wire key_filter;				//消抖后的按键
initial begin
	sys_clk = 1'b0;
	sys_rst_n = 1'b0;
	key_in <= 1;
	#180
	sys_rst_n = 1'b1;
	#2000
	//按下过程
	key_in <= 0;
	#8
	key_in <= 1;
	#8
	key_in <= 0;
	#8
	key_in <= 1;
	#2
	key_in <= 0;
	#2
	key_in <= 1;
	#20
	key_in <= 0;
	//稳定保持过程
	#2000
	//释放过程
	key_in <= 1;
	#8
	key_in <= 0;
	#4
	key_in <= 1;
	#3
	key_in <= 0;
	#3
	key_in <= 1;
end
//产生时钟
always #10 sys_clk = ~sys_clk;
//例化按键消抖模块
key_debounce #(
	.COUNT_WIDTH(20),					//按键延时消抖计数器宽度
	.KEY_DELAY(25),						//按键消抖延时
	.DEFAULT_LEVEL(1'b1)				//按键默认状态
)
tb_key_debounce_inst(
	.sys_clk(sys_clk),					//时钟
	.sys_rst_n(sys_rst_n),				//复位
	.key_in(key_in),					//外部输入的按键值
	.key_filter(key_filter)				//消抖后的按键值
);
endmodule
 
蜂鸣器控制代码
`timescale 1ns / 1ns	//仿真单位/仿真精度
module key_beep #(
	parameter BEEP_DEFAULT_STATE = 1'b1,		//蜂鸣器默认状态
	parameter KEY_DEFAULT_LEVEL = 1'b1			//按键默认状态
)
(
	input sys_clk,								//时钟
	input sys_rst_n,							//复位
	input key_filter,							//消抖后的按键值
	output reg beep								//蜂鸣器
);
//消抖后的按键信号延迟一个时钟周期,用于检测边沿跳变
reg key_filter_d1;
//下降沿标志
wire key_negedge;
//捕获按键下降沿,当上一个时钟周期为高电平,当前时钟周期为低电平即为下降沿
assign key_negedge = key_filter_d1 & (~key_filter);
//消抖后的按键信号延迟一个时钟周期,用于检测边沿跳变
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		key_filter_d1 <= KEY_DEFAULT_LEVEL;
	else
		key_filter_d1 <= key_filter;
end
//下降沿时刻翻转蜂鸣器状态
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		beep <= BEEP_DEFAULT_STATE;
	else if(key_negedge == 1'b1)
		beep <= ~beep;
end
endmodule
 
蜂鸣器仿真激励代码
`timescale 1ns / 1ns	//仿真单位/仿真精度
module tb_key_beep();
reg sys_clk;					//时钟
reg sys_rst_n;					//复位
reg key_filter;					//消抖后的按键状态
wire beep;						//蜂鸣器
initial begin
	sys_clk = 1'b0;
	sys_rst_n = 1'b0;
	key_filter <= 1;
	#200
	sys_rst_n = 1'b1;
	#200
	//按下
	key_filter <= 0;
	#2000
	//释放
	key_filter <= 1;
	#2000
	//按下
	key_filter <= 0;
end
//产生时钟
always #10 sys_clk = ~sys_clk;
key_beep #(
	.BEEP_DEFAULT_STATE(1'b1),			//蜂鸣器默认状态
	.KEY_DEFAULT_LEVEL(1'b1)			//按键默认状态
)
tb_key_beep_inst (
	.sys_clk(sys_clk),					//时钟
	.sys_rst_n(sys_rst_n),				//复位
	.key_filter(key_filter),			//消抖后的按键值
	.beep(beep)							//蜂鸣器
);
endmodule
 
顶层代码
`timescale 1ns / 1ns
module top_key_beep #(
	parameter BEEP_DEFAULT_STATE = 1'b1,		//蜂鸣器默认状态
	parameter KEY_DEFAULT_LEVEL = 1'b1,			//按键默认状态
	parameter COUNT_WIDTH = 20,					//按键延时消抖计数器宽度
	parameter KEY_DELAY = 20'd100_0000			//按键消抖延时
)
(
	input sys_clk,								//时钟
	input sys_rst_n,							//复位
	input key_in,								//外部输入的按键值
	
	output beep									//蜂鸣器
);
//消抖后的按键
wire key_filter;
//外部输入按键延时两拍,用于消除亚稳态(在FPGA中,对于外部输入信号均需要延迟两拍在使用,以消除亚稳态)
reg key_in_d1;
reg key_in_d2;
//按键延迟两拍
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		key_in_d1 <= KEY_DEFAULT_LEVEL;
		key_in_d2 <= KEY_DEFAULT_LEVEL;
	end
	else begin
		key_in_d1 <= key_in;
		key_in_d2 <= key_in_d1;
	end
end
//例化按键消抖模块
//例化按键消抖模块
key_debounce #(
	.COUNT_WIDTH(COUNT_WIDTH),				//按键延时消抖计数器宽度
	.KEY_DELAY(KEY_DELAY),					//按键消抖延时
	.KEY_DEFAULT_LEVEL(KEY_DEFAULT_LEVEL)	//按键默认状态
)
tb_key_debounce_inst(
	.sys_clk(sys_clk),						//时钟
	.sys_rst_n(sys_rst_n),					//复位
	.key_in(key_in_d2),						//外部输入的按键值
	.key_filter(key_filter)					//消抖后的按键值
);
//例化蜂鸣器控制模块
key_beep #(
	.BEEP_DEFAULT_STATE(BEEP_DEFAULT_STATE),	//蜂鸣器默认状态
	.KEY_DEFAULT_LEVEL(KEY_DEFAULT_LEVEL)	//按键默认状态
)
tb_key_beep_inst (
	.sys_clk(sys_clk),						//时钟
	.sys_rst_n(sys_rst_n),					//复位
	.key_filter(key_filter),				//消抖后的按键值
	.beep(beep)								//蜂鸣器
);
endmodule
 
系统仿真激励代码
`timescale 1ns / 1ns	//仿真单位/仿真精度
module tb_top_key_beep();
reg sys_clk;					//时钟
reg sys_rst_n;					//复位
reg key_in;						//输入按键
wire beep;						//蜂鸣器
initial begin
	sys_clk = 1'b0;
	sys_rst_n = 1'b0;
	key_in <= 1;
	#180
	sys_rst_n = 1'b1;
	
	#2000
	//按下过程
	key_in <= 0;
	#8
	key_in <= 1;
	#8
	key_in <= 0;
	#8
	key_in <= 1;
	#2
	key_in <= 0;
	#2
	key_in <= 1;
	#20
	key_in <= 0;
	//稳定保持过程
	#2000
	//释放过程
	key_in <= 1;
	#8
	key_in <= 0;
	#4
	key_in <= 1;
	#3
	key_in <= 0;
	#3
	key_in <= 1;
	//稳定保持过程
	#2000
	//按下过程
	key_in <= 0;
	#18
	key_in <= 1;
	#5
	key_in <= 0;
	#5
	key_in <= 1;
	#2
	key_in <= 0;
	#2
	key_in <= 1;
	#10
	key_in <= 0;
	//稳定保持过程
	#3000
	//释放过程
	key_in <= 1;
	#12
	key_in <= 0;
	#4
	key_in <= 1;
	#5
	key_in <= 0;
	#15
	key_in <= 1;
end
//产生时钟
always #10 sys_clk = ~sys_clk;
//例化按键消抖模块
top_key_beep #(
	.BEEP_DEFAULT_STATE(1'b1),			//蜂鸣器默认状态
	.KEY_DEFAULT_LEVEL(1'b1),			//按键默认状态
	.COUNT_WIDTH(20),					//按键延时消抖计数器宽度
	.KEY_DELAY(25)						//按键消抖延时
)
tb_top_key_beep_inst(
	.sys_clk(sys_clk),					//时钟
	.sys_rst_n(sys_rst_n),				//复位
	.key_in(key_in),					//外部输入的按键值
	.beep(beep)				//消抖后的按键值
);
endmodule
 
约束输入
管脚分配如下:
 
的 XDC 约束语句如下:
#时序约束
create_clock -period 20.000 -name sys_clk [get_ports sys_clk]
#IO 管脚约束
set_property -dict {PACKAGE_PIN R4 IOSTANDARD LVCMOS15} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U7 IOSTANDARD LVCMOS15} [get_ports sys_rst_n]
set_property -dict {PACKAGE_PIN T4 IOSTANDARD LVCMOS15} [get_ports key_in]
set_property -dict {PACKAGE_PIN V7 IOSTANDARD LVCMOS15} [get_ports beep]
                

















