FPGA学习之状态机

1. 理论学习

状态机简写为FSM,也称为同步有限状态机,我们简称为状态机。所以说同步时因为状态机中所有的状态跳转都是在时钟的作用下进行的,而有限则是说状态机的个数有限的。状态机分为两大类,即Moore状态机和Mealy状态机,其共同的特点是:状态的跳转只和输入有关。区别主要是在输出的时候:若最后的输出只和当前状态有关而与输入无关则称为Moore型状态机;若最后的输出只和当前状态有关还和输入有关则称为 Mealy型状态机
状态机的每一个状态代表一个事件。从执行当前事件到执行另一事件我们称为状态的跳转或状态的转移,我们需要做的就是执行该事件然后跳转到下一个事件。有研究显示状态机可以描述除相对论和量子力学以外的任何事情,但特别适合描述那些发生先后顺序或时序规律的事情,在数字电路系统中小到计数器达到微处理器都可以用状态机来进行描述。

其实状态机也是一种函数关系,如下图所示,一个计数器其实就可以看作是一个最简单的状态机,输入是时钟信号,状态是计数的值,输出是计数的值,我们可以列出一个时间和输出的函数关系,函数表达式是q=counter(t),坐标关系是如下图所示,在有限时间内,我们都可以根据具体的事件来算出当前输出的值。
在这里插入图片描述
在这里插入图片描述

2 实战演练

2.1 实验目标

可乐机每次只能投1枚1元硬币,且每瓶可乐卖3元钱,即投入3枚硬币就可以让可乐机出可乐,如果投币不够3元想放弃投币 需要按复位键,否则之前投入的钱不能退回。

2.2 模块框图

在这里插入图片描述
在这里插入图片描述

2.3 状态转移图与波形图绘制

如下图所示,每一个椭圆的框代表一个状态(也可以用其他图形表示),每个状态之间都有一个指向的箭头,表示的是状态跳转的过程,箭头上有标注的一组数字,斜杠坐标表达的是状态的输入,斜杠右边表达的是状态的输出。
在这里插入图片描述
总结出来就是一个完整的状态转移图需要知道以下三个要素:

  1. 输入:根据输入可以确定是否需要进行状态跳转及输出,是影响状态机系统执行过程的重要驱动力。
  2. 输出:根据当前时刻的状态以及输入,是状态机系统最终要执行的动作
  3. 状态:根据输入和上一状态决定当前时刻所处的状态。是状态机系统执行的一个稳定的过程

接下来我们套用上面的总结分析本例的状态转移图是如何绘制的。首先我们要将实际的问题抽象成我们需要的元素。就是要找到状态转移图所需要的输入、输出和状态分别对应着实际问题的哪些部分,分析结果如下:
4. 输入:投入一元硬币;
5. 输出:出可乐、不出可乐;
6. 状态:可乐机中有0元、可乐机中有1元、可乐机中有2元、可乐机中有3元。

根据这些抽象出的要素我们就可以绘制状态转移图了,首先我们根据分析的状态数先画出4个状态。如下图所示,每个状态我们取一个有意义的名字,可乐机中有0元的状态是最原始的状态我们称为IDLE状态,可乐机中有1元状态我们取名为ONE,可乐机中有2元的状态取名为TWO,可乐机中有3元的状态取名为THREE。
在这里插入图片描述
我们从第一个IDLE状态开始分析,从初始状态开始进行跳转。在IDLE状态下有两种情况,一种情况是我们什么也不干为0,状态还是继续维持在IDLE,另一种情况是我们投入1元钱,即在IDLE状态下的输入为1,此时并不会输出可乐,但是状态条状到ONE状态了。初始状态IDLE的跳转只有一项分析的这两种情况,有人可能还会说输入还有复位键呢,也会影响状态的跳转,为什么没有在状态转移图种表达呢?因为我们在状态图中使用异步复位,执行复位操作后直接跳转到初始状态,所以不用在用到状态转移图中单独表达。
如下图所示。
在这里插入图片描述
接着我们该分析ONE状态跳转的情况了。在ONE状态下同样有两种情况,一种情况是你没有再继续投钱而选择离开,需要按一下复位按键回到初始状态等待下一个人从初始状态开始继续投币(代码种我们没有退回硬币的输出,只将状态机回到初始状态),如果没有按复位下一个人可以继续在你之前的投币的基础上继续计数。另一种情况是可乐机种已经有1元钱的情况下再投入1元钱,即再ONE状态下的输入为1,此时并不会输出可乐,所以在该状态下的输出为0,而状态则是跳转到TWO状态了。那么从ONE状态可以跳转的情况也只有两种了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
有人可能会有疑问了,上图这两种状态转移图都是对的吗?直白告诉你这两种状态转移图都是对的,不要忘了我们一开始讲的存在两种状态机,一种是Moore状态机,一种是Mealy状态机。最后的输出只和当前状态有关而与输入无关则Moore状态机,图23-11就是Moore型的。最后的输出不仅和当前状态有关还和输入有关则称为Mealy状态机,图23-6所表达的状态转移图就是Mealy型的。

