FPGA状态机(读书笔记)

1,486 阅读9分钟

为什么使用状态机

  1. 高效的顺序控制模型;
  2. 容易利用现成的EDA工具进行优化设计;
  3. 性能稳定,状态机容易构成性能良好的同步时序逻辑模块,这对于解决大规模逻辑电路设计中的竞争和冒险现象大有益处。与其他方案相比,在消除电路的毛刺现象、强化系统稳定性方面,FSM使设计者拥有更多的解决方法;
  4. 高速性能,在高速通信和高速控制方面,状态机更具有巨大优势;在顺序控制方面,一个状态机的功能更类似于CPU;
  5. 高可靠性。状态机由纯硬件电路构成,运行不依赖软件指令的逐条执行,因此不存在CPU运行软件过程中许多固有缺陷;在状态机的设计中能够使用各种容错技术;当状态机进入非法状态并从中挑出进入正常状态所需时间十分短暂,通常只需要2-3个时钟既数十纳秒,对系统不会构成较大的危害。
  • Mealy 状态机:输出由当前状态及输入决定。

image.png

  • Moore 状态机:输出仅由当前状态决定。

image.png

为什么使用三段式状态机

  • FSM和其他设计一样,最好使用同步时序方法设计,以提高设计的稳定性,消除毛刺。
  • 状态机实现后,一般来说,状态转换部分是同步时序电路,而状态转换条件的判断是组合逻辑。两段式之所以比一段式编码合理,就在于两段编码将同步时序和组合逻辑分别放到不同的always程序块中实现。这样做不仅式便于阅读、理解、维护,更重要的是利于综合器优化代码、利于用户添加合适的时序约束条件、利于布局布线器实现设计。而一段式FSM描述不利于时序约束、功能更改、调试等,而且不能很好的表示米勒型FSM输出,容易写出Latches,导致逻辑功能错误。
  • 在一般的两段式描述中,为了便于描述当前状态的输出,很多设计者习惯将当前的输出用组合逻辑实现。这种组合逻辑仍然有可能产生毛刺,而且不利于约束、不利于综合器和布局布线器实现高性能的设计。因此如果设计允许额外的一个时钟节拍插入,则要求尽量对状态机的输出用寄存器寄存一拍。
  • 如果实际不允许插入语一个寄存节拍,此时可以通过三段式描述方法进行解决。三段与两段相比,关键在于根据状态转移规律,在上一状态根据输入条件判断当前状态的输出,从而在不插入额外时钟节拍的前提下实现了寄存器输出
  • 不能说一段FSM的描述中,使用了n个always语句块就是n段式描述方法。从语法角度说,可以将一个always模块拆分成多个always模块,或者反之将多个always模块合并为一个always模块。n段式FSM描述方法强调的是一种建模思路,绝不是简单的always语句块个数

三种状态机建模

image.png

image.png

image.png

各种建模方法之间的关系

一段式与三段式

  • 将三段式状态机中的组合逻辑合并起来,就和一段式建模方式一样了。所以这两种建模方式最大的区别在于:使用一段式建模FSM的寄存器输出时,必须要综合考虑现态在何种状态转移条件下会进入哪些次态。然后在每个现态的case分支下分别描述每个次态的输出,这不符合思维习惯。三段式建模描述FSM的输出时,只需要指定case敏感表为次态寄存器,然后直接在每个次态的case分支中描述该状态输出即可,不用考虑状态转移条件。 image.png

两段式与三段式

  • 从代码上看,三段式建模的前两段与两段式建模完全相同,仅仅多了一段寄存器FSM输出。一般来说,使用寄存器输出可以改善输出时序条件,还能避免组合电路的毛刺,所以更推荐。
  • 但电路设计不是一成不变的,在某些情况下,两段式结构比三段式结构更有优势。对比这种状态机建模图,两段式用状态寄存器分割了两部分组合逻辑(状态转换条件组合逻辑和输出组合逻辑);三段式结构中,从输入到寄存器状态输出的路径上,这两部分组合逻辑(状态转换条件组合逻辑和输出组合逻辑),从时序上,这两部分组合逻辑完全可以看为一体。这样这条路径的组合逻辑就比较繁杂,该路径的时序也相对紧张。
  • 两端式建模中用状态寄存器分割了组合逻辑,而三段式将寄存器移到组合逻辑的最后端。如果寄存器前的组合逻辑过于复杂,势必会成为整个设计的关键路径,此时就不宜再使用三段式建模,而使用两段式建模。解决两段式建模组合逻辑产生毛刺的方法是,额外的在FSM后级插入寄存器,调整时序,完成功能。

