【FPGA/IC】状态机FSM的各种写法(一段式、二段式、三段式、摩尔型Moore、米勒型Mealy)

1、有限状态机

1.1、理论

FPGA不同于CPU的一点特点就是CPU是顺序执行的,而FPGA是同步执行(并行)的。那么FPGA如何处理明显具有时间上先后顺序的事件呢?这个时候我们就需要使用到有限状态机了。

状态机简写为 FSM(Finite State Machine),也称为同步有限状态机,我们一般简称为状态机,之所以说“同步”是因为状态机中所有的状态跳转都是在时钟的作用下进行的,而“有限”则是说状态的个数是有限的。

状态机的每一个状态代表一个事件,从执行当前事件到执行另一事件我们称之为状态的跳转或状态的转移,我们需要做的就是执行该事件然后跳转到一下时间,这样我们的系统就“活”了,可以正常的运转起来了。状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显。

1.2、分类        

根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore)型状态机和米勒(Mealy) 型状态机。

  •  Mealy 状态机:输出不仅取决于当前状态,还取决于输入状态。

米勒状态机的模型如下图所示,模型中第一个方框是指产生下一状态的组合逻辑 F,F 是当前状态和输 入信号的函数,状态是否改变、如何改变,取决于组合逻辑 F 的输出;第二个方框是指状态寄存器,其由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在时钟的跳边沿;第三个方框是指产生输出的组合逻辑 G,状态机的输出是由输出组合逻辑 G 提供的,G 也是当前状态和输入信号的函数。

  •  Moore 状态机:组合逻辑的输出只取决于当前状态,而与输入状态无关。

摩尔状态机的模型如下图所示,对比米勒状态机的模型可以发现,其区别在于米勒状态机的输出是由当前状态和输入条件共同决定的,而摩尔状态机的输出只取决于当前状态。

1.3、写法

根据状态机的实际写法,状态机可以分为一段式、二段式和三段式状态机。

  • 一段式状态机:整个状态机写到一个 always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。 
  • 二段式状态机:用两个 always 模块来描述状态机,其中一个 always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。不同于一段式状态机的是,它需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑。
  •  三段式状态机:在两个 always 模块描述方法的基础上,使用三个always 模块,一个always 模块采用同步时序描述状态转移,一个 always 采用组合逻辑判断状态转移条件,描述状态转移规律,另一个 always 模块描述状态输出。

2、状态机实例分析 

接下来对一个简单的可乐售卖系统使用状态机的思想进行分析。

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

首先分析会有哪些输入、输出信号:

输入信号:

  • sys_clk_n:既然是同步状态机,那么时钟是肯定少不了的,这里设定时钟是50MHz;
  • sys_rst_n:一个稳健的系统肯定需要一个复位,这里设定位低电平有效;
  • money:投币输入,高电平表示投入一元,低电平表示没有投币;

输出信号:

  • cola:可乐输出,高电平表示掉落一瓶可乐,低电平表示没有可乐掉落;

接下来需要想一想这个状态机是怎么运作的,也就是要如何绘制这个系统的状态转移图。前面提到,状态机的状态转移有和输入挂钩的Mealy 状态机,也有和输入无关的Moore 状态机。所以接下来分别用Moore 状态机、Mealy 状态机的思想来绘制状态转移图:

Moore 状态机(输出和输入无关):

  • IDLE:首先是系统复位后的默认状态,这个状态下售卖机里没有钱,没有可乐输出;接下来的状态有两种情况:投了1元硬币则跳转状态ONE、没有投硬币则保持IDLE状态;
  • ONE:这个状态下售卖机里有1元硬币,所以也没有可乐输出;接下来的状态有两种情况:投了1元硬币则跳转状态TWO、没有投硬币则保持ONE状态;
  • TWO:这个状态下售卖机里有2元硬币,所以也没有可乐输出;接下来的状态有两种情况:投了1元硬币则跳转状态THREE、没有投硬币则保持TWO状态;
  • THREE:这个状态下售卖机里有3元硬币,但是因为是使用的时序逻辑,所以在这个时钟周期,是不会有可乐输出的,可乐会在状态跳转后(下一个时钟周期输出);接下来的状态有两种情况:投了1元硬币则跳转状态ONE、没有投硬币则跳转状态IDLE状态;而且状态跳转后会输出一瓶可乐(实际上可以理解为THREE状态来自于TWO状态投的一元硬币,也就是这个时钟周期如果输出发生了变化,则输出是和输入有关的了,那就不是Moore 状态机了);

根据上面列出的这些状态可以绘制出如下的状态转移图(1/0:前面的1代表输入,后面的0代表输出):

Mealy 状态机(输出和输入相关):

  • IDLE:首先是系统复位后的默认状态,没有可乐输出(分两种情况,投币和不投币);接下来的状态有两种情况:投了1元硬币则跳转状态ONE且没有可乐输出、没有投硬币则保持IDLE状态且没有可乐输出;
  • ONE:这个状态下售卖机里有1元硬币;接下来的状态有两种情况:投了1元硬币则跳转状态TWO且没有可乐输出、没有投硬币则保持ONE状态且没有可乐输出;
  • TWO:这个状态下售卖机里有2元硬币;接下来的状态有两种情况:投了1元硬币则跳转状态IDLE且输出可乐(根据和输入相关要求,此时输入一元,加上原来的2元,一共有三元,满足输出可乐的条件)、没有投硬币则保持TWO状态且没有可乐输出;

根据上面列出的这些状态可以绘制出如下的状态转移图(1/0:前面的1代表输入,后面的0代表输出):

从上面的分析可以得到以下结论:

  • Mealy 状态机比Moore状态机的状态个数要少
  • Mealy 状态机比Moore状态机的输出要早一个时钟周期

接下来就各种写法对状态机进行仿真分析。

3、一段式状态机

