1、原理

状态机全称是有限状态机(Finite State Machine,FSM),是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。这里将学习状态机的相关概念并使用状态机实现特定字符串的检测。理解一段式、二段式以及三段式状态机的区别以及优缺点。

Mealy状态机(与输入和当前状态有关)
Moore状态机(只与当前状态有关)

内容:输入、输出、状态、状态转移条件
状态机的描述方式:一段式、二段式以及三段式
(1)一段式,整个状态机写到一个always模块里面。在该模块中既描述状态和状态转移,又描述状态的输入和输出。写法简单,但不容易维护。适合简单的状态机使用,不过不推荐这种方法。

(2)二段式,用两个always模块来描述状态机。其中一个always模块采用同步时序描述状态转移,另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律及其输入输出。优点:写法便于阅读、理解和维护,而且有利于综合器优化代码,利于用户添加合适的时序约束条件,利于布局布线实现设计。但是在两段式描述中,当前状态的输出用组合逻辑实现,可能存在竞争和冒险,产生毛刺。要求对状态机的输出用寄存器打一拍,但是在很多情况下不允许插入寄存器节拍,此时使用三段式。
(3)三段式,在两个always模块描述方法基础上,使用三个always模块。一个always模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always模块描述状态输出(可用组合电路输出,也可以时序电路输出),时序逻辑的输出解决了两段式组合逻辑的毛刺问题,但是从资源消耗的角度看,三段式的资源消耗要多一些。
总结:两段式有限状态机与一段式状态机的区别在于将时序部分(状态转移)和组合部分(判断状态转移条件和产生输出)分开,写成了两个always语句。将组合部分中的判断状态转移条件和产生输出再分开写,即为三段式有限状态机。二段式在组合逻辑特别复杂时,注意需要在后面加一个触发器以及消除组合逻辑对输出产生的毛刺的影响。三段式则没有这个问题,这是因为由第三个always会生成触发器。而现在的器件根本不在乎这一点资源的消耗,推荐使用二段式或者三段式以及输出寄存的状态机输出来描述有限状态机。
编写状态机的注意事项:1、为了避免不必要的锁存器的生成,需要穷举所有状态对应的输出动作,或者使用default来定义未定义状态动作;在定义状态时,推荐使用本地化参数定义localparam这样可以在编写状态更清晰且不容易出错,也方便修改;在复位或者跑飞回到初始态或者预定态;要有异步或者同步复位来确保状态机上电有个初始值(复位的一个作用)。

2、模板

(1)三段式的模板:
图片来源
这个模板特别需要注意的是,第二个always块中触发条件哪里写的current_state,当然也可以写(*),下面的case都是赋值给next_current

(2)附上《深入浅出玩转FPGA》中的三段式、二段式、一段式的例子:
一段式状态机verilog代码如下:

//一段式状态机代码如下:
reg[3:0]cstate;//为啥是3:0而不是2:0,不是只有五个状态吗
always@(posedge clk or negedge rst_n)
	begin
		if(!rst_n)
			begin
				cstate<=IDLE;
				cmd<=3'b111; end else case(cstate) IDLE:if(wr_req) begin cstate<=WR_S1; cmd<=3'b011;
					end
				else if(rd_req)
					begin
						cstate<=RD_S1;
						cmd<=3'b011; end else begin cstate<=IDLE; cmd<=3'b111;
					end
			WR_S1:begin
						cstate<=WR_S2;
						cmd<=3'b101; end WR_S2:begin cstate<=IDLE; cmd<=3'b111;
				end
				
				
			RD_S1:if(wr_req)
					begin
						cstate<=WR_S1;
						cmd<=3'b101; end else begin cstate<=RD_S2; cmd<=3'b110;
					end
			RD_S2:if(wr_req)
					begin
						cstate<=WR_S1;
						cmd<=3'b011; end else begin cstate<=IDLE; cmd<=3'b111;
					end
			default:cstate<=IDLE;
			endcase
	end