状态机设计技巧

编码

Binary(二进制编码)、gray-code(格雷码)使用最少的触发器,较多的组合逻辑,one-hot(独热码)反之。CPLD组合逻辑资源多,使用gray-code;FPGA更多的触发器资源,使用one-hot。

二进制(顺序)编码 格雷码缺点

使用顺序编码,从"01"到"10"状态转换过程中可能会出现过度状态"00"、"11"。这是因为中间信号在状态转过程中,状态寄存器的高位和低位翻转时间有可能不一致,高位翻转速度快,会产生过渡态"00",反之会产生过渡态"00"。如果使用格雷码,由于相邻两个状态之间只有一位不同,所以可以很大程度上消除由延时引起的过渡状态。但如果一个状态到下一个状态有多种转换路径时,就不能保证跳转时仅有一个位变化。所以一般采用独热码。

当然格雷码也有两个特点:异步输出、低功率器件

如果状态机的输出或者状态机操作的任何逻辑是异步的,通常最好使用格雷码。由于异步电路不能防止竞争条件和毛刺,因此状态寄存器中两位之间的路径不同可能引起不可预测的行为,该行为与布局配置和寄生参数有关。格雷码对每次状态转换只经历一个单位的反转,因此可以消除异步逻辑内的竞争条件,除此之外,FPGA最好使用独热码,

FSM的初始化状态

一个完备的状态机(健壮性)应该具备初始化状态和默认状态。当芯片加电或者复位后,状态机应该能够自动将所有判断条件复位,并进入初始化状态。需要注明的一点是,大多是FPGA都有GSR(Globe Set/Reset)信号,当FPGA加电后,GSR信号拉高,对所有的寄存器、RAM等单元复位/置位,这时配置于FPGA的逻辑并未生效,所以不能保证正确进入初始化状态。所以使用GSR企图进入FPGA的初始化状态,常常会产生种种不必要的麻烦.一般方法是采用异步复位信号,当然也可以使用同步复位,但要注意同步复位逻辑的设计.解决这个问题的另一种方法是将默认的初始状态编码设置为全零,这样GSR复位后,状态机自动进入初始状态.

FSM的默认状态

完整的状态机还应该包含一个默认(default)状态,当转移条件不满足,或者状态发生了突变,要保证逻辑不会陷入"死循环".这是对状态机健壮性的一个重要要求,也就是常说的"自恢复"功能.对if...else语句使用完备的条件判断语句.case语句要用default建立默认状态.可以添加一个额外的default状态,一旦进入这个状态就自动转入IDLE状态,重新启动状态机.

FSM输出

两段式FSM描述Mealy状态机,输出逻辑可以用"?"语句描述,或者使用case语句判断转移条件与输入信号即可.输出条件比较复杂,而且多个状态共用某些输出,则建议使用task/endtask将输出封装起来,达到模块复用的目的.

状态机示例

一段式状态机示例

module state1 ( nrst,clk,
                i1,i2,
                o1,o2,
                err
               );

input          nrst,clk;
input          i1,i2;
output         o1,o2,err;
reg            o1,o2,err;

reg    [2:0]   NS; //NextState

parameter [2:0]      //one hot with zero idle
      IDLE   = 3'b000,
      S1     = 3'b001,
      S2     = 3'b010,
      ERROR  = 3'b100;