一段式状态机是将整个状态机写到一个 always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。不推荐采用这种状态机,因为从代码风格方面来讲,一般都会要求把组合逻辑和时序逻辑分开;从代码维护和升级来说,组合逻辑和时序逻辑混合在一起不利于代码维护和修改,也不利于约束。

3.1、Moore型(摩尔型)一段式状态机

 Verillog代码如下:

//==================================================================
//--    1段式状态机(Moore)
//==================================================================

//------------<模块及端口声明>----------------------------------------
module FSM_Moore_1(
	input 		sys_clk		,		//输入系统时钟、50M
	input 		sys_rst_n	,		//复位信号、低电平有效
	input 		money		,		//投币输入,高电平有效
	
	output reg	cola				//可乐输出,高电平有效
);

//------------<状态机参数定义>------------------------------------------
//这里使用独热码编码节省组合逻辑资源
//此外还可以使用格雷码 、二进制码
localparam	IDLE  = 4'b0001,
			ONE   = 4'b0010,
			TWO   = 4'b0100,
			THREE = 4'b1000;
			
//------------<reg定义>-------------------------------------------------
reg	[3:0]	state;					//定义状态寄存器

//-----------------------------------------------------------------------
//--    1段式状态机(Moore)
//-----------------------------------------------------------------------
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		cola <= 1'b0;				//复位初始状态
		state <= IDLE;
	end
	else
		case(state)					//根据当前状态、输入进行状态转换判断
									//根据当前状态进行输出
			IDLE:begin
				cola <= 1'b0;		//初始状态无可乐输出
				if(money)				
					state <= ONE;	//投币1元则状态跳转到ONE
				else
					state <= IDLE;	//否则保持原有状态
			end				
			ONE:begin
				cola <= 1'b0;		//该状态只有1元,无可乐输出
				if(money)
					state <= TWO;	//投币1元则状态跳转到TWO
				else
					state <= ONE;	//否则保持原有状态
			end
			TWO:begin
				cola <= 1'b0;		//该状态只有2元,无可乐输出
				if(money)
					state <= THREE;	//投币1元则状态跳转到THREE
				else
					state <= TWO;	//否则保持原有状态
			end 
			THREE:begin
				cola <= 1'b1;		//该状态有3元,有可乐输出
									//但是时序逻辑输出会落后一个时钟周期
				if(money)
					state <= ONE;	//投币1元则状态跳转到ONE
				else
					state <= IDLE;	//否则状态跳转到IDLE
			end			
			default:begin			//默认状态同IDLE
				cola <= 1'b0;
				if(money)
					state <= ONE;
				else
					state <= IDLE;
			end	
		endcase
end

endmodule

 使用QuartusII编码生成的状态机视图如下:

可以看到,这和我们之前绘制的状态转移图一致。

状态机的编码方式一般有三种,各有优劣,独热码算是用的比较多的:

  • 独热码
  • 格雷码
  • 二进制码

编写Testbench文件进行仿真,文件如下:

//-------------------------------------------------------------------
//--    1段式状态机(Moore)
//-------------------------------------------------------------------
`timescale 1ns/1ns

//------------<模块及端口声明>----------------------------------------
module tb_FSM_Moore_1();
reg 	sys_clk;
reg 	sys_rst_n;
reg 	money;
wire	cola;

//------------<例化被测试模块>----------------------------------------
FSM_Moore_1	FSM_Moore_1_inst(
	.sys_clk	(sys_clk)	,
	.sys_rst_n	(sys_rst_n)	,
	.money		(money)		,

	.cola       (cola)
);
//------------<设置初始测试条件>----------------------------------------
initial begin
	sys_clk = 1'b0;					//初始时钟为0
	sys_rst_n <= 1'b0;				//初始复位
	money <= 1'b0;					//投币初始化为0
	#5								
	sys_rst_n <= 1'b1;				//拉高复位,系统进入工作状态
	#20								
	money <= 1'b1;					//拉高投币信号	
	#35								
	money <= 1'b0;	    			//拉低投币信号	
	#15								
	money <= 1'b1;	    			//拉高投币信号	
	#75								
	money <= 1'b0;					//拉低投币信号	
	#50 $stop;						//结束仿真
end
//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;		//系统时钟周期20ns

//------------<状态机名称查看器>----------------------------------------
reg [39:0]	state_name;				//每字符8位宽,这里最多5个字符40位宽

always @(*) begin
    case(FSM_Moore_1_inst.state)
        4'b0001:	state_name = "IDLE";
        4'b0010: 	state_name = "ONE";
        4'b0100: 	state_name = "TWO";
        4'b1000: 	state_name = "THREE";
        default:	state_name = "IDLE";
    endcase
end

endmodule

 仿真出来的波形如下所示:

从上图可以看到:

  • 一共投入了6个硬币,理论上应该有2个可乐分别输出,实际也有两个可乐输出;
  • 第1次输出可乐滞后THREE状态一个时钟周期,且当前的输入为1;第2次输出可乐滞后THREE状态一个时钟周期,且当前的输入为0;这说明输出会之后当前状态一个时钟周期,且与输入无关(输入不管是0还是1都有输出);
  • 状态的跳转符合我们绘制的状态转移图。              

3.2、Mealy型(米勒型)一段式状态机

Verillog代码如下:

//==================================================================
//--    1段式状态机(Mealy)
//==================================================================

//------------<模块及端口声明>----------------------------------------
module FSM_Mealy_1(
	input 		sys_clk		,			//输入系统时钟、50M
	input 		sys_rst_n	,       	//复位信号、低电平有效
	input 		money		,       	//投币输入,高电平有效
										
	output reg	cola                	//可乐输出,高电平有效
);

//------------<状态机参数定义>------------------------------------------
//这里使用独热码编码节省组合逻辑资源
//此外还可以使用格雷码 、二进制码
localparam	IDLE  = 3'b001,
			ONE   = 3'b010,
			TWO   = 3'b100;

//------------<reg定义>------------------------------------------------
reg	[2:0]	state;						//定义状态寄存器

//-----------------------------------------------------------------------
//--    1段式状态机(Mealy)
//-----------------------------------------------------------------------
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		cola <= 1'b0;					//复位初始状态
		state <= IDLE;					//复位初始状态
	end
	else
		case(state)						//根据当前状态、输入进行状态转换判断
										//根据当前状态、输入进行输出
			IDLE:begin				
				if(money)begin			//投入1元
					state <= ONE;		//状态跳转到ONE
					cola <= 1'b0;		//一共1元 ,没有可乐输出
				end	
				else begin				//没有投入
					state <= IDLE;		//保持原有状态
					cola <= 1'b0;		//一共0元 ,没有可乐输出
				end	
			end				
			ONE:begin				
				if(money)begin			//投入1元
					state <= TWO;		//状态跳转到TWO
					cola <= 1'b0;       //一共2元 ,没有可乐输出
				end                     
				else begin              //没有投入
					state <= ONE;       //保持原有状态
					cola <= 1'b0;       //一共1元 ,没有可乐输出
				end
			end
			TWO:begin					
				if(money)begin			//投入1元
					state <= IDLE;      //状态跳转到IDLE(一共3元了,需要输出可乐)
					cola <= 1'b1;       //一共3元 ,输出可乐
				end                     
				else begin              //没有投入
					state <= TWO;       //保持原有状态
					cola <= 1'b0;       //一共2元 ,没有可乐输出
				end
			end 		
			default:begin				//默认状态同初始状态
				if(money)begin
					state <= ONE;
					cola <= 1'b0;
				end
				else begin
					state <= IDLE;
					cola <= 1'b0;
				end	
			end		
		endcase
end

endmodule

使用QuartusII编码生成的状态机视图如下:

可以看到,这和我们之前绘制的状态转移图一致。

编写Testbench文件进行仿真,仿真激励设置和Moore型一段式状态机一致,文件如下:

//-------------------------------------------------------------------
//--    1段式状态机(Mealy)
//-------------------------------------------------------------------
`timescale 1ns/1ns