在这里插入代码片

二段式状态机verilog模板

//两段式状态机代码
reg[3:0]  cstate;
reg[3:0]  nstate;
always@(posedge clk or negedge rst_n)//描述状态跳转
	if(!rst_n)cstate<=IDLE;
	else cstate<=nstate;
	
always@(cstate or wr_req or rd_req)//状态判断及输出
	begin
		case(cstate)
		IDLE:if(wr_req)
				begin
					nstate=WR_S1;
					cmd=3'b011; end else if(rd_req) begin nstate=RD_S1; cmd=3'b011;
				end
			else 
				begin
					nstate=IDLE;
					cmd=3'b111; end WR_S1:begin nstate=WR_S2;cmd=3'b101;end
		WR_S2:begin nstate=IDLE;cmd=3'b111;end RD_S1:if(wr_req) begin nstate=WR_S1;cmd=3'b101;end
			else begin nstate=RD_S2;cmd=3'b110;end RD_S2:if(wr_req)begin nstate=RD_S2;cmd=3'b011;end
				else begin nstate=IDLE;cmd=3'b111;end
		default:nstate=IDLE;
		endcase
	end
	
在这里插入代码片

三段式状态机verilog代码

//三段式状态机代码
reg [3:0]cstate;
reg [3:0]nstate;
always@(posedge clk or negedge rst_n)//时序,描述状态跳转
	if(!rst_n)cstate<=IDLE;
	else cstate<=nstate;
always@(cstate or wr_req or rd_req)//组合逻辑,判断状态
	begin
		case(cstate)
			IDLE:if(wr_req)nstate=WR_S1;
					else if(rd_req)nstate=RD_S1;
					else nstate=IDLE;
			WR_S1:nstate=WR_S2;
			WR_S2:nstate=IDLE;
			RD_S1:if(wr_req)nstate=WR_S1;else nstate=RD_S2;
			RD_S2:if(wr_req)nstate=WR_S1;else nstate=IDLE;
			default:nstate=IDLE;
		endcase
	end
	
always@(posedge clk or negedge rst_n)
	begin
		if(!rst_n)cmd<=3'b111;
			else case(nstate)
			IDLE:if(wr_req)cmd<=3'b011;
				else if(rd_req)cmd<=3'b011;
					else cmd<=3'b111;
			WR_S1:cmd<=3'b101;
			WR_S2:cmd<=3'b111;
			RD_S1:if(wr_req)cmd<=3'b101;else cmd<=3'b110;
			RD_S2:if(wr_req)cmd<=3'b011;else cmd<=3'b111;
			default:;
			endcase
	end
	
在这里插入代码片

书上对三段式的总结

3、例子:Hello序列状态的检测

这个例子来源于小梅哥,我在一段式的基础上,改写了二段式和三段式。功能:检测到Hello后,使LED翻转。属于Moore状态机。
1):等待“H”的到来。如果检测到"H",进入状态2.检测"e".否则一直等待"H”

2):检测当前字符是否是“e”,如果是“e",跳转到状态3,检测“l”,否则,回到状态1,重新等待“H”
3):检测当前字符是否是“l”,如果是“l”,跳转到状态4,检测“l”,否则,回到状态1.重新等待“H”

4):检测当前字符是否是“l”,如果是“l”,跳转到状态5检测“o"。 否则,回到状态1.重新等待“H”
5):检测当前字符是否为“o”。如果是"o”。驱动led控制引脚发生状态翻转。同时回到状态1.等待下一个“H”的到来,否则。直接回到状态1。 重新等待“H”。

(1)一段式状态机:

`timescale 1ns/1ns
module Hello(clk,Rst_n,data,led);
input clk;//50M
input Rst_n;
input [7:0]data;
output reg led;
localparam 
	CHECK_H=5'b0_0001,
	CHECK_e=5'b0_0010,
	CHECK_la=5'b0_0100,
	CHECK_lb=5'b0_1000,
	CHECK_o=5'b1_0000;//独热码编码,降低译码逻辑
reg[4:0] state;
//采用一段式进行编写,便于入门学习	
always @(posedge clk or negedge Rst_n)
if(!Rst_n)
	begin state<=CHECK_H;//
			led<=1'b1;//熄灭,低电平有效
	end
else begin
	case(state)
		CHECK_H:if(data=="H")state<=CHECK_e;else state<=CHECK_H;
		CHECK_e:if(data=="e")state<=CHECK_la;else state<=CHECK_H;
		CHECK_la:if(data=="l")state<=CHECK_lb;else state<=CHECK_H;
		CHECK_lb:if(data=="l")state<=CHECK_o;else state<=CHECK_H;
		//CHECK_o:if(data=="o")begin led<=~led;state<=CHECK_H;end else state<=CHECK_H;
	    CHECK_o:begin state<= CHECK_H;if(data=="o")led<=~led;else led<=led;end
		default: state<=CHECK_H;
	endcase
end
endmodule

//testbench
module Hello_tst();
reg clk;
reg Rst_n;
reg [7:0]ASCII;
wire led;
Hello uut(.clk(clk),
.Rst_n(Rst_n),
.data(ASCII),
.led(led));
initial
	begin
	ASCII=0;
	clk=1;
	Rst_n=0;
	#400 Rst_n=1;
	#401;
	forever begin

	#20 ASCII="H";
	#20 ASCII="i";
	#20 ASCII="H";
	#20 ASCII="e";
	#20 ASCII="l";
	#20 ASCII="l";
	#20 ASCII="o";
	#20 ASCII="h";
	//ASCII="niHelloh"
	end
	end
	always#10 clk=~clk;
	
endmodule

在这里插入代码片

仿真结果
刚开始led一直不翻转,我真是醉了,第一个错误是clock初始值写成了0;应该是1,这样20的时候才是上升沿;第二仿真文件中,将“o”写成了数字“0”(哭死,找了一下午呀)

led在“o”出现之后的时钟上升沿才出现翻转,不符合要求,如下图所示:
将testbench中的#401注释掉后,就好了,也就是要刚好在时钟上升沿。如下图所示

调试过程的心得:如果有仿真出来,但是不是预期的结果,那么说明那里的逻辑写错了,这个时候不仅需要去查看对应的RTL代码(ctr +F相关输出并高亮便于查找错误),也要看看仿真文件写错了没,包括初始值那里对不对,赋值对不对(比如“o”是否写错数字0),例化是否出错,时序(时间)是否正确等。

之前很好奇,为啥看书说要有建立时间和保持时间,而我们写代码的时候却偏偏刚好在时钟上升沿给数据。问了同学,好像是因为我们使用的Vivado或者其他工具,已经处理了,使我们设计的逻辑和底层寄存器需求做了一个衔接,我们做逻辑设计的时候不用管。

(2)二段式

现在来改成成二段式:

`timescale 1ns/1ns
module Hello(clk,Rst_n,data,led);
input clk;//50M
input Rst_n;
input [7:0]data;
output reg led;
reg [4:0]state,n_state;//state代表当前状态,n_state代表下一个状态
localparam 
	CHECK_H=5'b0_0001,
	CHECK_e=5'b0_0010,
	CHECK_la=5'b0_0100,
	CHECK_lb=5'b0_1000,
	CHECK_o=5'b1_0000;//独热码编码,降低译码逻辑
//reg[4:0] state;

always@(posedge clk or negedge Rst_n)//第一个always块描述状态的跳转
if(!Rst_n)
state<=CHECK_H;
else
state<=n_state;

always @(*)//第二个always块,判断状态跳转
if(!Rst_n)
	begin state=CHECK_H;//
			led<=1'b1;//熄灭,低电平有效
	end
