---------------------------------------------------------------------------------------------------笔记(正点原子等)
Verilog是硬件描述语言,硬件电路是并行执行的,当需要按照流程或者步骤来完成某个
功能时,代码中通常会使用很多个if嵌套语句来实现,这样就增加了代码的复杂度,以及降低
了代码的可读性,这个时候就可以使用状态机来编写代码。状态机相当于一个控制器,它将一
项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态
之间进行转换,状态转换的过程就是实现逻辑功能的过程。
状态机,全称是有限状态机(Finite State Machine,缩写为FSM),是一种在有限个状
态之间按一定规律转换的时序电路,可以认为是组合逻辑和时序逻辑的一种组合。状态机通过
控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,
状态机优势明显,因此基本上都会用到状态机,如SDRAM控制器等。
根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore)型状态
机和米勒(Mealy)型状态机。
➢ Mealy状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。
➢ Moore状态机:组合逻辑的输出只取决于当前状态。
(1)Mealy状态机
米勒状态机的模型如下图所示,模型中第一个方框是指产生下一状态的组合逻辑F,F是当
前状态和输入信号的函数,状态是否改变、如何改变,取决于组合逻辑F的输出;第二框图是
指状态寄存器,其由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在
时钟的跳边沿;第三个框图是指产生输出的组合逻辑G,状态机的输出是由输出组合逻辑G提供
的,G也是当前状态和输入信号的函数。
(2)Moore状态机
摩尔状态机的模型如下图所示,对比米勒状态机的模型可以发现,其区别在于米勒状态机
的输出由当前状态和输入条件决定的,而摩尔状态机的输出只取决于当前状态。
三段式状态机:
根据状态机的实际写法,状态机还可以分为一段式、二段式和三段式状态机。
一段式:整个状态机写到一个always模块里面,在该模块中既描述状态转移,又描述状态
的输入和输出。不推荐采用这种状态机,因为从代码风格方面来讲,一般都会要求把组合逻辑
和时序逻辑分开;从代码维护和升级来说,组合逻辑和时序逻辑混合在一起不利于代码维护和
修改,也不利于约束。
二段式:用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转
移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出。不同于一段式
状态机的是,它需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑。
三段式:在两个always模块描述方法基础上,使用三个always模块,一个always模块采用
同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另
一个always模块描述状态输出(可以用组合电路输出,也可以时序电路输出)。
实际应用中三段式状态机使用最多,因为三段式状态机将组合逻辑和时序分开,有利于综
合器分析优化以及程序的维护;并且三段式状态机将状态转移与状态输出分开,使代码看上去
更加清晰易懂,提高了代码的可读性,推荐大家使用三段式状态机,本文也着重讲解三段式。
三段式状态机的基本格式是:
第一个always语句实现同步状态跳转;
第二个always语句采用组合逻辑判断状态转移条件;
第三个always语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。
(1)七分频状态机:
在开始编写状态机代码之前,一般先画出状态跳转图,这样在编写代码时思路会比较清晰,
下面以一个7分频为例(对于分频等较简单的功能,可以不使用状态机,这里只是演示状态机
编写的方法),状态跳转图如下图所示:
状态跳转图画完之后,接下来通过parameter来定义各个不同状态的参数,如下代码所示:
parameter S0 = = 7'b0000001; //独热码定义方式
parameter S1 = = 7'b0000010;
par ameter S2 = = 7'b0000100;
parameter S3 = = 7'b0001000;
parameter S4 = = 7'b0010000;
parameter S5 = = 7'b0100000;
parameter S6 = = 7'b1000000;
这里是使用独热码的方式来定义状态机,每个状态只有一位为1,当然也可以直接定义成
十进制的0,1,2……7。
因为我们定义成独热码的方式,每一个状态的位宽为7位,接下来还需要定义两个7位的寄
存器,一个用来表示当前状态,另一个用来表示下一个状态,如下所示:
reg [ [6: :0] ] curr_st ; //当前状态
reg [ [6: :0] ] next_st ; //下一个状态
接下来就可以使用三个always语句来开始编写状态机的代码,第一个always采用同步时序
描述状态转移,第二个always采用组合逻辑判断状态转移条件,第三个always是描述状态输出,
一个完整的三段式状态机的例子如下代码所示:
module divider7_fsm (
//系统时钟与复位
input sys_clk ,
input sys_rst_n ,
//输出时钟
output reg clk_divide_7
);
//parameter define
parameter S0=7'b0000001; //独热码定义方式
parameter S1=7'b0000010;
parameter S2=7'b0000100;
parameter S3=7'b0001000;
parameter S4=7'b0010000;
parameter S5=7'b0100000;
parameter S6=7'b1000000;
//reg define
reg [6:0] curr_st; //当前状态
reg [6:0] next_st; //下一个状态
//*****************************************************
//** main code
//*****************************************************
//状态机的第一段采用同步时序描述状态转移
always @( posedge sys_clk or negedge sys_rst_n ) begin
if (!sys_rst_n)
curr_st <= S0;
else
curr_st <= next_st;
end
//状态机的第二段采用组合逻辑判断状态转移条件
always @(*) begin
case ( (curr_st) )
S0:next_st = S1;
S1:next_st = S2;
S2:next_st = S3;
S3:next_st = S4;
S4:next_st = S5;
S5:next_st = S6;
S6:next_st = S0;
default: next_st = S0;
endcase
end
//状态机的第三段描述状态输出(这里采用时序电路输出)
always @( posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_divide_7 <= 1'b0;
else if ((curr_st == S0)||( curr_st == S1 )||(curr_st == S2)||(curr_st == S3 ))
clk_divide_7 <= 1'b0;
else if ((curr_st == S4)||(curr_st == S5)||( curr_st == S6 ))
clk_divide_7 <= 1'b1;
else
clk_divide_7<=clk_divide_7;
end
endmodule
在编写状态机代码时首先要定义状态变量(代码中的参数S0~S6)与状态寄存器(curr_st、
next_st),如代码中第10行至第21行所示;接下来使用三个always语句来实现三段状态机,
第一个always语句实现同步状态跳转,在复位的时候,当前状 态处在S0状态,否则将下一个状态赋值给当前状态;第二个always采用组合逻辑判断状态转移条件(,这里每一个状态只保持一个时钟周期,也就是直接跳转到下一个状态,在实际应用中,一般根据输入的条件来判断是否跳转到其它状态或者停留在当前转态,最后在case语句后面增加一个default语句,来防止状态机处在异常的状态;第三个always输出分频后的时钟,状态机的第三段可以使用组合逻辑电路输出,也可以使用时序逻辑电路输出,一般推荐使用时序电路输出,因为状态机的设计和其它设计一样,最好使用同步时序方式设计,以提高设计的稳定性,消除毛刺。
从代码中可以看出,输出的分频时钟clk_divide_7只与当前状态(curr_st)有关,而与
输入状态无关,所以属于摩尔型状态机。状态机的第一段对应摩尔状态机模型的状态寄存器,
用来记忆状态机当前所处的状态;状态机的第二段对应摩尔状态机模型产生下一状态的组合逻
辑F;状态机的第三段对应摩尔状态机产生输出的组合逻辑G,因为采用时序电路输出有很大的
优势,所以这里第三段状态机是由时序电路输出的。
状态机采用时序逻辑输出的状态机模型如下图所示:
采用这种描述方法虽然代码结构复杂了一些,但是这样做的好处是可以有效地滤去组合逻
辑输出的毛刺,同时也可以更好的进行时序计算与约束,另外对于总线形式的输出信号来说,
容易使总线数据对齐,减小总线数据间的偏移,从而降低接收端数据采样出错的频率。
(2)售卖机状态机:
自动售卖机
◆自动售卖机的功能描述如下:
饮料单价 2 元,该售卖机只能接受 0.5 元、1 元的硬币。考虑找零和出货。投币和出货过程都是一次一次的进行,不会出现一次性投入多币或一次性出货多瓶饮料的现象。每一轮售卖机接受投币、出货、找零完成后,才能进入到新的自动售卖状态。
◆该售卖机的工作状态转移图如下所示,包含了输入、输出信号状态。
其中,coin = 1 代表投入了 0.5 元硬币,coin = 2 代表投入了 1 元硬币。
状态机设计:3 段式(推荐)
◆状态机设计如下:
(0) 首先,根据状态机的个数确定状态机编码。利用编码给状态寄存器赋值,代码可读性更好。
(1) 状态机第一段,时序逻辑,非阻塞赋值,传递寄存器的状态。
(2) 状态机第二段,组合逻辑,阻塞赋值,根据当前状态和当前输入,确定下一个状态机的状态。
(3) 状态机第三代,时序逻辑,非阻塞赋值,因为是 Mealy 型状态机,根据当前状态和当前输入,确定输出信号。
// vending-machine
// 2 yuan for a bottle of drink
// only 2 coins supported: 5 jiao and 1 yuan
// finish the function of selling and changing
module vending_machine_p3 (
input clk ,
input rstn ,
input [1:0] coin , //01 for 0.5 jiao, 10 for 1 yuan
output [1:0] change ,
output sell //output the drink
);
//machine state decode
parameter IDLE = 3'd0 ;
parameter GET05 = 3'd1 ;
parameter GET10 = 3'd2 ;
parameter GET15 = 3'd3 ;
//machine variable
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
//(2) state switch, using block assignment for combination-logic
//all case items need to be displayed completely
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
//(3) output logic, using non-block assignment
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 == GET15 && coin ==2'h1)
|| (st_cur == GET10 && coin ==2'd2)) begin
change_r <= 2'b0 ;
sell_r <= 1'b1 ;
end
else if (st_cur == GET15 && 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
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 ; //store state of the buy operation
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 也都能根据输入的硬币场景输出正确的是否找零信号。
状态机修改:2 段式
◆将 3 段式状态机 2、3 段描述合并,其他部分保持不变,状态机就变成了 2 段式描述。
修改部分如下:
//(2) state switch, and output logic
//all using block assignment for combination-logic
reg [1:0] change_r ;
reg sell_r ;
always @(*) begin //all case items need to be displayed completely
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 // case (coin)
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 // case (coin)
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 // case (coin)
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 段式状态机输出提前了一个时钟周期,这是因为输出信号都是阻塞赋值导致的。
如图中红色圆圈部分,输出信号都出现了干扰脉冲,这是因为输入信号都是异步的,而且输出信号是组合逻辑输出,没有时钟驱动。
实际中,如果输入信号都是与时钟同步的,这种干扰脉冲是不会出现的。如果是异步输入信号,首先应当对信号进行同步。
状态机修改:1 段式(慎用)
◆将 3 段式状态机 1、 2、3 段描述合并,状态机就变成了 1 段式描述。
修改部分如下:
//(1) using one state-variable do describe
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 // else: !if(!rstn)
end
◆将上述修改的新模块例化到 3 段式的 testbench 中即可进行仿真,结果如下。
由图可知,输出信号与 3 段式状态机完全一致。
1 段式状态机的缺点就是许多种逻辑糅合在一起,不易后期的维护。当状态机和输出信号较少时,可以尝试此种描述方式。
状态机修改:Moore 型
如果使用 Moore 型状态机描述售卖机的工作流程,那么还需要再增加 2 个状态编码,用以描述 Mealy 状态机输出时的输入信号和状态机状态。
3 段式 Moore 型状态机描述的自动售卖机 Verilog 代码如下:
module vending_machine_moore (
input clk ,
input rstn ,
input [1:0] coin , //01 for 0.5 jiao, 10 for 1 yuan
output [1:0] change ,
output sell //output the drink
);
//machine state decode
parameter IDLE = 3'd0 ;
parameter GET05 = 3'd1 ;
parameter GET10 = 3'd2 ;
parameter GET15 = 3'd3 ;
// new state for moore state-machine
parameter GET20 = 3'd4 ;
parameter GET25 = 3'd5 ;
//machine variable
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
//(2) state switch, using block assignment for combination-logic
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 // case (st_cur)
end // always @ (*)
// (3) output logic,
// one cycle delayed when using non-block assignment
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 型状态机的特点。
◆输出信号赋值时,用阻塞赋值,则可以提前一个时钟周期。
输出逻辑修改如下。
// (3.2) output logic, using block assignment
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 型状态机一致。