//------------<模块及端口声明>----------------------------------------
module tb_FSM_Mealy_1();
reg 	sys_clk;
reg 	sys_rst_n;
reg 	money;
wire	cola;

//------------<例化被测试模块>----------------------------------------
FSM_Mealy_1	FSM_Mealy_1_inst(
	.sys_clk	(sys_clk)	,
	.sys_rst_n	(sys_rst_n)	,
	.money		(money)		,

	.cola       (cola)
);
//------------<设置初始测试条件>----------------------------------------
initial begin
	sys_clk = 1'b0;					//初始时钟为0
	sys_rst_n <= 1'b0;				//初始复位
	money <= 1'b0;					//投币初始化为0
	#5								//5个时钟周期后
	sys_rst_n <= 1'b1;				//拉高复位,系统进入工作状态
	#20								
	money <= 1'b1;					//拉高投币信号	
	#35								
	money <= 1'b0;	    			//拉低投币信号	
	#15								
	money <= 1'b1;	    			//拉高投币信号	
	#75								
	money <= 1'b0;					//拉低投币信号
	#40 $stop;                      //结束仿真
end
//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;		//系统时钟周期20ns

//------------<状态机名称查看器>----------------------------------------
reg [31:0]	state_name;				//每字符8位宽,这里最多4个字符32位宽

always @(*) begin
    case(FSM_Mealy_1_inst.state)
        3'b001:		state_name = "IDLE";
        3'b010: 	state_name = "ONE";
        3'b100: 	state_name = "TWO";
        default:	state_name = "IDLE";
    endcase
end

endmodule

 使用ModelSim执行仿真,仿真出来的波形如下所示:

从仿真结果可以看到: 

  • 一共投入了6个硬币,理论上应该有2个可乐分别输出,实际也有两个可乐输出;
  • 第1次输出可乐滞后TWO状态一个时钟周期,且当前的输入为1;第2次输出可乐滞后TWO状态一个时钟周期,且当前的输入也为1;这说明输出会之后当前状态一个时钟周期,且与输入相关(只有输入为 1才有输出,中间TWO状态维持了两个时钟周期,因为有一个时钟输入为0,所以没有输出);
  • 状态的跳转符合我们绘制的状态转移图。       

4、二段式状态机

二段式状态机用两个 always 模块来描述状态机,其中一个 always 模块采用同步时序描述状态转移;另一个 模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。不同于一段式状态机的是,它需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑。输出如果用组合逻辑,则非常容易产生毛刺,所以请尽量在后面补一个寄存器。

4.1、Moore型(摩尔型)二段式状态机

 Verillog代码如下:

//==================================================================
//--    2段式状态机(Moore)
//==================================================================

//------------<模块及端口声明>----------------------------------------
module FSM_Moore_2(
	input 		sys_clk		,		//输入系统时钟、50M
	input 		sys_rst_n	,   	//复位信号、低电平有效
	input 		money		,   	//投币输入,高电平有效
									
	output reg	cola            	//可乐输出,高电平有效
);

//------------<状态机参数定义>------------------------------------------
localparam	IDLE  = 4'b0001,
			ONE   = 4'b0010,
			TWO   = 4'b0100,
			THREE = 4'b1000;
//------------<reg定义>-------------------------------------------------
reg	[3:0]	cur_state;				//定义现态寄存器
reg	[3:0]	next_state;				//定义次态寄存器

//-----------------------------------------------------------------------
//--状态机第一段:同步时序描述状态转移
//-----------------------------------------------------------------------
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		cur_state <= IDLE;			//复位初始状态
	else
		cur_state <= next_state;	//次态转移到现态
end