//1 always block to describe state transition, state output, state input condition
always @ (posedge clk or negedge nrst)
 if (!nrst)
    begin
       NS         <= IDLE;
      {o1,o2,err} <= 3'b000;
    end
 else
    begin
       NS         <=  3'bx;
      {o1,o2,err} <=  3'b000;
      case (NS)
        IDLE:  begin
                 if (~i1)         begin{o1,o2,err}<=3'b000;NS <= IDLE; end
                 if (i1 && i2)    begin{o1,o2,err}<=3'b100;NS <= S1;   end
                 if (i1 && ~i2)   begin{o1,o2,err}<=3'b111;NS <= ERROR;end
               end
        S1:    begin
                 if (~i2)         begin{o1,o2,err}<=3'b100;NS <= S1;   end
                 if (i2 && i1)    begin{o1,o2,err}<=3'b010;NS <= S2;   end
                 if (i2 && (~i1)) begin{o1,o2,err}<=3'b111;NS <= ERROR;end
               end
        S2:    begin
                 if (i2)          begin{o1,o2,err}<=3'b010;NS <= S2;   end
                 if (~i2 && i1)   begin{o1,o2,err}<=3'b000;NS <= IDLE; end
                 if (~i2 && (~i1))begin{o1,o2,err}<=3'b111;NS <= ERROR;end
               end
        ERROR: begin
                 if (i1)          begin{o1,o2,err}<=3'b111;NS <= ERROR;end
                 if (~i1)         begin{o1,o2,err}<=3'b000;NS <= IDLE; end
               end
      endcase
   end

endmodule

两段式状态机示例

module state2 ( nrst,clk,
                i1,i2,
                o1,o2,
                err
               );
         
input          nrst,clk;
input          i1,i2;
output         o1,o2,err;
reg            o1,o2,err;

reg    [2:0]   NS,CS;

parameter [2:0]      //one hot with zero idle
      IDLE   = 3'b000,
      S1     = 3'b001,
      S2     = 3'b010,
      ERROR  = 3'b100;

//sequential state transition
always @ (posedge clk or negedge nrst)
      if (!nrst)            
         CS <= IDLE;        
      else                  
         CS <=NS;           

//combinational condition judgment
always @ (nrst or CS or i1 or i2)
          begin
               NS = 3'bx;
               ERROR_out;
               case (CS)
                    IDLE:     begin
                                   IDLE_out;
                                   if (~i1)           NS = IDLE;
                                   if (i1 && i2)      NS = S1;
                                   if (i1 && ~i2)     NS = ERROR;
                              end
                    S1:       begin
                                   S1_out;
                                   if (~i2)           NS = S1;
                                   if (i2 && i1)      NS = S2;
                                   if (i2 && (~i1))   NS = ERROR;
                              end
                    S2:       begin
                                   S2_out;
                                   if (i2)            NS = S2;
			           if (~i2 && i1)     NS = IDLE;
                                   if (~i2 && (~i1))  NS = ERROR;
                              end
                    ERROR:    begin
                                   ERROR_out;
                                   if (i1)            NS = ERROR;
                                   if (~i1)           NS = IDLE;
                              end
               endcase
         end

//output task
task IDLE_out;
     {o1,o2,err} = 3'b000;
endtask

task S1_out;
     {o1,o2,err} = 3'b100;
endtask

task S2_out;
     {o1,o2,err} = 3'b010;
endtask

task ERROR_out;
     {o1,o2,err} = 3'b111;
endtask

endmodule

三段式状态机示例

三段式状态机的输出态可以是组合逻辑也可以是时序逻辑,一般使用时序逻辑,可避免电路毛刺。输出同步进程通常是case(现态),下例是书中例程,是case(次态)

module state3 ( nrst,clk,
                i1,i2,
                o1,o2,
                err
               );
         
input          nrst,clk;
input          i1,i2;
output         o1,o2,err;
reg            o1,o2,err;

reg    [2:0]   NS,CS;

parameter [2:0]      //one hot with zero idle
      IDLE   = 3'b000,
      S1     = 3'b001,
      S2     = 3'b010,
      ERROR  = 3'b100;

//1st always block, sequential state transition
always @ (posedge clk or negedge nrst)
      if (!nrst)            
         CS <= IDLE;        
      else                  
         CS <=NS;           

