FPGA設計中,最重要的設計思想就是狀態機的設計思想!狀態機的本質就是對具有邏輯順序和時序規律的事件的一種描述方法,它有三個要素:狀態、輸入、輸出:狀態也叫做狀態變量(比如可以用電機的不同轉速作爲狀態),輸出指在某一個狀態的特定輸出,輸入指狀態機中進入每個狀態的條件。根據狀態機的輸出是否和輸入有關,可分爲摩爾(Moore)型狀態機和米勒型(Mealy)狀態機:摩爾型狀態機的輸出只取決於當前狀態,而米勒型狀態機的輸出不僅取決於當前狀態,還與當前輸入有關。通常,我們描述狀態機有三種方法:狀態轉移圖、狀態轉移表、HDL描述,狀態轉移圖直觀,設計用,而HDL語言方便描述,實現時用。
那麼,如何用HDL描述一個好的狀態機呢?主要有以下四點:安全、穩定性高,速度快,面積小,設計清晰;在描述過程中,我們會引用兩個新的verilog語法:localparam描述參數(等價於parameter)以及用task/endtask將輸出功能塊封裝,增強代碼可讀性;描述狀態機的關鍵是要描述清楚狀態機的三大要素:如何進行狀態轉移?每個狀態的輸出?狀態輸出是否和輸入條件相關?通常有三種寫法,下面通過一個實例說明;
實例.檢測“Hello”序列狀態機
1、功能:在輸入一串字符中檢測“Hello”序列,檢測到後將led狀態進行翻轉;
2、根據設計的FSM狀態轉移圖(visio繪製):
3、一段式描述法:在一個always塊裏既描述狀態轉移,又描述狀態的輸入和輸出;
verilog代碼如下:
//檢測“Hello”後led狀態翻轉
module check_hello(
input clk, //50M時鐘信號
input rst, //低電平復位
input [7:0]asci, //字符輸入
output reg led //控制led
);
//狀態寄存器
reg [4:0]NS; //nextstate
//狀態獨熱編碼
localparam
CHECK_H = 5'b0_0001,
CHECK_e = 5'b0_0010,
CHECK_la = 5'b0_0100,
CHECK_lb = 5'b0_1000,
CHECK_o = 5'b1_0000;
//一段式狀態機
always@(posedge clk,negedge rst)
if(!rst)begin
NS <= CHECK_H;
led <= 1'b1; //led熄滅
end
else begin
case(NS)
CHECK_H:
begin
led <= 1'b1;
if(asci == "H")
NS <= CHECK_e;
else
NS <= CHECK_H;
end
CHECK_e:
begin
led <= 1'b1;
if(asci == "e")
NS <= CHECK_la;
else
NS <= CHECK_H;
end
CHECK_la:
begin
led <= 1'b1;
if(asci == "l")
NS <= CHECK_lb;
else
NS <= CHECK_H;
end
CHECK_lb:
begin
led <= 1'b1;
if(asci == "l")
NS <= CHECK_o;
else
NS <= CHECK_H;
end
CHECK_o:
begin
if(asci == "o")
led <= 1'b0;
else
led <= 1'b1;
NS <= CHECK_H;
end
//排除任意情況,增強FSM安全性
default:
begin
NS <= CHECK_H;
led <= 1'b1;
end
endcase
end
endmodule
testbench測試文件如下:
`timescale 1ns/1ps
`define clk_period 20
module check_hello_tb();
reg clk; //50M時鐘信號
reg rst; //低電平復位
reg [7:0]asci; //字符輸入
wire led; //控制led
//例化測試模塊
check_hello check_hello_test(
.clk(clk), //50M時鐘信號
.rst(rst), //低電平復位
.asci(asci), //字符輸入
.led(led) //控制led
);
//產生50M時鐘信號
initial clk = 1;
always #(`clk_period / 2)clk <= ~clk;
//開始測試
initial begin
rst = 0; //系統復位
asci = 3'bx;
#(`clk_period * 2);
rst = 1;
#(`clk_period);
asci = "H";
#(`clk_period);
asci = "e";
#(`clk_period);
asci = "l";
#(`clk_period);
asci = "l";
#(`clk_period);
asci = "o";
#(`clk_period);
asci = "2";
#(`clk_period);
asci = "e";
#(`clk_period);
asci = "h";
#(`clk_period);
asci = "l";
#(`clk_period);
$stop;
end
endmodule
測試結果如下,可以看到,剛開始輸入數據是任意數據,FSM爲CHECK_H狀態,led輸出高電平,保持熄滅;當檢測到Hello序列時,led輸出低電平,狀態翻轉;
該測試中隱藏了一個重要的問題,我們在編寫testbench的時候,將數據變化與時鐘上升沿對齊,但在實際中數據變化會產生滯後,所以這時候我們爲了更好地模擬實際情況,編寫testbench就有一定的技巧:將輸出變化延遲1-2ns,所以我們將testbench中的這行代碼進行修改:
initial begin
rst = 0; //系統復位
asci = 3'bx;
#(`clk_period * 2);
rst = 1;
#(`clk_period); //將這行修改爲 #(`clk_period + 1); 將整體數據改變時間較時鐘上升沿滯後1ns觀察
asci = "H";
然後再運行仿真觀察波形,可以看到,這次的波形更好地說明了實際情況,因爲我們編寫的一段式FSM整體在一個時序邏輯中,所以FSM只在時鐘上升沿檢測數據變化,也就是說,在第一個時鐘內發生的變化,下一個時鐘沿才能檢測到:
狀態轉移圖如下,可以看到按照預定設計執行:
綜合出來的電路圖如下,可以看到FSM實現的重點在於狀態寄存器,耗費資源很少,由綜合報告也可看出:
在一段式描述方法中可以看到,雖然一個alaways塊就可以解決問題,但描述不清晰,不利於維護修改,並且不利用附加約束,不利於綜合其和佈局佈線器對設計的優化;
4、兩段式描述法:一個always塊描述狀態轉移,另一個always塊描述狀態判斷轉移條件
verilog代碼如下:
//檢測“Hello”後led狀態翻轉
module check_hello(
input clk, //50M時鐘信號
input rst, //低電平復位
input [7:0]asci,//字符輸入
output reg led //控制led
);
//狀態寄存器
reg [4:0]NS; //nextstate
reg [4:0]CS; //currentstate
//狀態獨熱編碼
localparam
CHECK_H = 5'b0_0001,
CHECK_e = 5'b0_0010,
CHECK_la = 5'b0_0100,
CHECK_lb = 5'b0_1000,
CHECK_o = 5'b1_0000;
//兩段式狀態機
//第一個always塊描述狀態轉移
always@(posedge clk,negedge rst)
if(!rst)
CS <= CHECK_H;
else
CS <= NS; //狀態轉移到下一狀態
//第二個always塊描述狀態輸出以及判斷狀態轉移
always@(CS,asci)
case(CS)
CHECK_H:
begin
led = 1'b1;
if(asci == "H")
NS <= CHECK_e;
else
NS <= CHECK_H;
end
CHECK_e:
begin
led = 1'b1;
if(asci == "e")
NS <= CHECK_la;
else
NS <= CHECK_H;
end
CHECK_la:
begin
led = 1'b1;
if(asci == "l")
NS <= CHECK_lb;
else
NS <= CHECK_H;
end
CHECK_lb:
begin
led = 1'b1;
if(asci == "l")
NS <= CHECK_o;
else
NS <= CHECK_H;
end
CHECK_o:
begin
if(asci == "o")
led = 1'b0;
else
led = 1'b1;
NS <= CHECK_H;
end
default:
begin
NS <= CHECK_H;
led = 1'b1;
end
endcase
endmodule
用之前修改後的testbench測試文件進行測試,測試結果如下,可以看到,兩段式狀態機描述結果和之前一段式描述並沒有差異:
綜合後狀態轉移圖如下,與之前也沒有差異:
再查看綜合後的電路圖,與之前有了較大的差異,可以看到,這次led輸出採用組合邏輯輸出,之前採用一個帶有使能端的D觸發器輸出:
再來分析一下資源佔用情況,在上次設計中,總共佔用了14個LE,6個寄存器資源,而這次佔用了12個LE,5個寄存器資源,因爲兩段式描述更清晰,便於軟件進行分析優化,所以設計也更加優良:
接下來我們再次人爲上演上一個testbench中的錯誤,將數據改變與時鐘上升沿對齊,測試波形如下:
可以看出來,因爲我們描述輸出採用組合邏輯,所以這樣的測試是錯誤的,不僅NS與CS變化沒有同步,而且狀態也沒有翻轉,但在實際中,數據變化與時鐘上升沿有可能會同時發生,顯然這個設計就會出錯。所以爲了從根本上避免這種情況,一般在輸出部分插入一級額外的時鐘信號,用來保證信號穩定性,所以我們引入接下來的三段式FSM描述。
5、三段式描述法:一個always塊採用時序邏輯描述狀態轉移,一個always塊採用組合邏輯判斷狀態轉移條件,一個always塊採用時序邏輯描述狀態輸出
verilog代碼如下:
//檢測“Hello”後led狀態翻轉
module check_hello(
input clk, //50M時鐘信號
input rst, //低電平復位
input [7:0]asci,//字符輸入
output reg led //控制led
);
//狀態寄存器
reg [4:0]NS; //nextstate
reg [4:0]CS; //currentstate
//狀態獨熱編碼
localparam
CHECK_H = 5'b0_0001,
CHECK_e = 5'b0_0010,
CHECK_la = 5'b0_0100,
CHECK_lb = 5'b0_1000,
CHECK_o = 5'b1_0000;
//三段式狀態機
//第一個always塊描述狀態轉移
always@(posedge clk,negedge rst)
if(!rst)
CS <= CHECK_H;
else
CS <= NS; //狀態轉移到下一狀態
//第二個always塊判斷狀態轉移
always@(CS,asci)
case(CS)
CHECK_H:
begin
if(asci == "H")
NS <= CHECK_e;
else
NS <= CHECK_H;
end
CHECK_e:
begin
if(asci == "e")
NS <= CHECK_la;
else
NS <= CHECK_H;
end
CHECK_la:
begin
if(asci == "l")
NS <= CHECK_lb;
else
NS <= CHECK_H;
end
CHECK_lb:
begin
if(asci == "l")
NS <= CHECK_o;
else
NS <= CHECK_H;
end
CHECK_o:
begin
NS <= CHECK_H;
end
default:
begin
NS <= CHECK_H;
end
endcase
//第三個always塊描述狀態輸出
always@(posedge clk,negedge rst)
if(!rst)
led <= 1'b1; //led熄滅
else begin
case(CS)
CHECK_H:
led <= 1'b1;
CHECK_e:
led <= 1'b1;
CHECK_la:
led <= 1'b1;
CHECK_lb:
led <= 1'b1;
CHECK_o:
if(asci == "o")
led <= 1'b0;
else
led <= 1'b1;
default:
led <= 1'b1;
endcase
end
endmodule
testbench依然採用之前修改後的測試文件(數據整體延遲時鐘1ns)進行測試,結果如下,測試結果和之前相同:
再次人爲進行查錯,將數據與時鐘對齊進行測試,結果如下:
結果是不是很令人驚喜^_^,可以看到當數據變化與時鐘上升沿對齊的時候,結果依然正確,這是因爲狀態輸出不是組合邏輯,而是時序邏輯,在輸出前面插入了一級時鐘信號就有效的解決了問題,所以一般描述FSM時選用三段式描述法描述;
再來看看狀態轉移圖,與之前兩種描述也沒有差異:
綜合後的電路如下,可以驗證之前說的,在輸出前插入了一級時鐘信號:
再來對比一下資源佔用,可以看到三段式描述法中和了前面兩種描述方法的優點,總共耗費了12個LE,6個寄存器資源,雖然多了一個寄存器資源,卻有效的避免了重大錯誤,這也暗示了一個數字設計裏最重要的思想,中和設計思想,有的時候可以需要性能的提升,但在提升性能的同時也會增加資源佔用,設計面積增大,所以兩者中和往往是最優秀的設計;
至此,一個完整的示例就設計實現完成了,最後再進行幾點補充:
1、因爲這個示例中輸出只有led,所以我們採用了直接寫在FSM描述裏面,如果輸出較多,可以利用task/endtask將輸出進行封裝;
2、在整個設計中,不管是任何一種描述方式,只要case,就會寫入default選項,這一點也一直在設計過程中沒有提到;
default選項是必須要寫入的,這樣就符合了剛開始提到的狀態機評判標準最重要的一點——安全性,因爲不管我們採用二進制編碼還是one-hot編碼,在實際應用中可能會由於其他因素(比如噪聲)產生突變,這時候就會進入default選項,然後重新啓動狀態機,可謂優點多多;
3、這一點也是最重要的一點,狀態機設計不是一種具體的事物,比如說verilog語法就是固定的,它更多的是一種對具有邏輯規律和時序邏輯事件的一種描述思想,所以,即使前面提到了一段式,兩段式,三段式FSM描述方法,在實際中,如果需要,我們可以分離出來4個always塊,5個always塊等等,這裏的一段式,兩段式,三段式反映的只是一種設計思想,希望在以後的數字設計中有更多的體會!