FPGA學習筆記(七)——FSM(Finite State Machine,有限狀態機)設計

  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塊等等,這裏的一段式,兩段式,三段式反映的只是一種設計思想,希望在以後的數字設計中有更多的體會!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

    

  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章