else begin
	case(state)//不知道为啥这里把<=换成=就跑不出正确结果???
		CHECK_H:if(data=="H")n_state<=CHECK_e;else n_state<=CHECK_H;
		CHECK_e:if(data=="e")n_state<=CHECK_la;else n_state<=CHECK_H;
		CHECK_la:if(data=="l")n_state<=CHECK_lb;else n_state<=CHECK_H;
		CHECK_lb:if(data=="l")n_state<=CHECK_o;else n_state<=CHECK_H;
		//CHECK_o:if(data=="o")n_state<=CHECK_H;
		CHECK_o:begin state<=CHECK_H;if(data=="o") led<=~led; else  led<=led;end
		default: n_state<=CHECK_H;
	endcase
end

//always@(posedge clk or negedge Rst_n)//输出
//if(!Rst_n)
//led<=1'b1;//熄灭,低电平有效
//else begin
//		case(n_state)
//		CHECK_H:led<=led;
//		CHECK_e:led<=led;
//		CHECK_la:led<=led;
//		CHECK_lb:led<=led;
//		CHECK_o:if(data=="o") led<=~led; else  led<=led;
//		default: led<=led;
//	endcase
//end
Endmodule
//
Testbench 同下面的三段式;

在这里插入代码片

仿真结果
(3)三段式

`timescale 1ns/1ns
module Hello(clk,Rst_n,data,led);
input clk;//50M
input Rst_n;
input [7:0]data;
output reg led;
reg [4:0]state,n_state;//state代表当前状态,n_state代表下一个状态
localparam 
	CHECK_H=5'b0_0001,
	CHECK_e=5'b0_0010,
	CHECK_la=5'b0_0100,
	CHECK_lb=5'b0_1000,
	CHECK_o=5'b1_0000;//独热码编码,降低译码逻辑
//reg[4:0] state;

always@(posedge clk or negedge Rst_n)//第一个always块描述状态的跳转
if(!Rst_n)
state<=CHECK_H;
else
state<=n_state;

always @(*)//第二个always块,判断状态跳转
if(!Rst_n)
	begin state=CHECK_H;//
			//led<=1'b1;//熄灭,低电平有效
	end
else begin
	case(state)//不知道为啥这里把<=换成=就跑不出正确结果???
		CHECK_H:if(data=="H")n_state<=CHECK_e;else n_state<=CHECK_H;
		CHECK_e:if(data=="e")n_state<=CHECK_la;else n_state<=CHECK_H;
		CHECK_la:if(data=="l")n_state<=CHECK_lb;else n_state<=CHECK_H;
		CHECK_lb:if(data=="l")n_state<=CHECK_o;else n_state<=CHECK_H;
		CHECK_o:if(data=="o")n_state<=CHECK_H;
		//CHECK_o:state<=CHECK_H;if(data=="o") led<=~led; else  led<=led;
		default: n_state<=CHECK_H;
	endcase
end

always@(posedge clk or negedge Rst_n)//输出
if(!Rst_n)
led<=1'b1;//熄灭,低电平有效
else begin
		case(n_state)
		CHECK_H:led<=led;
		CHECK_e:led<=led;
		CHECK_la:led<=led;
		CHECK_lb:led<=led;
		CHECK_o:if(data=="o") led<=~led; else  led<=led;
		default: led<=led;
	endcase
end
endmodule


//三段式的testbench

module Hello_tst();
reg clk;
reg Rst_n;
reg [7:0]ASCII;
wire led;
Hello uut(.clk(clk),
.Rst_n(Rst_n),
.data(ASCII),
.led(led));
initial
	begin
	ASCII=0;
	clk=1;
	Rst_n=0;
	#400 Rst_n=1;
	//#395;
	forever begin

	#20 ASCII="H";
	#20 ASCII="i";
	#20 ASCII="H";
	#20 ASCII="e";
	#20 ASCII="l";
	#20 ASCII="l";
	#20 ASCII="o";
	#20 ASCII="h";
	//ASCII="niHelloh"
	end
	end
	always#10 clk=~clk;
	
endmodule
在这里插入代码片