//-----------------------------------------------------------------------
//--状态机第二段:组合逻辑判断状态转移条件,描述状态转移规律以及输出
//-----------------------------------------------------------------------
always@(*)begin						//组合逻辑
	case(cur_state)					//根据当前状态、输入进行状态转换判断
	                                //根据当前状态进行输出
		IDLE:begin
			cola = 1'b0;			//初始状态无可乐输出
			if(money)				//投币1元
				next_state = ONE;	//次态(下个状态)为ONE		
			else 
				next_state = IDLE;	//次态为现态	
		end					
		ONE:begin
			cola = 1'b0;			//无可乐输出
			if(money)				//投币1元
				next_state = TWO;	//次态(下个状态)为TWO
			else 
				next_state = ONE;	//次态为现态			
		end
		TWO:begin					
			cola = 1'b0;	        //无可乐输出
			if(money)               //投币1元
				next_state = THREE; //次态(下个状态)为THREE
			else                    			
				next_state = TWO;	//次态为现态
		end	
		THREE:begin	
			cola = 1'b1;			//输出可乐输出
			if(money)               //投币1元
				next_state = ONE;	//次态(下个状态)为ONE		
			else                    			
				next_state = IDLE;  //次态为IDLE
		end
		default:begin				//默认状态同IDLE
			cola = 1'b0;		
			if(money)
				next_state = ONE;
			else 
				next_state = IDLE;
		end	
	endcase
end

endmodule

 使用QuartusII编码生成的状态机视图如下:

可以看到,这和我们之前绘制的状态转移图一致。

编写Testbench文件进行仿真:

//------------------------------------------------
//--    2段式状态机(Moore)
//------------------------------------------------
`timescale 1ns/1ns

//------------<模块及端口声明>----------------------------------------
module tb_FSM_Moore_2();

reg 	sys_clk;
reg 	sys_rst_n;
reg 	money;

wire	cola;

//------------<例化被测试模块>----------------------------------------
FSM_Moore_2	FSM_Moore_2_inst(
	.sys_clk	(sys_clk)	,
	.sys_rst_n	(sys_rst_n)	,
	.money		(money)		,

	.cola       (cola)
);

//------------<设置初始测试条件>----------------------------------------
initial begin
	sys_clk = 1'b0;					//初始时钟为0
	sys_rst_n <= 1'b0;				//初始复位
	money <= 1'b0;					//投币初始化为0
	#5								
	sys_rst_n <= 1'b1;				//拉高复位,系统进入工作状态
	#20								
	money <= 1'b1;					//拉高投币信号	
	#35								
	money <= 1'b0;	    			//拉低投币信号	
	#15								
	money <= 1'b1;	    			//拉高投币信号	
	#75								
	money <= 1'b0;					//拉低投币信号	
	#50 $stop;						//结束仿真
end
//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;		//系统时钟周期20ns

//------------------------------------------------
//--    状态机名称查看器
//------------------------------------------------
reg [39:0]	state_name_cur;			//每字符8位宽,这里最多5个字符40位宽
reg [39:0]	state_name_next;		//每字符8位宽,这里最多5个字符40位宽

always @(*) begin
    case(FSM_Moore_2_inst.cur_state)
         4'b0001:    	state_name_cur = "IDLE";
         4'b0010:   	state_name_cur = "ONE";
         4'b0100:   	state_name_cur = "TWO";
         4'b1000:		state_name_cur = "THREE"; 
		 default:		state_name_cur = "IDLE";
    endcase
end

always @(*) begin
    case(FSM_Moore_2_inst.next_state)
         4'b0001:    	state_name_next = "IDLE";
         4'b0010:   	state_name_next = "ONE";
         4'b0100:   	state_name_next = "TWO";
         4'b1000:		state_name_next = "THREE"; 
		 default:		state_name_next = "IDLE";
    endcase
end

endmodule

仿真出来的波形如下所示:

从仿真结果可以看到:

  • 一共投入了6个硬币,理论上应该有2个可乐分别输出,实际也有两个可乐输出;
  • 现态落后次态一个时钟周期,这是因为需要用次态去描述现态;
  • 与一段式状态机不同,可乐的输出不会滞后一个时钟周期,这是因为采用了组合逻辑来描述输出;
  • 第1次输出可乐的输入为1,第2次输出可乐的输入为0;这说明输出与输入无关(输入不管是0还是1都有输出);
  • 状态的跳转符合我们绘制的状态转移图;

4.2、Mealy型(米勒型)二段式状态机

Verillog代码如下:

//------------------------------------------------
//--    2段式状态机(Mealy )
//------------------------------------------------

//------------<模块及端口声明>----------------------------------------
module FSM_Mealy_2(
	input 		sys_clk		,			//输入系统时钟、50M
	input 		sys_rst_n	,   		//复位信号、低电平有效
	input 		money		,   		//投币输入,高电平有效
										
	output reg	cola            		//可乐输出,高电平有效
);

//------------<状态机参数定义>------------------------------------------
localparam	IDLE  = 3'b001,
			ONE   = 3'b010,
			TWO   = 3'b100;

//------------<reg定义>------------------------------------------------
reg	[2:0]	cur_state;					//定义现态
reg	[2:0]	next_state;					//定义次态

//-----------------------------------------------------------------------
//--状态机第一段:同步时序描述状态转移
//-----------------------------------------------------------------------
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		cur_state <= IDLE;				//复位初始状态
	else                                
		cur_state <= next_state;        //次态转移到现态
end

//-----------------------------------------------------------------------
//--状态机第二段:组合逻辑判断状态转移条件,描述状态转移规律以及输出
//-----------------------------------------------------------------------
always@(*)begin
	case(cur_state)						//组合逻辑
		IDLE:begin				        //根据当前状态、输入进行状态转换判断
										//根据当前状态、输入进行输出
			if(money)begin				//当前输入为1
				next_state = ONE;		//次态为ONE
				cola = 1'b0;			//一共1元 ,没有可乐输出
			end
			else begin					//当前输入为0
				next_state = IDLE;		//次态为IDLE
				cola = 1'b0;			//一共0元 ,没有可乐输出
			end	
		end					
		ONE:begin				
			if(money)begin				//当前输入为1
				next_state = TWO;       //次态为TWO
				cola = 1'b0;            //一共2元 ,没有可乐输出
			end                         
			else begin                  //当前输入为0
				next_state = ONE;       //次态为ONE
				cola = 1'b0;            //一共1元 ,没有可乐输出
			end	
		end
		TWO:begin				
			if(money)begin				//当前输入为1
				next_state = IDLE;		//次态为IDLE
				cola = 1'b1;            //一共3元 ,输出可乐
			end                         
			else begin                  //当前输入为0
				next_state = TWO;       //次态为TWO
				cola = 1'b0;            //一共2元 ,没有可乐输出
			end	
		end		
		default:begin					//默认状态同初始状态
			if(money)begin
				next_state = ONE;
				cola = 1'b0;
			end
			else begin
				next_state = IDLE;
				cola = 1'b0;
			end	
		end
	endcase
end

endmodule

 使用QuartusII编码生成的状态机视图如下:

可以看到,这和我们之前绘制的状态转移图一致。

编写Testbench文件进行仿真:

//------------------------------------------------
//--    2段式状态机(Mealy)
//------------------------------------------------
`timescale 1ns/1ns

