1、什么是抖动
是指当按下或释放一个机械按键时,按键的物理触点由于机械结构的弹性和惯性作用,发生了瞬时的不稳定接触,导致电路在短时间内多次开关(“跳动”)。这种抖动现象通常会引起多个不必要的触发信号,进而影响系统的输入结果。这种抖动的时间尺度通常很短,通常在几毫秒(ms)以内,但如果不加以处理,它会在电路或系统中造成误判。例如,按键本应只触发一次信号,但由于抖动,可能会多次触发系统响应。
2、解决方法
1、硬件去抖动:
- 电容去抖动:在按键的两端并联一个小电容,利用电容对接触时的瞬时波动进行滤波,减少抖动的影响。这个是最常用方法之一。
- 机械改进:选择更加精确和耐用的按键材料和设计,改善触点的接触面和按键结构,减少机械抖动的产生。
2、软件去抖动:
延时滤波:在接收到按键信号后,通过软件延时一定的时间(如10ms至50ms),然后检查按键状态是否稳定(例如,是否持续按下),如果是,才认为按键已被有效按下。这样可以避免在按键触发的短时间内多次触发事件。
计时器抖动检测:通过设定计时器对连续的信号变化进行判断,避免在短时间内的快速变化被误认为有效输入。
这里以软件消抖为例,是通过持续检测按键输入,直到按键状态稳定为止。其实现原理为:假设按键未按下时输入为1,按下时输入为0,抖动期间输入信号会不稳定。具体步骤如下:当检测到按键输入为0时,先延时20ms,再次检测按键状态。如果按键仍为0,则可以确认按键已被有效按下。这个延时可以避开抖动期,从而消除前沿抖动的干扰。类似地,在检测到按键释放后,也需要延时20ms,以消除后沿抖动的影响,确保按键释放稳定后再进行后续处理。如下图所示:
3、实战任务
我们使用两个按键来控制数码管的数字加减。在初始化时,数码管默认显示数字5。按下key1时,数码管的数字会依次递增,直到显示数字9,再按一次时数字会循环回0。按下key0时,数码管的数字会依次递减,直到显示数字0,再按一次时数字会循环回9。
4、系统框图
根据实际任务需求,我们需要实现通过两个按键控制数码管显示的功能。因此,本实验设计了两种功能:一种是使数码管上的数字依次递增,直到显示数字9;另一种是使数码管上的数字依次递减,直到显示数字0。
因此,本实验需要四个输入端口:系统时钟和系统复位,两个按键输入信号,输出端口为一个八位共阳数码管,模块框图如下图所示:
5、时序图
这里只对按键消抖时序做讲解:以 key0 为例,首先,当我们按下 key0 按键时,由于机械抖动,输入信号会出现上下跳动。如果不对这些抖动进行处理,可能会导致输出值反复波动,从而引发误判,影响系统的稳定性。因此,我们需要等待按键状态稳定。在检测到按键按下后,我们设定一个延时,通常大于20ms,确保抖动期已经过去,并且按键状态稳定后,才认为按键已被有效按下。
6、程序编写
6.1、数码管显示
数码管显示 (seg_display.v) 代码编写如下:
module seg_display(
input seg_clk, // 全局时钟输入
input seg_rst_n, // 全局复位输入,低电平有效
input [1:0] key_flag, // 按键标志输入,2位,表示两种按键的状态
output reg [7:0] seg // 数码管输出,8位,控制数码管显示的值
);
// 内部寄存器定义
reg [3:0] number; // 存储当前显示的数码管的数字,4位表示0-9的数字
reg [1:0] key_new; // 延迟一个时钟周期的按键信号(用于消除抖动)
reg [1:0] key_last; // 延迟两个时钟周期的按键信号(用于消除抖动)
// 防止按键按下一直加\减
always @ (posedge seg_clk or negedge seg_rst_n) begin
if(!seg_rst_n) begin
key_new <= 2'b11; // 复位时将key_new和key_last设为默认值
key_last <= 2'b11;
end
else begin
key_new <= key_flag; // key_new存储当前按键状态
key_last <= key_new; // key_last存储上一个时钟周期的按键状态
end
end
// 数字显示控制,根据按键状态变化调整显示数字
always @(posedge seg_clk or negedge seg_rst_n) begin
if(!seg_rst_n)
number <= 4'd5; // 复位时,数码管显示数字5
else begin
if(key_new != key_last) // 按键状态变化时才进行处理
case(key_new) // 根据按键标志位进行加减
2'b01 : begin // key0按下(加)
number <= number + 4'd1; // 数字加1
if(number == 4'd9) // 如果数字达到9,重置为0
number <= 4'd0;
end
2'b10 : begin // key1按下(减)
number <= number - 4'd1; // 数字减1
if(number == 4'd0) // 如果数字达到0,重置为9
number <= 4'd9;
end
default : number <= number; // 默认情况下数字保持不变
endcase
end
end
// 数码管显示驱动,根据当前数字设置显示值
always @(posedge seg_clk or negedge seg_rst_n) begin
if(!seg_rst_n)
seg <= 8'h92; // 复位时,显示数字5(7段数码管编码:0为0x92)
else begin
case(number) // 根据number的值选择对应的7段显示编码
4'd0: seg <= 8'hC0; // 显示数字0
4'd1: seg <= 8'hF9; // 显示数字1
4'd2: seg <= 8'hA4; // 显示数字2
4'd3: seg <= 8'hB0; // 显示数字3
4'd4: seg <= 8'h99; // 显示数字4
4'd5: seg <= 8'h92; // 显示数字5
4'd6: seg <= 8'h82; // 显示数字6
4'd7: seg <= 8'hF8; // 显示数字7
4'd8: seg <= 8'h80; // 显示数字8
4'd9: seg <= 8'h90; // 显示数字9
default:seg <= 8'h92; // 默认情况显示数字5
endcase
end
end
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
程序注释:
- 第14 - 23 行:这里实现了一个节拍器,或者可以说是一个延迟模块。它的作用是将输入数据延迟两个时钟周期。这意味着,输入的值将在经过两个时钟周期后被输出,确保数据传递时有足够的延迟以便稳定采样。
- 第26 - 47 行:这部分实现了数字显示控制,依据按键状态的变化调整数码管显示的数字。特别需要注意的是第 30 行,这里使用节拍器确保按键输入的数据在一个固定的时钟周期点被读取。这样做的目的是避免按下按键时,数码管持续处于加(或减)状态,而是通过节拍控制,使得数据变化稳定、可控。
- 第50 - 68 行:这部分使用了状态机来控制数码管显示 0 到 9 的数字。在系统复位时,数码管会显示数字 5。正常工作时,数码管会根据 number 的值显示相应的数字。
6.2、按键处理
按键处理 (key_filter.v) 代码编写如下:
module key_filter(
input key_clk, // 系统时钟输入
input key_rst_n, // 系统复位,低电平有效
input [1:0] key_in, // 按键输入
output reg [1:0] key_flag // 按键状态输出
);
parameter CNT_MAX = 20'd100_000; // 计数器的最大值,用于设定按键消抖的时间为20ms
//parameter CNT_MAX = 20'd10; // 仿真调试
wire [1:0] stable_key; // 2位信号,存储每个按键的稳定状态
reg [19:0] cnt; // 用于计数的寄存器,判断按键稳定时间
reg [1:0] new_key; // 当前的按键输入状态
reg [1:0] next_key; // 下一个时钟周期的按键输入状态
// 脉冲边沿检测,当按键检测到下降沿时,stable_key产生一个时钟周期的高电平
assign stable_key = (~new_key) & next_key;
// 按键状态的更新
always @(posedge key_clk or negedge key_rst_n) begin
if(!key_rst_n) begin
new_key <= 2'b11;
next_key <= 2'b11;
end
else begin
new_key <= key_in; // new_key 获取当前按键输入
next_key <= new_key; // next_key 获取当前的值
end
end
// 用于按键消抖的计数器
always @(posedge key_clk or negedge key_rst_n) begin
if(!key_rst_n)
cnt <= 20'd0; // 复位时清零计数器
else if(stable_key) // 当按键检测到下降沿时,将计数器清零
cnt <= 20'd0;
else
cnt <= cnt + 20'd1; // 否则,计数器增加
end
// 根据计数器的值更新
always @(posedge key_clk or negedge key_rst_n) begin
if(!key_rst_n)
key_flag <= 2'b11;
else if(cnt == CNT_MAX - 20'd1) // 当按键按下超过20ms时,认为按键处于稳定状态
key_flag <= next_key; // 更新更新状态
else
key_flag <= key_flag; // 如果没有达到最大值,保持
end
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
注释:
- 第 20 行:这一行实现了脉冲边沿检测。当按键检测到下降沿时,stable_key 会产生一个时钟周期的高电平信号。这种脉冲的产生有助于捕捉到按键状态的变化,避免因按键抖动导致的多次触发。
- 第 23 - 32 行:此处实现了一个节拍器(延迟模块)。该模块的作用是将输入信号延迟两个时钟周期,即输入的信号将在经过两个时钟周期后被输出。
- 第 34 - 42 行:实现了一个用于按键消抖的延时器。当按键被按下时,计数器会被清零并重新开始计数。通过这种方式,消除因按键触发时的短期抖动或干扰,确保系统只会响应有效的按键事件。
- 第 45 - 52 行:如果按键处于稳定状态(即消抖时间超过 20ms),则会更新按键状态;如果在此期间按键没有稳定,或者检测到抖动,则保持原有的按键状态。这保证了在按键触发事件时,只响应稳定有效的输入信号,避免多次无效触发。
6.3、顶层模块
顶层模块 (key_seg_top.v) 代码编写如下:
module key_seg_top(
input sys_clk, // 系统时钟输入
input sys_rst_n, // 系统复位输入,低电平有效
input [1:0] key, // 按键输入
output [7:0] seg // 数码管输出
);
// 定义一个 wire 类型的信号,用于连接按键消抖模块和数码管模块之间的信号
wire [1:0] key_flag;
// 实例化数码管显示模块
seg_display u_seg_display(
.seg_clk(sys_clk), // 将全局时钟传递给数码管模块
.seg_rst_n(sys_rst_n), // 将复位信号传递给数码管模块,低电平有效
.key_flag(key_flag), // 将稳定的按键标志传递给数码管模块,用于控制数码管显示
.seg(seg) // 数码管的输出信号,控制数码管的显示内容
);
// 实例化按键消抖模块
key_filter u_key_filter(
.key_clk(sys_clk), // 将全局时钟传递给按键消抖模块
.key_rst_n(sys_rst_n), // 将复位信号传递给按键消抖模块,低电平有效
.key_in(key), // 将按键输入传递给按键消抖模块
.key_flag(key_flag) // 将消抖后的按键状态传递给 key_flag 信号,给再给数码管模块使用
);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
注释:
- 第 13 - 27 行:实例化了数码管显示模块(seg)和按键消抖模块(key_filter),并将它们的输入和输出信号进行连接。按键信号 key 通过 key_filter 模块进行处理,生成稳定的输出信号 key_flag(这个通过wire 定义的 key_flag 的信号进行连接),并作为输入传递给 seg 的显示模块,通过判断是那个按键按下,根据对应状态更新数码管的显示内容。
7 、仿真程序
仿真模块 (key_seg_mod.v) 代码编写如下:
`timescale 1ns / 1ns // 仿真单位为 1ns,仿真精度为 1ns
module key_seg_mod();
// reg define
reg sys_clk; // 系统时钟信号
reg sys_rst_n; // 系统复位信号,低电平有效
reg [1:0] key; // 按键输入信号
// wire define
wire [7:0] seg; // 数码管输出
// 产生时钟信号:每 10ns 改变一次时钟电平,形成一个周期为 20ns 的时钟信号
always #10 sys_clk = ~sys_clk;
// 信号初始化
initial begin
sys_clk <= 1'b0; // 系统时钟初始为低电平
sys_rst_n <= 1'b0; // 系统复位初始为低电平,表示复位状态
key <= 2'b11; // 按键输入初始为 2'b11,表示按键没有按下
#200;
sys_rst_n <= 1'b1; // 将复位信号置为高电平,解除复位,系统开始正常运行
// 模拟按键按下的前期机械抖动信号变化:这里只模拟 key[0] 按键的变化
#500;
key <= 2'b10;
#20;
key <= 2'b11;
#50;
key <= 2'b10;
#100;
key <= 2'b11;
#100;
// 模拟按键按下时的稳定状态
key <= 2'b10;
#1000;
// 模拟按键按下的后期机械抖动信号变化:这里只模拟 key[0] 按键的变化
key <= 2'b11;
#20;
key <= 2'b10;
#50;
key <= 2'b11;
#10;
end
// 例化
key_seg_top u_key_seg_top (
.sys_clk(sys_clk), // 将系统时钟传递给顶层模块
.sys_rst_n(sys_rst_n), // 将复位信号传递给顶层模块
.key(key), // 将按键输入传递给顶层模块
.seg(seg) // 获取数码管的输出,连接到 seg 信号
);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
注释:
- 第 24 - 33 行:模拟按键按下的前期机械抖动信号变化:这里只模拟 key[0] 按键的变化
- 第 35 - 37 行:模拟按键按下时的稳定状态。
- 第 39 - 45 行:模拟按键按下的后期机械抖动信号变化:这里只模拟 key[0] 按键的变化
接下来打开 Modelsim 软件对代码进行仿真,需要添加文件如下图所示:
运行仿真一段时间后,仿真的波形如下图所示:
由于我们模拟的是一个20纳秒周期的时钟信号,在仿真中,稳态时钟设置为 CNT_MAX = 20'd10,即需要至少保持200纳秒以上的稳定时间才能满足触发条件。在仿真过程中,我们观察到前期的抖动对输出结果没有影响。由于稳定时间不足200纳秒,我们认为此时处于不稳定状态,因此不进行任何处理。
当按键的当前值与下一次的值不一致时,会产生一个下降沿,表示开始计数判断是否在稳定状态。
8、I/O 引脚绑定
进行 I/O 约束,这里只需要对 clk 、 2个输入按键和数码管 8 个引脚进行约束,管脚分配如下表所示:
信号 | 方向 | 引脚 | 端口作用 | 电平标准 |
---|---|---|---|---|
sys_clk | input | T7 | 时钟 | LVCMOS33 |
key0 | input | F10 | 按键减 | LVCMOS33 |
key1 | input | D11 | 按键加 | LVCMOS33 |
seg[0] | output | G13 | A | LVCMOS33 |
seg[1] | output | H16 | B | LVCMOS33 |
seg[2] | output | H12 | C | LVCMOS33 |
seg[3] | output | H13 | D | LVCMOS33 |
seg[4] | output | H14 | E | LVCMOS33 |
seg[5] | output | G12 | F | LVCMOS33 |
seg[6] | output | G11 | G | LVCMOS33 |
seg[7] | output | L14 | DP | LVCMOS33 |
Gowin 软件中 I/O Constraints 界面如下图所示:
注:由于我们的FPGA端板载了2个按键这里的复位按键则不用绑定
9、程序下载
连接开发板的下载器,将码流文件下载到开发板后,按下 F10 按键数码管将从数字 5 显示到数字 4 ,松开 F10 按下 D11 按键,又从数字 4 显示回到数字 5 。