FPGA实战篇:Moore/Mealy状态机
什么是状态机?
状态机是根据当前输入信号和自身当前所处状态来改变输出逻辑的一种逻辑系统,目前它也被抽象应用于软件设计当中,本文从硬件设计角度来解释状态机,使用Verilog语言来抽象描述并实现状态机。
状态机类型
状态分为两种类型:
Moore(摩尔)状态机:输出逻辑只与当前状态有关,与输入信号无关
Mealy(米勒)状态机:输出逻辑不仅与当前状态有关,也与输出信号有关
以上两种都为有限状态机,为什么称之为有限状态机?因为它们的状态是固定的,是有限的,例如一个状态机一开始就是256个状态,它的状态不会随着时间而变长或变少不是非固定的。
Moore状态机
Moore状态机的原理在状态机类型处已经简单解释了一遍,可以简单看下它的抽象逻辑图:
整个逻辑图分为两大部分:
第一部分用于给状态寄存器一个初始状态初始化电路(这块一般是写在第二部分状态传递组合逻辑里,这里分出来是为了体现它),第二部分则是Moore状态机逻辑流程图。
第一部分只有一个逻辑:初始化逻辑,主要作用是在rst电平到来时对状态寄存器进行一个初始赋值,确保每次复位都有一个稳定的状态。
第二部分则是根据状态寄存器作为输入去生成新的状态的组合逻辑,生成完成之后将次态传递给状态寄存器变为现态,最后现态在传递给输出组合逻辑来改变输出结果。
原理说完了,开始使用Verilog来实现它
TIPS
状态机的实现不一定是死的,可以自由发挥,本文给出的实现只是为了方便大家理解,多数情况下大家只要理解状态机的思想,可以按自己想法实现它,改造它。
如果你是初学者那么可以先不考虑如何更完美的实现它,例如考虑是否会生成锁存器、信号毛刺等现象,优先考虑的是如何实现它,并理解它。
本文以Flow Led电路为例,使用Moore来实现流水灯(总共4个LED灯)控制
首先定义一个基本构建模块,有两个输入一个输出,分别是clk、rst和led
module flow_led(input sys_clk,input sys_rst_n,output reg [3:0] led
);
endmodule
按照刚刚的抽象逻辑图我们来一步一步实现moore状态机,首先定义5个状态:
parameter LED_LIGHT_0 = 4'b0000,LED_LIGHT_1 = 4'b0001,LED_LIGHT_2 = 4'b0010,LED_LIGHT_3 = 4'b0100,LED_LIGHT_4 = 4'b1000;
这四个状态分别对应哪个LED灯在亮,LED_LIGHT_0 代表每个灯都不亮,然后定义次态寄存器和现态寄存器用于保存下一个状态和当前状态:
reg [3:0] next_state;
reg [3:0] now_state;
接着我们就可以编写moore状态机了,接下来第一部分要写的就是状态转移逻辑,如果要实现状态转移那么就需要有转移条件,moore状态机的转移条件是对内而非外部输入信号,首先我们需要思考我们要实现什么,我们需要实现的是一个流水灯,为了实现流水灯我们还需要一个计数器,用于实现延迟流水效果,那么计数器就是我们让状态发生改变的因变量,那么我们先定义它:
reg [31:0] counter;
我们想要实现流水灯的时间间隔为1秒,可以根据当前时钟频率来计算,我当前实验环境给芯片提供时钟频率的晶振是50Mhz,也就是1秒会跳变50000000次时钟周期,每个周期为1/50000000=0.00000002(20ns),那么我们只需要让counter每个周期递增一次,递增到50000000 - 1次就可以了,这里**-1是因为当前周期也算在内**。
所以生成次态的组合逻辑代码就如下所示:
always @(*) begincase (now_state) beginLED_LIGHT_0:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_1;elsenext_state = LED_LIGHT_0;endendcasecounter = counter + 1;
end
上面的代码逻辑是组合逻辑,当now_state、counter发生改变时它才会触发,它的作用就是根据当前状态来生产新的状态,生产新的状态的因变量就是counter,当counter计数到一秒时切换置下一个状态,例如当前状态是LED_LIGHT_0那么下一个状态就是LED_LIGHT_1,这里为了防止产生锁存器所以加了一个else,让它保持不变。
简单来说就是当达到一秒时根据当前状态来确定下一次应该是什么状态,这里可以看到是依据内部变量counter来判断的,这里简单提一句,其实mealy只需要将counter改为input的寄存器就可以了。
以上是一个基础示例,它会先根据当前状态然后进行切换,如果当前状态为LED_LIGHT_0就代表当前没有一个灯是亮的,就判断counter是否到达一秒钟了,如果到了则将next_state设置为LED_LIGHT_1,该下一个灯亮了,然后将counter置零从新技术,否则让next_state继续保持LED_LIGHT_0,这里的else是为了防止verilog综合生成锁存器电路,也可以不写。
在最后递增counter
然后按照上面的思路,我们依次把其它状态补充齐全:
always @(*) begincase (now_state)LED_LIGHT_0:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_1;elsenext_state = LED_LIGHT_0;endLED_LIGHT_1:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_2;elsenext_state = LED_LIGHT_1;endLED_LIGHT_2:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_3;elsenext_state = LED_LIGHT_2;endLED_LIGHT_3:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_4;elsenext_state = LED_LIGHT_3;endLED_LIGHT_4:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_1;elsenext_state = LED_LIGHT_4;enddefault:next_state = LED_LIGHT_0;endcase
end
注意在LED_LIGHT_4时我们就不需要让它回归初始状态了,让它从第一个LED开始继续循环,这里counter在别的逻辑里会进行处理,最后的default是在其它情况下例如电路出现其它以外时让它置初始状态,灯灭。
状态生成组合逻辑到此就写完了,接下来就是去编写传递组合逻辑,这里我进行了一些考虑,决定将它写成时序逻辑,而非组合逻辑,目的是为了包含初始逻辑与递增因变量counter:
always @(posedge sys_clk or negedge sys_rst_n) beginif (!sys_rst_n) beginnow_state <= LED_LIGHT_0;counter <= 32'b0;endelse if (now_state != next_state) beginnow_state <= next_state;counter <= 32'b0;endelsecounter <= counter + 1;
end
上面的代码逻辑包含了初始逻辑,在会对now_state和counter进行初始化,然后会判断当前状态是否不等于下一个状态,这里就是检查状态是否发生了改变,如果发生了改变那么意味着要从LED0切换到LED1或从LED2切换到LED3,如果发生了改变那么就将状态进行转移并将计数器置0,如果状态相同代表当前处于LED0或LED1下,那么就对counter进行递增,保持1秒后切换到下一个LED。
最后就是输出逻辑了,输出逻辑就比较简单,只需要根据当前状态来设置哪个LED灯亮即可:
always @(*) begincase (now_state)LED_LIGHT_0: led = 4'b0000;LED_LIGHT_1: led = 4'b0001;LED_LIGHT_2: led = 4'b0010;LED_LIGHT_3: led = 4'b0100;LED_LIGHT_4: led = 4'b1000;default: led = 4'b0000;endcase
end
代码编写完了, 接下来仿真试试,这里我使用的是modesim,testbench代码如下:
`timescale 1 ns/ 1 psmodule flow_led_test_bench();reg sys_clk;
reg sys_rst_n;
wire [3:0] led; flow_led i1 (.led(led),.sys_clk(sys_clk),.sys_rst_n(sys_rst_n)
);initial
begin
sys_clk <=1'b0;
sys_rst_n <= 1'b0;
#100 sys_rst_n <= 1'b1;
endalways #10 sys_clk=~sys_clk;endmodule
仿真时序图:
通过下面的步长可以看到每秒next_state和now_state都在发生变化,按代码预期的在切换状态。
RTL View:
moore状态机verilog完整代码:
module flow_led(input sys_clk,input sys_rst_n,output reg [3:0] led
);parameter LED_LIGHT_0 = 4'b0000,LED_LIGHT_1 = 4'b0001,LED_LIGHT_2 = 4'b0010,LED_LIGHT_3 = 4'b0100,LED_LIGHT_4 = 4'b1000;reg [3:0] next_state;
reg [3:0] now_state;
reg [31:0] counter;always @(*) begincase (now_state)LED_LIGHT_0:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_1;elsenext_state = LED_LIGHT_0;endLED_LIGHT_1:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_2;elsenext_state = LED_LIGHT_1;endLED_LIGHT_2:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_3;elsenext_state = LED_LIGHT_2;endLED_LIGHT_3:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_4;elsenext_state = LED_LIGHT_3;endLED_LIGHT_4:beginif (counter >= 32'd49999999)next_state = LED_LIGHT_1;elsenext_state = LED_LIGHT_4;enddefault:next_state = LED_LIGHT_0;endcase
endalways @(posedge sys_clk or negedge sys_rst_n) beginif (!sys_rst_n) beginnow_state <= LED_LIGHT_0;counter <= 32'b0;endelse if (now_state != next_state) beginnow_state <= next_state;counter <= 32'b0;endelsecounter <= counter + 1;
endalways @(*) begincase (now_state)LED_LIGHT_0: led = 4'b0000;LED_LIGHT_1: led = 4'b0001;LED_LIGHT_2: led = 4'b0010;LED_LIGHT_3: led = 4'b0100;LED_LIGHT_4: led = 4'b1000;default: led = 4'b0000;endcase
endendmodule
写完了moore状态机接下来来实现mealy状态机,mealy刚刚在写moore时也说过,只需要将counter换成input寄存器就可以了,虽然代码上改动不大但是对于电路实现来说其实还是有一点差距的,那么这里我们来改变一下想要实现的功能,接下来实现一个用按键作为输入信号来实现控制led灯的工作模式,首先声明一下需求:
- 输入有4个key
- 每个key对应一个led工作状态
- key1对应led灯1亮
- key2对应led灯2亮
- key3对应led灯3亮
- key4对应led灯4亮
- 所有效果仅在key按下有效
- key按下为低电平
理解了需求那么我们开始编码,首先定义输入部分:
module flow_led(input sys_clk,input sys_rst_n,input reg [3:0] key,output reg [3:0] led
);
输入分别是时钟信号、复位信号、按键信号和输出led灯。
然后我们生成不同的状态,对应不同按键按下的情况:
parameter STATE_DEFAULT = 4'b0000,STATE_LED_LIGHT_1 = 4'b0001,STATE_LED_LIGHT_2 = 4'b0010,STATE_LED_LIGHT_3 = 4'b0100,STATE_LED_LIGHT_4 = 4'b1000;
STATE_DEFAULT:默认状态,没有任何按键按下
STATE_LED_LIGHT_1:按键1按下,LED1亮
STATE_LED_LIGHT_2:按键2按下,LED2亮
STATE_LED_LIGHT_3:按键3按下,LED3亮
STATE_LED_LIGHT_4:按键4按下,LED4亮
在定义状态存储的中间寄存器与当前时刻状态的寄存器:
reg [3:0] next_state;
reg [3:0] now_state;
然后接着我们修改状态生成的组合逻辑:
always @(*) begincase (now_state)STATE_DEFAULT:beginif (key == 4'b1110)next_state = STATE_LED_LIGHT_1;else if (key == 4'b1101)next_state = STATE_LED_LIGHT_2;else if (key == 4'b1011)next_state = STATE_LED_LIGHT_3;else if (key == 4'b0111)next_state = STATE_LED_LIGHT_4;elsenext_state = STATE_DEFAULT; endendcase
end
上面的代码将其它状态改掉了,只保留一个STATE_DEFAULT,也就是按键没有按下的状态,只有在按键没有按下时我们才去对按键进行检测,当这个时刻有按键按下时,根据按下的按键来使能不同的状态。
状态生成组合逻辑写好了接下来就是写状态转移逻辑了:
always @(posedge sys_clk or negedge sys_rst_n) beginif (!sys_rst_n) beginnow_state <= STATE_DEFAULT;endelse if (now_state != next_state) beginnow_state <= next_state;endelse if (key ==4'b1111)now_state <= STATE_DEFAULT;elsenow_state <= now_state;
end
初始化逻辑依旧在状态转移这里做,上面的逻辑就是判断当前状态是否等于下一个状态,用来判断是否生成新的状态,如果生成了新的状态那么将新状态进行转移,如果当前处于新状态则判断按键是否松开了,如果松开将状态置为defulat,这里就实现了按键的按下与松开的逻辑把控,最后的else是为了防止生成锁存器,只要key!=4’b1111就意味着当前有个按键被按下了,那么当前状态一定被赋予对应的条件状态了,那么接下来就是输出组合逻辑了。
always @(*) begincase (now_state)STATE_DEFAULT: led = 4'b0000;STATE_LED_LIGHT_1: led = 4'b0001;STATE_LED_LIGHT_2: led = 4'b0010;STATE_LED_LIGHT_3: led = 4'b0100;STATE_LED_LIGHT_4: led = 4'b1000;default: led = 4'b0000;endcase
end
输出组合逻辑只需要根据当前状态来改变灯的点亮就可以了。
接下来就进行仿真测试,testbench代码如下:
`timescale 1 ns/ 1 psmodule flow_led_test_bench();reg sys_clk;
reg sys_rst_n;
reg [3:0] key;
wire [3:0] led; flow_led i1 (.led(led),.sys_clk(sys_clk),.sys_rst_n(sys_rst_n),.key(key)
);initial
begin
sys_clk <=1'b0;
sys_rst_n <= 1'b0;
key <= 4'b1111;
#100 sys_rst_n <= 1'b1;#1000000000 key=4'b1110;
#1000000000 key=4'b1111;#1000000000 key=4'b1101;
#1000000000 key=4'b1111;#1000000000 key=4'b1011;
#1000000000 key=4'b1111;#1000000000 key=4'b0111;
#1000000000 key=4'b1111;endalways #10 sys_clk=~sys_clk;endmodule
在init里使用延迟来模拟按键的按下与松开。
RTL VIEW:
仿真结果如下:
可以看到时序图随着按键每秒按下/松开,state都在发生变化,led也在发生变化。
完整verilog代码如下:
module flow_led(input sys_clk,input sys_rst_n,input wire [3:0] key,output reg [3:0] led
);parameter STATE_DEFAULT = 4'b0000,STATE_LED_LIGHT_1 = 4'b0001,STATE_LED_LIGHT_2 = 4'b0010,STATE_LED_LIGHT_3 = 4'b0100,STATE_LED_LIGHT_4 = 4'b1000;reg [3:0] next_state;
reg [3:0] now_state;always @(*) begincase (now_state)STATE_DEFAULT:beginif (key == 4'b1110)next_state = STATE_LED_LIGHT_1;else if (key == 4'b1101)next_state = STATE_LED_LIGHT_2;else if (key == 4'b1011)next_state = STATE_LED_LIGHT_3;else if (key == 4'b0111)next_state = STATE_LED_LIGHT_4;elsenext_state = STATE_DEFAULT; endendcase
endalways @(posedge sys_clk or negedge sys_rst_n) beginif (!sys_rst_n) beginnow_state <= STATE_DEFAULT;endelse if (now_state != next_state) beginnow_state <= next_state;endelse if (key ==4'b1111)now_state <= STATE_DEFAULT;elsenow_state <= now_state;
endalways @(*) begincase (now_state)STATE_DEFAULT: led = 4'b0000;STATE_LED_LIGHT_1: led = 4'b0001;STATE_LED_LIGHT_2: led = 4'b0010;STATE_LED_LIGHT_3: led = 4'b0100;STATE_LED_LIGHT_4: led = 4'b1000;default: led = 4'b0000;endcase
endendmodule