//------------<模块及端口声明>----------------------------------------
module tb_FSM_Mealy_2();

reg 	sys_clk;
reg 	sys_rst_n;
reg 	money;
wire	cola;

//------------<例化被测试模块>----------------------------------------
FSM_Mealy_2		FSM_Mealy_2_inst(
	.sys_clk	(sys_clk),
	.sys_rst_n	(sys_rst_n),
	.money		(money),

	.cola       (cola)
);

//------------<设置初始测试条件>----------------------------------------
initial begin
	sys_clk = 1'b0;					//初始时钟为0
	sys_rst_n <= 1'b0;				//初始复位
	money <= 1'b0;					//投币初始化为0
	#5								
	sys_rst_n <= 1'b1;				//拉高复位,系统进入工作状态
	#25								
	money <= 1'b1;					//拉高投币信号	
	#40								
	money <= 1'b0;	    			//拉低投币信号	
	#20								
	money <= 1'b1;	    			//拉高投币信号	
	#80								
	money <= 1'b0;					//拉低投币信号	
	#40 $stop;						//结束仿真
end
//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;		//系统时钟周期20ns

//------------------------------------------------
//--    状态机名称查看器
//------------------------------------------------
//1字符8位宽
reg [31:0]	state_name_cur;			//每字符8位宽,这里最多4个字符32位宽
reg [31:0]	state_name_next;		//每字符8位宽,这里最多4个字符32位宽

always @(*) begin
    case(FSM_Mealy_2_inst.cur_state)
        3'b001:    	state_name_cur = "IDLE";
        3'b010:   	state_name_cur = "ONE";
        3'b100:   	state_name_cur = "TWO";
        default:	state_name_cur = "IDLE";
    endcase
end

always @(*) begin
    case(FSM_Mealy_2_inst.next_state)
        3'b001:    	state_name_next = "IDLE";
        3'b010:   	state_name_next = "ONE";
        3'b100:   	state_name_next = "TWO";
        default:	state_name_next = "IDLE";
    endcase
end

endmodule

仿真出来的波形如下所示:

从仿真结果可以看到:

  • 一共投入了6个硬币,理论上应该有2个可乐分别输出,实际也有两个可乐输出;
  • 现态落后次态一个时钟周期,这是因为需要用次态去描述现态;
  • 与一段式状态机不同,可乐的输出不会滞后一个时钟周期,这是因为采用了组合逻辑来描述输出;
  • 第1次输出可乐的输入为1,第2次输出可乐的输入也为1,这说明输出与输入有关;
  • 状态的跳转符合我们绘制的状态转移图;

5、三段式状态机

三段式状态机在两个 always 模块描述方法基础上,使用三个always 模块,一个always 模块采用同步时序描述状态转移,一个 always 采用组合逻辑判断状态转移条件,描述状态转移规律,另一个 always 模块描述状态输出。

5.1、Moore型(摩尔型)三段式状态机

 Verilog代码如下:

//==================================================================
//--    3段式状态机(Moore)
//==================================================================

//------------<模块及端口声明>----------------------------------------
module FSM_Moore_3(
	input 		sys_clk		,			//输入系统时钟、50M
	input 		sys_rst_n	,   		//复位信号、低电平有效
	input 		money		,   		//投币输入,高电平有效
										
	output reg	cola            		//可乐输出,高电平有效
);

//------------<状态机参数定义>------------------------------------------
localparam	IDLE  = 4'b0001,
			ONE   = 4'b0010,
			TWO   = 4'b0100,
			THREE = 4'b1000;
			
//------------<reg定义>-------------------------------------------------
reg	[3:0]	cur_state;					//定义现态寄存器
reg	[3:0]	next_state;					//定义次态寄存器

//-----------------------------------------------------------------------
//--状态机第一段:同步时序描述状态转移
//-----------------------------------------------------------------------
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		cur_state <= IDLE;				//复位初始状态
	else
		cur_state <= next_state;		//次态转移到现态
end

//-----------------------------------------------------------------------
//--状态机第二段:组合逻辑判断状态转移条件,描述状态转移规律以及输出
//-----------------------------------------------------------------------
always@(*)begin
	case(cur_state)						//组合逻辑
										//根据当前状态、输入进行状态转换判断										
		IDLE:begin				
			if(money)					
				next_state = ONE;		//投币1元,则状态转移到ONE
			else 
				next_state = IDLE;		//没有投币,则状态保持	
		end					
		ONE:begin				
			if(money)
				next_state = TWO;		//投币1元,则状态转移到TWO
			else 
				next_state = ONE;		//没有投币,则状态保持
		end
		TWO:begin				
			if(money)
				next_state = THREE;		//投币1元,则状态转移到THREE
			else                        
				next_state = TWO;       //没有投币,则状态保持
		end	
		THREE:begin				
			if(money)
				next_state = ONE;		//投币1元,则状态转移到ONE
			else                        
				next_state = IDLE;      //没有投币,则状态保持
		end
		default:begin					//默认状态同IDLE
			if(money)
				next_state = ONE;
			else 
				next_state = IDLE;	
		end
	endcase
end

//-----------------------------------------------------------------------
//--状态机第三段:时序逻辑描述输出
//-----------------------------------------------------------------------
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		cola <= 1'b0;					//复位、初始状态 
	else
		case(next_state)					//根据当前状态进行输出
			IDLE:	cola <= 1'b0;		//无可乐输出			
			ONE:	cola <= 1'b0;		//无可乐输出
			TWO:	cola <= 1'b0;		//无可乐输出
			THREE:	cola <= 1'b1;		//输出可乐
			default:cola <= 1'b0;		//默认无可乐输出
		endcase
end

endmodule

 使用QuartusII编码生成的状态机视图如下:

可以看到,这和我们之前绘制的状态转移图一致。

编写Testbench文件进行仿真:

//------------------------------------------------
//--    3段式状态机(Moore)
//------------------------------------------------
`timescale 1ns/1ns

//------------<模块及端口声明>----------------------------------------
module tb_FSM_Moore_3();

reg 	sys_clk;
reg 	sys_rst_n;
reg 	money;

wire	cola;

//------------<例化被测试模块>----------------------------------------
FSM_Moore_3		FSM_Moore_3_inst(
	.sys_clk	(sys_clk),
	.sys_rst_n	(sys_rst_n),
	.money		(money),

	.cola       (cola)
);

//------------<设置初始测试条件>----------------------------------------
initial begin
	sys_clk = 1'b0;					//初始时钟为0
	sys_rst_n <= 1'b0;				//初始复位
	money <= 1'b0;					//投币初始化为0
	#5								//5个时钟周期后
	sys_rst_n <= 1'b1;				//拉高复位,系统进入工作状态
	#25								//25个时钟周期后
	money <= 1'b1;					//拉高投币信号	
	#40								//40个时钟周期后
	money <= 1'b0;	    			//拉低投币信号	
	#20								//25个时钟周期后
	money <= 1'b1;	    			//拉高投币信号	
	#80								//25个时钟周期后
	money <= 1'b0;					//拉低投币信号	
end
//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;		//系统时钟周期20ns

//------------------------------------------------
//--    状态机名称查看器
//------------------------------------------------
reg [39:0]	state_name_cur;			//每字符8位宽,这里最多5个字符40位宽
reg [39:0]	state_name_next;		//每字符8位宽,这里最多5个字符40位宽

always @(*) begin
    case(FSM_Moore_3_inst.cur_state)
         4'b0001:    	state_name_cur = "IDLE";
         4'b0010:   	state_name_cur = "ONE";
         4'b0100:   	state_name_cur = "TWO";
         4'b1000:		state_name_cur = "THREE"; 
        default:		state_name_cur = "IDLE";
    endcase
end

always @(*) begin
    case(FSM_Moore_3_inst.next_state)
         4'b0001:    	state_name_next = "IDLE";
         4'b0010:   	state_name_next = "ONE";
         4'b0100:   	state_name_next = "TWO";
         4'b1000:		state_name_next = "THREE"; 
		 default:		state_name_next = "IDLE";
    endcase
end

endmodule

使用ModelSim执行仿真,仿真出来的波形如下所示:

从仿真结果可以看到:

  • 一共投入了6个硬币,理论上应该有2个可乐分别输出,实际也有两个可乐输出;
  • 现态落后次态一个时钟周期,这是因为需要用次态去描述现态;
  • 第1次输出可乐的输入为1,第2次输出可乐的输入为0;这说明输出与输入无关;
  • 状态的跳转符合我们绘制的状态转移图;

5.2、Mealy型(米勒型)三段式状态机

Verilog代码如下:

//==================================================================
//--    3段式状态机(Mealy)
//==================================================================

//------------<模块及端口声明>----------------------------------------
module FSM_Mealy_3(
	input 		sys_clk		,			//输入系统时钟、50M
	input 		sys_rst_n	,   		//复位信号、低电平有效
	input 		money		,   		//投币输入,高电平有效
										
	output reg	cola            		//可乐输出,高电平有效
);

//------------<状态机参数定义>------------------------------------------
localparam	IDLE  = 3'b0001,
			ONE   = 3'b0010,
			TWO   = 3'b0100;
			
//------------<reg定义>-------------------------------------------------
reg	[3:0]	cur_state;					//定义现态寄存器
reg	[3:0]	next_state;					//定义次态寄存器

//-----------------------------------------------------------------------
//--状态机第一段:同步时序描述状态转移
//-----------------------------------------------------------------------
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		cur_state <= IDLE;				//复位初始状态
	else
		cur_state <= next_state;		//次态转移到现态
end

//-----------------------------------------------------------------------
//--状态机第二段:组合逻辑判断状态转移条件,描述状态转移规律以及输出
//-----------------------------------------------------------------------
always@(*)begin
	case(cur_state)						//组合逻辑
										//根据当前状态、输入进行状态转换判断										
		IDLE:begin				
			if(money)					
				next_state = ONE;		//投币1元,则状态转移到ONE
			else 
				next_state = IDLE;		//没有投币,则状态保持	
		end					
		ONE:begin				
			if(money)
				next_state = TWO;		//投币1元,则状态转移到TWO
			else 
				next_state = ONE;		//没有投币,则状态保持
		end
		TWO:begin				
			if(money)
				next_state = IDLE;		//投币1元,则状态转移到IDLE
			else                        
				next_state = TWO;       //没有投币,则状态保持
		end	
		default:begin					//默认状态同IDLE
			if(money)
				next_state = ONE;
			else 
				next_state = IDLE;	
		end
	endcase
end

//-----------------------------------------------------------------------
//--状态机第三段:时序逻辑描述输出
//-----------------------------------------------------------------------
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		cola <= 1'b0;					//复位、初始状态 
	else
		case(cur_state)					//根据当前状态进行输出
			IDLE:	cola <= 1'b0;		//无可乐输出(因为输入不管是0、1都是输出0,所以省略写法)			
			ONE:	cola <= 1'b0;		//无可乐输出(因为输入不管是0、1都是输出0,所以省略写法)	
			TWO:begin					
				if(money)
					cola <= 1'b1;		//如果输入1,则输出可乐
				else
					cola <= 1'b0;		//如果输入0,则无可乐输出
			end
			default:cola <= 1'b0;		//默认无可乐输出
		endcase
end

endmodule

 使用QuartusII编码生成的状态机视图如下:

可以看到,这和我们之前绘制的状态转移图一致。

编写Testbench文件进行仿真:

//------------------------------------------------
//--    3段式状态机(Mealy)
//------------------------------------------------
`timescale 1ns/1ns

