按键仿真的例程,仿照黑金开发板03_key_detect_1例程,分为两个模块,分别是detect模块、delay模块,detect模块检测输入key_in信号的变化,delay模块负责延时去抖;
两个模块并不复杂,但是在test bench上仿真花费相当多的时间,波形图一直不是我想要的输出类型,折腾了一天,决定先下载到开发板上看看效果,结果效果却是我想要的输出类型,这就更加让我郁闷了,怎么仿真都不行呢?
按说当时应该可以明确的一点就是程序本省应该没有多大的问题,但是当时并没有意识到这里,这点以后还是要加强啊。随后我又多次查看的程序本身,并单独对detect模块进行仿真测试,波形是正确的,delay模块的波形始终有问题,经过分析,我将原来的test bench文件又看了看,并对应波形比较了比较,发现问题出现仿真的周期上。看下test bench文件,
`timescale 10ns / 1ps
module key_tf;
// Inputs
reg clk;
reg rst;
reg key_in;
parameter T = 5'd20;
parameter TMS = 21'd1500_000; //15ms
// Outputs
wire LED;
// Instantiate the Unit Under Test (UUT)
key uut (
.clk(clk),
.rst(rst),
.key_in(key_in),
.LED(LED)
);
always begin
clk = 1;
# (T/2);
clk = 0;
# (T/2);
end
always begin
key_in = 1;
#(TMS);
key_in = 0;
#(TMS*10);
end
initial begin
// Initialize Inputs
clk = 0;
rst = 1;
key_in = 0;
// Wait 100 ns for global reset to finish
#100;
rst = 0;
// Add stimulus here
end
endmodule
其实这里没什么东西,很简单的语句,就是自己在clk这里犯了浑,搞错了,而detect模块中基本上算是没有用到clk,所以delay模块出现问题时没有及时的找到问题的根源,我们知道test bench文件中已经设定clk的周期,这是我们通过always文件自己定义的,现在仿真的周期是10ns,clk的周期是我们设定的T = 20*10 = 200ns,而我却把10ns作为了我delay模块的时钟周期,所以造成了波形时钟延后很长一段时间,造成逻辑因此出现错误,在这个仿真周期上我算是栽了一个跟头,今后一定要引以为戒。把周期调整之后,波形就正确了,但是有一定的延时,估计是布线或者其他原因引起的问题,我暂时还没有学习到这一块,加油,继续FPGA之旅。
看了许久的按键消除抖动的例程,特权的、黑金的,对于按键的消抖有了一些了解,首先先说说我们单片机通常的消抖是怎样做的,read按键值,如果按键被按下,延时10ms,再次读取按键值,如果按键确实被按下,则确认按键动作执行。这种按键去抖的方式有时候并不是特别管用,可以采用按键按下,然后抬起作为一次按键操作,执行流程是如下程序,
uint8_t keydata = 0;
static uint8_t keyup = 0;
if(keyup && (keydata ^ 0xf0))
{
keyup = 0;
//延时10ms
Delay(10);
//读取按键值
keydata = GPIO_ReadInputPin(GPIOG, (GPIO_Pin_TypeDef)(HR1|HR2|HR3));
if(keydata ^ 0xf0)
{
return keydata;
}
}
//无按键按下时至keyup标志位
else if((keydata ^ 0xf0) == 0)
{
keyup = 1;
return 0;
}
这个程序是我用的最多的按键去抖程序,他的优点很明显就是能够防止按键误动作,不会出现按一次按键出现多次按键值的情况,缺点就是按键按一次只能有一次按键值,不能做连续的按键值输出,比如说有需要按键按下按键值能够连续相加的情况。
FPGA的按键去抖与单片机的去抖就会有些不同,这里也需要对按键进行去抖,当然消抖必然会进行延时,毕竟这些抖动信号是我们不需要的,黑金的例程中按键消抖处理感觉有点小问题,这里先看按键去抖的例程:
去抖程序分为两个模块,detect_module.v为电平检查模块
module detect_module
(
CLK, RSTn, Pin_In, H2L_Sig, L2H_Sig
);
input CLK;
input RSTn;
input Pin_In;
output H2L_Sig;
output L2H_Sig;
/**********************************/
parameter T100US = 11'd4_999;//DB4CE15开发板使用的晶振为50MHz,50M*0.0001-1=4_999
/**********************************/
reg [10:0]Count1;
reg isEn;
always @ ( posedge CLK or negedge RSTn )
if( !RSTn )
begin
Count1 <= 11'd0;
isEn <= 1'b0;
end
else if( Count1 == T100US )
isEn <= 1'b1;
else
Count1 <= Count1 + 1'b1;
/********************************************/
reg H2L_F2;
reg L2H_F1;
reg L2H_F2;
always @ ( posedge CLK or negedge RSTn )
if( !RSTn )
begin
H2L_F1 <= 1'b1;
H2L_F2 <= 1'b1;
L2H_F1 <= 1'b0;
L2H_F2 <= 1'b0;
end
else
begin
H2L_F1 <= Pin_In;
H2L_F2 <= H2L_F1;
L2H_F1 <= Pin_In;
L2H_F2 <= L2H_F1;
end
/***********************************/
assign H2L_Sig = isEn ? ( H2L_F2 & !H2L_F1 ) : 1'b0;
assign L2H_Sig = isEn ? ( !L2H_F2 & L2H_F1 ) : 1'b0;
/***********************************/
Endmodule
这里先说这个isEn,因为电平检测模块是非常敏感,在复位的一瞬间,电平容易处于不稳定的状态,我们需要延迟 100us。 isEn = 1 寄存器是表示 100us 的延迟已经完成。H2L_F1, H2L_F2, L2H_F1, L2H_F2四个reg变量主要是用来检测电平变化的H2L_F2 & !H2L_F1这里是检测高电平到低电平的变化,也就是按键按下时电平会由高电平下降到低电平,这时信号为输出为高,一个时钟节拍之后信号输出为低电平(信号持续一个时钟周期),!L2H_F2 & L2H_F1这里是检测低电平到高电平的变化,也就是按键抬起时电平会由低电平上升至高电平,电平变化的信号有了,后面的delay_module.v模块就能够进行延时输出信号了。
module delay_module
(
CLK, RSTn, H2L_Sig, L2H_Sig, Pin_Out
);
input CLK;
input RSTn;
input H2L_Sig;
input L2H_Sig;
output Pin_Out;
/****************************************/
parameter T1MS = 16'd49_999;//DB4CE15开发板使用的晶振为50MHz,50M*0.001-1=49_999
/***************************************/
reg [15:0]Count1;
always @ ( posedge CLK or negedge RSTn )
if( !RSTn )
Count1 <= 16'd0;
else if( isCount && Count1 == T1MS )
Count1 <= 16'd0;
else if( isCount )
Count1 <= Count1 + 1'b1;
else if( !isCount )
Count1 <= 16'd0;
/****************************************/
reg [3:0]Count_MS;
always @ ( posedge CLK or negedge RSTn )
if( !RSTn )
Count_MS <= 4'd0;
else if( isCount && Count1 == T1MS )
Count_MS <= Count_MS + 1'b1;
else if( !isCount )
Count_MS <= 4'd0;
/******************************************/
reg isCount;
reg rPin_Out;
reg [1:0]i;
always @ ( posedge CLK or negedge RSTn )
if( !RSTn )
begin
isCount <= 1'b0;
rPin_Out <= 1'b0;
i <= 2'd0;
end
else
case ( i )
2'd0 :
if( H2L_Sig ) i <= 2'd1;
else if( L2H_Sig ) i <= 2'd2;
2'd1 :
if( Count_MS == 4'd10 ) begin isCount <= 1'b0; rPin_Out <= 1'b1;
i <= 2'd0; end
else
isCount <= 1'b1;
2'd2 :
if( Count_MS == 4'd10 ) begin isCount <= 1'b0; rPin_Out <= 1'b0;
i <= 2'd0; end
else isCount <= 1'b1;
endcase
/********************************************/
assign Pin_Out = rPin_Out;
/********************************************/
Endmodule
延时程序在接收到H2L_Sig和L2H_Sig时会跳转到相应的case条件,如果是H2L_Sig高到低信号产生,首先延时10ms,然后Pin_Out输出高电平,当接收到L2H_Sig低到高信号产生,同样首先延时10ms,然后Pin_Out输出低电平,这样程序就会在按键按下时点亮LED灯,按键抬起后熄灭LED灯,延时10ms达到消除抖动的目的。
这样的延时消抖是不是存在弊端?在延时之后并没有查看按键的当前值,如果出现误触发的情况,时间小于10ms,这样就会本来是误动作却引起输出为高电平的情况(L2H_Sig由于延时并不能响应),当然这是理论上的情况,实际中很难发生。
黑金的按键去抖程序总体而言是非常清晰,容易理解,也比较实用,接下来看看特权的按键消抖程序。
/说明:当三个独立按键的某一个被按下后,相应的LED被点亮;
// 再次按下后,LED熄灭,按键控制LED亮灭
module sw_debounce(
clk,rst_n,
sw1_n,sw2_n,sw3_n,
led_d1,led_d2,led_d3
);
input clk; //主时钟信号,50MHz
input rst_n; //复位信号,低有效
input sw1_n,sw2_n,sw3_n; //三个独立按键,低表示按下
output led_d1,led_d2,led_d3; //发光二极管,分别由按键控制
//---------------------------------------------------------------------------
reg[2:0] key_rst;
always @(posedge clk or negedge rst_n)
if (!rst_n) key_rst <= 3'b111;
else key_rst <= {sw3_n,sw2_n,sw1_n};
reg[2:0] key_rst_r;
always @ ( posedge clk or negedge rst_n )
if (!rst_n) key_rst_r <= 3'b111;
else key_rst_r <= key_rst;
//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期
wire[2:0] key_an = key_rst_r & ( ~key_rst);
//wire [2:0]key_an = key_rst_r ^ key_rst; // 注:也可以这样写
//---------------------------------------------------------------------------
reg[19:0] cnt; //计数寄存器
always @ (posedge clk or negedge rst_n)
if (!rst_n) cnt <= 20'd0; //异步复位
else if(key_an) cnt <=20'd0;
else cnt <= cnt + 1'b1;
reg[2:0] low_sw;
always @(posedge clk or negedge rst_n)
if (!rst_n) low_sw <= 3'b111;
else if (cnt == 20'hfffff) //满20ms,将按键值锁存到寄存器low_sw,cnt == 20'hfffff
low_sw <= {sw3_n,sw2_n,sw1_n};
//---------------------------------------------------------------------------
reg [2:0] low_sw_r; //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中
always @ ( posedge clk or negedge rst_n )
if (!rst_n) low_sw_r <= 3'b111;
else low_sw_r <= low_sw;
//当寄存器low_sw由1变为0时,led_ctrl的值变为高,维持一个时钟周期
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);
reg d1;
reg d2;
reg d3;
always @ (posedge clk or negedge rst_n)
if (!rst_n) begin
d1 <= 1'b0;
d2 <= 1'b0;
d3 <= 1'b0;
end
else begin //某个按键值变化时,LED将做亮灭翻转
if ( led_ctrl[0] ) d1 <= ~d1;
if ( led_ctrl[1] ) d2 <= ~d2;
if ( led_ctrl[2] ) d3 <= ~d3;
end
assign led_d3 = d1 ? 1'b1 : 1'b0; //LED翻转输出
assign led_d2 = d2 ? 1'b1 : 1'b0;
assign led_d1 = d3 ? 1'b1 : 1'b0;
endmodule
其实这段程序并没有什么比较难理解的逻辑,与黑金的程序有所不同,这里的特权的程序是确定按键按下之后产生一个信号,LED输出一个状态(黑金是两个状态),程序的执行顺序是,按键扫描到按键值由1变位0时,通过移位比较的方式得到电平变化的信号,信号触发计数器重新计数,当计数值为20ms时,读取按键值,再次进行移位比较,如果按键值由1变位0,则确实为按键按下,执行led信号输出(信号反转)。这段程序中我自己认为比较难以理解的地方,主要是两次移位比较,像黑金那样进行高到低,低到高的移位比较比较容易理解,这里只是进行了高到低的移位比较,并且进行了两次,因此理解起来可能有点不太习惯(我自己就是这样的)。
wire[2:0] key_an = key_rst_r & ( ~key_rst);
这里明显是1到0时key_an为高,这里与单个信号是操作不同的是key_rst用的是取反而不是取非操作,状态由1到0是按键刚按下的时刻,key_an输出为高才能进行下面的操作。
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);
这里与黑金的有所不同,黑金是对两种状态1到0和0到1,产生两种信号,而这里与上次产生的key_an信号都是由1到0产生,也上个信号存在一个20ms的时间差,这样就起到了延时去抖的目的,这里说明一点是,延时过程中low_sw信号是没有更新的,也就是说一旦key_an信号产生,low_sw信号20ms之后才能更新,这样就能够保证low_sw_r是20ms之前的,也就是未发生改变的按键值,而延时之后跟新的low_sw则是最新的值,这样就能够判断是否是1到0的信号变化了。led_ctrl信号为高就会产生led信号反转,也就是LED的亮或灭。也黑金比较的话这里能够真正起到延时去抖的作用,按键按下会产生一个触发信号。另外一点必须要说的的是这个延时else if (cnt == 20'hfffff)这里,20ms的延时时间不算长,在key_an为低电平时,这里的计数值也是在增加的,也是达到20'hfffff,这样low_sw也是在更新之中,这也是通常会忽视的地方,这里的更新能够很好的记录下按键未触发之前的状态,虽然不能保证很实时吧(毕竟20ms的更新速度),相对于按键触发来说很短暂的,如果将led_ctrl修改为由0到1的信号触发其实也能达到按下一次按键led点亮或者熄灭,理论与实践我都测试了,通过信号波形也看到了结果,确实可行,只不过是两个产生的原因是不同的,不是key_an激发的,是按键本身触发的,毕竟有20ms延时的采样。
按键去抖是一个简单的小程序,但是有时候一点改变影响的效果确实是不一样的,就像用C语言写的按键去抖,一个抬起有效的按键去抖方式就能很好解决按键抖动的问题,FPGA也一样。