//2nd always block, combinational condition judgment
always @ (nrst or CS or i1 or i2)
          begin
               NS = 3'bx;
               case (CS)
                    IDLE:     begin
                                   if (~i1)           NS = IDLE;
                                   if (i1 && i2)      NS = S1;
                                   if (i1 && ~i2)     NS = ERROR;
                              end
                    S1:       begin
                                   if (~i2)           NS = S1;
                                   if (i2 && i1)      NS = S2;
                                   if (i2 && (~i1))   NS = ERROR;
                              end
                    S2:       begin
                                   if (i2)            NS = S2;
			           if (~i2 && i1)     NS = IDLE;
                                   if (~i2 && (~i1))  NS = ERROR;
                              end
                    ERROR:    begin
                                   if (i1)            NS = ERROR;
                                   if (~i1)           NS = IDLE;
                              end
               endcase
         end

//3rd always block, the sequential FSM output
always @ (posedge clk or negedge nrst)
 if (!nrst)
      {o1,o2,err} <= 3'b000;
 else
    begin
       {o1,o2,err} <=  3'b000;
       case (NS)
           IDLE:  {o1,o2,err}<=3'b000;

           S1:    {o1,o2,err}<=3'b100;
           S2:    {o1,o2,err}<=3'b010;
           ERROR: {o1,o2,err}<=3'b111;
       endcase
    end

endmodule

ADI-AD7980示例代码

两个always块,三段式的思路

// -----------------------------------------------------------------------------
// KEYWORDS : AD7980
// -----------------------------------------------------------------------------
// PURPOSE : Driver for the AD7980  16-Bit, 1 MSPS PulSAR ADC in MSOP/QFN
// -----------------------------------------------------------------------------
// REUSE ISSUES        
// Reset Strategy      : Active low reset signal
// Clock Domains       : 2 clocks - the system clock that drives the internal logic 
//                     : and a clock for ADC conversions
// Critical Timing     : N/A
// Test Features       : N/A
// Asynchronous I/F    : N/A
// Instantiations      : N/A
// Synthesizable (y/n) : Y
// Target Device       : AD7980
// Other               : The driver is intended to be used for AD7980 ADCs configured
//                     : in /CS MODE, 3-WIRE, WITHOUT BUSY INDICATOR 
// -----------------------------------------------------------------------------

