状态机,全称是有限状态机( Finite State Machine,缩写为FSM),是一种在有限个状态之间按一定规律转换的时序电路,可以认为是组合逻辑和时序逻辑的一种组合。 状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显,因此基本上都会用到状态机,如SDRAM控制器等。
状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。
根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即**摩尔(Moore)型状态机和米勒(Mealy)**型状态机。
摩尔(Moore)型状态机
Moore状态机:组合逻辑的输出只取决于当前状态
米勒(Mealy)型状态机
Mealy状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。
状态机写法
根据状态机的实际写法,状态机还可以分为一段式、二段式和三段式状态机。
- 一段式:整个状态机写到一个always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。不推荐采用这种状态机,因为从代码风格方面来讲,一般都会要求把组合逻辑和时序逻辑分开;从代码维护和升级来说,组合逻辑和时序逻辑混合在一起不利于代码维护和修改,也不利于约束。
- 二段式:用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。 不同于一段式状态机的是, 它需要定义两个状态, 现态和次态,然后通过现态和次态的转换来实现时序逻辑。
- 三段式:在两个always模块描述方法基础上,使用三个always模块,一个always模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always模块描述状态输出(可以用组合电路输出,也可以时序电路输出)。
实际应用中三段式状态机使用最多,因为三段式状态机将组合逻辑和时序分开,有利于综合器分析优化以及程序的维护;并且三段式状态机将状态转移与状态输出分开,使代码看上去更加清晰易懂,提高了代码的可读性, 推荐大家使用三段式状态机, 本文也着重讲解三段式。
三段式状态机的基本格式是:
- 第一个always语句实现同步状态跳转;
- 第二个always语句采用组合逻辑判断状态转移条件;
- 第三个always语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。
实例分析
可乐售卖系统:可乐机每次只能投入 1 枚 1 元硬币,且每瓶可乐卖 3 元钱,即投入 3 个硬币就可以让可乐机出可乐,如果投币不够 3 元想放弃投币需要按复位键,否则之前投入的钱不能退回。
信号 | 含义 |
---|---|
clk(输入信号) | 同步状态机,那么时钟是肯定少不了的,这里设定时钟是50MHz; |
rst_n(输入信号) | 系统复位,这里设定位低电平有效; |
money(输入信号) | 系统肯定需要一个复位,这里设定位低电平有效; |
cola(输入信号) | 系统肯定需要一个复位,这里设定位低电平有效; |
Moore 状态机(输出和输入无关)的状态转移图:
1/0:前面的1代表输入,后面的0代表输出
Mealy 状态机(输出和输入无关)的状态转移图:
有两种状态机的状态转移图可知:
Mealy 状态机比Moore状态机的状态个数要少
Mealy 状态机比Moore状态机的输出要早一个时钟周期
代码
状态机的编码方式一般有三种,各有优劣,独热码算是用的比较多的:
- 独热码
- 格雷码
- 二进制码
一段式和二段式在实际工程应用中相对较少,在此不再赘述,本文将详细介绍三段式状态机代码:
Moore型(摩尔型)三段式状态机
verilog代码:
module fsm_moore_3(
input clk,
input rst_n,
input money,
output reg cola
);
parameter IDLE = 4'b0001,
one = 4'b0010,
two = 4'b0100,
three= 4'b1000;
reg [3:0]c_state;
reg [3:0]n_state;
//状态机第一段,时序逻辑描述状态转移
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
c_state <= 4'b0;
else
c_state <= n_state;
end
//状态机第二段,组合逻辑描述状态转移条件
always@(*)begin
case(c_state)
IDLE:n_state = money?one:IDLE;
one :n_state = money?two:one;
two :n_state = money?three:two;
three:n_state = money?one:IDLE;
default:n_state = IDLE;
endcase
end
//状态机第三段,时序逻辑描述输出
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cola <= 1'b0;
else if(c_state == three)
cola <= 1'b1;
else
cola <= 1'b0;
end
endmodule
状态转移图:
Testbench:
`timescale 1ns/1ns
module fsm_moore_3_tb();
reg clk;
reg rst_n;
reg money;
wire cola;
fsm_moore_3 u0(
.clk(clk),
.rst_n(rst_n),
.money(money),
.cola(cola)
);
always #10 clk = ~clk;
initial begin
clk = 0;
rst_n = 0;
money = 0;
#5
rst_n = 1;
#25
money = 1;
#40
money = 0;
#20
money = 1;
#80
money = 0;
#200
$stop;
end
//状态机名称查看
reg [39:0] state_name_cur; //每字符8位宽,这里最多5个字符40位宽
reg [39:0] state_name_next; //每字符8位宽,这里最多5个字符40位宽
always@(*)begin
case(u0.c_state)
4'b0001:state_name_cur = "IDLE";
4'b0010:state_name_cur = "one";
4'b0100:state_name_cur = "two";
4'b1000:state_name_cur = "three";
default:state_name_cur = "IDLE";
endcase
end
always@(*)begin
case(u0.n_state)
4'b0001:state_name_next = "IDLE";
4'b0010:state_name_next = "one";
4'b0100:state_name_next = "two";
4'b1000:state_name_next = "three";
default:state_name_next = "IDLE";
endcase
end
endmodule
仿真结果:
可发现:
- 现态落后次态一个时钟周期,这是因为需要用次态去描述现态;
- 可乐的输出会滞后一个时钟周期,这是因为采用了时序逻辑来描述输出;
- **
当第三段的输出判断条件变成使用n_state(次态)来判断的话,仿真图如下:输出结果相较于使用c_state(现态)判断早一个时钟周期,这是由于现态落后次态一个时钟周期
而当状态机第三段的输出使用组合逻辑输出时,即改成:
assign cola = (c_state == three)?1:0;
仿真结果如下,因为用判断条件时c_state(现态),相较于时序逻辑使用c_state判断的输出结果也要早一个时钟周期。
Mealy型(摩尔型)三段式状态机
verilog代码:
module fsm_mealy_3(
input clk,
input rst_n,
input money,
output reg cola
);
parameter IDLE = 3'b0001,
one = 3'b0010,
two = 3'b0100;
reg [2:0]c_state;
reg [2:0]n_state;
//状态机第一段,时序逻辑描述状态转移
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
c_state <= 3'b0;
else
c_state <= n_state;
end
//状态机第二段,组合逻辑描述状态转移条件
always@(*)begin
case(c_state)
IDLE:n_state = money?one:IDLE;
one :n_state = money?two:one;
two :n_state = money?IDLE:two;
default:n_state = IDLE;
endcase
end
//状态机第三段,时序逻辑描述输出
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cola <= 1'b0;
else if(money&&(n_state == two))
cola <= 1'b1;
else
cola <= 1'b0;
end
endmodule
生成的状态转移图:
Testbench:
`timescale 1ns/1ns
module fsm_mealy_3_tb();
reg clk;
reg rst_n;
reg money;
wire cola;
fsm_mealy_3 u0(
.clk(clk),
.rst_n(rst_n),
.money(money),
.cola(cola)
);
always #10 clk = ~clk;
initial begin
clk = 0;
rst_n = 0;
money = 0;
#5
rst_n = 1;
#25
money = 1;
#40
money = 0;
#20
money = 1;
#80
money = 0;
#200
$stop;
end
//状态机名称查看
reg [39:0] state_name_cur; //每字符8位宽,这里最多5个字符40位宽
reg [39:0] state_name_next; //每字符8位宽,这里最多5个字符40位宽
always@(*)begin
case(u0.c_state)
4'b0001:state_name_cur = "IDLE";
4'b0010:state_name_cur = "one";
4'b0100:state_name_cur = "two";
4'b1000:state_name_cur = "three";
default:state_name_cur = "IDLE";
endcase
end
always@(*)begin
case(u0.n_state)
4'b0001:state_name_next = "IDLE";
4'b0010:state_name_next = "one";
4'b0100:state_name_next = "two";
4'b1000:state_name_next = "three";
default:state_name_next = "IDLE";
endcase
end
endmodule
仿真结果:
通过以上,针对三段式状态机可以得出如下小结:
Moore型状态机输出滞后Mealy型状态机一个时钟周期 三段式状态机的输出使用时序逻辑输出,避免了二段式状态机使用组合逻辑输出从而无法避免的“毛刺”问题
moore型和mealy型状态机的区别
波形上
以一个序列检测器为例,检测到输入信号11时输出z为1,其他时候为0。用摩尔型FSM实现需要用到三个状态(A,B,C)。而用米利型FSM实现则需要两个状态(A,B)。摩尔型FSM输出函数的输入只由状态变量决定,要想输出z=1,必须C状态形成,即寄存器中的两个1都打进去后才可以。输出z=1会在下一个有效沿到来的时候被赋值。
而米利型FSM输出函数是由输入和状态变量共同决定的。状态在B的时候如果输入为1,则直接以组合电路输出z=1,不需要等到下个有效沿到来
。从而也就不需要第三个状态C。
优缺点:
1、moore型更安全。
输出在时钟边沿变化(总是在一个周期后)。在Mealy机器中,输入更改可能会在逻辑完成后立即导致输出更改, 当两台机器互连时出现大问题 - 如果不小心,可能会发生异步反馈。
2、mealy型反应更快
在相同的周期内反应 - 不需要等待时钟。在Moore机器中,可能需要更多逻辑来将状态解码为输出 - 在时钟边沿之后更多的门延迟。并非所有时序电路都可以使用Mealy模型实现。 一些时序电路只能作为摩尔机器实现
总结与思考
状态机的三种描述方法:
一段式:整个状态机写到一个always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。
二段式:用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。
三段式:在两个always模块描述方法基础上,使用三个always模块,一个always模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always模块描述状态输出。
应该选择哪一种状态机 ?
一段式状态机写法不够模块化 ,且过于臃肿不利于维护,及布局布线;
二段式状态机将同步时序和组合逻辑分别放到不同的always模块中实现,这样做的好处不仅仅是便于阅读、理解、维护,更重要的是利于综合器优化代码,利于用户添加合适的时序约束条件,利于布局布线器实现设计。但是其当前状态的输出用组合逻辑实现,组合逻辑很容易产生毛刺,而且不利于约束,不利于综合器和布局布线器实现高性能的设计。
三段式状态机与二段式状态机相比,关键在于根据状态转移规律,在上一状态根据输入条件判断出当前状态的输出,从而在不插入额外时钟节拍的前提下,实现了寄存器输出,解决了毛刺问题。实际应用中三段式状态机使用最多,因为三段式状态机将组合逻辑和时序分开,有利于综合器分析优 化以及程序的维护;并且三段式状态机将状态转移与状态输出分开,使代码看上去更加清晰易懂,提高了代码的可读性,推荐大家使用三段式状态机。
三段式状态机的基本格式:
第一个 always 语句实现同步状态跳转;
第二个 always 语句采用组合逻辑判断状态转移条件;
第三个 always 语句采用时序逻辑描述状态输出。(状态机的第三段可以使用组合逻辑电路输出,也可以使用时序逻辑电路输出,一般推荐使用时序电路输出,因为状态机的设计和其它设计一样,最好使用同步时序方式设计,以提高设计的稳定性,消除毛刺)
状态机的编码方式:
独热码:和格雷码相比,虽然独热码多用了触发器,但所用组合电路可以省一些,因而使电路的速度和可靠性有显著提高,而总的单元数并无显著增加。因为独热码只有一位的变化,所以更适用于高速系统。
格雷码:使用了更多的组合逻辑资源,但是比独热码能表示更多的状态。
2进制:使用了更多的组合逻辑资源,但是比独热码能表示更多的状态,稳定性不如格雷码。
三段式状态机的第三段采用next_state还是cur_state:
第三段使用next_state和cur_state的区别在于,当状态跳转时,基于next_state的输出是立刻变化的,而基于cur_state输出会延迟一个周期,其他情况都一样,应该根据自己的时序要求选择。
modelsim显示状态机名称的方法:
1.在Testbench中添加如下语句:实质上就是在测试文件里添加一个变量来代替你要观察的变量;
2.在modelsim的波形仿真见面右击波形,选择Radix--ASSIC。
//------------------------------------------------
//-- 状态机名称查看器
//------------------------------------------------
reg [39:0] state_name_cur; //每字符8位宽,这里最多5个字符40位宽(THREE)
reg [39:0] state_name_next; //每字符8位宽,这里最多5个字符40位宽(THREE)
always @(*) begin
case(FSM_Mealy_3_inst.cur_state) //这里写你例化的状态机模块里你想查看的参数
4'b0001: state_name_cur = "IDLE"; //编码对应你的状态机的编码
4'b0010: state_name_cur = "ONE";
4'b0100: state_name_cur = "TWO";
4'b1000: state_name_cur = "THREE";
default: state_name_cur = "IDLE";
endcase
end
always @(*) begin
case(FSM_Mealy_3_inst.next_state)
4'b0001: state_name_next = "IDLE";
4'b0010: state_name_next = "ONE";
4'b0100: state_name_next = "TWO";
4'b1000: state_name_next = "THREE";
default: state_name_next = "IDLE";
endcase
end
更多内容:FPGA三段式状态机的思维陷阱