Fpga 學習筆記7(狀態(tài)機設計實例):因內容比較簡單,而且在這篇日志中也有相關的知識點,就不寫了。
該集主要知識點:
1、利用狀態(tài)機實現濾除物理按鍵所產生的抖動波形。
2、非阻塞賦值的巧妙運用
3、將狀態(tài)機與計數器功能組合使用
4、在仿真代碼中利用隨機數產生延時隨機的時間。
5、task 運用方式、以及將仿真測試代碼模塊化
按鍵抖動的現象與狀態(tài)機對應的狀態(tài):
一、源程序
/* 實驗名稱:按鍵消抖模塊設計與驗證
* 功能實現:濾除按鍵抖動的波形
*/
`define DEC_TIME_CNT ((20 * 1000 * 1000) / 20 - 1)
module mytest(clk, rst_n, key_in, key_flag, key_state);
input clk, rst_n, key_in;
output reg key_flag, key_state;
reg[3:0] state; // 狀態(tài)機狀態(tài)
reg key_old, key_cur; // key狀態(tài)
wire pedge, nedge; // 邊沿狀態(tài)
// 50MHz的時鐘 = 1/50M = 0.02us = 20ns
// 20ms = 20_000_000ns / 20ns = 1000_000
reg[19:0] time_cnt; // 計數器計數值
reg time_full; // 計數器已經達到指定的時間
reg time_en; // 計數器使能
localparam // 狀態(tài)機幾種狀態(tài)的標志
IDLE = 4'b0001, // 空閑,即高電平狀態(tài),為按下狀態(tài)
DING = 4'b0010, // 按下時抖動狀態(tài)
DOWN = 4'b0100, // 可以確定是處于按下狀態(tài)而不是抖動狀態(tài)
UING = 4'b1000; // 彈起時抖動狀態(tài)
// 邊沿檢測
always@(posedge clk, negedge rst_n)
if(!rst_n)begin
key_cur <= 1'b0;
key_old <= 1'b0;
end
else
begin
key_cur <= key_in; // 這里由于采用了非阻塞賦值
key_old <= key_cur; // 所以在同一個時鐘內 key_old 采樣 key_curr 的是舊的值
end
// 判斷上升沿、下降沿
assign pedge = !key_old & key_cur; // 原來為低電平,現在為高電平,則表示檢測到上升沿
assign nedge = key_old & !key_cur; // 原來為高電平,現在為低電平,則表示檢測到下降沿
// 計數功能
always@(posedge clk, negedge rst_n)
if(!rst_n)begin
time_cnt <= 20'd0;
// time_en <= 1'b0; // 這里不能再次賦值,因為在狀態(tài)機的程序塊中需要對該信號賦值
// 一個信號不能在多個 always 塊中賦值
end
else if(time_en)
time_cnt <= time_cnt + 1'b1;
else
time_cnt <= 20'd0;
// 檢測時間是否已經到了 這里指定 20ms 的時間
always@(posedge clk, negedge rst_n)
if(!rst_n)
time_full <= 1'b0;
else if(time_cnt == `DEC_TIME_CNT)
time_full <= 1'b1;
else
time_full <= 1'b0;
// 狀態(tài)機
always@(posedge clk, negedge rst_n)
if(!rst_n)begin
state <= IDLE;
key_flag <= 1'b0;
key_state <= 1'b1;
end
else begin
case(state)
IDLE: begin // 空閑狀態(tài):按鍵沒有被按下
key_flag <= 1'b0; // 在空閑狀態(tài) 按鍵需要清零
if(nedge) begin // 檢測到下降沿
state <= DING; // 設置狀態(tài)為 DING,下個時鐘上升沿將會進入另外一個分支
time_en <= 1'b1; // 啟動定時器
end
else
state <= IDLE; // 依據是高電平,設置狀態(tài)為 DING
end
DING: begin // 濾波抖動,按下時產生的抖動狀態(tài)
if(time_full) begin // 如果指定的時間內沒有上升沿
key_flag <= 1'b1; // 則表示處于穩(wěn)定狀態(tài),進入按下狀態(tài)
key_state <= 1'b0; // 表示按下
state <= DOWN; // 設置狀態(tài)為按下
time_en <= 1'b0; // 關閉定時器
end
else if(pedge) begin // 如果指定的時間內出現上升沿說明是處于抖動狀態(tài)
state <= IDLE; // 重新設置為空閑狀態(tài)
time_en = 1'b0; // 并且關閉定時器
end
else // 時間未到,但也沒有出現電平變化則進行維持此狀態(tài)
state <= DING;
end
DOWN: begin // 按鍵按下狀態(tài):此時經過濾波之后處于按下狀態(tài)
key_flag <= 1'b0; // 將標志位清0
if(pedge) begin // 如果出現上升沿則表示要彈起
state <= UING;
time_en = 1'b1; // 啟動定時器
end
else // 如果沒有出現上升沿則維持此狀態(tài)
state <= DOWN;
end
UING: begin
if(time_full)begin // 到達指定時間
key_flag <= 1'b1; // 設置按鍵標志
key_state <= 1'b1; // 設置按鍵狀態(tài)為彈起狀態(tài)
state <= IDLE; // 將狀態(tài)設置為空閑
time_en <= 1'b0; // 關閉定時器
end
else if(nedge)begin // 如果出現下降沿
time_en <= 1'b0; // 關閉定時器
state <= DOWN; // 仍設置為按下狀態(tài),即跳回之前的狀態(tài)
end
else
state <= UING; // 如果時間未到并且未出現電平變化則維持此狀態(tài)
end
default: begin // 如果出現其他狀態(tài),意味著被干擾出現錯誤的狀態(tài)
time_en <= 1'b0; // 關閉定時器
state <= IDLE;
key_flag <= 1'b0; // 設置按鍵標志
key_state <= 1'b1; // 設置按鍵狀態(tài)為彈起狀態(tài)
end
endcase
end
endmodule
源碼中的知識點:
1、一個信號不允許在兩個或兩個以上的always 程序塊中被賦值
2、利用非阻塞賦值語句實現識別上升沿下降沿
// 邊沿檢測 …
always@(posedge clk, negedge rst_n)
// 代碼省略..
begin
key_cur <= key_in;
key_old <= key_cur;
end
// 判斷上升沿、下降沿
assign pedge = !key_old & key_cur;
assign nedge = key_old & !key_cur;
3、狀態(tài)機的使用
二、RTL視圖(黃色是狀態(tài)機模塊)

