05.4 狀態機設計模式#
從 gate level 到 RTL:FSM coding 的演進#
有限狀態機是數位設計最古老的抽象:Mealy 與 Moore 在 1950 年代的形式化理論,比 HDL 早了三十多年。在 RTL 尚未普及的 1980 年代,狀態機是用 gate level 描述的——工程師手寫次態方程與輸出方程,或用狀態機圖工具(如早期的 StateCAD 類產品)自動產生方程。1990 年代 RTL 合成普及後,FSM 的描述方式轉為「兩個程序塊」的 coding style:一個時序塊存狀態暫存器,一個組合塊算次態。Synopsys 的 Design Compiler 也因此發展出 FSM extraction/FSM compiler 之類的功能,試圖從 RTL 反向辨識出狀態機再做重編碼最佳化,但實務上效果有限,「coding style 決定狀態機品質」成為共識。
其中最大的爭議是狀態編碼(state encoding)。binary encoding 用 \(\lceil \log_2 N \rceil\) 個 flip-flop 表示 \(N\) 個狀態,最省面積,但狀態解碼邏輯深、且未用到的狀態編碼需要防護(防非法狀態)。one-hot encoding 每個狀態一個 flip-flop,面積換速度:次態邏輯只需要看一個位元,解碼淺、時脈頻率容易做高,FPGA 尤其喜愛(FPGA 內 abundant 的正反器與相對稀缺的查找表輸入位數,讓 synthesis tool 對 FPGA 目標預設 one-hot)。Verilog 提供了 onehot/binary 之類的 synthesis pragma 建議(如 (* syn_encoding = "onehot" *)),但那終究是對工具的暗示,不是語言的能力。Veryl 的作法不同:用 enum 表示狀態,讓「狀態機的完整性」成為可被編譯器檢查的性質。
Veryl 的 enum#
Veryl 的 enum 語法與 Rust 風格相近:
enum State: logic<2> {
Idle,
Run,
Done,
}
使用時以 State::Idle 這樣的路徑語法存取。enum 也可以宣告在 package 中,供多個模組共用,以 import P::*; 引入。
兩段式 FSM:序列偵測器#
以一個「偵測輸入序列 1011」的序列偵測器為例。兩段式(two-always-block)結構:狀態暫存器一段、次態組合邏輯一段。
module SeqDetector (
clk : input clock,
rst : input reset,
din : input logic,
hit : output logic,
) {
enum State: logic<3> {
Idle,
S1, // 看到一個 1
S10, // 看到 10
S101, // 看到 101
}
var r_state: State;
var w_next : State;
// 狀態暫存器
always_ff (clk, rst) {
if_reset {
r_state = State::Idle;
} else {
r_state = w_next;
}
}
// 次態組合邏輯
always_comb {
w_next = r_state; // default:保持
hit = 0;
case r_state {
State::Idle: if din { w_next = State::S1; }
State::S1 : if !din { w_next = State::S10; }
State::S101: {
if din {
w_next = State::S1;
hit = 1;
} else {
w_next = State::S10;
}
}
default: {
if din {
w_next = State::S101;
} else {
w_next = State::Idle;
}
}
}
}
}
這個範例示範了幾個重要 pattern:
- 程序塊開頭先給 default 值(
w_next = r_state;、hit = 0;),確保所有路徑都有賦值——這正是 05.1 章談過的 anti-latch 準則,在 FSM 中尤其關鍵。 - 用
var宣告次態訊號w_next,因為它在 case 中被多次賦值;狀態暫存器r_state在always_ff中只有一個賦值點。 - case 的每個分支只處理一個狀態,非常適合工具做 exhaustive 檢查。
enum 的完整性檢查#
傳統 Verilog 用 localparam 定義狀態碼,case 遺漏一個狀態,工具不會說話(除非開 lint)。Veryl 的 case 對 enum 值可以做完整性檢查:當 case 的 scrutinee 是 enum 型別,編譯器知道狀態空間的全集,遺漏分支(或加上不存在的分支)會被指出。這把「漏寫一個狀態」這類 bug 從 gate-level 模擬或 FPGA 上線那天,提前到編譯當天。若刻意不想列舉全部狀態,寫明 default 分支即可表達意圖。
三段式:輸出暫存#
兩段式的輸出是組合邏輯(如上例的 hit),輸出延遲與次態邏輯同路徑,對時脈頻率不利。三段式(three-block style)把輸出也暫存一拍:
var r_hit: logic;
always_ff (clk, rst) {
if_reset {
r_hit = 0;
} else {
r_hit = w_hit;
}
}
assign hit = r_hit;
輸出暫存的代價是輸出延遲一拍,設計時要把這一拍納入協定考量。Mealy 機(輸出依賴輸入)與 Moore 機(輸出只依賴狀態)的取捨也在此浮現:Moore 機天然適合輸出暫存,時序乾淨;Mealy 機反應快一拍但輸出路徑含輸入,timing 較緊。
簡單握手 controller 的骨架#
module Handshake (
clk : input clock,
rst : input reset,
req : input logic,
gnt : output logic,
done : input logic,
) {
enum State: logic<1> {
Idle,
Busy,
}
var r_state: State;
always_ff (clk, rst) {
if_reset {
r_state = State::Idle;
} else {
case r_state {
State::Idle: if req { r_state = State::Busy; }
State::Busy: if done { r_state = State::Idle; }
}
}
}
assign gnt = r_state == State::Busy;
}
這裡直接在 always_ff 中寫 case 是單程序(one-block)風格的變體:狀態轉移簡單時,省掉次態組合塊反而更清晰。風格沒有絕對對錯,重點是 default 值與完整性檢查的紀律要維持。
小結#
FSM coding style 從 gate level 方程、到 RTL 兩段式、到語言層級的 enum 完整性檢查,反映了整個硬體描述語言的演進主軸:把人肉紀律變成機器檢查。one-hot 與 binary 的編碼取捨則留給合成工具與 synthesis pragma,Veryl 專注在它能檢查的地方——狀態空間的完備性。下一章進入參數化:如何讓一份設計服務多種位寬。