`timescale 1ns/1ns //Use a timescale that is best for simulation.

//----------- Module Declaration -----------------------------------------------  

module AD7980
//----------- Ports Declarations -----------------------------------------------
(
    //clock and reset signals
    input               fpga_clk_i,      //system clock
    input               adc_clk_i,       //clock to be applied to ADC to read the conversions results
    input               reset_n_i,       //active low reset signal
    
    //IP control and data interface
    output     [15:0]   data_o,          //data read from the ADC
    output reg          data_rd_ready_o, //when set to high the data read from the ADC is available on the data_o bus
    
    //ADC control and data interface
    input               adc_sdo,        //ADC SDO signal
    input               adc_sdi,        //ADC SDI signal (not used in 3-WIRE mode)
    output              adc_sclk_o,     //ADC serial clock
    output              adc_cnv_o       //ADC CNV signal
);

//----------- Registers Declarations -------------------------------------------
reg [ 3:0]  adc_state;      //current state for the ADC control state machine
reg [ 3:0]  adc_next_state; //next state for the ADC control state machine
reg [ 3:0]  adc_state_m1;   //current state for the ADC control state machine in the ADC clock domain

reg [ 6:0]  adc_tcycle_cnt; //counts the number of FPGA clock cycles to determine when an ADC cycle is complete
reg [ 6:0]  adc_tcnv_cnt;   //counts the number of FPGA clock cycles to determine when an ADC conversion is complete
reg [ 4:0]  sclk_clk_cnt;   //counts the number of clocks applied to the ADC to read the conversion result

reg         adc_clk_en;     //gating signal for the clock sent to the ADC
reg         adc_cnv_s;      //internal signal used to hold the state of the ADC CNV signal
reg [15:0]  adc_data_s;     //interal register used to store the data read from the ADC

//----------- Wires Declarations -----------------------------------------------
wire        adc_sclk_s;     //internal signal for the clock sent to the ADC

//----------- Local Parameters -------------------------------------------------
//ADC states
parameter ADC_IDLE_STATE            = 4'b0001;
parameter ADC_START_CNV_STATE       = 4'b0010;
parameter ADC_END_CNV_STATE         = 4'b0100;
parameter ADC_READ_CNV_RESULT       = 4'b1000;

//ADC timing
parameter real FPGA_CLOCK_FREQ  = 100000000;    //FPGA clock frequency [Hz]
parameter real ADC_CYCLE_TIME   = 0.000001000;  //minimum time between two ADC conversions (Tcyc) [s]
parameter real ADC_CONV_TIME    = 0.000000670;  //conversion time (Tcnvh) [s]
parameter [6:0] ADC_CYCLE_CNT   = FPGA_CLOCK_FREQ * ADC_CYCLE_TIME - 1;
parameter [6:0] ADC_CNV_CNT     = FPGA_CLOCK_FREQ * ADC_CONV_TIME;

//ADC serial clock periods
parameter ADC_SCLK_PERIODS  = 5'd15; //number of clocks to be sent to the ADC to read the conversion result

//----------- Assign/Always Blocks ---------------------------------------------
assign adc_cnv_o    = adc_cnv_s;
assign adc_sclk_s   = adc_clk_i & adc_clk_en;
assign adc_sclk_o   = adc_sclk_s;
assign data_o       = adc_data_s;

//update the ADC timing counters
always @(posedge fpga_clk_i)
begin
    if(reset_n_i == 1'b0)
    begin
        adc_tcycle_cnt  <= 0;
        adc_tcnv_cnt    <= ADC_CNV_CNT;
    end
    else
    begin
        if(adc_tcycle_cnt != 0)
        begin
            adc_tcycle_cnt <= adc_tcycle_cnt - 1;
        end
        else if(adc_state == ADC_IDLE_STATE)
        begin
            adc_tcycle_cnt <= ADC_CYCLE_CNT;
        end
        
        if(adc_state == ADC_START_CNV_STATE)
        begin
            adc_tcnv_cnt <= adc_tcnv_cnt - 1;
        end
        else
        begin
           adc_tcnv_cnt <= ADC_CNV_CNT;
        end
    end    
end

//read data from the ADC
always @(negedge adc_clk_i)
begin
    if(adc_clk_en == 1'b1)
    begin
        adc_data_s   <= {adc_data_s[14:0], adc_sdo};
        sclk_clk_cnt <= sclk_clk_cnt - 1;
    end
    else
    begin
        sclk_clk_cnt <= ADC_SCLK_PERIODS;	
    end
end

//determine when the ADC clock is valid to be sent to the ADC
always @(negedge adc_clk_i)
begin
    adc_state_m1 <= adc_state;
    adc_clk_en   <= ((adc_state_m1 == ADC_END_CNV_STATE) || (adc_state_m1 == ADC_READ_CNV_RESULT) && (sclk_clk_cnt != 0)) ? 1'b1 : 1'b0;
end

//update the ADC current state and the control signals
always @(posedge fpga_clk_i)
begin
    if(reset_n_i == 1'b0)
    begin
        adc_state <= ADC_IDLE_STATE;
    end
    else
    begin
        adc_state <= adc_next_state;
        case (adc_state)
            ADC_IDLE_STATE:
            begin
                adc_cnv_s       <= 1'b0;
                data_rd_ready_o <= 1'b0;
            end
            ADC_START_CNV_STATE:
            begin
                adc_cnv_s       <= 1'b1;
                data_rd_ready_o <= 1'b1;
            end
            ADC_END_CNV_STATE:
            begin
                adc_cnv_s       <= 1'b0;
                data_rd_ready_o <= 1'b0;
            end
                ADC_READ_CNV_RESULT:
            begin
                adc_cnv_s       <= 1'b0;
                data_rd_ready_o <= 1'b0;
            end
        endcase
    end    
end

//update the ADC next state
always @(adc_state, adc_tcycle_cnt, adc_tcnv_cnt, sclk_clk_cnt)
begin
    adc_next_state <= adc_state;
    case (adc_state)
        ADC_IDLE_STATE:
        begin
            if(adc_tcycle_cnt == 0)
                begin
                    adc_next_state <= ADC_START_CNV_STATE;
                end
        end
        ADC_START_CNV_STATE:
        begin
            if(adc_tcnv_cnt == 0)
            begin
                adc_next_state <= ADC_END_CNV_STATE;   
            end
        end
        ADC_END_CNV_STATE:
        begin
            adc_next_state <= ADC_READ_CNV_RESULT;
        end
        ADC_READ_CNV_RESULT:
        begin
            if(sclk_clk_cnt == 1)
            begin
                adc_next_state <= ADC_IDLE_STATE;
            end
        end
        default:
        begin
            adc_next_state <= ADC_IDLE_STATE;
        end
    endcase
end

endmodule

另一种状态机机写法

module  simple_fsm(
     input  wire  sys_clk    ,   //系统时钟50MHz
     input  wire  sys_rst_n  ,   //全局复位
     input  wire  pi_money   ,   //投币方式可以为:不投币(0)、投1元(1)
     outputreg    po_cola       //po_cola为1时出可乐,po_cola为0时不出可乐 
);

//只有三种状态,使用独热码
parameter  IDLE =3'b001;
parameter  ONE  =3'b010;
parameter  TWO  =3'b100;

reg[2:0]  state;           

//第一段状态机,描述当前状态state如何根据输入跳转到下一状态
always@(posedge sys_clk ornegedge 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 ornegedge 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 

公众号-达尔闻里的解释

状态机中最为关键的部分,综合器能不能将RTL代码综合为状态机的样子主要看这部分代码如何来实现的。大家看到代码使用的是二段式状态机,但是又感觉怪怪的,这个状态机之所以和其他资料上的有所区别,其实是使用了新的写法。很多人都见过其他资料上总结的状态机代码写法有一段式、二段式、三段式(一段式指的是在一段状态机中使用时序逻辑既描述状态的转移,也描述数据的输出;二段式指在第一段状态机中使用时序逻辑描述状态转移,在第二段状态机中使用组合逻辑描述数据的输出;三段式指在第一段状态机中采用时序逻辑描述状态转移,在第二段在状态机中采用组合逻辑判断状态转移条件描述状态转移规律,在第三段状态机中描述状态输出,可以用组合电路输出,也可以时序电路输出)。这种一段式、二段式、三段式其实都是之前经典的老写法,也是一些老工程师仍然习惯用的写法,老方法是根据状态机理论建立的模型抽象后设计的,其实现的代码要严格按照固定的格式来写代码,否则综合器将无法识别出你写的代码是个状态机,因为早期的开发工具只能识别出固定的状态机格式,如果不按照标准格式写代码综合器最后无法综合成为状态机的样子。这样往往增加了设计的难度,很多人学习的时候还要去了解理论模型,反复学习理解很久才能够设计好的状态机,所以需要我们改进。

王金明老师的<数字系统设计与Verilog HDL>相关叙述

状态机设计中包括三个对象
  • 当前状态,即现态(Current State,CS)
  • 下一个状态,即次态(Next State,NS)
  • 输出逻辑(Output Logic,OL)
描述状态机的方式
  • 三过程描述:CS NS OL各用一个always过程块描述
  • 双过程描述1:CS+NS OL
  • 双过程描述2:CS NS+OL
  • 单过程描述:CS+NS+OL

书中其他补充

同步复位与异步复位

  • 同步复位信号在时钟跳变的边沿到来时,对有限状态机进行复位操作,同时把复位值赋给输出信号并使有限状态机回到初始状态.在状态转移的开始部分加入对同步复位信号进行判断的if语句.这样,如果不指定输出信号的值,那么输出信号值将保持不变.这种情况会需要额外的寄存器来保持原值,从而增加了资源消耗,因此应该在if语句中指定输出信号的值.
  • 如果只需要在上电和系统错误时进行复位操作,那么异步复位方式比同步复位方式好.原因:同步复位方式占用较多的额外资源,而异步复位可以消除引入额外寄存器的可能性;而且带有异步复位信号的Verilog语言描述简单,只需要在描述状态寄存器的过程中引入异步复位信号即可.