三、狀態(tài)機

從源碼和圖中可以看到狀態(tài)機實際上就是利用多個標志位去對應多種狀態(tài),按照我們的邏輯進行組合,每一種狀態(tài)對應一系列行為,每一種狀態(tài)都依賴前一種狀態(tài)的變化,從而實現模擬順序執(zhí)行的邏輯。
四、仿真測試代碼
/* 實驗名稱:按鍵消抖模塊的驗證
* 功能實現:驗證按鍵消抖模塊是否符合設計要求
*/
`timescale 1ns/1ns
`define clock_period 20
module mytest_tb;
reg clk, rst_n, key_in;
wire key_flag, key_state;
mytest u1(clk, rst_n, key_in, key_flag, key_state);
initial clk = 1;
always #(`clock_period / 2) clk = ~clk;
initial begin
rst_n = 1'b0;
key_in = 1'b1;
#(`clock_period * 10) rst_n = 1'b1;
// 延時10時鐘周期,再加1ns 錯開完整的時鐘這樣可以更加真實的模擬
#(`clock_period * 10 + 1);
key_Event; // 模擬按鍵事件
#10000;
key_Event; // 模擬按鍵事件
#10000;
key_Event; // 模擬按鍵事件
#10000;
key_Event; // 模擬按鍵事件
#10000;
key_Event; // 模擬按鍵事件
#10000;
$stop;
end
reg[15:0] myrand;
// 按鍵事件
task key_Event;
begin
key_down; // 按鍵按下
key_up; // 按鍵彈起
end
endtask
// 按鍵按下
task key_down;
begin
repeat(50) begin // 模擬按下時的抖動
myrand = {$random} % 65536; // 產生 0 ~ 65535 隨機數
//myrand = $random % 65536; // 產生 -65535 ~ 65535 隨機數
#myrand key_in = ~key_in;
end
key_in = 0;
#50000000;
end
endtask
// 按鍵彈起
task key_up;
begin
repeat(50) begin // 模擬彈起時的抖動
myrand = {$random} % 65536; // 產生 0 ~ 65535 隨機數
//myrand = $random} % 65536; // 產生 -65535 ~ 65535 隨機數
#myrand key_in = ~key_in;
end
key_in = 1;
#50000000;
end
endtask
endmodule
源碼中的知識點:
1、隨機數的使用
reg[15:0] myrand;
myrand = {$random} % 65536; // 產生 0 ~ 65535 隨機數
//myrand = $random % 65536; // 產生 -65535 ~ 65535 隨機數
2、task的使用方法,task沒有返回值。
3、初始化延時時,不延時一個完整的時鐘周期可以更加真實的模擬實際電路的波形
initial begin
rst_n = 1'b0;
key_in = 1'b1;
#(`clock_period * 10) rst_n = 1'b1;
// 延時10時鐘周期,再加1ns 錯開完整的時鐘這樣可以更加真實的模擬
#(`clock_period * 10 + 1);
五、波形圖

