目录
有限状态机(Finite-State Machine,FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。状态机不仅是一种电路的描述工具,而且也是一种思想方法,在电路设计的系统级和 RTL 级有着广泛的应用。
都说状态机是 FPGA 设计的灵魂,可见其重要之处,在 Verilog 的设计中,状态机其实可以等同于 if 语句和 case 语句,但是由于在某些情况下,状态的种类多且复杂,各种状态跳转起来非常麻烦,所以 一般利用状态机设计是一种可靠便捷的方法。
规范的状态机代码可以极大地提高设计效率, 在减少状态出错可能的同时缩短调试时间, 从而设计出稳健的系统。
在设计状态机时,最好能够满足以下要求:
Verilog 中状态机主要用于同步时序逻辑的设计,能够在有限个状态之间按要求和规律切换时序电路的状态。状态的切换方向不但取决于各个输入值,还取决于当前所在状态。 状态机可分为两类:
Moore 型状态机的输出只与当前状态有关,与当前输入无关。
输出会在一个完整的时钟周期内保持稳定,即使此时输入信号有变化,输出也不会变化。输入对输出的影响要到下一个时钟周期才能反映出来。这也是 Moore 型状态机的一个重要特点:输入与输出是隔离开来的。
Mealy 型状态机的输出,不仅与当前状态有关,还取决于当前的输入信号。
Mealy 型状态机的输出是在输入信号变化以后立刻发生变化,且输入变化可能出现在任何状态的时钟周期内。因此,同种逻辑下,Mealy 型状态机输出对输入的响应会比 Moore 型状态机早一个时钟周期。
根据设计需求画出状态转移图,确定使用状态机类型,并标注出各种输入输出信号,更有助于编程。一般使用最多的是 Mealy 型 3 段式状态机,下面用通过设计一个自动售卖机的具体实例来说明状态机的设计过程。
自动售卖机的功能描述如下:
饮料单价 2 元,该售卖机只能接受 0.5 元、1 元的硬币。考虑找零和出货。投币和出货过程都是一次一次的进行,不会出现一次性投入多币或一次性出货多瓶饮料的现象。每一轮售卖机接受投币、出货、找零完成后,才能进入到新的自动售卖状态。
该售卖机的工作状态转移图如下所示,包含了输入、输出信号状态。
其中,coin = 1 代表投入了 0.5 元硬币,coin = 2 代表投入了 1 元硬币。
状态机设计如下:
- module vending_machine_p3 (
- input clk ,
- input rstn ,
- input [1:0] coin ,
-
- output [1:0] change , //找零
- output sell //输出饮料
- );
-
- //machine state decode
- parameter IDLE = 3'd0 ;
- parameter GET05 = 3'd1 ;
- parameter GET10 = 3'd2 ;
- parameter GET15 = 3'd3 ;
-
-
-
- reg [2:0] st_next ;
- reg [2:0] st_cur ;
- reg [1:0] change_r ;
- reg sell_r ;
-
- //第一段状态机,时序逻辑 非阻塞赋值
- always @(posedge clk or negedge rstn) begin
- if (!rstn) begin
- st_cur <= 'b0 ;
- end
- else begin
- st_cur <= st_next ;
- end
- end
-
- //第二段状态机 ,组合逻辑 阻塞赋值
- always @(*) begin
- st_next = st_cur ; //如果条件选项考虑不全,可以赋初值消除latch
- case(st_cur)
- IDLE:
- case (coin)
- 2'b01: st_next = GET05 ;
- 2'b10: st_next = GET10 ;
- default: st_next = IDLE ;
- endcase
- GET05:
- case (coin)
- 2'b01: st_next = GET10 ;
- 2'b10: st_next = GET15 ;
- default: st_next = GET05 ;
- endcase
-
- GET10:
- case (coin)
- 2'b01: st_next = GET15 ;
- 2'b10: st_next = IDLE ;
- default: st_next = GET10 ;
- endcase
- GET15:
- case (coin)
- 2'b01,2'b10:
- st_next = IDLE ;
- default: st_next = GET15 ;
- endcase
- default: st_next = IDLE ;
- endcase
- end
-
- //第三段状态机,时序逻辑 非阻塞赋值
- always @(posedge clk or negedge rstn) begin
- if (!rstn) begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- else begin
- case (st_cur)
- IDLE:
- begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- GET05:
- begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- GET10:
- begin
- if (coin ==2'd2) begin
- change_r <= 2'b0 ;
- sell_r <= 1'b1 ;
- end
- else begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- end
- GET15:
- begin
- if (coin ==2'h1) begin
- change_r <= 2'b0 ;
- sell_r <= 1'b1 ;
- end
- else if (coin == 2'h2) begin
- change_r <= 2'b1 ;
- sell_r <= 1'b1 ;
- end
- else begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- end
- default:
- begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- endcase
- end
- end
-
- assign sell = sell_r ;
- assign change = change_r ;
-
- endmodule
testbench 设计如下。仿真中模拟了 4 种情景,分别是:
case1 对应连续输入 4 个 5 角硬币;case2 对应 1 元 - 5 角 - 1 元的投币顺序;case3 对应 5 角 - 1 元 - 5 角的投币顺序;case4 对应连续 3 个 5 角然后一个 1 元的投币顺序。
- `timescale 1ns/1ps
- module test ;
- reg clk;
- reg rstn ;
- reg [1:0] coin ;
- wire [1:0] change ;
- wire sell ;
-
- //clock generating
- parameter CYCLE_200MHz = 10 ;
- always begin
- clk = 0 ; #(CYCLE_200MHz/2) ;
- clk = 1 ; #(CYCLE_200MHz/2) ;
- end
-
- //motivation generating
- reg [9:0] buy_oper ;
- initial begin
- buy_oper = 'h0 ;
- coin = 2'h0 ;
- rstn = 1'b0 ;
- #8 rstn = 1'b1 ;
- @(negedge clk) ;
-
- //case(1) 0.5 -> 0.5 -> 0.5 -> 0.5
- #16 ;
- buy_oper = 10'b00_0101_0101 ;
- repeat(5) begin
- @(negedge clk) ;
- coin = buy_oper[1:0] ;
- buy_oper = buy_oper >> 2 ;
- end
- //case(2) 1 -> 0.5 -> 1, taking change
- #16 ;
- buy_oper = 10'b00_0010_0110 ;
- repeat(5) begin
- @(negedge clk) ;
- coin = buy_oper[1:0] ;
- buy_oper = buy_oper >> 2 ;
- end
-
- //case(3) 0.5 -> 1 -> 0.5
- #16 ;
- buy_oper = 10'b00_0001_1001 ;
- repeat(5) begin
- @(negedge clk) ;
- coin = buy_oper[1:0] ;
- buy_oper = buy_oper >> 2 ;
- end
- //case(4) 0.5 -> 0.5 -> 0.5 -> 1, taking change
- #16 ;
- buy_oper = 10'b00_1001_0101 ;
- repeat(5) begin
- @(negedge clk) ;
- coin = buy_oper[1:0] ;
- buy_oper = buy_oper >> 2 ;
- end
- end
-
- //(1) mealy state with 3-stage
- vending_machine_p3 u_mealy_p3 (
- .clk (clk),
- .rstn (rstn),
- .coin (coin),
- .change (change),
- .sell (sell)
- );
-
- //simulation finish
- always begin
- #100;
- if ($time >= 10000) $finish ;
- end
-
- endmodule
仿真结果如下:
由图可知,代表出货动作的信号 sell 都能在投币完毕后正常的拉高,而代表找零动作的信号 change 也都能根据输入的硬币场景输出正确的是否找零信号。
将 3 段式状态机 2、3 段描述合并,其他部分保持不变,状态机就变成了 2 段式描述。
修改部分如下:
- reg [1:0] change_r ;
- reg sell_r ;
- always @(*) begin
- case(st_cur)
- IDLE: begin
- change_r = 2'b0 ;
- sell_r = 1'b0 ;
- case (coin)
- 2'b01: st_next = GET05 ;
- 2'b10: st_next = GET10 ;
- default: st_next = IDLE ;
- endcase
- end
- GET05: begin
- change_r = 2'b0 ;
- sell_r = 1'b0 ;
- case (coin)
- 2'b01: st_next = GET10 ;
- 2'b10: st_next = GET15 ;
- default: st_next = GET05 ;
- endcase
- end
-
- GET10:
- case (coin)
- 2'b01: begin
- st_next = GET15 ;
- change_r = 2'b0 ;
- sell_r = 1'b0 ;
- end
- 2'b10: begin
- st_next = IDLE ;
- change_r = 2'b0 ;
- sell_r = 1'b1 ;
- end
- default: begin
- st_next = GET10 ;
- change_r = 2'b0 ;
- sell_r = 1'b0 ;
- end
- endcase
- GET15:
- case (coin)
- 2'b01: begin
- st_next = IDLE ;
- change_r = 2'b0 ;
- sell_r = 1'b1 ;
- end
- 2'b10: begin
- st_next = IDLE ;
- change_r = 2'b1 ;
- sell_r = 1'b1 ;
- end
- default: begin
- st_next = GET15 ;
- change_r = 2'b0 ;
- sell_r = 1'b0 ;
- end
- endcase
- default: begin
- st_next = IDLE ;
- change_r = 2'b0 ;
- sell_r = 1'b0 ;
- end
-
- endcase
- end
将上述修改的新模块例化到 3 段式的 testbench 中即可进行仿真,结果如下:
由图可知,出货信号 sell 和 找零信号 change 相对于 3 段式状态机输出提前了一个时钟周期,这是因为输出信号都是阻塞赋值导致的。
如图中红色圆圈部分,输出信号都出现了干扰脉冲,这是因为输入信号都是异步的,而且输出信号是组合逻辑输出,没有时钟驱动。
实际中,如果输入信号都是与时钟同步的,这种干扰脉冲是不会出现的。如果是异步输入信号,首先应当对信号进行同步。
将 3 段式状态机 1、 2、3 段描述合并,状态机就变成了 1 段式描述。
修改部分如下:
- reg [1:0] change_r ;
- reg sell_r ;
- always @(posedge clk or negedge rstn) begin
- if (!rstn) begin
- st_cur <= 'b0 ;
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- else begin
- case(st_cur)
-
- IDLE: begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- case (coin)
- 2'b01: st_cur <= GET05 ;
- 2'b10: st_cur <= GET10 ;
- endcase
- end
- GET05: begin
- case (coin)
- 2'b01: st_cur <= GET10 ;
- 2'b10: st_cur <= GET15 ;
- endcase
- end
-
- GET10:
- case (coin)
- 2'b01: st_cur <= GET15 ;
- 2'b10: begin
- st_cur <= IDLE ;
- sell_r <= 1'b1 ;
- end
- endcase
-
- GET15:
- case (coin)
- 2'b01: begin
- st_cur <= IDLE ;
- sell_r <= 1'b1 ;
- end
- 2'b10: begin
- st_cur <= IDLE ;
- change_r <= 2'b1 ;
- sell_r <= 1'b1 ;
- end
- endcase
-
- default: begin
- st_cur <= IDLE ;
- end
-
- endcase // case (st_cur)
- end
- end
将上述修改的新模块例化到 3 段式的 testbench 中即可进行仿真,结果如下:
由图可知,输出信号与 3 段式状态机完全一致。
1 段式状态机的缺点就是许多种逻辑糅合在一起,不易后期的维护。当状态机和输出信号较少时,可以尝试此种描述方式。
如果使用 Moore 型状态机描述售卖机的工作流程,那么还需要再增加 2 个状态编码,用以描述 Mealy 状态机输出时的输入信号和状态机状态。
3 段式 Moore 型状态机描述的自动售卖机 Verilog 代码如下:
- module vending_machine_moore (
- input clk ,
- input rstn ,
- input [1:0] coin ,
-
- output [1:0] change ,
- output sell
- );
-
- parameter IDLE = 3'd0 ;
- parameter GET05 = 3'd1 ;
- parameter GET10 = 3'd2 ;
- parameter GET15 = 3'd3 ;
-
- parameter GET20 = 3'd4 ;
- parameter GET25 = 3'd5 ;
-
- reg [2:0] st_next ;
- reg [2:0] st_cur ;
-
- //(1) state transfer
- always @(posedge clk or negedge rstn) begin
- if (!rstn) begin
- st_cur <= 'b0 ;
- end
- else begin
- st_cur <= st_next ;
- end
- end
-
- always @(*) begin //all case items need to be displayed completely
- case(st_cur)
- IDLE:
- case (coin)
- 2'b01: st_next = GET05 ;
- 2'b10: st_next = GET10 ;
- default: st_next = IDLE ;
- endcase
- GET05:
- case (coin)
- 2'b01: st_next = GET10 ;
- 2'b10: st_next = GET15 ;
- default: st_next = GET05 ;
- endcase
-
- GET10:
- case (coin)
- 2'b01: st_next = GET15 ;
- 2'b10: st_next = GET20 ;
- default: st_next = GET10 ;
- endcase
- GET15:
- case (coin)
- 2'b01: st_next = GET20 ;
- 2'b10: st_next = GET25 ;
- default: st_next = GET15 ;
- endcase
- GET20: st_next = IDLE ;
- GET25: st_next = IDLE ;
- default: st_next = IDLE ;
- endcase
- end
-
- reg [1:0] change_r ;
- reg sell_r ;
- always @(posedge clk or negedge rstn) begin
- if (!rstn) begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- else if (st_cur == GET20 ) begin
- sell_r <= 1'b1 ;
- end
- else if (st_cur == GET25) begin
- change_r <= 2'b1 ;
- sell_r <= 1'b1 ;
- end
- else begin
- change_r <= 2'b0 ;
- sell_r <= 1'b0 ;
- end
- end
- assign sell = sell_r ;
- assign change = change_r ;
-
- endmodule
将上述修改的 Moore 状态机例化到 3 段式的 testbench 中即可进行仿真,结果如下:
由图可知,输出信号与 Mealy 型 3 段式状态机相比延迟了一个时钟周期,这是因为进入到新增加的编码状态机时需要一个时钟周期的时延。此时,输出再用非阻塞赋值就会导致最终的输出信号延迟一个时钟周期。这也属于 Moore 型状态机的特点。
输出信号赋值时,用阻塞赋值,则可以提前一个时钟周期。
输出逻辑修改如下。
- reg [1:0] change_r ;
- reg sell_r ;
- always @(*) begin
- change_r = 'b0 ;
- sell_r = 'b0 ; //not list all condition, initializing them
- if (st_cur == GET20 ) begin
- sell_r = 1'b1 ;
- end
- else if (st_cur == GET25) begin
- change_r = 2'b1 ;
- sell_r = 1'b1 ;
- end
- end
输出信号阻塞赋值的仿真结果如下:
由图可知,输出信号已经和 3 段式 Mealy 型状态机一致。