0 环境配置
软件:Windows 10、TD5.6.5、modelsim SE10.4、vscode
硬件:EG4S20BG256
1 按键消抖简介
基本概念
按键一般为机械弹性开关,由于机械触点具有弹性作用,其断开、闭合时刻会伴随着一连串的抖动,导致短时间内输出信号在高低电平间连续多次切换,如下图所示。如果直接用该不稳定信号作为开关来控制后续电路,会造成电路的不稳定,因此需要采用按键消抖来消除开关闭合、断开时刻的抖动。
程序框图与端口信号
Port name | Direction | Type | Description |
clk | input | wire | 24MHz时钟 |
rstn | input | wire | 复位 |
key_in | input | wire | 按下/松开 |
key_out | output | wire | 优化后的按下/松开 |
2 设计思路
机械按键按下时候有一个不稳定的抖动期,一般会在20ms以内,因此需要用计数器CNT_20MS
来进行20ms的计数。假设50MHz的时钟,时钟周期为20ns,计数20ms需要20×106ns ÷ 20ns = 106,即0106-1.
- 若在计满20ms之前检测到电平变化,表明按键按下后(1→0)仍有电平变化,实为抖动造成,应清零计数器重新计数。
- 若计满20ms无电平变化,表明按键按下后(1→0)的20ms内无任何电平变化,以及稳定,可以认为是按键真正被按下,此时可将输入信号
key_in
赋值给key_out
,用于控制后续电路。
3 程序设计
按键消抖主模块key_filter.v
module key_filter
#(
parameter CLK_FRE = 24
)
(
input wire clk,
input wire rstn,
input wire key_in,
output reg key_out
);
localparam CNT_20MS = 20_000_000/(1000/CLK_FRE)-1;
//边沿检测:双边检测
reg key_in_r;
wire pos_neg;
always @(posedge clk or negedge rstn) begin
if(~rstn)
key_in_r <=0;
else
key_in_r <=key_in;
end
assign pos_neg =(key_in ^ key_in_r);
//计数:检测到上升沿或者下降沿后计数
reg [31:0]cnt;
always @(posedge clk or negedge rstn) begin
if(~rstn)
cnt <=0;
else if(pos_neg)
cnt <=0; //和glitch_filter一样,检测到clk↑或↓则重新计数
else
cnt <=cnt +1'b1;
end
//输出和输入的关系
always @(posedge clk or negedge rstn) begin
if(~rstn)
key_out <=1;
else if(cnt==CNT_20MS)
key_out <=key_in_r;
end
endmodule //key_filter
tb文件tb_key_filter.v
`timescale 1ns/1ns
module tb_key_filter();
reg clk;
reg rstn;
reg key_in;
wire key_out;
reg [31:0] flip_time;
//时钟
localparam CLK_FRE = 50;
localparam T=(1000/CLK_FRE);
initial begin
clk =1;
forever #(T/2) clk=~clk;
end
//赋值:高电平先保持25ms,再波动100次,再低电平保持50ms,再波动,再拉高
initial begin
rstn =0;
key_in =1;
#100
rstn =1;
#25_000_000
press_key();
#10
$finish();
end
task press_key();
begin
key_in =0;
repeat(100)begin
flip_time ={$random}%65536;
#flip_time key_in =~key_in;
end
key_in =0;
#50_000_000
key_in =1;
repeat(100)begin
flip_time ={$random}%65536;
#flip_time key_in =~key_in;
end
key_in =1;
#25_000_000;
end
endtask
key_filter #(
.CLK_FRE (CLK_FRE)
)
key_filter_inst(
.clk (clk),
.rstn (rstn),
.key_in (key_in),
.key_out (key_out)
);
endmodule
波形仿真
如图,tb文件中假设key_in
按下后(1→0)进行了100次抖动,每次抖动时长随机在00.06ms。而在按下后,key_out
并未立即跳变到0,而是在最后一次抖动后的20ms,才输出低电平。松开按键时同理。
4 上板验证
验证思路
将按下按键、松开按键视为一次完整的按键操作。一次操作中,消抖前的信号会发生多次抖动,有多个电平变化;消抖后的信号仅两次电平变化(1→0→1)。因此可统计一次操作中电平由高变为低的次数,以验证按键消抖是否有效。
将按键与数码管相关联,数码管初始显示0000,每当检测到下降沿信号后显示加1。一次按键操作后,消抖后的信号正常加1;消抖前的信号由于存在连续抖动,数码管显示有可能会加若干次。
| Port name | Direction | Type | Description |
| :-------: | :-------: | :-------: | :---------: |
| clk_24m | input | wire | 24MHz时钟 |
| rstn | input | wire | 复位键 |
| key_in | input | wire | 按下/松开 |
| sm_seg | output | wire[7:0] | 数码管段选 |
| sm_bit | output | wire[3:0] | 数码管位选 |
上板验证代码设计
数码管计数器模块counter.v
module counter(
input wire clk_24m,
input wire rstn,
input wire key_in,
output wire[15:0] num_out
);
//计数器
reg [3:0] sm_bit1_num;
reg [3:0] sm_bit2_num;
reg [3:0] sm_bit3_num;
reg [3:0] sm_bit4_num;
//——————————————————————————————按键消抖后的的下降沿——————————————————————————————
reg key_in_r;
wire neg_edge;
assign neg_edge =(~key_in & key_in_r);
always @(posedge clk_24m or negedge rstn) begin
if(~rstn)
key_in_r <=0;
else
key_in_r <=key_in;
end
reg flag;
always @(posedge clk_24m or negedge rstn) begin
if(~rstn)
flag <=0;
else if(neg_edge)
flag <=1;
else
flag <=0;
end
//——————————————————————————————各位的计数逻辑——————————————————————————————
assign num_out ={sm_bit4_num, sm_bit3_num, sm_bit2_num, sm_bit1_num};
//个位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit1_num <=0;
else if(flag) begin
if(sm_bit1_num==9)
sm_bit1_num <=0;
else
sm_bit1_num <=sm_bit1_num + 1'b1;
end
//else保持,省略
end
//十位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit2_num <=0;
else if(flag && sm_bit1_num==9) begin //个位 //必须要flag
if(sm_bit2_num==9)
sm_bit2_num <=0;
else
sm_bit2_num <=sm_bit2_num + 1'b1;
end
//else保持,省略
end
//百位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit3_num <=0;
else if(flag && sm_bit2_num==9 && sm_bit1_num==9) begin
if(sm_bit3_num==9)
sm_bit3_num <=0;
else
sm_bit3_num <=sm_bit3_num + 1'b1;
end
//else保持,省略
end
//千位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit4_num <=0;
else if(flag && sm_bit3_num==9 && sm_bit2_num==9 && sm_bit1_num==9) begin
if(sm_bit4_num==9)
sm_bit4_num <=0;
else
sm_bit4_num <=sm_bit4_num + 1'b1;
end
//else保持,省略
end
endmodule
数码管驱动模块seg_driver.v
module seg_driver
(
input wire clk_24m,
input wire rstn,
input wire [15:0] num_out,
output wire [7:0] sm_seg, //数码管段选
output wire [3:0] sm_bit //数码管位选
);
reg [15:0] cnt_w;
reg [3:0] sm_seg_num; //具体数字
reg [7:0] sm_seg_reg; //数码管段选,由具体数字译码而来
reg [3:0] sm_bit_reg; //数码管位选
reg dp;
localparam S0 = 0 ,
S1 = 1 ,
S2 = 2 ,
S3 = 3 ,
S4 = 4 ,
S5 = 5 ,
S6 = 6 ,
S7 = 7 ,
S8 = 8 ,
S9 = 9 ;
//———————————————————————————————————扫描计数器——————————————————————————————————
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt_w <=0;
else if(&cnt_w) //按位与,即计数到最大,16位都是1
cnt_w <=0;
else
cnt_w <=cnt_w + 1'b1;
end
//———————————————————————————————————位选与段选——————————————————————————————————
//sm_seg_num用于显示具体的数据
always@(posedge clk_24m or negedge rstn) begin
if(~rstn) begin //这两行自定义复位时哪几个会亮,亮的显示什么
sm_bit_reg <=4'b1111;
sm_seg_num <=0;
end
else begin
case( cnt_w[15:14] )
2'b00: begin //最高两位为00,则点亮第一个管子,且具体数字由sm_bit_num决定
sm_bit_reg <=4'b1110;
sm_seg_num <=num_out[3:0];
end
2'b01: begin
sm_bit_reg <=4'b1101;
sm_seg_num <=num_out[7:4];
end
2'b10: begin
sm_bit_reg <=4'b1011;
sm_seg_num <=num_out[11:8];
end
2'b11: begin
sm_bit_reg <=4'b0111;
sm_seg_num <=num_out[15:12];
end
endcase
end
end
//段选译码,控制每一位的显示数字
always@(*) begin //共阳极数码管,最高位是小数点,低七位是七段管。0为亮,1为暗。
case ( sm_seg_num )
S0: sm_seg_reg =8'b1100_0000;
S1: sm_seg_reg =8'b1111_1001;
S2: sm_seg_reg =8'b1010_0100;
S3: sm_seg_reg =8'b1011_0000;
S4: sm_seg_reg =8'b1001_1001;
S5: sm_seg_reg =8'b1001_0010;
S6: sm_seg_reg =8'b1000_0010;
S7: sm_seg_reg =8'b1111_1000;
S8: sm_seg_reg =8'b1000_0000;
S9: sm_seg_reg =8'b1001_0000;
default:sm_seg_reg =8'b1100_0000;
endcase
end
assign sm_seg =sm_seg_reg;
assign sm_bit =sm_bit_reg;
endmodule
数码管顶层模块top_key_filter.v
module top_key_filter(
input wire clk_24m,
input wire rstn,
input wire key_in,
output wire[7:0] sm_seg, //数码管段选
output wire[3:0] sm_bit //数码管位选
);
wire [15:0] num_out; //0~9,10个数,需要4bit来存储。现在有四个数码管,需要16bit
wire key_out;
counter counter_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out),
.key_in (key_out)
// .key_in (key_in)
);
seg_driver seg_driver_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out),
.sm_seg (sm_seg),
.sm_bit (sm_bit)
);
key_filter key_filter_inst (
.clk (clk_24m),
.rstn (rstn),
.key_in (key_in),
.key_out (key_out)
);
endmodule
引脚分配文件key_filter.adc
# 时钟
set_pin_assignment { clk_24m } { LOCATION = K14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
# 复位
set_pin_assignment { rstn } { LOCATION = G11; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
# 按下
set_pin_assignment { key_in } { LOCATION = H14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
# 段选与位选
set_pin_assignment { sm_bit[0] } { LOCATION = B1; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[1] } { LOCATION = C3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[2] } { LOCATION = C2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[3] } { LOCATION = F3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[0] } { LOCATION = E3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[1] } { LOCATION = B3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[2] } { LOCATION = F4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[3] } { LOCATION = E4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[4] } { LOCATION = F5; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[5] } { LOCATION = D3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[6] } { LOCATION = B2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[7] } { LOCATION = A2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
烧录至开发板
在TD5.6.5中新建工程文件,导入设计文件key_filter.v
、counter.v
、seg_driver.v
、top_key_filter.v
,导入引脚文件key_filter.adc
,编译后将生成的比特流下载到开发板中。具体步骤可参考基于TD5.6.5与vscode的FPGA简易开发教程——以多路选择器为例。
上板验证:按键消抖后
按下按键,数码管显示增加1,表明按键消抖后的输出信号只检测到一次下降沿,即信号无抖动。连续快速点击时数码管显示每次只增加1,表明无粘连。
上板验证:按键消抖前
作为对比,需要验证无按键消抖的程序:在top_key_filter.v
中,注释掉整个key_filter
的例化程序,且将counter
例化中的.key_in(key_out)
修改为.key_in(key_in)
,即让输入按键直接连接给数码管。
//`top_key_filter.v`
……
counter counter_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out),
// .key_in (key_out) //按键消抖后的
.key_in (key_in) //无按键消抖的
);
……
// key_filter key_filter_inst (
// .clk (clk_24m),
// .rstn (rstn),
// .key_in (key_in),
// .key_out (key_out)
// );
……
重新生成比特流后重复上述操作,发现点击产生了粘连,即单次点击可能会计数若干次而非一次,此现象在连续快速点击时尤为明显。