//------------<模块及端口声明>----------------------------------------
module tb_FSM_Mealy_3();

reg 	sys_clk;
reg 	sys_rst_n;
reg 	money;

wire	cola;

//------------<例化被测试模块>----------------------------------------
FSM_Mealy_3		FSM_Mealy_3_inst(
	.sys_clk	(sys_clk),
	.sys_rst_n	(sys_rst_n),
	.money		(money),

	.cola       (cola)
);

//------------<设置初始测试条件>----------------------------------------
initial begin
	sys_clk = 1'b0;					//初始时钟为0
	sys_rst_n <= 1'b0;				//初始复位
	money <= 1'b0;					//投币初始化为0
	#5								//5个时钟周期后
	sys_rst_n <= 1'b1;				//拉高复位,系统进入工作状态
	#25								//25个时钟周期后
	money <= 1'b1;					//拉高投币信号	
	#40								//40个时钟周期后
	money <= 1'b0;	    			//拉低投币信号	
	#20								//25个时钟周期后
	money <= 1'b1;	    			//拉高投币信号	
	#80								//25个时钟周期后
	money <= 1'b0;					//拉低投币信号
	#40 $stop;
end
//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;		//系统时钟周期20ns

//------------------------------------------------
//--    状态机名称查看器
//------------------------------------------------
reg [39:0]	state_name_cur;			//每字符8位宽,这里最多5个字符40位宽
reg [39:0]	state_name_next;		//每字符8位宽,这里最多5个字符40位宽

always @(*) begin
    case(FSM_Mealy_3_inst.cur_state)
         4'b0001:    	state_name_cur = "IDLE";
         4'b0010:   	state_name_cur = "ONE";
         4'b0100:   	state_name_cur = "TWO";
         4'b1000:		state_name_cur = "THREE"; 
        default:		state_name_cur = "IDLE";
    endcase
end

always @(*) begin
    case(FSM_Mealy_3_inst.next_state)
         4'b0001:    	state_name_next = "IDLE";
         4'b0010:   	state_name_next = "ONE";
         4'b0100:   	state_name_next = "TWO";
         4'b1000:		state_name_next = "THREE"; 
		 default:		state_name_next = "IDLE";
    endcase
end

endmodule

使用ModelSim执行仿真,仿真出来的波形如下所示:

从仿真结果可以看到:

  • 一共投入了6个硬币,理论上应该有2个可乐分别输出,实际也有两个可乐输出;
  • 现态落后次态一个时钟周期,这是因为需要用次态去描述现态;
  • 第1次输出可乐的输入为1,第2次输出可乐的输入也为1,这说明输出与输入相关;
  • 状态的跳转符合我们绘制的状态转移图;

6、总结与思考

状态机的三种描述方法:

  • 一段式:整个状态机写到一个always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。
  • 二段式:用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。
  • 三段式:在两个always模块描述方法基础上,使用三个always模块,一个always模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always模块描述状态输出。

应该选择哪一种状态机 ?

一段式状态机写法不够模块化 ,且过于臃肿不利于维护,及布局布线;

二段式状态机将同步时序和组合逻辑分别放到不同的always模块中实现,这样做的好处不仅仅是便于阅读、理解、维护,更重要的是利于综合器优化代码,利于用户添加合适的时序约束条件,利于布局布线器实现设计。但是其当前状态的输出用组合逻辑实现,组合逻辑很容易产生毛刺,而且不利于约束,不利于综合器和布局布线器实现高性能的设计。

三段式状态机二段式状态机相比,关键在于根据状态转移规律,在上一状态根据输入条件判断出当前状态的输出,从而在不插入额外时钟节拍的前提下,实现了寄存器输出,解决了毛刺问题。实际应用中三段式状态机使用最多,因为三段式状态机将组合逻辑和时序分开,有利于综合器分析优 化以及程序的维护;并且三段式状态机将状态转移与状态输出分开,使代码看上去更加清晰易懂,提高了代码的可读性,推荐使用三段式状态机。              

三段式状态机的基本格式:

  • 第一个 always 语句实现同步状态跳转;
  • 第二个 always 语句采用组合逻辑判断状态转移条件;
  • 第三个 always 语句采用时序逻辑描述状态输出。

状态机的编码方式:

