状态机的Verilog写法
“硬件设计很讲究并行设计思想,虽然用Verilog描述的电路大都是并行实现的,但是对于实际的工程应用,往往需要让硬件来实现一些具有一定顺序的工作,这就要用到状态机思想。什么是状态机呢?简单的说,就是通过不同的状态迁移来完成一些特定的顺序逻辑。硬件的并行性决定了用Verilog描述的硬件实现(譬如不同的always语句)都是并行执行的,那么如果希望分多个时间完成一个任务,怎么办?也许可以用多个使能信号来衔接多个不同的模块,但是这样做多少显得繁琐。状态机的提出会大大简化这一工作。”
——《深入浅出玩转FPGA》
一、FPGA状态机基础
1.1、基本概念
FPGA状态机是一种能够描述对象在运行周期内的所有状态,以及从一个状态到另一个状态转换的过程的抽象模型。状态机可归纳为4个要素,即现态、条件、动作、次态。
①现态:当前所处的状态。
②条件:当一个条件被满足,将会触发一个动作,或者执行一次运行状态的变化。
③动作:条件满足后执行的动作。动作不是必需的,也可以直接迁移到新状态而不进行任何动作。
④次态:条件满足后要跳转到的新状态。其中,“次态”是相对于“现态”而言的,一旦被跳转后,“次态”就转变成新的“现态”了。
1.2、状态机分类
“其实这几种状态机之间,只要做一些改变,便可以从一种形式转变为另一种形式。把状态机精确的分为这类或那类,其实并不重要,重要的是设计者如何把握输出的结构能满足设计的整体目标,包括定时的准确性和灵活性。”
——夏宇闻《Verilog数字系统设计》
通常情况下,FPGA一般有两种类型:
- Moore型状态机:状态机的状态变化仅与当前状态有关;时序逻辑电路的输出只取决于当前状态。设计高速电路时常用此类状态机,把状态变化直接用作输出。
- Mealy型状态机:状态机的状态变化不仅与当前状态有关,还取决于单签的输入条件;时序逻辑的输出不但取决于状态还取决于输入。平常使用较多的的是此类状态机。
由于Mealy型状态机的输出与输入有关,输出信号很容易出现毛刺,所以一般采用Moore型状态机。
毛刺产生原因:state_n 会因为组合逻辑原因不断出现临时状态,这些状态是无效的,而输出也因为组合逻辑原因产生这些临时状态,即毛刺。
二、状态机的编码方式
状态机的参数定义采用的都是独热码,和格雷码相比,虽然独热码多用了触发器,但所用组合电路可以省一些,因而使电路的速度和可靠性有显著提高,而总的单元数并无显著增加。采用独热编码后有了多余的状态,就有一些不可达到的状态。为此在case语句的最后需要增加default分支向。这可以用默认项表示该项,也可以用确定项表示,以确保回到初始状态。一般综合器都可以通过综合指令的控制来合理地处理默认项。
2.1、独热码
独热码(One-hot)是一种状态编码方式,其特点是对于任意给定的状态,状态寄存器中只有1位为1,其余位都为0。使用独热码可以简化译码逻辑电路,因为状态机只需对寄存器中的一位进行译码,同时可用省下的面积抵消额外触发器占用的面积。相比于其他类型的有限状态机,加入更多的状态时,独热码的译码逻辑并不会变得更加复杂,速度仅取决于到某特定状态的转移数量。
此外,独热码还具有诸如设计简单、修改灵活、易于综合和调试等优点。但值得注意的是,相对于二进制码,独热码速度更快但占用面积较大。
2.2、格雷码
格雷码是一种相邻的两个码组之间仅有一位不同的编码方式。在格雷码中,相邻的两个码组之间仅有一位不同,这种编码方式可以用于实现相邻的两个状态之间只有一位不同的状态机;
FPGA 中的状态机通常需要高速运行,因此使用格雷码可以减少状态转换的开销,并提高时序性能。
2.3、普通二进制码
FPGA状态机可以用普通二进制码表示,不同状态按照二进制数累加表示,是常用的一种方式,仿真调试时,状态显示清晰,易于理解代码。
三、FPGA状态机实现方式
状态机一般有3种写法,即一段式、二段式和三段式的状态机写法,它们在速度、面积、代码可维护性等各个方面忽有优劣,不要对任何一种写法给出“一棍子打死”的定论
3.1、要求
售货机里有4元的脉动饮料,支持1元与2元硬币。设计一个状态机,检测投入的硬币,当累计投入币值大于等于脉动价格时,售货机自动找零并弹出1瓶脉动饮料。硬币和商品都是一个一个的进出,不会出现一次性投多个硬币弹出多瓶脉动的情况。
信号 | 含义 |
---|---|
clk | 时钟信号 |
rst_n | 复位信号 |
in | 输入信号,币值,有1和2两种,投钱 |
out | 输出信号,币值为1,找零 |
out_vld | 输出信号,脉动,为1则输出1瓶脉动 |
3.2、状态转移图
3.3、 代码设计
(1)一段式状态机
只定义一个转移状态:state,总体结构为一段always时序逻辑,用于描述状态转移和输出。由于是时序逻辑能够自动保持,所以可以省略else。但建议在初始状态时,else处赋一下初值。
代码:
//======================================================================
// --- 名称 : FSM_1
// --- 作者 : 又菜又爱玩
// --- 日期 : 2023-11-1
// --- 描述 : 售货机练习,采用一段式状态机
//======================================================================
module FSM_1
//---------------------<端口声明>---------------------------------------
(
input clk ,
input rst_n ,
input [1:0] in ,
output reg [1:0] out ,
output reg out_vld
);
//---------------------<信号定义>---------------------------------------
reg [3:0] state ;
//---------------------<状态机参数>-------------------------------------
localparam S0 = 4'b0001 ;
localparam S1 = 4'b0010 ;
localparam S2 = 4'b0100 ;
localparam S3 = 4'b1000 ;
//----------------------------------------------------------------------
//-- 状态机第1段
//----------------------------------------------------------------------
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
state <= S0;
out <= 0 ;
out_vld <= 0 ;
end
else begin
case(state)
S0: begin
if(in==1)begin
state <= S1;
end
else if(in==2)begin
state <= S2;
end
else begin
out <= 0 ;
out_vld <= 0 ;
end
end
S1: begin
if(in==1)begin
state <= S2;
end
else if(in==2)begin
state <= S3;
end
end
S2: begin
if(in==1)begin
state <= S3;
end
else if(in==2)begin
state <= S0;
out_vld <= 1 ;
end
end
S3: begin
if(in==1)begin
state <= S0;
out_vld <= 1 ;
end
else if(in==2)begin
state <= S0;
out <= 1 ;
out_vld <= 1 ;
end
end
default:state <= S0;
endcase
end
end
endmodule
(2)二段式状态机
二段式状态机,第一段用时序逻辑state_c(现态)和state_n(次态),第二段用组合逻辑描述状态转移和输出。由于是组合逻辑,为避免产生锁存器,else处一定要写上if中使用了的信号。
代码:
//======================================================================
// --- 名称 : FSM_2
// --- 作者 : 又菜又爱玩
// --- 日期 : 2023-11-1
// --- 描述 : 售货机练习,采用二段式状态机
//======================================================================
module FSM_2
//---------------------<端口声明>---------------------------------------
(
input clk ,
input rst_n ,
input [1:0] in ,
output reg [1:0] out ,
output reg out_vld
);
//---------------------<信号定义>---------------------------------------
reg [3:0] state_c ;
reg [3:0] state_n ;
//---------------------<状态机参数>-------------------------------------
localparam S0 = 4'b0001 ;
localparam S1 = 4'b0010 ;
localparam S2 = 4'b0100 ;
localparam S3 = 4'b1000 ;
//----------------------------------------------------------------------
//-- 状态机第1段
//----------------------------------------------------------------------
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
state_c <= S0;
else
state_c <= state_n;
end
//----------------------------------------------------------------------
//-- 状态机第2段
//----------------------------------------------------------------------
always@(*)begin
case(state_c)
S0: begin
if(in==1)begin
state_n = S1;
end
else if(in==2)begin
state_n = S2;
end
else begin
state_n = state_c;
out = 0 ;
out_vld = 0 ;
end
end
S1: begin
if(in==1)begin
state_n = S2;
end
else if(in==2)begin
state_n = S3;
end
else begin
state_n = state_c;
end
end
S2: begin
if(in==1)begin
state_n = S3;
end
else if(in==2)begin
state_n = S0;
out_vld = 1 ;
end
else begin
state_n = state_c;
out_vld = 0;
end
end
S3: begin
if(in==1)begin
state_n = S0;
out_vld = 1 ;
end
else if(in==2)begin
state_n = S0;
out = 1 ;
out_vld = 1 ;
end
else begin
state_n = state_c;
out = 0;
out_vld = 0;
end
end
default:state_n = S0;
endcase
end
endmodule
(3)三段式状态机
三段式状态机,第一段用时序逻辑描述state_c(现态)和state_n(次态),第二段有组合逻辑描述状态转移,第三段用时序逻辑描述输出,可以是多个always块。
代码:
//======================================================================
// --- 名称 : FSM_3
// --- 作者 : 又菜又爱玩
// --- 日期 : 2023-11-1
// --- 描述 : 售货机练习,采用三段式状态机
//======================================================================
module FSM_3
//---------------------<端口声明>---------------------------------------
(
input clk ,
input rst_n ,
input [1:0] in ,
output reg [1:0] out ,
output reg out_vld
);
//---------------------<信号定义>---------------------------------------
reg [3:0] state_c ;
reg [3:0] state_n ;
//---------------------<状态机参数>-------------------------------------
localparam S0 = 4'b0001 ;
localparam S1 = 4'b0010 ;
localparam S2 = 4'b0100 ;
localparam S3 = 4'b1000 ;
//----------------------------------------------------------------------
//-- 状态机第1段
//----------------------------------------------------------------------
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
state_c <= S0;
else
state_c <= state_n;
end
//----------------------------------------------------------------------
//-- 状态机第2段
//----------------------------------------------------------------------
always @(*)begin
case(state_c)
S0: begin
if(in==1)
state_n = S1;
else if(in==2)
state_n = S2;
else
state_n = state_c;
end
S1: begin
if(in==1)
state_n = S2;
else if(in==2)
state_n = S3;
else
state_n = state_c;
end
S2: begin
if(in==1)
state_n = S3;
else if(in==2)
state_n = S0;
else
state_n = state_c;
end
S3: begin
if(in==1 || in==2) // in != 0也行
state_n = S0;
else
state_n = state_c;
end
default:state_n = S0;
endcase
end
//----------------------------------------------------------------------
//-- 状态机第3段
//----------------------------------------------------------------------
//找零钱
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
out <= 0;
else if(state_c==S3 && in==2)
out <= 1;
else
out <= 0;
end
//输出脉动
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)
out_vld <= 0;
else if((state_c==S2 && in==2) || (state_c==S3 && in!=0))
out_vld <= 1;
else
out_vld <= 0;
end
endmodule
(4)一段式与三段式结合的状态机
只定义一个转移状态:state。第一段用时序逻辑描述state状态转移,第二段用时序逻辑描述输出,可以是多个always块。由于是时序逻辑能够自动保持,所以可以省略else。这种状态机的优点是既消除了组合逻辑可能产生的毛刺,又减少了代码量。
代码:
//======================================================================
// --- 名称 : FSM_V3
// --- 作者 : 木子
// --- 日期 :2023-11-8
// --- 描述 : 状态机练习,一段与三段式结合
//======================================================================
module FSM_V3
//---------------------<端口声明>---------------------------------------
(
input clk ,
input rst_n ,
input [1:0] in ,
output reg [1:0] out ,
output reg out_vld
);
//---------------------<信号定义>---------------------------------------
reg [3:0] state ;
//---------------------<状态机参数>-------------------------------------
localparam S0 = 4'b0001 ;
localparam S1 = 4'b0010 ;
localparam S2 = 4'b0100 ;
localparam S3 = 4'b1000 ;
//----------------------------------------------------------------------
//-- 状态机
//----------------------------------------------------------------------
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
state <= S0;
else begin
case(state)
S0: begin
if(in==1)
state <= S1;
else if(in==2)
state <= S2;
end
S1: begin
if(in==1)
state <= S2;
else if(in==2)
state <= S3;
end
S2: begin
if(in==1)
state <= S3;
else if(in==2)
state <= S0;
end
S3: begin
if(in==1 || in==2) // in != 0也行
state <= S0;
end
default:state <= S0;
endcase
end
end
//----------------------------------------------------------------------
//-- 输出
//----------------------------------------------------------------------
//找零钱
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
out <= 0;
else if(state==S3 && in==2)
out <= 1;
else
out <= 0;
end
//输出脉动
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)
out_vld <= 0;
else if((state==S2 && in==2) || (state==S3 && in!=0))
out_vld <= 1;
else
out_vld <= 0;
end
endmodule
3.4、testbench
`timescale 1ns/1ps //时间精度
`define Clock 20 //时钟周期
module FSM_3_tb;
//--------------------< 端口 >------------------------------------------
reg clk ;
reg rst_n ;
reg [1:0] in ;
wire [1:0] out ;
wire out_vld ;
//----------------------------------------------------------------------
//-- 模块例化
//----------------------------------------------------------------------
FSM_3 u_FSM_3
(
.clk (clk ),
.rst_n (rst_n ),
.in (in ),
.out (out ),
.out_vld (out_vld )
);
//----------------------------------------------------------------------
//-- 状态机名称查看器
//----------------------------------------------------------------------
localparam S0 = 4'b0001 ;
localparam S1 = 4'b0010 ;
localparam S2 = 4'b0100 ;
localparam S3 = 4'b1000 ;
//2字符16位
reg [15:0] state_name ;
always@(*)begin
case(u_FSM_3.state_c)
S0: state_name = "S0";
S1: state_name = "S1";
S2: state_name = "S2";
S3: state_name = "S3";
default:state_name = "S0";
endcase
end
//----------------------------------------------------------------------
//-- 时钟信号和复位信号
//----------------------------------------------------------------------
initial begin
clk = 1;
forever
#(`Clock/2) clk = ~clk;
end
initial begin
rst_n = 0; #(`Clock*20+1);
rst_n = 1;
end
//----------------------------------------------------------------------
//-- 设计输入信号
//----------------------------------------------------------------------
initial begin
#1;
in = 0;
#(`Clock*20+1); //初始化完成
//情况1--------------------------
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*10);
//情况2--------------------------
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 2; //2块钱
#(`Clock*1);
in = 0;
#(`Clock*10);
//情况3--------------------------
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 2; //2块钱
#(`Clock*1);
in = 0;
#(`Clock*10);
//情况4--------------------------
in = 1; //1块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 2; //2块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 2; //2块钱
#(`Clock*1);
in = 0;
#(`Clock*10);
//情况5--------------------------
in = 2; //2块钱
#(`Clock*1);
in = 0;
#(`Clock*1);
in = 2; //2块钱
#(`Clock*1);
in = 0;
#(`Clock*10);
$stop;
end
endmodule
仿真波形如下: