一
按键做为基本的人机输入接口,在很多电子设计中都能见到。由于它的机械特性,在按键按下或松开的时候,会伴随一段时间的抖动,通常小于10ms,我们的目标是把这段抖动的时间滤除掉,使按键按下或松开的过程只有一个跳变沿,处理后的最终的效果如下图所示:
很明显整个消除抖动的过程可以划分为四个阶段。
- 按键没有按下,高电平,IDLE
- 按键按下,抖动状态
- 按键按下后的平稳状态
- 按键解除后的抖动状态
状态划分好了之后,再去考虑下再每个状态都需要做哪些处理才能实现我们的目标。经过分析发现在IDLE态检测到按键按下之后需要实现一个周期大于10ms的计数器,当计数器计数到设定的值之后再去检测按键是否按下,如果检测到按键按下了,那么此时到了平稳态。在平稳态需要做什么事情呢,就是判断按键有没有松开,如果按键松开了,那就再计数到一个特定值,到了特定值之后再去检测按键是否是松开后的状态。经过这样的处理后我们就能得到滤除后的理想波形了。整个操作流程用状态机可以轻松实现。对于何时读取按键值,可以定在按键接触后回到初始态,也可以定在平稳态,这个看个人习惯。经过处理之后的确可以达到按键按下一次触发一次操作的功能,但对于自锁式按键则不行,这时候就需要产生两个vld信号来标记哪里是按键按下后的平稳态,哪里是按键解除后的初始态。但是大都数板载按键都是按一次触发一次操作,所以目前的代码就产生一个vld信号。
// 2023/5/10 17:05:09
// 模块实现对按键的消抖处理
module key_filter #(
parameter SIMULATION = 1'b0
)(
input clk , //50MHz
input rstn , //低电平有效
input [2:0] key_in ,
output reg key_vld ,
output reg [2:0] key_value
);
localparam S_IDLE = 4'b0001 ;
localparam S_PRESS = 4'b0010 ;
localparam S_STABLE = 4'b0100 ;
localparam S_RELEASE = 4'b1000 ;
reg [3:0] state, nxt_state ;
reg [9:0] cnt ;
//-----------------------------------------------------
localparam TIME_20MS = (SIMULATION == 1'b1) ? 10 : 1000000; // just for sim
reg [20*8-1:0] sta_sim ;
reg [20*8-1:0] nxt_sta_sim ;
generate if (SIMULATION == 1'b1) begin
always @(*) begin
case (state)
4'b0001 : sta_sim = "S_IDLE " ;
4'b0010 : sta_sim = "S_PRESS " ;
4'b0100 : sta_sim = "S_STABLE " ;
4'b1000 : sta_sim = "S_RELEASE " ;
endcase
end
always @(*) begin
case (nxt_state)
4'b0001 : nxt_sta_sim = "S_IDLE " ;
4'b0010 : nxt_sta_sim = "S_PRESS " ;
4'b0100 : nxt_sta_sim = "S_STABLE " ;
4'b1000 : nxt_sta_sim = "S_RELEASE " ;
endcase
end
end
endgenerate
//-----------------------------------------------------
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
state <= S_IDLE;
end else begin
state <= nxt_state;
end
end
//--处理流程
always @(*) begin
nxt_state = state;
case (state)
S_IDLE : if (key_in != 3'b111) nxt_state = S_PRESS;
S_PRESS : if ((cnt == TIME_20MS-1) && (key_in == 3'b111))
nxt_state = S_IDLE;
else if ((cnt == TIME_20MS-1) && (key_in != 3'b111))
nxt_state = S_STABLE;
S_STABLE : if (key_in == 3'b111) nxt_state = S_RELEASE;
S_RELEASE : if ((cnt == TIME_20MS-1) && (key_in == 3'b111))
nxt_state = S_IDLE;
default : nxt_state = S_IDLE;
endcase
end
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
cnt <= 'd0;
end else if (state == S_PRESS || state == S_RELEASE) begin
cnt <= cnt + 1;
end else if (state != S_PRESS || state != S_RELEASE) begin
cnt <= 'd0;
end
end
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
key_value <= 3'b000;
end else if (state == S_PRESS && nxt_state == S_STABLE) begin
key_value <= key_in;
end
end
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
key_vld <= 1'b0;
end else if (state == S_RELEASE && nxt_state == S_IDLE) begin
key_vld <= 1'b1;
end else begin
key_vld <= 1'b0;
end
end
endmodule
代码虽然简单易懂,但这里也有一些小技巧分享给大家。在编写RTL代码时经常会使用状态机,不管使用什么编码方式在仿真中都是一串数字,尤其当状态较多时,时常看着看着自己就糊涂了,在这里我们使用generate语法,当在仿真时把对应的信号用ASCII码表示就会很容易显示处状态值,这样看着就清晰很多。另外,有时候在RTL中需要计数一个很大的值,但在仿真中直接用这个大的值会非常不方便,同样的我们也可以用示例代码中的方法,只需要在仿真文件中修改参数就可以很方便的改变相应的值。请看下面的仿真图,可以很直观的看出上面体现的两种办法。
二
由于按键的机械特性,按下或松开后会抖动一段时间,通常小于10ms,那我们是不是可以考虑实现一个10ms的计数器,当计数到10ms时去锁定此时的key_in值,以单个按键为例,可以定义一个[1:0] key_reg,如果在按键平稳期锁定的输入值,那么直接key_reg[0] key_reg[1] 根据按键的初始态和按下后的稳定态的高低电平做适当的运算可以判断按键是否被按下,然后产生一个触发信号给其他模块使用就行了;如果在按键抖动时间内锁定输入值,那么不管此时按键锁定的是高电平还是低电平,都不会影响按键判断,因为下一次锁定时一定是按键稳定后的值。只是会延时10ms。这里有一个隐含条件就是按键按下后持续的平稳时间要大于20ms,一般情况下都会达到。代码就不贴了,有兴趣的同学可以自己尝试下,然后通过仿真看看这样做是不是可行。帖一张手画的示意图可以对着理解下。