18-状态机
1.状态机
FPGA是并行执行的,如果想要处理具有前后顺序的事件就要使用状态机
1.1 状态机是什么?
- 状态机简写为 FSM(Finite State Machine),也称为同步有限状态机,我们一般简称为状态机,之所以说“同步”是因为状态机中所有的状态跳转都是在时钟的作用下进行的,而“有限”则是说状态的个数是有限的。
- 状态机根据影响输出的原因分为两种:Moore型或者是Mealy型
- 两种状态机的共同点:状态的跳转都只和输入有关。区别主要是在输出的时候:若最后的输出只和当前状态有关而与输入无关则称为 Moore 型状态机;****若最后的输出不仅和当前状态有关还和输入有关则称为 Mealy 型状态机。
- 状态机是时序逻辑电路中非常重要的一个应用,常在大型复杂的系统中使用较多。
状态机的每一个状态代表一个事件,从执行当前事件到执行另一事件我们称之为状态的跳转或状态的转移,我们需要做的就是执行该事件然后跳转到一下时间,这样我们的系统就“活”了,状态机特别适合描述那些发生有先后顺序或时序规律的事情,在数字电路系统中小到计数器大到微处理器都可以用状态机来进行描述。
其实状态机也是一种函数关系,如上图所示,一个计数器其实就可以看作是一个最简单的状态机,输入是时钟信号,状态是计数的值,输出是计数的值,我们可以列出一个时间和输出的函数关系,函数表达式为 q = counter(t),坐标关系如图所示,在有限的时间内,我们都可以根据具体的时间来算出当前输出的值是多少。
2. 可乐售卖机案例
可乐机每次只能投入 1 枚 1 元硬币,且每瓶可乐卖 3 元钱,即投入 3 个硬币就可以让可乐机出可乐,如果投币不够 3 元想放弃投币需要按复位键,否则之前投入的钱不能退回。
2.1 状态机框图
- 必不可少的是时钟和复位信号;其次是投币 1 元的输入信号,我们取名为 pi_money;可乐机输出我们购买的可乐,取名为 po_cola。
- 只能投入1元的硬币,没有找零
2.2 状态转移图
- 每个椭圆的框表示一个状态(也可以用其他图形表示)
- 每个状态之间都有一个指向的箭头,表示的是状态跳转的过程
- 箭头上有标注的一组数字,斜杠左边表达的是状态的输入,斜杠右边表达的是状态的输出
完整的状态转移图需要知道以下三个要素:
1、输入:根据输入可以确定是否需要进行状态跳转以及输出,是影响状态机系统执行过程
的重要驱动力;
2、输出:根据当前时刻的状态以及输入,是状态机系统最终要执行的动作;
3、状态:根据输入和上一状态决定当前时刻所处的状态,是状态机系统执行的一个稳定的
过程。
2.3 可乐机状态转移图
1、输入:投入 1 元硬币;
2、输出:出可乐、不出可乐;
3、状态:可乐机中有 0 元、可乐机中有 1 元、可乐机中有 2 元、可乐机中有 3 元。
- 从IDLE状态开始进行分析,IDLE状态有两种情况,投入硬币或者不投入硬币,不投入硬币维持IDLE状态,投入硬币跳转到下一个状态,依次进行分析
- 这是Moore型的状态机,输入只与当前的状态有关,与输入没有关系
- 如果不写THREE状态,在TWO状态的时候投入一元,此时出可乐,并且状态变为IDLE,如果此时不投钱,维持TWO状态 - 这种就是Mealy型状态机
2.4 波形图
状态转移图画出了之后可以不用画波形图
- 首先是三个输入信号,我们随机模拟输入信号 pi_money 的输入情况,根据状态转移图来分析继续绘
制波形。 - 因为有不同的状态之间的跳转关系,所以我们需要一个用于表示状态的变量,一般都取一个名为 state 的状态变量,state 处于哪个状态、何时跳转都需要根据输入信号pi_money 来决定
- 而输出信号 po_cola 的结果则由输入 pi_money 和当前 state 的状态共同决定。
2.5 RTL
-
信号:时钟和复位信号,还有输入信号(可能有多个输入信号)、输出信号
-
状态参数(通过参数定义中间状态方便阅读及修改):多个状态需要定义多个状态参数
-
状态参数编码:多个状态可以使用独热码的编码方式或者其他的编码方式(二进制码、格雷码)
-
状态变量 - 使用独热码的话,有多少个中间状态,中间变量就有多少位
-
中间变量(计数器等)
-
对状态变量和中间变量进行赋值
-
对输出信号进行赋值
2.5.1 两段式状态机
第一段状态机写状态的跳转,第二段状态机写数据的输出
module simple_fsm(
input wire sys_clk,
input wire sys_rst_n,
input wire pi_money,
output reg po_cola
);
parameter IDLE = 3'b001;
ONE = 3'b010;
TWO = 3'b100;
reg [2:0] state;
// 第一段状态机描述当前状态如何跳转到下一个状态
always @ (posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else case(state)
IDLE : if(pi_money == 1'b1)
state <= ONE;
else
state <= IDLE;
ONE : if(pi_money == 1'b1)
state <= TWO;
else
state <= ONE;
TWO : if(pi_money == 1'b1)
state <= IDLE;
else
state <= TWO;
default : state <= IDLE;
endcase
//第二段状态机,描述当前状态 state 和输入 pi_money 如何影响 po_cola 输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_cola <= 1'b0;
else if((state == TWO) && (pi_money == 1'b1))
po_cola <= 1'b1;
else
po_cola <= 1'b0;
endmodule
- 端口列表部分
- 状态编码部分
- 状态变量
- 第一段状态机部分
- 第二段状态机部分
2.5.2 三段式状态机
module simple_fsm(
input wire sys_clk,
input wire sys_rst_n,
input wire pi_money,
output reg po_cola
);
parameter IDLE = 3'b001;
ONE = 3'b010;
TWO = 3'b100;
reg [2:0] cur_state;
reg [2:0] next_state;
// 第一段状态机描述当前状态
always @(posedge sys_clk or sys_rst_n)
if(sys_rst_n == 1'b0)
cur_state <= IDLE;
else
cur_state <= next_state;
// 第二段描述如何跳转到下一个状态
always @ (*)
case(state)
IDLE : if(pi_money == 1'b1)
state <= ONE;
else
state <= IDLE;
ONE : if(pi_money == 1'b1)
state <= TWO;
else
state <= ONE;
TWO : if(pi_money == 1'b1)
state <= IDLE;
else
state <= TWO;
default : state <= IDLE;
endcase
//第三段状态机,描述当前状态 state 和输入 pi_money 如何影响 po_cola 输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_cola <= 1'b0;
else if((state == TWO) && (pi_money == 1'b1))
po_cola <= 1'b1;
else
po_cola <= 1'b0;
// 第三段
assign po_cola = state == TWO && pi_money == 1'b1 ? 1'b1 : 1'b0;
endmodule
2.6 状态机分段
状态机代码写法有一段式、二段式、三段式
- 一段式指的是在一段状态机中使用时序逻辑既描述状态的转移,也描述数据的输出
- 二段式指在第一段状态机中使用时序逻辑描述状态转移,在第二段状态机中使用组合逻辑描述数据的输出
- 三段式指在第一段状态机中采用时序逻辑描述状态转移,在第二段在状态机中采用组合逻辑判断状态转移条件描述状态转移规律,在第三段状态机中描述状态输出,可以用组合电路输出,也可以时序电路输
出)。
这种一段式、二段式、三段式其实都是之前经典的老写法,也是一些老工程师仍然习惯用的写法,老方法是根据状态机理论建立的模型抽象后设计的,其实要严格按照固定的格式来写代码,否则综合器将无法识别出你写的代码是个状态机,因为早期的开发工具只能识别出固定的状态机格式,如果不按照标准格式写代码综合器最后无法综合成为状态机的样子。这样往往增加了设计的难度,很多人学习的时候还要去了解理论模型,反复学习理解很久才能够设计好的状态机,所以需要我们改进。
老的一段式、二段式、三段式各有优缺点 - 一段式在描述大型状态机时会比较困难,会使整个系统显得十分臃肿,不够清晰
- 二段式状态机的好处是其结构和理想的理论模型完全吻合,即不会有附加的结构存在,比较精简,但是由于二段状态机的第二段是组合逻辑描述数据的输出,所以有一些情况是无法描述的,比如输出时需要类似计数的累加情况,这种情况在组合逻辑中会产生自迭代,自迭代在组合逻辑电路中是严格禁止的,而且第二段状态机主要是描述数据的输出,输出时使用组合逻辑往往会产生更多的毛刺,所以并不推荐。
- 三段式状态机,三段状态机的输出就可是时序逻辑了,但是其结构并不是最精简的了。三段式状态机的第一段状态机是用时序逻辑描述当前状态,第二段状态机是用组合逻辑描述下一状态,如果把这两个部分进行合并而第三段状态机保持不变,就是我们现在最新的二段式状态机了。这种新的写法在现在不同综合器中都可以被识别出来,这样既消除了组合逻辑可能产生的毛刺,又减小了代码量,还更加容易上手,不
必再去关心理论模型是怎样的,仅仅根据状态转移图就非常容易实现,对初学者来说十分友好。所以我们习惯性的使用两个均采用时序逻辑的 always 块,第一个 always 块描述状态的转移为第一段状态机,第二个 always 块描述数据的输出为第二段状态机(如果我们遵
循一个 always 块只描述一个变量的原则,如果有多个输出时第二段状态机就可以分为多个always 块来表达,但理论上仍属于新二段状态机,所以几段式状态机并不是由 always 块的数量简单决定的)。
2.7 Testbench
`timescale 1ns/1ns
module tb_simple_fsm();
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg pi_money ;
//wire define
wire po_cola;
//初始化系统时钟、全局复位
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
//sys_clk:模拟系统时钟,每 10ns 电平翻转一次,周期为 20ns,频率为 50MHz
always #10 sys_clk = ~sys_clk;
//pi_money:产生输入随机数,模拟投币 1 元的情况
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
pi_money <= 1'b0;
else
pi_money <= {$random} % 2; //取模求余数,产生非负随机数 0、1
//将 RTL 模块中的内部信号引入到 Testbench 模块中进行观察,使用wire类型变量
wire [2:0] state = simple_fsm_inst.state;
initial begin
$timeformat(-9, 0, "ns", 6);
$monitor("@time %t: pi_money=%b state=%b po_cola=%b",
$time, pi_money, state, po_cola);
end
//------------------------simple_fsm_inst------------------------
simple_fsm simple_fsm_inst(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.pi_money (pi_money ), //input pi_money
.po_cola (po_cola ) //output po_cola
);
endmodule