
前言
Verilog編程技巧系列文章將聚焦于介紹Verilog的各種編程范式或者說技巧,編程技巧和編程規范有部分重合,但并非完全一樣。規范更注重編碼的格式,像變量命名、縮進、注釋風格等,而編程技巧則更偏重更直觀易讀、更便于維護、綜合效果更好的Verilog/SV代碼寫法,像:如何編寫狀態機、如何進行參數化設計、如何進行流水線設計等。可以說編程技巧就是一些編程套路,熟練掌握這些技巧可幫助我們更高效的完成FPGA開發工作。
本文是Verilog編程技巧系列的第一篇文章,介紹了如何編寫三段式狀態機。
在FPGA設計中,三段式狀態機因其結構清晰、可靠性高等特點,成為實現狀態機的最佳方式。本文主要說明了以下問題:
-
如何評價一個狀態機寫的好不好; -
為什么三段式狀態機更好; -
為什么更推薦獨熱碼; -
第三段輸出邏輯應該怎么寫。
最后,本文分享了Verilog與SV的三段式狀態機模板。通過閱讀本文,你將能夠更深入的理解三段式狀態機的寫法及其背后的原理。
本文主要參考了Clifford E. Cummings的文章,他是數字電路設計領域的技術先驅,享有一定國際聲譽。他的文章可從此網站(界面如下圖)下載:http://www.sunburst-design.com/papers/。他的一些關于狀態機的文章給本文的一些結論提供了理論和實驗支撐。