独热码:和格雷码相比,虽然独热码多用了触发器,但所用组合电路可以省一些,因而使电路的速度和可靠性有显著提高,而总的单元数并无显著增加。因为独热码只有一位的变化,所以更适用于高速系统。

格雷码:使用了更多的组合逻辑资源,但是比独热码能表示更多的状态。

2进制:使用了更多的组合逻辑资源,但是比独热码能表示更多的状态,稳定性不如格雷码。

三段式状态机的第三段采用next_state还是cur_state:

Mealy型状态机的第三段输出采用cur_state,因为它的输出是与输入挂钩的,而next_state也是个组合逻辑变量,如果采用cur_state输出,则输出是完全基于组合逻辑的,就容易有问题。

Moore型状态机第三段使用next_state和cur_state的区别在于,当状态跳转时,基于next_state的输出是立刻变化的,而基于cur_state输出会延迟一个周期,其他情况都一样,应该根据自己的时序要求选择。

比如下图中的基于next_state的Mealy型状态机的输出,在投入第2个硬币后就拉高了输出cola,明显的功能错误。因为组合逻辑的跳转不是基于时钟的,而且Mealy型的输出也是基于输入的。

同样的输入激励在Moore状态机上就不会产生这种问题。二者的区别仅仅是next_state的输出比cur_state快一个时钟周期而已。

状态机的容错

一个完备的状态机应该具有初始状态或默认状态,当FPGA上电或者复位后,状态机应该能将所有状态参数都复位,并进入初始状态。解决复位问题的一个好办法是把初始状态的编码设置为全零,这样当复位信号起作用后,所有的寄存器都会默认进入复位状态即全零,等价于强行进入了复位状态。

此外,在状态编码时,可能会存在大量不使用的多余状态编码,如果不对这些编码进行合理的处理,那么状态机就有可能会进入到不可预测的状态,从而导致功能失常,严重的话甚至会一直卡死在异常状态。当然,对剩余状态的处理要不同程度地耗用逻辑资源,因此设计人员需要在状态机结构、状态编码方式、容错技术及系统的工作速度与资源利用率等诸多方面进行权衡,以得到 最佳的状态机。

常用的剩余状态处理方法如下:

  • 转入空闲状态,等待下一个工作任务的到来

  • 转入指定的状态,去执行特定任务

  • 转入预定义的专门处理错误的状态,如预警状态

在程序编写时,如果通过 if 语句来实现状态调转或者下一状态的计算,记得不要漏掉 else 分支;如果使用 case 语句则记得不要漏掉 default 分支。

状态机的设计准则

(1)基本的设计要求

  • 状态机设计要稳定 。所谓稳定就是指状态机不会进入死循环,不会进入一些未知状态,即使由于某些不可抗拒原因(系统故障、干扰等)进入不正常状态,也能够很快恢复正常。

  • 工作速度快。由于在设计中,状态机大都面向电路级设计,因此状态机必须满足电路的频率要求。

  • 所占资源少 。在满足工作频率要求的前提下,使用尽可能少的逻辑资源。

  • 代码清晰易懂、易维护 。首先,代码书写要规范;其次,要做好文档维护,注重注释语句的添加。

需要说明的,前3项要求不是绝对独立的,它们之间存在相互转化的关系。例如,安全性高就意味着必须处理所有条件判断的分支语句,但这必然导致所用逻辑资源加多; 至于面积和速度,二者的互换更是逻辑设计的关键思想。因此,各条要求要综合考虑,但无论如何,稳定性总是第一位的。

(2)设计的注意事项

  • 单独用一个 Verilog HDL 模块来描述一个有限状态机。这样不仅可以简化状态的定义、修改和调试,还可以利用 EDA 工具来进行优化和综合,以达到更优的 效果。

  • 使用代表状态名的参数 parameter/局部参数localparam 来给状态赋值,而不是用宏定义(`define)。因为宏定义产生的是一个全局的定义,而参数则定义了一个模块内的局部常量。这样当一个设计具有多个有重复状态名的状态机时也不会发生冲突!

  • 在组合 always 块中使用阻塞赋值,在时序 always 块中使用非阻塞赋值。这样可以使软件仿真的结果和真实硬件的结果相一致。

modelsim显示状态机名称的方法:

  1. 在Testbench中添加如下语句:实质上就是在测试文件里添加一个变量来代替你要观察的变量;
  2. 在modelsim的波形仿真见面右击波形,选择Radix--ASSIC
//------------------------------------------------
//--    状态机名称查看器
//------------------------------------------------
reg [39:0]	state_name_cur;			    //每字符8位宽,这里最多5个字符40位宽(THREE)
reg [39:0]	state_name_next;		    //每字符8位宽,这里最多5个字符40位宽(THREE)

always @(*) begin
    case(FSM_Mealy_3_inst.cur_state)    //这里写你例化的状态机模块里你想查看的参数
         4'b0001:    	state_name_cur = "IDLE";    //编码对应你的状态机的编码
         4'b0010:   	state_name_cur = "ONE";
         4'b0100:   	state_name_cur = "TWO";
         4'b1000:		state_name_cur = "THREE"; 
        default:		state_name_cur = "IDLE";
    endcase
end

always @(*) begin
    case(FSM_Mealy_3_inst.next_state)
         4'b0001:    	state_name_next = "IDLE";
         4'b0010:   	state_name_next = "ONE";
         4'b0100:   	state_name_next = "TWO";
         4'b1000:		state_name_next = "THREE"; 
		 default:		state_name_next = "IDLE";
    endcase
end

  • 📣您有任何问题,都可以在评论区和我交流📃!
  • 📣本文由 孤独的单刀 原创,首发于CSDN平台🐵,博客主页:wuzhikai.blog.csdn.net
  • 📣您的支持是我持续创作的最大动力!如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐

  • 312
    点赞
  • 1259
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 79
    评论
评论 79
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孤独的单刀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值