芯航線——普利斯隊長精心奉獻
實驗目的: 1.復習狀態機的設計思想並以此為基礎實現按鍵消抖
2.單bit異步信號同步化以及邊沿檢測
3.在激勵文件中學會使用隨機數發生函數$random
4.仿真模型的概念
實驗平台:芯航線FPGA核心板
實驗原理:
按鍵在電子設計中使用的最多,從復位到控制設置均可以看到其身影。現在按鍵的功能也種類也越來越多,例如多向按鍵、自鎖按鍵、薄膜按鍵等。普通按鍵其硬件示意圖如圖9-1所示。
圖9-1 按鍵示意圖
芯航線開發板所載的為兩腳貼片按鍵,分別位於開發板正面的左下角以及右下角,原理圖如圖9-2所示。按鍵不按下時IO口為高電平,按鍵按下時則變為電平,因此系統即可通過檢測IO的電平來判斷按鍵的狀態。
圖9-2 按鍵原理圖
從圖9-1中可以看到按鍵存在一個反作用彈簧,因此當按下或者松開時均會產生額外的物理抖動,然而物理抖動便會產生電平的抖動,總的來說,按鍵從按下再到松開的過程,若檢測其電平變化,即如圖9-4所示。
圖9-4 按鍵從按下到松開的電平變化
圖9-4中產生的抖動次數以及間隔時間均是不可預期的,這就需要濾波來消除抖動過程中可能造成的影響。一般情況抖動的總時間會持續20ms以內。這種抖動,可以通過硬件電路或者邏輯設計的方式來消除,其中硬件電路消除的電路圖之一如圖9-5所示。
圖9-5 利用RS觸發器進行硬件消抖
圖9-5中兩個"與非"門構成一個RS觸發器。此時即使B點的電壓波形是抖動的,但經雙穩態電路之后,其輸出為正規的矩形波。當按鍵未按下時,輸出為1;當鍵按下時,輸出為0。
此外還可以通過利用電容器和電阻對噪聲波形積分,吸收因觸點跳動產生的噪聲。也可以通過555定時器組成的單穩態觸發器同樣可以消除開關的抖動。這兩種電路此處不再詳述。
對於FPGA通常使用狀態機來進行消抖設計,在圖9-4中可看出若按照第08講的狀態機概念對其進行狀態編碼即存在以下狀態:未按下時空閑狀態IDLE、按下抖動濾除狀態FILTER0、按下穩定狀態DOWN以及釋放抖動濾除狀態FILTER1。若對其進行獨熱碼編碼分別為0001,0010,0100以及1000。其狀態轉換圖如圖9-6所示。因此其為一個米利型狀態機,輸出與輸入相關。
圖9-6 狀態轉換圖
其狀態轉移條件如表9-1所示。
當前狀態 |
下一狀態 |
轉移條件 |
IDLE |
FILTER0 |
nedge |
FILTER0 |
IDLE |
pedge |
FILTER0 |
DOWN |
Cnt_full |
DOWN |
FILTER1 |
Pedge |
FILTER1 |
DOWN |
nedge |
FILTER1 |
IDLE |
Cnt_full |
表9-1 狀態轉移條件
實驗步驟:
新建一個以名為key_filter的工程保存在prj下,並在本工程目錄的rtl文件夾下新建verilog file文件並以key_filter.v保存。
其模塊接口如圖9-7所示,即可得出如下的接口聲明。
圖9-7 按鍵消抖模塊接口
input Clk; input Rst_n; input key_in;
output reg key_flag; output reg key_state; |
這里的按鍵輸入信號key_in相對於FPGA內部信號來說是一個異步信號,這里就需要對其使用兩級同步D觸發器進行進行異步信號的同步化,同步后的信號為key_in_sb。
reg key_in_sa,key_in_sb; always@(posedge Clk or negedge Rst_n) if(!Rst_n)begin key_in_sa <= 1'b0; key_in_sb <= 1'b0; end else begin key_in_sa <= key_in; key_in_sb <= key_in_sa; end |
由實驗原理中的狀態轉移表可以看出其轉換條件中需要檢測到下降沿以及上升沿,而邊沿檢測其原理就是利用兩級寄存器寄存的不同值來進行比較判斷,如圖9-8所示。
圖9-8 邊沿檢測原理圖
其檢測過程,可以假設data_in從0變1,也就是上升沿:
第一個時鍾到來第一個寄存器regc的輸出為0;
第二個時鍾沿到來后第一個寄存器輸出為1,第二個寄存器輸出此時為0,這樣對兩個寄存器輸出進行相關組合邏輯運算則可檢測出,
同理data_in從1變為0,也就是下降沿:
第一個時鍾到來第一個寄存器regc的輸出為1;
第二個時鍾沿到來后第一個寄存器輸出為0,第二個寄存器輸出此時為1。
本部分邏輯設計如下,這樣就實現了當有上升沿時信號pedge就會產生一個時鍾周期的高電平,當有下降沿時信號nedge也會產生一個時鍾周期的高電平,沒有上升沿或者下降沿變化時pedge以及nedge保持低電平狀態。 這里使用的"!"是邏輯非運算,對0110邏輯非運算后是0000;而"~"是按位取反,對0110按位取反后是1001。
reg key_tmpa,key_tmpb; wire pedge,nedge; always@(posedge Clk or negedge Rst_n) if(!Rst_n)begin key_tmpa <= 1'b0; key_tmpb <= 1'b0; end else begin key_tmpa <= key_in_sb; key_tmpb <= key_tmpa; end
assign nedge = !key_tmpa & key_tmpb; assign pedge = key_tmpa & (!key_tmpb); |
還應有20ms計數器模塊以及計數器使能模塊,這里也可以合並成一個always模塊。一般還是推薦一個always塊只對一個信號進行操作。
reg [19:0]cnt; reg en_cnt; //使能計數寄存器 //計數使能模塊 always@(posedge Clk or negedge Rst_n) if(!Rst_n) cnt <= 20'd0; else if(en_cnt) cnt <= cnt + 1'b1; else cnt <= 20'd0; //計數模塊 always@(posedge Clk or negedge Rst_n) if(!Rst_n) cnt_full <= 1'b0; else if(cnt == 999_999) cnt_full <= 1'b1; else cnt_full <= 1'b0; |
現在開始狀態機設計,首先用本地參數化定義來定義其狀態機。
localparam IDEL = 4'b0001, FILTER0 = 4'b0010, DOWN = 4'b0100, FILTER1 = 4'b1000; |
由於狀態以及判斷條件較少,此處先用一段式狀態機來進行描述。當復位時候將計數器清零,狀態回到IDLE,key_flag與key_state也回到初始態。
reg [3:0]state; always@(posedge Clk or negedge Rst_n) if(!Rst_n)begin en_cnt <= 1'b0; state <= IDEL; key_flag <= 1'b0; key_state <= 1'b1; end else begin case(state) //。。。。。。。。 default: begin state <= IDEL; en_cnt <= 1'b0; key_flag <= 1'b0; key_state <= 1'b1; end
endcase end |
在未按下時空閑狀態IDLE時,如果檢測到下降沿則狀態進入按下抖動濾除狀態FILTER0並使能計數器,否則繼續保持IDLE狀態。
IDEL : begin key_flag <= 1'b0; if(nedge)begin state <= FILTER0; en_cnt <= 1'b1; end else state <= IDEL; end |
當在FILTER0狀態時,如果20ms尚未計時結束就有上升沿到來,則認為此時還是按鍵按下抖動過程,狀態回到IDLE並清0計數器。按下過程中當最后一次抖動后不會存在上升沿,計數器則可以一直計數,計數滿后則將key_flag置1、key_state置0、狀態進入按下穩定狀態DOWN並將計數器清0。這樣就可以通過判斷key_flag && !key_state來確定按鍵的狀態,為1則按下。
FILTER0: if(cnt_full)begin key_flag <= 1'b1; key_state <= 1'b0; en_cnt <= 1'b0; state <= DOWN; end else if(pedge)begin state <= IDEL; en_cnt <= 1'b0; end else state <= FILTER0; |
進入DOWM狀態后將key_flag清0,如果檢測到上升沿則進入釋放抖動濾除狀態FILTER1,否則保持當前態。
DOWN: begin key_flag <= 1'b0; if(pedge)begin state <= FILTER1; en_cnt <= 1'b1; end else state <= DOWN; end |
進入FILTER1狀態后,如果20ms計數尚未結束就檢測到下降沿,則認為此時還是按鍵釋放抖動過程,狀態回到DOWN並清0計數器。釋放過程中當最后一次抖動后不會存在下降沿,計數器則可以一直計數,計數滿后則將key_flag與key_state均置1、狀態進入IDLE並將計數器清0等待下一次按鍵被按下。
FILTER1: if(cnt_full)begin key_flag <= 1'b1; key_state <= 1'b1; state <= IDEL; en_cnt <= 1'b0; end else if(nedge)begin en_cnt <= 1'b0; state <= DOWN; end else state <= FILTER1; |
這里如果改寫為兩段式,則如下所示,狀態轉移部分省略。
always@(posedge Clk or negedge Rst_n) if(!Rst_n) begin state <= IDLE;end else state <= nx_state;
always@(state or key_in or key_flag or key_state) begin en_cnt = 1'b0; key_flag = 1'b0; key_state = 1'b1; case(state) ……………. end |
進行分析和綜合直至沒有錯誤以及警告。
為了測試仿真編寫測試激勵文件,新建key_filter_tb.v文件保存到testbench文件夾下,本激勵文件除產生正常的時鍾以及復位信號外,還人為模擬了按鍵從按下到松手釋放的過程,人為產生的抖動之一如下所示,復制幾次並在合理范圍內修改參數。再次進行分析和綜合直至沒有錯誤以及警告。
//模擬按下抖動20ms內 key_in = 0;#1000; key_in = 1;#2000; key_in = 0;#1400; key_in = 1;#2600; key_in = 0;#1300; key_in = 1;#200; //產生一個低電平大於20ms key_in = 0;#20_000_100; #50_000_000; //模擬釋放抖動20ms內 key_in = 1;#2000; key_in = 0;#1000; key_in = 1;#2000; key_in = 0;#1400; key_in = 1;#2600; key_in = 0;#1300; key_in = 1;#200; //產生一個高電平大於20ms key_in = 1;#50_000_100; #30000; |
設置好仿真腳本后進行功能仿真,可以看到產生的按下抖動以及松開抖動如圖9-9所示。
圖9-9-1按下抖動
圖9-9-2釋放抖動
其局部仿真圖如圖9-10所示,可以看出當key_in從正常態到按下穩定后狀態經歷了0001、 0010、0100三個狀態,從按下到釋放經歷了0100、1000、0001三個狀態,且按下穩定以及釋放穩定均會產生一個時鍾周期的key_flag高電平信號,key_state也會正常變化。
圖9-10按鍵消抖仿真波形圖
從圖9-9可以看出是人為的設計了一些抖動,不具有隨機性且編寫出來的文件太長,因此繼續采用隨機數發生函數來產生抖動。
$random這一系統函數可以產生一個有符號的32bit隨機整數。一般的用法是$random%b,其中b>0。這樣就會生成一個范圍在(-b+1):(b-1)中的隨機數。如果只想要正數的隨機數即可采用{$random}%b來產生。在工程中需要產生在20ms的按下與松手抖動,道理上應該產生20_000_000以內隨機數的抖動,這里為了節約仿真時間,只產生一個16位的隨機數也就是0到65535。
在人為產生抖動時復制多次抖動后可以看出激勵文件比較長,這里采用使用任務task,其語法如下:
task <任務名>; <端口及數據類型聲明語句> <語句1> <語句2> <語句n> endtask |
任務的調用的語法如下:
<任務名>(端口1,端口2,...端口n); |
綜合以上兩點,編寫出以下設計文件,這里實現了五十次的0-65535ns按下抖動然后key_in賦固定值0且延時50ms(大於20ms即為穩定),同時也實現了釋放抖動后key_in賦固定值1且延時50ms。
reg [15:0]myrand;
task press_key; begin repeat(50) begin myrand = {$random}%65536; #myrand key_in = ~key_in; end key_in = 0; #50_000_000;
repeat(50) begin myrand = {$random}%65536; #myrand key_in = ~key_in; end key_in = 1; #50_000_000; end endtask |
這樣在key_filter_tb.v中除了產生時鍾以及復位信號后只需調用這個任務即可。
initial begin Rst_n = 1'b0; key_in = 1'b1; #(`clk_period*10) Rst_n = 1'b1; #30000; press_key; #10000;
press_key; #10000;
press_key; #10000; $stop; end |
編譯無誤后再次啟動仿真,可以看到如圖9-11所示的仿真波形圖,按下穩定以及釋放穩定均會產生一個時鍾周期的key_flag高電平信號,key_state也會正常變化。這里放大產生的抖動過程可以看到每一個抖動時間均不一樣,這樣就成功模擬了隨機抖動過程。
圖9-11-1引入task后的仿真波形圖
圖9-11-2 隨機抖動過程
這里再提出一個仿真模型的概念,新建key_module.v保存到testbench文件夾下並輸入以下內容。
`timescale 1ns/1ns
module key_model(key);
output reg key;
reg [15:0]myrand;
initial begin key = 1'b1; press_key; #10000; press_key; #10000; press_key; $stop; end
task press_key; begin ******* end endtask
endmodule |
這樣在key_filter_tb中只調用這個仿真模型以及產生復位以及時鍾信號即可,更簡化了激勵文件。這樣key_in就是一個內部信號了,需要將reg型改為wire型。
key_model key_model(.key(key_in)); |
這樣整個激勵文件的內部結構即為圖9-12所示:
圖9-12 激勵文件內部結構
這里的仿真模型也是不可綜合,需要在設置腳本時需要額外添加進來,如圖9-13所示。
圖9-13 將仿真模型加入激勵文件
再次運行仿真,同樣可以看到圖9-14的仿真波形圖。
圖9-14 引入按鍵仿真模型后的仿真波形圖
這樣就完成了單個按鍵的消抖設計與驗證,且復習了狀態機相關設計思路以及仿真模型的概念與應用。