六、波形分析
全局分析

局部分析

七、仿真測試代碼模塊化
模擬手動按下和釋放按鍵的仿真測試模塊(這里我對代碼做了一些修改)
1、模擬按鍵的模塊代碼
/* 模塊名稱:模擬物理按鍵按下釋放
* 功能描述:利用 random 產生隨機數來產生隨機的時間模擬按鍵抖動
* 端口描述:
* i_key_in: 輸入,連接被測試模塊所檢測的按鍵信號
* o_key_end: 輸出,1:啟動模擬 0:模擬完成
*/
`timescale 1ns/1ns
`define clock_period 20
// i_key_in:按鍵信號輸入 o_key_end:啟動結束標志
module key_module_tb(i_key_in, o_key_end);
output reg i_key_in;
output reg o_key_end;
initial begin
key_start;
i_key_in = 1'b1;
key_Event;
#10000;
key_Event;
#10000;
key_Event;
#10000;
key_stop;
end
task key_start;
o_key_end <= 1'b1; // 修改端口狀態(tài)來通知其他模塊啟動測試
endtask
task key_stop;
o_key_end <= 1'b0; // 修改端口狀態(tài)來通知其他模塊測試結束
endtask
reg[15:0] myrand;
task key_Event;
begin
key_down; // 按鍵按下
key_up; // 按鍵彈起
end
endtask
task key_down;
begin
repeat(50) begin // 模擬按下時的抖動
myrand = {$random} % 65536; // 產生 0 ~ 65535 隨機數
//myrand = $random} % 65536; // 產生 -65535 ~ 65535 隨機數
#myrand i_key_in = ~i_key_in;
end
i_key_in = 0;
#50000000;
end
endtask
task key_up;
begin
repeat(50) begin // 模擬彈起時的抖動
myrand = {$random} % 65536; // 產生 0 ~ 65535 隨機數
//myrand = $random} % 65536; // 產生 -65535 ~ 65535 隨機數
#myrand i_key_in = ~i_key_in;
end
i_key_in = 1;
#50000000;
end
endtask
endmodule
該仿真代碼并不是完全照搬視頻中的代碼,我覺得既然要將仿真代碼模塊化,那么就不應該在模塊里面調用 $stop 去終止仿真執(zhí)行。這樣頂層仿真模塊,調用這些子模塊而不會因為子模塊調用 $stop 終止仿真,導致之后的仿真無法繼續(xù)。為此我利用自己目前所掌握的知識做了一點改動,增加了輸出端口,這個端口是為了標識仿真模塊啟動和停止的狀態(tài)。在頂層模塊中只需要通過always檢測下降沿就能得知模塊什么時候結束。
2、頂層仿真測試代碼
/* 實驗名稱:仿真代碼模塊化的驗證
* 功能實現:驗證按鍵消抖模塊是否符合設計要求
*/
`timescale 1ns/1ns
`define clock_period 20
module mytest_tb;
reg clk, rst_n;
wire key_flag, key_state;
wire key_in, key_end;
mytest u1(clk, rst_n, key_in, key_flag, key_state);
key_module_tb key(key_in, key_end);
initial clk = 1;
always #(`clock_period / 2) clk = ~clk;
initial begin
rst_n = 1'b0;
#(`clock_period * 10) rst_n = 1'b1;
// 延時10時鐘周期,再加1ns 錯開完整的時鐘這樣可以更加真實的模擬
#(`clock_period * 10 + 1);
end
always@(negedge key_end)
$stop; // 檢測到模擬按鍵的模塊完成模擬,所以停止仿真
endmodule
3、波形圖
