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輸出低電平,狀态翻轉;
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yYiVmZkNTOjljMwQWZxcTOjNDMzEDN3ADZ0IGMjRWM08CX4IzLcZDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLzM3Lc9CX6MHc0RHaiojIsJye.png)
該測試中隐藏了一個重要的問題,我們在編寫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塊描述狀态判斷轉移條件
//檢測“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塊采用時序邏輯描述狀态輸出
//檢測“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塊等等,這裡的一段式,兩段式,三段式反映的隻是一種設計思想,希望在以後的數字設計中有更多的體會!