FPGA越往底層走,越發現很多問題只是知其然,而不知其所以然。狀態機編碼原則就是其中之一。我們在實際開發中,只記住了建議使用獨熱碼(one hot)作為狀態編碼,至於為什么(大概也就記得不容易跑飛),可能早就忘了。
以經典的案例來說明其中的一些問題:
- 序列檢測,每檢測到一組“11011”,然后輸出一個高電平。
狀態轉移圖如下圖所示:
狀態機的Verilog代碼如下:
module FSM_test(
input clk,
input rst_n,
input d_in,
output d_out
);
/* parameter S0 = 5'b000000,
S1 = 5'b000001,
S2 = 5'b000011,
S3 = 5'b000010,
S4 = 5'b000110,
S5 = 5'b000111; */
parameter S0 = 5'b00000,
S1 = 5'b00001,
S2 = 5'b00010,
S3 = 5'b00100,
S4 = 5'b01000,
S5 = 5'b10000;
/* parameter S0 = 5'd0,
S1 = 5'd1,
S2 = 5'd2,
S3 = 5'd3,
S4 = 5'd4,
S5 = 5'd5; */
reg r_d_out;
reg [4:0] cs,ns;
always@(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0) begin
cs <= S0;
end else begin
cs <= ns;
end
end
always@(*) begin
ns = 5'dx;
case(cs)
S0: begin
if(d_in == 1'b1)
ns = S1;
else
ns = S0;
end
S1: begin
if(d_in == 1'b1)
ns = S2;
else
ns = S0;
end
S2: begin
if(d_in == 1'b1)
ns = S2;
else
ns = S3;
end
S3: begin
if(d_in == 1'b1)
ns = S4;
else
ns = S0;
end
S4: begin
if(d_in == 1'b1)
ns = S5;
else
ns = S0;
end
S5: begin
if(d_in == 1'b1)
ns = S2;
else
ns = S3;
end
default:
ns = S0;
endcase
end
always@(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0) begin
r_d_out <= 1'b0;
end else if(cs == S5)begin
r_d_out <= 1'b1;
end else begin
r_d_out <= 1'b0;
end
end
assign d_out = r_d_out;
endmodule
上面代碼中,定義了格雷碼、獨熱碼以及二進制碼(序列碼)的狀態編碼方式,本想在vivado下看綜合后的原理圖,發現三種編碼方式綜合后的結果一樣!不符合理論啊,所以想是不是被vivado優化了。果然,查書發現在綜合選項中,“-fsm_extraction”選項為auto,在auto下,本段狀態就會被優化成格雷碼的編碼方式。
取消該優化之后,僅對比格雷碼和獨熱碼的綜合結果,原理圖如下:
- 格雷碼
- 獨熱碼
格雷碼消耗4個LUT和3個Register,而獨熱碼消耗7個LUT和6個Register。不是說獨熱碼更省組合邏輯嗎??為什么反而消耗LUT更多。到了這一層,還是不能往下理解,是代碼設計問題還是綜合問題?
做了一個嘗試之后,還是把前人結論和經驗總結一下:
- 二進制碼:
有過渡狀態,容易跑飛。
- 格雷碼:
減少過渡狀態,每次只有一位變化,因此可以降低功耗。但是如果當一個狀態到下一個狀態有多種轉換路徑時,就不能保證狀態跳轉時只有一個位變化,這樣就無法發揮格雷碼的特點了。
- 獨熱碼:
少用組合邏輯,多了寄存器。速度更快、可靠性更好。
至於二進制碼咱不用討論,因為對於狀態少的情況(小於4),也可以使用,因為跑飛的概率極其小。
對於格雷碼和獨熱碼,到底應該怎么選擇才能達到最優綜合?查閱書籍及網絡資料,總結起來就是:
- 格雷碼:適合所有狀態是順序序列,可以用格雷碼來消除毛刺,但如果有復雜分支判斷,則格雷碼也不能達到消毛刺的目的,簡單的說,格雷碼適合條件不復雜,狀態多的情況;
- 獨熱碼:消耗較少組合邏輯,消耗更多寄存器,因此在FPGA中有利於速度和可靠性。適合條件復雜,狀態少的情況。
另外,在CPLD中由於組合邏輯多,而寄存器少,所以可能不適合獨熱碼,更適合格雷碼和二進制編碼。