一. 什么是狀態機
狀態機是描述系統行為的數學模型。包含狀態
(系統某時刻狀況)、轉移
(狀態間切換)、事件
(觸發轉移因素)和動作
(轉移時執行的操作),用于分析和設計系統邏輯,在多領域廣泛應用。
依據狀態數量,狀態機可分為兩類:
特征 | 有限狀態機(FSM) | 無限狀態機(ISM) |
---|---|---|
全稱與簡稱 | Finite State Machine,簡稱FSM | Infinite State Machine,簡稱ISM |
狀態數量 | 有限 | 無限 |
轉移方式 | 離散、明確條件 | 可能連續(如基于變量變化) |
典型應用 | 離散邏輯控制 | 連續系統或復雜動態系統 |
在HDL編碼時,我們構建的總是有限狀態機,所以這里我們說的狀態機默認是指有限狀態機
。
依據輸出與當前狀態的關系,狀態機也可分為兩類:
狀態機類型 | 輸出決定因素 | 特點描述 |
---|---|---|
Moore型(摩爾型) | 僅取決于當前狀態 | 任意時刻,只要狀態確定,輸出就確定,輸出與輸入無關 |
Mealy型(米利型) | 取決于當前狀態和輸入信號 | 輸出不僅和當前狀態有關,還受當前輸入的影響 |
這兩種狀態機的命名均來源于人名,看著比較奇怪。
實際應用中,無需刻意去區分狀態機屬于哪種類型
,沒有什么意義。
在Verilog/SV編碼中,狀態機應用極其廣泛,它幾乎可以描述所有邏輯,也就是說如果你想,所有的模塊的內部邏輯都可以寫成狀態機。所以,研究如何編寫更好的狀態機很有必要。
二. 一段、二段、三段與四段式狀態機
在Verilog/SV中,狀態機按功能可以分為三個部分:
-
狀態轉移
:負責在不同的狀態之間進行切換,它決定了狀態機如何從當前狀態遷移到下一個狀態。 -
狀態判斷
:根據當前狀態和輸入信號來確定下一個狀態是什么。 -
輸出邏輯
:根據當前狀態機的狀態來產生相應的輸出信號。
根據這三部分在代碼中用幾個always塊來描述,可將狀態機分為以下幾種:
2.1 一段式狀態機
狀態轉移、狀態判斷與輸出邏輯全都寫在同一個always塊中,稱為一段式狀態機
。示例如下:
//?狀態定義
localparam?S0?=?1'b0,?S1?=?1'b1;
reg?state;
//?狀態轉移、判斷與輸出
always?@(posedge?clk)?begin
??if?(~rstn)?{state,?out}?<=?{S0,?1'b0};
??else
????case?(state)
??????S0:?if?(in)?state?<=?S1;?else?state?<=?S0;?out?<=?1'b0;
??????S1:?if?(in)?{state,?out}?<=?{S0,?1'b1};?else?state?<=?S0;
??????default:?{state,?out}?<=?{S0,?1'b0};
????endcase
end
出于排版的考慮,去掉了非必要的begin-end,盡量縮短了代碼行數,下同。
2.2 二段式狀態機
狀態轉移寫在一個時序always塊中
,狀態判斷與輸出寫在一個組合always塊中
,稱為二段式狀態機。示例如下:
//?狀態定義
localparam?S0?=?1'b0,?S1?=?1'b1;
reg?state,?next;
//?第一段:時序邏輯,狀態轉移
always?@(posedge?clk)?begin
??if?(~rstn)?state?<=?S0;
??else?state?<=?next;
end
//?第二段:組合邏輯,次態和輸出邏輯
always?@(*)?begin
??case?(state)
????S0:?if?(in)?{next,?out}?=?{S1,?1'b0};?else?{next,?out}?=?{S0,?1'b0};
????S1:?if?(in)?{next,?out}?=?{S0,?1'b1};?else?{next,?out}?=?{S0,?1'b0};
????default:?{next,?out}?=?{S0,?1'b0};
??endcase
end
2.3 三段式狀態機
狀態轉移寫在一個時序always塊中
,狀態判斷寫在一個組合always塊中
,輸出寫在一個時序或組合always塊中
,稱為三段式狀態機。示例如下:
//?狀態定義
localparam?S0?=?1'b0,?S1?=?1'b1;
reg?state,?next;
//?第一段:時序邏輯,狀態轉移
always?@(posedge?clk)?begin
??if?(~rstn)?state?<=?S0;
??else?state?<=?next;
end
//?第二段:組合邏輯,計算次態
always?@(*)?begin
??case?(state)
????S0:?next?=?in???S1?:?S0;
????S1:?next?=?in???S0?:?S0;
????default:?next?=?S0;
??endcase
end
//?第三段:時序邏輯,輸出邏輯
always?@(posedge?clk)?begin
??if?(~rstn)?out_a?<=?1'b0;
??else?out_a?<=?(state?==?S1?&&?in)???1'b1?:?1'b0;
end
//?第三段:組合邏輯,輸出邏輯
always?@(*)?begin
??out_b?=?(state?==?S1?&&?in)???1'b1?:?1'b0;
end
對于第三段,因為時序邏輯的輸出相對組合邏輯會慢一個時鐘周期
,如果對輸出延時有極高的要求,則可以考慮用組合邏輯。但通常還是建議用時序邏輯
,時序邏輯的輸出更加穩定(無競爭和冒險),也更便于整體的時序分析和約束。
2.4 四段式狀態機
四段式狀態機是將三段式狀態機的輸出寫成兩個always塊,一個固定為組合always塊,寫輸出使能邏輯;另一個固定為時序always塊,寫輸出邏輯。示例如下:
//?前面與三段式相同,僅第三段輸出邏輯并分為兩段
//?組合邏輯,輸出使能
always?@(*)?begin
??out_en?=?(state?==?S1?&&?in);
end
//?時序邏輯,輸出邏輯
always?@(posedge?clk)?begin
??if?(~rstn)
????out?<=?1'b0;
??else?if?(out_en)?
????out?<=?1'b1;
??else?
????out?<=?1'b0;
end
從實現的功能上來說,三段式和四段式完全相同,但在設計理念
、靈活性
和可維護性
等方面,兩者存在明顯區別:
對比維度 | 三段式狀態機 | 四段式狀態機 |
---|---|---|
核心思想 | 輸出與狀態/輸入直接耦合,無中間控制信號。 | 引入輸出使能信號(out_en ),分離輸出條件與賦值邏輯。 |
代碼結構 | 輸出邏輯與狀態轉移混合在時序邏輯中。 | 輸出使能(組合邏輯)與輸出賦值(時序邏輯)分離,模塊化更清晰。 |
輸出條件修改 | 需直接修改時序邏輯中的條件判斷,可能影響其他邏輯。 | 只需修改組合邏輯中的out_en ,輸出賦值邏輯保持不變。 |
擴展性 | 新增輸出條件需修改時序邏輯,可能導致冗余。 | 新增條件只需擴展out_en 邏輯,不影響輸出賦值部分。 |
適用場景 | 簡單場景(如固定序列檢測、單一輸出控制)。 | 復雜場景(如多條件輸出、動態使能控制)。 |
總的來說,四段式狀態機在負責場景有一些代碼維護上的優勢。
根據2019 CummingsSNUG2019SV_FSM 狀態機設計.pdf
中的內容,四段式狀態機在復雜邏輯的綜合中會有一定優勢,但三段式代碼更加簡潔,更符合直觀邏輯,所以在FPGA開發中,作者推薦總是優先使用三段式
,僅在復雜場景開發的最后,將三段式改為四段式,以提升綜合效果。
綜合效果
:指的是綜合工具對不同代碼寫法的優化支持程度。現代綜合工具對四段式會有更好的優化效果,使得四段式在資源效率和時序性能方面通常優于三段式,但具體效果還需實測。
綜上,針對FPGA開發,本文僅討論三段式狀態機,如需針對復雜場景進行優化,各位同學可根據需要將三段式改為四段式
,并實際比較綜合后的資源效率和時序性能確定最終方案。
三. 什么樣的狀態機是好的狀態機?
換句話說,評價一個狀態機好壞的標準是什么
?一般來說,可以從以下4個維度進行評價:
-
可維護性:代碼易于修改、擴展,適應需求變更。
-
可讀性:代碼結構清晰、邏輯緊湊,避免冗余,便于閱讀理解。
-
可調試性:支持高效的仿真驗證,便于在仿真或實測時快速定位問題。
-
綜合效果:對綜合工具友好,代碼在轉化為硬件時的面積、速度與功耗等性能指標優異。
上述四種狀態機從這四個維度進行評價,表格如下:
指標 | 一段式 | 二段式 | 三段式 | 四段式 |
---|---|---|---|---|
可維護性 | ★ | ★★★ | ★★★ | ★★★ |
可讀性 | ★ | ★★★ | ★★★ | ★★ |
可調試性 | ★ | ★★ | ★★★ | ★★★ |
綜合效果 | ★★★ | ★ | ★★★ | ★★★ |
說明:
-
一顆星(★)表示“差”,兩顆星(★★)表示“良”,三顆星(★★★)表示“優” 。 -
四段式的綜合效果在非常復雜場景可能略好于三段式,但絕大多數場景下,三段式的綜合效果同樣優異,兩者區別不大,所以兩者都標注為三顆星。
對于一個功能需求來說,最重要的兩件事是開發效率
和模塊性能
。可維護性、可讀性和可調試性影響開發效率,綜合效果則影響模塊性能。
綜上,我們可以得出結論:
-
永遠不應該用一段式狀態機
:一段式狀態機邏輯不清,導致代碼難以維護、調試和擴展,極易成為"屎山代碼"的一部分。其唯一優勢(綜合效果高)完全無法抵消后期維護的災難性代價。即使在非常簡單的場景中,也不應該使用。 -
不推薦用二段式狀態機
:二段式雖然分離了部分邏輯,但輸出仍依賴組合邏輯,存在 輸出毛刺風險和 時序收斂困難,且可調試性和綜合效果低于三段式,沒有獨特的應用場景優勢。 -
不推薦直接用四段式狀態機
:四段式雖然邏輯分層更徹底,但會導致 代碼冗余和 開發效率下降。它僅適用于極少數復雜場景(例如多路獨立輸出需嚴格時序控制),對大多數設計屬于"過度設計"。 -
僅推薦使用三段式狀態機
:三段式是經過驗證的最佳實踐,沒有短板。
四. 狀態編碼的類型和優缺點
狀態編碼指的是狀態機的不同狀態用怎樣的二進制去表示,狀態編碼一般有以下幾種類型:
-
二進制碼(Binary):用二進制自然序列表示狀態,N個狀態需滿足 2^N ≥ S
(S為狀態總數)。8個狀態編碼為000, 001, 010, 011, 100, 101, 110, 111
。 -
格雷碼(Gray Code):相鄰狀態僅有一位不同,形成循環單步跳變。3位格雷碼為 000, 001, 011, 010, 110, 111, 101, 100
。 -
獨熱碼(One-Hot):每個狀態獨占一個比特位,總位數等于狀態數(N = S)。3個狀態編碼為 100, 010, 001
。 -
零空閑獨熱碼(Zero-Idle One-Hot):獨熱碼基礎上引入全零( 000...
)作為空閑狀態,其他狀態為單一高位。5個狀態編碼為0000(空閑態), 1000, 0100, 0010, 0001
。 -
獨冷碼(One-Cold):與獨熱碼相反,每個狀態由唯一一個低電平位(0)表示,其他位為1。3個狀態編碼為 011, 101, 110
。 -
約翰遜碼(Johnson Code):也稱為扭環碼(Twisted Ring Code),是一種特殊的環形移位碼,相鄰狀態通過左移或右移,然后補移出位的反碼生成,最終形成一個環。3位右移約翰遜碼為 000, 100, 110, 111, 011, 001
(循環)。 -
混合編碼(Hybrid Encoding):高位獨熱碼 + 低位二進制碼。狀態數>20時,平衡資源與性能。
在FPGA設計中,零空閑獨熱碼、獨冷碼與獨熱碼的資源消耗和實現邏輯高度相似,而約翰遜碼和混合編碼的實際應用場景較為局限。因此,工程中的核心選擇通常集中在以下三種編碼:二進制碼、格雷碼和獨熱碼。這三種編碼方式的優缺點對比如下表:
指標 | 二進制碼(Binary) | 格雷碼(Gray Code) | 獨熱碼(One-Hot) |
---|---|---|---|
時序邏輯資源消耗 (觸發器,FF) | ★★★★☆ 極低:僅需 ?log2(N)? 個觸發器(N為狀態數) | ★★★☆☆ 低:觸發器數與二進制相同,但需額外觸發器存儲轉換碼(可選) | ★★☆☆☆ 高:需 N 個觸發器,但FPGA中觸發器資源充足 |
組合邏輯資源消耗 (LUT) | ★★☆☆☆ 中高:狀態跳變需復雜組合邏輯(如狀態譯碼器) | ★★★☆☆ 中:需異或門實現碼值轉換 | ★★★★☆ 極低:狀態跳變僅需單比特操作 |
時序性能 | ★★☆☆☆ 中:多比特翻轉限制頻率 | ★★★☆☆ 中高:單比特跳變,但轉換邏輯增加延遲 | ★★★★★ 最優:單比特跳變,路徑最短,適合高頻 |
功耗 | ★★☆☆☆ 高:多比特翻轉動態功耗大 | ★★★★☆ 低:單比特翻轉降低功耗 | ★★★☆☆ 中:靜態功耗略高(觸發器多),動態功耗低 |
容錯性 | ★☆☆☆☆ 差:非法狀態多,需額外檢測 | ★★★☆☆ 中:非法狀態較少 | ★★★★☆ 優:非法狀態極易檢測(非單比特為1) |
可讀性 | ★★★☆☆ 中:自然順序易理解,跳變邏輯復雜 | ★★☆☆☆ 中低:編碼非自然排列,需查表輔助 | ★★★★★ 極優:每個狀態對應唯一比特,仿真直觀 |
推薦優先級 | ★★☆☆☆ - 資源敏感型設計 - 簡單狀態機 | ★★★☆☆ - 低功耗/跨時鐘域 - 異步FIFO | ★★★★★ - FPGA高頻設計 - 狀態數≤16 |
星級
說明:
-
★★★★★: 強烈推薦(顯著優勢,無替代方案) -
★★★★☆: 推薦(優勢明顯,適用場景明確) -
★★★☆☆: 一般推薦(需權衡利弊) -
★★☆☆☆: 不推薦(僅限特定場景) -
★☆☆☆☆: 避免使用(缺陷明顯)
由此可以得出結論
:
-
二進制碼(★★☆☆☆): -
僅限極簡設計(如ASIC或低成本FPGA),需承擔時序和功耗風險。 -
添加輸出寄存器過濾毛刺。
-
-
格雷碼(★★★☆☆): -
低功耗/跨時鐘域專用,如異步FIFO指針同步,但需手動添加轉換邏輯。 -
避免在復雜狀態機中濫用。
-
-
獨熱碼(★★★★★): -
FPGA設計首選,因LUT資源占用低、時序性能最優、容錯性高,完美適配FPGA架構。 -
僅需注意狀態數≤16(超過時可混合編碼)。
-
所以,在FPGA中進行狀態機設計時,總是應該使用獨熱碼
。
五. 第三段輸出應該用組合邏輯還是時序邏輯?
指標 | 組合邏輯輸出 | 時序邏輯輸出 |
---|---|---|
優點 | ? |