我们再绘制波形图,首先是三个输入信号,我们随机模拟输入信号pi_money 的输入情况,根据状态转移图来分析继续绘制波形。因为有不同的状态之间的跳转关系,所以我们需要一个用于表示状态的变量,一般都取一个名为state的状态变量,state处于哪个状态、何时跳转都需要根据输入信号pi_money 来决定,而输出信号 po_cola 的结果则由输入 pi_money 和当前state的状态共同决定。

在这里插入图片描述

2.4 代码编写

简单可乐机参考代码:

//  第一部分
module simple_fsm(
    input wire sys_clk,                 // 系统时钟50Mhz
    input wire sys_rst_n,
    input wire pi_money,                // 投币方式可以分为,不投币0 投币1

    output reg po_cola                  // po_cola = 1 出可乐 =0 不出可乐
);
// 第二部分状态编码
//  parameter define
// 只有三种状态使用独热码
parameter IDLE = 3'b001;
parameter ONE = 3'b010;
parameter TWO  = 3'b100;

// 第三部分状态变量
// reg define
reg [2:0] state;


// 第四部分 状态机
// 第一段状态机,描述当前状态state如何根据输入跳转到下一个状态
always @(posedge sys_clk or negedge sys_rst_n) begin

    if(sys_rst_n == 1'b0)
        state <= IDLE;                  // 任何情况下只要按下复位键就回到初始状态
    else begin
        case(state)
            IDLE: if (pi_money == 1'b1) begin
                    state <= ONE;
                end
                else begin
                    state <= IDLE;    
                end
            ONE: if (pi_money == 1'b1) begin
                     state <= TWO;   
                end
                else begin
                     state <= ONE; 
                end
            TWO: if (pi_money == 1'b1) begin
                     state <= IDLE;
                end 
                else begin
                    state <= TWO;
                end
            // 如果状态机跳转到编码的状态之外也回到初始状态
            default:    state <= IDLE;
        endcase        
    end
    
end

// 第五部分 输出
// 第二段状态机。描述当前状态 state 和输入pi_money 如何影响po_cola输出
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (sys_rst_n == 1'b0) begin
        po_cola <= 1'b0;
    end
    else if ((state == TWO) && (pi_money == 1'b1)) begin
        po_cola <= 1'b1;
    end
    else begin
        po_cola <= 1'b0;
    end
    
end




//<statements>

endmodule


上面是一个用verilog描述的简单状态机,我们可以发现它是按照我们总结好的一套格式来编写的。我们按照这种格式再结合状态转移图可以编写出更复杂的状态机代码。所以我们总结一下我们套用的格式有哪些主要部分构成:其中01-09行是端口列表部分;17-19行是状态编码部分;22行是定义的状态变量;后面是第一段状态机部分;后面是第二段状态机部分。一共五部分。我们写状态机的时候根据这5部分对照着状态机依次编写,非常容易的可以实现。
在这里插入图片描述
第四部分和第五部分也是有联系的,也是状态机中最关键的部分,综合器能不能将RTL代码综合为状态机的样子主要看这部分代码如何实现的。大家看到我们使用的代码是二段式状态机,但是又感觉怪怪的,我们描述的状态机之所以和其他资料有所区别,其实是使用了新的写法。很多人都见过一段式、二段式、三段式(一段式指的是在一段状态机中使用时序逻辑即描述状态的转移,也描述数据的输出;二段式指在第一段状态机中使用时序逻辑描述状态转移,在第二段状态机中采用组合逻辑描述数据的输出;三段式指的是第一段状态机中采用时序逻辑描述状态转移,在第二段状态机中采用组合逻辑判断状态转移条件描述状态转移规律,在第三段状态机中描述状态输出,可以用组合电路输出,也可以用时序电路输出)。
老的一段式子、二段式、三段式,各有优缺点。我们主要是把三段式状态机的第一段状态机是用时序逻辑描述当前状态,第二段状态机用组合逻辑描述下一个状态,如果把这两个部分进行合并而第三段状态机保持不变,就是我们现在最新的写法。这种写法在现在不同综合器中都可以被识别出来,这样即消除了组合逻辑可能产生的毛刺,又减小了代码量,还更容易上手,不必去关心理论模型是怎样的,仅仅根据状态转移图就非常容易实现。
所以我们习惯采用两个时序逻辑的always块,第一个always块描述状态的转移为第一段状态机,第二个always块描述数据的输出为第二段状态机(如果我们遵守一个always块只描述一个变量的原则,如果有多个输出时第二段状态机就可以分为多个always块来表达,但理论上还是新二段状态机,所以几段状态机并不是由always块的数量简单决定的)

2.5 仿真验证

//


`timescale 1ns/1ns

module tb_simple_fsm;

// reg define
reg sys_clk;
reg sys_rst_n;
reg pi_money;

// wire define
wire po_cola;

// main code
// initial sys clk and rst
initial begin
    sys_clk = 1'b1;
    sys_rst_n <= 1'b0;
    #20
    sys_rst_n <= 1'b1;
end

// sys clk: 10ns , T 20ns , 50Mhz
always #10 sys_clk = ~sys_clk;

// pi_money: set random number 
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(sys_rst_n == 1'b0) begin
        pi_money <= 1'b0;
    end
    else begin
        pi_money <= {
    
    $random} % 2;   // random number 0 or 1
    end
end


// 将RTL模块的内部信号引入到Testbench模块中进行观察

wire [2:0] state = simple_fsm_0.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

//
// Instantiate Unit Under Test:  simple_fsm
//
simple_fsm simple_fsm_0 (
    // Inputs
    .sys_clk(sys_clk),
    .sys_rst_n(sys_rst_n),
    .pi_money(pi_money),

    // Outputs
    .po_cola(po_cola)

    // Inouts

);

endmodule


在这里插入图片描述
在这里插入图片描述

3 实战演练升级版

3.1 实验目标

可乐定价为2.5元一瓶,可能投入0.5元、1元硬币、投币不够2.5元需要按复位键退回钱款,投币超过2.5元需要找零

3.2 模块框图

在这里插入图片描述
在这里插入图片描述

3.3 状态转移图与波形图的绘制

在绘制状态转移图时我们仍套用上一节中的三要素法来分析。首先部门要将实际的问题抽象成我们需要的元素,找到实际问题中对应状态转移图所需要的输入、输出和状态的部分。

  1. 输入:投入0.5元硬币、投入1元硬币
  2. 输出:不出可乐/ 不找零、出可乐/不找零、出可乐/找零
  3. 状态:可乐机中有0元、可乐机中有0.5元、可乐机中有1元、可乐机中有1.5元、可乐机中有2元、可乐机中有2.5元、可乐机中有3元。

根据这些抽象出的要素我们就可以会只

在这里插入图片描述
大家在画状态转移图的时候容易出现状态跳转情况遗漏的问题,这里我们给大家总结一个小技巧:我们可以观察到,输入有多少种情况(上一节是两种输入情况,本节是三种输入情况),每个状态的跳转就有多少种情况(上一节每个状态都有两种跳转情况,本届每个状态有三种跳转情况)

我们根据状态转移图将波形图画出,首先是四个输入信号,我们随机模拟输入信号 pi_money_one 和 pi_money_half 的组合到一起,取名为一个pi_money 的中间变量。然后在画出用于表示状态的状态变量state,根据输入信号pi_money 和状态变量 state 确定输出信号 po_cola 和 po_money 的波形,输出信号我们就不用子啊及逆行组合了单独输出即可。

在这里插入图片描述

3.4 代码和仿真

///
// Company: <Name>
//
// File: complex_fsm.v
// File history:
//      <Revision number>: <Date>: <Comments>
//      <Revision number>: <Date>: <Comments>
//      <Revision number>: <Date>: <Comments>
//
// Description: 
//
// <Description here>
//
// Targeted device: <Family::SmartFusion> <Die::A2F060M3E> <Package::288 CS>
// Author: <Name>
//
/// 

//`timescale <time_units> / <precision>

// 第一部分 模块信号
module complex_fsm( 
    input wire sys_clk, 
    input wire sys_rst_n, 
    input wire pi_money_half, 
    input wire pi_money_one, 
    
    output reg po_money, 
    output reg po_cola);

// 第二部分状态编码
parameter IDLE = 5'b00001;
parameter HALF = 5'b00010;
parameter ONE = 5'b00100;
parameter ONE_HALF = 5'b01000;
parameter TWO = 5'b10000;

// 第三部分状态变量和内部信号定义
reg [4:0] state;

wire [1:0] pi_money;  // pi_money 为了减少变量的个数,我们用位拼接把输入的两个1bit信号拼接成1个2bit信号

assign pi_money = {
    
    pi_money_one, pi_money_half}; // 投币方式可以为:不投币00、投0.5元01 、投1元 10,每次只投一个币


// 第四部分 状态机   
// 第一段
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(sys_rst_n == 1'b0) begin
        state <= IDLE;                           //  任何情况下只要复位 就回到初始状态
    end
    else case (state)
        IDLE :  if (pi_money == 2'b01) 
                    state <= HALF;
                else if(pi_money == 2'b10)
                    state <= ONE;
                else
                    state <= IDLE;
        
        HALF:   if(pi_money == 2'b01)
                    state <= ONE;
                else if(pi_money == 2'b10)
                    state <= ONE_HALF;
                else
                    state <= HALF;
        
        ONE:    if(pi_money == 2'b01)
                    state <= ONE_HALF;
                else if(pi_money == 2'b10)
                    state <= TWO;
                else
                    state <= ONE;

        ONE_HALF:   if(pi_money == 2'b01)
                        state <= ONE_HALF;
                    else if(pi_money == 2'b10)
                        state <= IDLE;
                    else
                        state <= ONE_HALF;
        
        TWO:    if(pi_money == 2'b01)
                    state <= IDLE;
                else if(pi_money == 2'b10)
                    state <= IDLE;
                else 
                    state <= TWO;
        
        default:    state <= IDLE; 
    endcase
end


// 第二段状态机,描述当前状态state和输入pi_money 如何影响po_cola输出

always @(posedge sys_clk or negedge sys_rst_n) begin
    if(sys_rst_n == 1'b0)
        po_cola <= 1'b0;
    else if((state == TWO && pi_money == 2'b01) || (state == TWO && pi_money == 2'b10) || (state == ONE_HALF && pi_money == 2'b10))
        po_cola <= 1'b1;
    else
        po_cola <= 1'b0;
end

// 第二段状态机,描述当前状态state和输入pi_money 如何影响po_money输出

always @(posedge sys_clk or negedge sys_rst_n) begin
    if(sys_rst_n == 1'b0)
        po_money <= 1'b0;
    else if((state == TWO && pi_money == 2'b10))
        po_money <= 1'b1;
    else
        po_money <= 1'b0;
end
    


endmodule


tb文件




`timescale 1ns/1ns

module tb_complex_fsm();


// reg define

reg sys_clk;
reg sys_rst_n;
reg pi_money_one;
reg pi_money_half;

reg random_data_gen;

// wire define
wire po_cola;
wire po_money;

// main code

// 初始化系统时钟、全局复位
initial begin
    sys_clk = 1'b1;
    sys_rst_n <= 1'b0;
    #20
    sys_rst_n <= 1'b1;
end

// 模拟系统时钟 每19ns电平翻转依次 周期为20ns 频率为50Mhz
always #10 sys_clk = ~sys_clk;

// random_data_gen: 产生非负随机数 0 1 
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(sys_rst_n == 1'b0)
        random_data_gen <= 1'b0;
    else
        random_data_gen <= {
    
    $random} % 2;
end

// pi_money_one 模拟投入1元的情况
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(sys_rst_n == 1'b0)
        pi_money_one <= 1'b0;
    else
        pi_money_one <= random_data_gen;
end

// pi_money_half: 模拟投入0.5元的情况
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(sys_rst_n == 1'b0)
        pi_money_half <= 1'b0;
    else
        pi_money_half <= ~random_data_gen;
end

// 将RTL模块中的内部信号引入到Testbench模块中进行观察
wire [4:0] state = complex_fsm_0.state;
wire [1:0] pi_money = complex_fsm_0.pi_money;

initial begin
    $timeformat(-9, 0, "ns", 6);
    $monitor("@time %t: pi_money_one=%b pi_money_half=%b pi_money=%b state=%b po_cola=%b po_money=%b", $time, pi_money_one, pi_money_half, pi_money, state, po_cola, po_money);
end

complex_fsm complex_fsm_0(
    .sys_clk(sys_clk),
    .sys_rst_n(sys_rst_n),
    .pi_money_one(pi_money_one),
    .pi_money_half(pi_money_half),

    .po_cola(po_cola),
    .po_money(po_money)
);

endmodule

在这里插入图片描述
在这里插入图片描述

4 总结

  1. 首先分析实际问题,然后抽象出我们设计的状态机系统所需要的输入、输出有哪些,以及每个状态都是什么
  2. 根据分析绘制状态转移图,状态转移图是可以简化的,我们一般简化到最少状态
  3. 根据状态转移图编写代码,代码的编写也是有古松套路的,我们也进行了方法总结
  4. 通过综合器综合的状态转移图以及Modelsim仿真验证状态机的设计

小作业:
在这里插入图片描述

参考:FPGA Verilog 开发实战指南

猜你喜欢

转载自blog.csdn.net/qq_30093417/article/details/127367728