按鍵仿真的例程,仿照黑金開發板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也一樣。