Verilog基础语法——状态机(类型、写法、状态编码方式)
- 写在前面
- 一、状态机类型
- 二、状态机写法
- 2.1 一段式
- 2.2 两段式
- 2.3 三段式
- 三、状态机状态编码方式
- 写在后面
写在前面
在FPGA设计过程,经常会设计状态机用于控制整个硬件电路的工作进程,也称为有限状态机(Finite State Machine,FSM)。本文将对状态机的类型、写法以及状态编码方式进行介绍。
一、状态机类型
状态机根据类型可以分为Moore型状态机和Mealy型状态机,其中Moore型状态机的输出只与当前状态有关,如下图所示。其中包含三个部分:(1)用于产生次态的组合逻辑;(2)用于寄存器现态的状态寄存器;(3)用于产生输出的组合逻辑;
而Mealy型状态机不仅与当前状态有关,且与当前时刻的输入有关,如下图所示。Mealy型状态机产生次态的组合逻辑和状态寄存器与Moore型状态机是一致的,而不同的是,Mealy型状态机的输出不仅与现态有关,而且与当前时刻的输入也相关。
进一步地,可以通过状态转换图来详细分析两者之间的不同之处。以”101“的非重叠序列检测为例,Moore型状态机的状态转换图如下图所示。
Tip:非重叠序列检测是指检测到的多组“101”序列之间是非重叠、独立的,比如序列110101进行“101”的非重叠序列检测,输出检测结果为000100。而重叠序列检测值检测到的多组“101”序列之间可以重叠,即第一组“101”和第二组“101”之间可以有重叠部分,第一组“101”序列的最后一个1是第二组“101”序列的第一个1,比如序列110101进行“101”的重叠序列检测,输出检测结果为000101。
而Mealy型状态机的状态转换图如下图所示。
在这里可以看出,同样情况下Moore型状态机要比Mealy型状态机多一个状态,也就是说在实际设计中Moore型状态机会比Mealy型状态机多一个时钟周期。总结来说,Moore型状态机的相比于Mealy型状态机会慢一拍,但是时序会更好,而Mealy状态机相比于Moore状态机快一拍,速度会更快,但时序会差一些。在设计中一般考虑使用Moore型状态机。
二、状态机写法
对于状态机的写法,可以分为一段式、两段式和三段式,不同的写法将在下面三个小节介绍。
2.1 一段式
一段式状态机中只有一个always块,在该always块中同时描述状态寄存、状态转移条件的判断与数据输出。对于大规模电路设计,状态机控制的数据输出有多个,该写法会导致状态机代码复杂且杂乱,后续开发过程难以修改和维护。以”101“序列的Moore型状态机为例,其一段式状态机写法如下:
module check1(input wire clk ,input wire rst_n ,input wire din ,output reg check_pass
);localparam S0 = 4'b0001; // 初始化状态
localparam S1 = 4'b0010; // 计数状态
localparam S2 = 4'b0100; // 计算状态
localparam S3 = 4'b1000; // 结束状态reg din_r;
reg [3:0] state;// 输入数据打拍
always @(posedge clk) din_r <= din;//-------- 一段式状态机 --------//
always @(posedge clk or negedge rst_n) beginif(!rst_n) beginstate <= S0;check_pass <= 1'b0;endelse begincase(state)S0 :beginif(din_r == 1'b1) beginstate <= S1;check_pass <= 1'b0;endelse beginstate <= S0;check_pass <= 1'b0;endendS1 : beginif(din_r == 1'b0) beginstate <= S2;check_pass <= 1'b0;endelse beginstate <= S1;check_pass <= 1'b0;endendS2 : beginif(din_r == 1'b1) beginstate <= S3;check_pass <= 1'b0;endelse beginstate <= S2;check_pass <= 1'b0;endendS3 : beginif(din_r == 1'b1) beginstate <= S1;check_pass <= 1'b1;endelse beginstate <= S0;check_pass <= 1'b1;endenddefault: beginstate <= S0;check_pass <= 1'b0;endendcaseend
endendmodule
2.2 两段式
一段式状态机写法中代码杂乱,不利于后续修改和维护,两段式状态机则克服了该缺点,将三个部分(状态转移寄存器、状态转移条件判断、输出)分成三个always块进行描述,同时输出使用组合逻辑进行赋值,而组合逻辑赋值会导致毛刺等现象,增加电路的不稳定性。以”101“序列的Moore型状态机为例,其两段式状态机写法如下:
module check2(input wire clk ,input wire rst_n ,input wire din ,output reg check_pass
);localparam S0 = 4'b0001; // 初始化状态
localparam S1 = 4'b0010; // 计数状态
localparam S2 = 4'b0100; // 计算状态
localparam S3 = 4'b1000; // 结束状态reg din_r;
reg [3:0] curr_state;
reg [3:0] next_state;// 输入数据打拍
always @(posedge clk) din_r <= din;//-------- 两段式状态机 --------//
// 第一段(时序逻辑,用于描述状态寄存器)
always @(posedge clk or negedge rst_n) beginif(!rst_n)curr_state <= S0; //初始化状态elsecurr_state <= next_state;
end// 第二段(组合逻辑,用于状态转移条件判断与输出)
always @(*) beginnext_state = S0;case(curr_state)S0 :beginif(din_r == 1'b1) beginnext_state = S1;check_pass = 1'b0;endelse beginnext_state = S0;check_pass = 1'b0;endendS1 :beginif(din_r == 1'b0) beginnext_state = S2;check_pass = 1'b0;endelse beginnext_state = S1;check_pass = 1'b0;endendS2 :beginif(din_r == 1'b1) beginnext_state = S3;check_pass = 1'b0;endelse beginnext_state = S2;check_pass = 1'b0;endendS3 :begincheck_pass = 1'b1;if(din_r == 1'b1) beginnext_state = S1;endelse beginnext_state = S0;endenddefault:beginnext_state = S0;check_pass = 1'b0;endendcase
endendmodule
有一点值得注意的是,在两段式状态机中数据输出check_pass与次态next_state采用组合逻辑进行控制,一定要补全if-else中else情况下的数值,否则在电路综合时会产生Latch。
2.3 三段式
三段式状态机在两段式状态机的基础上,将控制输出数据赋值的组合逻辑改成时序逻辑,消除了组合逻辑输出中存在毛刺
module check3(input wire clk ,input wire rst_n ,input wire din ,output reg check_pass
);localparam S0 = 4'b0001; // 初始化状态
localparam S1 = 4'b0010; // 计数状态
localparam S2 = 4'b0100; // 计算状态
localparam S3 = 4'b1000; // 结束状态reg din_r;
reg [3:0] curr_state;
reg [3:0] next_state;// 输入数据打拍
always @(posedge clk) din_r <= din;//-------- 三段式状态机 --------//
// 第一段(时序逻辑,用于描述状态寄存器)
always @(posedge clk or negedge rst_n) beginif(!rst_n)curr_state <= S0; //初始化状态elsecurr_state <= next_state;
end// 第二段(组合逻辑,用于状态转移条件判断)
always @(*) beginnext_state = S0;case(curr_state)S0 :beginif(din_r == 1'b1)next_state = S1;elsenext_state = S0;endS1 :beginif(din_r == 1'b0)next_state = S2;elsenext_state = S1;endS2 :beginif(din_r == 1'b1)next_state = S3;elsenext_state = S2;endS3 :beginif(din_r == 1'b1)next_state = S1;elsenext_state = S0;enddefault:next_state = S0;endcase
end// 第三段(时序逻辑,用于描述输出)
always @(posedge clk or negedge rst_n) beginif(!rst_n)check_pass <= 1'b0;elsecase(curr_state)S0 : check_pass <= 1'b0;S1 : check_pass <= 1'b0;S2 : check_pass <= 1'b0;S3 : check_pass <= 1'b1;default: check_pass <= 1'b0;endcase
endendmodule
Tip:这里需要注意的是,两段式状态机于三段式状态机的区别之处并不是状态机有几个always块,而是输出由组合逻辑还是时序逻辑控制。两段式状态机的输出由组合逻辑控制,容易产生毛刺,三段式状态机的输出由时序逻辑控制,避免了两段式状态机输出存在毛刺的问题。
总结来说,一段式、两段式、三段式状态机的特点与优缺点如下表所示:
一段式写法 | 两段式写法 | 三段式写法 | |
---|---|---|---|
特点 | 一个always块(时序逻辑),同时描述状态寄存器、状态转移条件判断与数据输出 | 两个always块,第一个always块(时序逻辑)描述状态寄存器,第二个always块(组合逻辑)描述状态转移条件判断与数据输出 | 三个always块,第一个always块(时序逻辑)描述状态寄存器,第二个always块(组合逻辑)描述状态转移条件判断,第三个always块(时序逻辑)描述数据输出 |
优点 | 结构简单,易于实现 | 代码清晰,易于修改和调试 | 便于修改和维护,输出输出不存在毛刺 |
缺点 | 不利于修改和维护 | 数据输出存在毛刺 |
问题1:一段式、两段式和三段式状态机写法不同,在RTL电路与时序上具体差异是什么样的?
(1)RTL电路
下图分别为一段式、两段式和三段式状态机的RTL电路,由于一段式状态机中输出check_pass均由时序逻辑控制,区别在于将写在一个always块中的三个部分(状态转移寄存器、状态转移条件判断、输出)分成三个always块进行描述,所以两者的RTL电路一致。而两段式状态机中输出check_pass采用组合逻辑进行控制,所以与两段式、三段式状态机的RTL电路是不一致的。
(2)时序波形
为了对比三种不同写法状态机的时序差异,编写测试文件TestBench如下:
`timescale 1ns/1ns
module tb_check();
reg clk,rst_n,din;
wire check_pass1,check_pass2,check_pass3;initial beginclk = 1'b1;rst_n = 1'b0;din = 1'b0;#100rst_n = 1'b1;#20din = 1'b1;#20din = 1'b1;#20din = 1'b0;#20din = 1'b1;#20din = 1'b1;#20din = 1'b0;#20din = 1'b1;
endalways #10 clk = ~clk;check1 check1_inst(.clk (clk ),.rst_n (rst_n ),.din (din ),.check_pass(check_pass1)
);check2 check2_inst(.clk (clk ),.rst_n (rst_n ),.din (din ),.check_pass(check_pass2)
);check3 check3_inst(.clk (clk ),.rst_n (rst_n ),.din (din ),.check_pass(check_pass3)
);endmodule
仿真波形如下图所示,有图可知,两段式状态机输出要比一段式状态机和三段式状态机快一拍。
三、状态机状态编码方式
常用的状态机状态编码方式有二进制码(Binary)、格雷码(Gray)、独热码(One-Hot)等,不同的编码方式适用于不同的应用场景。以8个状态的状态机为例,介绍不同编码方式的优缺点,如下表所示。
二进制码(Binary) | 格雷码(Gray) | 独热码(One-Hot) | |
---|---|---|---|
8个状态 | 3’b000 3’b001 3’b010 3’b011 3’b100 3’b101 3’b110 3’b111 | 3’b000 3’b001 3’b011 3’b010 3’b110 3’b111 3’b101 3’b100 | 8’b00000001 8’b00000010 8’b00000100 8’b00001000 8’b00010000 8’b00100000 8’b01000000 8’b10000000 |
优点 | 使用寄存器较少,编码方式简单 | 使用寄存器较少,且出现毛刺的可能性低 | 编码方式简单,容易增加状态 |
缺点 | 每次状态变化涉及多个比特位反转,容易出现毛刺,且消耗较多LUT | 编码方式复杂,状态跳转需要较多组合逻辑 | 使用寄存器多 |
适用场景 | 小型设计(状态数小于4) | 大型状态机(状态个数大于24) | 状态数在4~24之间,适用于判断条件复杂但是状态少的情况下 |
对于上述表述,在学习的过程中仍然存在下面几个问题:
问题1:如果状态机的状态个数不为 2 n 2^n 2n,或者状态机的状态跳转不仅仅是按照相邻的状态跳转(如下图红色箭头),那么格雷码相对于二进制码是否存在优势?
回答:这个问题没有比较明确的答案,后续更新… …
问题2:独热码的状态不是用了更多的位宽,又是如何做到节省资源的?
回答:比如一个8个状态的状态机,独热码编码方式需要8位宽,在使用到状态时,无需将8位数值进行比较,只需比较单个位宽即可,如下:
assign dout = curr_state[0] ? 1'b1 : 1'b0;
assign check_pass = curr_state[3] ? 1'b1 : 1'b0;
而如果使用二进制编码或者格雷编码,需要3位宽表示8个状态,在使用到状态时,需要将3位数值全部进行比较,如下:
assign dout = (curr_state == S0) ? 1'b1 : 1'b0;
assign check_pass = (curr_state == S3) ? 1'b1 : 1'b0;
写在后面
在本文中我们学习了FPGA设计中状态机的一些基础知识,包括状态机类型、状态机的写法与状态机的编码方式。其中,状态机的类型包括Moore型和Mealy型两种,Moore型状态机输出仅与当前状态有关,与当前输入无关,Mealy型状态机输出不仅与当前状态有关,而且与当前输入有关。状态机的写法包括一段式、两段式和三段式写法,三种状态机中,一段式状态机不符合时序逻辑和组合逻辑分开描述的代码风格,代码冗长,不利于修改和维护;两段式状态机中,输出一般使用组合逻辑描述,而组合逻辑容易产生毛刺;三段式状态机既解决了一段式状态机中代码冗长与代码风格混乱、不利于修改维护的问题,也解决了两段式状态机中组合逻辑输出产生毛刺的问题,是目前设计中常用的状态机设计方法。状态机常用的编码方式包括二进制码、格雷码和独热码三种,三种不同编码方式适用于不同的场景下,在实际设计过程中应根据状态的个数与设计的复杂度等多方面综合考虑。
本文到此结束,欢迎评论区交流探讨。