读大学时学习数字电路,老师会告诉我们,时序电路和组合电路的根本区别就在于,时序电路包含记忆器件(锁存器或触发器)。有了记忆器件,电路的输出就不单单决定于当前输入,同时也跟记忆器件存储的先前信息有关。记忆器件存储的信息可以理解为电路的状态,如果这些状态的数量是有限的,我们就可以利用它们来构建有限状态机(FSM)。
根据输出条件的不同,状态机分为Moore型(输出仅跟状态有关)和Mealy型(输出跟状态和输入都有关)。
在Moore型状态机中,输出只跟状态有关,而状态即寄存器存储的信息(可以理解为FF的Q端),所以说输出相对于输入总是有1 clock cycle的延时,于是我们管这种叫同步状态机。由于输出只由状态驱动(FF/Q),所以对下一级寄存器来说,它是一个同步信号。(严格来说,并不能完全避免输出的冒险,因为多bit状态如果不是Grey或One-Hot编码,也会存在竞争;但只要满足timing closure,冒险就不会传递到下一级寄存器)
在Mealy型状态机中,输出跟状态和输入都有关,当输入变化时,输出也会立即变化,所以我们管这种叫异步状态机。如果输入是异步信号(虽然它不应该是),那么输出也将是异步信号,对后级寄存器来说就存在亚稳态风险;正确的做法是状态机的输入都应该同步到本地时钟域,这样Mealy型的输出也是同步信号(跟Moore型一样,不能完全避免冒险),而且不存在1 clock cycle的延时。
下图为全同步设计的Mealy状态机:
不管哪种状态机,如果后级电路对毛刺敏感,最好都在输出端加一级寄存器。前面提到,只要满足timing closure,毛刺就不会传递。
Glitch Free Moore型状态机:
Glitch Free Mealy型状态机:
对比两种类型的状态机,Moore的输出逻辑简单,但是状态译码器比较复杂;Mealy输出逻辑复杂,但是状态数量较少,而且可以做到无延时输出。两者没有优劣之分,而且在实际设计中,往往会混用两种类型,具体问题要具体分析。
在代码层面,状态机有一段式、两段式、三段式的写法,本文推荐三段式写法,因为不管站在综合工具还是代码维护的角度,三段式都更有优势。所谓三段式,就是将状态转移、状态条件和输出逻辑分成三个always块,下面是Verilog Template:
// FSM Template
module state_machine_template(
input clk,
input rstn,
input input1,
input input2,
input ...,
output output1,
output output2,
output ...
);
localparam [STATE_NUM-1: 0] // Here using binary code
S0 = 0, // Use meaningful parameters instead in your design
S1 = 1,
S2 = 2,
... ;
reg [STATE_NUM-1: 0] current_state, next_state; // next_state is actually the output of state condition combinational logics
// State transition
always @(posedge clk or negedge rstn)
begin
if(!rstn)
current_state <= S0;
else
current_state <= next_state;
end
// State condition combinational logics
always @(*)
begin
case(current_state)
S0:
if(<input1 condition is true>)
next_state = S1;
else if(<input2 condition is true>)
next_state = S2;
else
next_state = S0;
S1:
if(<input3 condition is true>)
next_state = S2;
else if(<input4 condition is true>)
next_state = S0;
else
next_state = S1;
S2:
...
default:
next_state = current_state/S0/S1/...; // carefully deal with default case to avoid dead state
endcase
end
`ifdef MOORE_TYPE
// Moore combinational outputs
always @(*)
begin
case(current_state)
S0:
begin
output1 = <value>;
output2 = <value>;
...
end
S1:
begin
output1 = <value>;
output2 = <value>;
...
end
...
default:
begin
output1 = <value>;
output2 = <value>;
...
end
endcase
end
// Moore sequential outputs
always@(posedge clk or negedge rstn)
begin
if(!rstn)
begin
output1_r <= <default_value>;
output2_r <= <default_value>;
...
end
else
begin
output1_r <= output1;
output2_r <= output2;
...
end
end
`else // MEALY_TYPE
// Mealy combinational outputs
always @(*)
begin
case(current_state)
S0:
begin
if(<condition1>)
begin
output1 = <value>;
output2 = <value>;
...
end
else if(<condition2>)
begin
output1 = <value>;
output2 = <value>;
...
end
else
begin
output1 = <value>;
output2 = <value>;
...
end
end
...
default:
begin
output1 = <value>;
output2 = <value>;
...
end
endcase
end
// Mealy sequential outputs
always@(posedge clk or negedge rstn)
begin
if(!rstn)
begin
output1_r <= <default_value>;
output2_r <= <default_value>;
...
end
else
begin
output1_r <= output1;
output2_r <= output2;
...
end
end
`endif
endmodule
需要注意,状态机一般用于控制信号(control path),而数据信号(data path)最好与之分隔开来,这样代码清晰,而且便于进行低功耗设计。
最后讨论一下如何提升状态机的性能。复杂的状态机可能有几十个状态和输入条件,如果不做优化,其状态译码逻辑将非常复杂,极有可能成为设计中的关键路径。一般我们通过打断组合逻辑并插入pipeline reg来提升电路性能,但状态机的译码电路是不能加入流水的,怎么办?思路是拆分组合逻辑。比如我们可以对状态和输出进行分类,按类别将大状态机切割成若干个小状态机,这时候输入也被分组后去触发相应的小状态机;再比如状态机中经常用到计数器,直接将它作为条件嵌入状态机中也会拖累性能,我们可以用它产生类似enable这样的信号去触发状态转移,就能简化状态译码电路。当然,性能优化往往伴随着面积增大,所以也不能一味地去拆分组合逻辑。