在状态机部分,moore和mealy也算是老生常谈了吧。
说白了就是通过时钟信号不断改变当前的状态,可能是根据输入的数据,也可能是自身发生改变(比如一些计时器),所以少不了触发器,虽然我们有功能十分多的JK,有RS,但是我们一般采用的触发器类型都是D触发器。
实际电路的设计和verilog设计还不大相同。因为verilog好歹还不是那么底层,不需要自己进行搭线(除非采用结构化描述),也就是我们可以省略一些步骤。
好了么,基本上省了很多了,只需要将状态找出来,进行分配,然后给出状态转移情况即可。
很明显了,mealy的输出不但有当前的Q,还有input的参与,再看看moore就没有这么多事,直接受现态Q的控制。
说是这么说,但是不真正给一个例子分析一下,就这样口嗨很难看懂的,而看不懂就很难明白为什么两者可以相互转换。
例子:从八位拨码开关检测序列01011的存在(别问为什么都是并行输入了还需要使用状态机)
首先,我们先给出一个个的状态:
(一般是默认0为S0,1为S1)
状态 | 内容 | 输出 |
---|---|---|
S0 | 0 | 0 |
S1 | 1 | 0 |
S2 | 01 | 0 |
S3 | 010 | 0 |
S4 | 0101 | 0 |
S5 | 01011 | 1 |
我们可以看出来,上面那组输出是只和状态有关,而和输入无关(因为根本就没有给出输入情况)
所以这个的状态分配就是moore型了
状态转换:
那么mealy是什么样的呢?
状态 | 输入0/输出 | 输入1/输出 | 内容 |
---|---|---|---|
S0 | S0/0 | S2/0 | 0 |
S1 | S0/0 | S1/0 | 1 |
S2 | S3/0 | S1/0 | 01 |
S3 | S0/0 | S4/0 | 010 |
S4 | S3/0 | S1/1 | 0101 |
可以看出,输出不但和状态有关,也看输入的内容(S4+输入1才能输出Z = 1)
我们可以发现,在Moore和mealy中,状态的个数是不同的,其实在一些情况下,两者的状态种类都是不一样的。(如自动贩卖机中,Moore是投入的总硬币价值,mealy是每次投入硬币的价值)
抓住问题的本质,才能设计出Moore和mealy型的状态。
我们的例子还没完呢,我们现在只是得到了基本的状态,还没有进行分配并实现。
状态分配很简单,Si = i即可。
在实现的过程中,我们就不得不提一下三段式的问题了。
这里声明一下,因为被检测串的第一位是0,所以这里采用001状态(S1)为起始状态。(如果采用0为起始状态,会将1011检测为正确)
在数字逻辑设计中,我们知道时序电路有三个重要的方程:输入方程、驱动方程和输出方程,不论怎么叫,本质上就是:
而这三个方程我们分别使用三个always块来实现,这就是三段式。这种描述方法思路更加清晰、便于维护,并且输出变量由时序逻辑控制,不会产生毛刺现象。
接着叙述我们的题目:
利用拨码开关输入8位数,按下s0进行数据输入,使用状态机判断8位中是否存在01011字串,如果有,led灯亮;没有则熄灭。另外我们还有一个异步复位按键,将状态机状态归零并熄灭led灯。
有一说一这个例子不太好,看一下三段式的写法即可,就别cv了。(其实虽然实现了,但代码不是很完美)
细节:
输入:set、rst、时钟信号和8位拨码开关输入
输出:led灯使能信号detect_o
代码:
`timescale 1ns / 1ps
module moor_top(
input rst_n_i, //异步复位&状态机回到初始状态,高电平有效
input set_i, //同步使能端,高电平有效
input clk_i,
input [7:0]data,
output reg detect_o
);
reg x=1'b0; //输入
reg [7:0]in=8'b0000_0000; //存储八位
reg [3:0]number=4'b0000; //计数器
reg [2:0]state=3'b000; //当前状态
reg if_in=1'b0; //判断是否停止计数
//当前状态的状态寄存器
always @(posedge clk_i)
begin
if(set_i) //有效输入
begin
in <= data;
if_in <= 1'b1; //开始计数
end
else if(if_in)
begin
if(number != 4'b1000)
begin
x <= in[number];
number <= number + 1;
end
else //结束计数
begin
number <= 0;
if_in <= 1'b0;
end
end
else;
end
//描述下一状态的状态寄存器
always @(posedge clk_i)
begin
if(rst_n_i) //复位
begin
state <= 3'b001;
end
else if(!if_in) //输入结束,不需要状态转移,直接归零
state <= 3'b001;
else
case({
x,state})
4'b0000:state <= 3'b000;
4'b0001:state <= 3'b000;
4'b0010:state <= 3'b011;
4'b0011:state <= 3'b000;
4'b0100:state <= 3'b011;
4'b0101:state <= 3'b000;
4'b1000:state <= 3'b010;
4'b1001:state <= 3'b001;
4'b1010:state <= 3'b001;
4'b1011:state <= 3'b100;
4'b1100:state <= 3'b101;
4'b1101:state <= 3'b001;
default;
endcase
end
//描述输出
always @(*)
begin
if(set_i || rst_n_i) //再次输入&重置
begin
detect_o = 1'b0;
end
else if(detect_o == 1'b1) //保持亮
detect_o = 1'b1;
else
begin
if(state == 3'b101)
begin
detect_o = 1'b1;
end
else
begin
detect_o = 1'b0;
end
end
end
endmodule
这里面有一个部分写的不是很好,如果能看出来就会发现我是从数据的低位到高位读入,也就是会将高低位反过来,所以就需要将约束文件中拨码开关的管脚和数组下标反着对应。这一点在仿真中十分明显。
仿真文件:(包含了三个例子,记得数据是反着输入的,第一个亮,但是会被置零第二个不亮第三个亮)
`timescale 1ns / 1ps
module EX5_moore_sim( );
reg rst_n_i=1'b0;
reg set_i=1'b0;
reg clk_i=1'b0;
reg [7:0]data=8'b1110_1010;
wire detect;
moor_top test(rst_n_i,set_i,clk_i,data,detect);
always #5 clk_i = ~clk_i;
initial
begin
#10 set_i = 1;
#10 set_i = 0;
#100 rst_n_i = 1;
#10 rst_n_i = 0;
#100 data = 8'b1111_1011;
#5 set_i = 1;
#10 set_i = 0;
#100 data = 8'b0110_1010;
#5 set_i = 1;
#10 set_i = 0;
end
endmodule
报错:
我们先看一下第一个,是因为我们采用了将set_i作为判断程序是否开始的信号,也相当于一个时钟信号,但我们又没有说明,所以就会报错。
解决方式:在约束文件中加上尖括号中的语句(在后来的修改过程中,我们不需要这样处理,所以在最终的版本中我们没有使用这条语句)。
第二个问题:
形成了锁存器,这个是因为两个always块分别为时序和组合逻辑,而vivado又是一个偏向时序的软件,所以我们有时候会看到这个报错,但也是迫不得已吧,其实不需要处理的。
在网上看到一个说法,就是在在报错的形成锁存器的地方先赋初值,然后进行处理,没有尝试过,先给出这样的说法。
第三个,是因为组合逻辑打环?也是按照error的弹出,将对应内容添加到xdc文件即可。
set_property ALLOW_COMBINATORIAL_LOOPS true [get_nets -of_objects [get_cells+报错中的内容]](可能有好几条语句,这里只有一个)
set_property SEVERITY {Warning} [get_drc_checks LUTLP-1]
set_property SEVERITY {Warning} [get_drc_checks NSTD-1]
复习一下,mealy型是输出和输入有关的那种。
mealy基本上和moore的结构相同,这个问题刚好状态分配还比较像,只需要改一下状态转移表,然后修改一下判断输出正确的判定即可。
在之前我们都是直接使用这个状态,但是我们有没有想过,我们应该是有两个状态的,那么我们在Moore中使用的是哪个呢?
次态
那会不会造成错误呢?
其实还是会有一点的,但是我们因为是限定八位输入,所以影响不大。
mealy型呢?
这里就要声明一下了,因为我们次态和输入都是卡时钟周期的(前面说过为了保证能够实现八位依次输入)所以在看过仿真波形会发现,我们的状态(次态)是相比输入晚一个周期的,那么也就是说,在我们相比的过程中,两者其实是都晚了一个周期(我们可以采取现态和次态的方式,然后再整一个x_pre,但是在尝试过后感觉实在是太蠢了就放弃了(其实这个串行输入我也感觉很蠢)。
代码:
`timescale 1ns / 1ps
module mealy(
input rst_n_i, //低有效的复位变量
input set_i, //ͬ高有效的输入使能
input clk_i,
input [7:0]data,
output reg detect_o,
output reg [2:0]state,
output reg x
);
reg [7:0]in = 8'b0000_0000;
reg [3:0]number = 4'b0000;
reg if_in=1'b0; //判断输入的使能信号
wire clk_o;
//输入->触发器内容
divider_1s div(clk_i,clk_o);
always@(posedge clk_o)
begin
if(set_i)
begin
in <= data;
if_in <= 1'b1;
end
else if(if_in)
begin
if(number != 4'b1000)
begin
x <= in[number];
number <= number + 1'b1;
end
else
begin
number <= 4'b0000;
if_in <= 1'b0;
x <= 1'b0;
end
end
else;
end
//触发器状态转换
always @(posedge clk_o)
begin
if(rst_n_i)//重置
begin
state <= 3'b001;
end
else if(!if_in)
begin
state <= 3'b001;
end
else
case({
x,state})
4'b0000:state <= 3'b000;
4'b0001:state <= 3'b000;
4'b0010:state <= 3'b011;
4'b0011:state <= 3'b000;
4'b0100:state <= 3'b011;
4'b1000:state <= 3'b010;
4'b1001:state <= 3'b001;
4'b1010:state <= 3'b001;
4'b1011:state <= 3'b100;
4'b1100:state <= 3'b001;
default;
endcase
end
//输出
always @(*)
begin
if(set_i || rst_n_i)
begin
detect_o = 1'b0;
end
else if(detect_o == 1'b1)
detect_o = 1'b1;
else
begin
//需要看当前的输入和现态
if(state == 3'b100 && x == 1'b1)
begin
detect_o = 1'b1;
end
else
begin
detect_o = 1'b0;
end
end
end
endmodule
(仿真和之前的差不多,就没有贴出)
之前有一个01010101的串一直有一个问题,我就添加了一些东西来检测:
将状态和输入显示在led灯上,之前说过了状态是次态,输入也是下一个输入,不过不耽误我们分析内部逻辑。
但是100MHZ还是太快,所以我加了分频。(点击查看分频部分代码原理)
然后就可以开心的debug了(bushi
三段式是将三个always块分开写,那么我们也可以考虑只写两个,甚至于一个always块。
一段式的问题还是比较多的,而且只写一个块并不利于维护,因此在这里不做过多说明。
二段式:两个always块
虽然我上面的代码中输入方程是和时钟周期有关,但是要知道实际上确定输入时钟信号无关(因为要处理每一个节拍读入一位,所以才选择了这样的方式。)
那么就很明显,状态转换模块一定是要时序电路了,那么我完全可以将剩下的两个部分(都是组合逻辑)放在一起,这样也有利于代码的阅读和维护,但是是可能产生毛刺和险象的。
至于三段式,也不是那么完美,三个块占用的资源也更多,不过胜在稳定,但是我们说过vivado更偏爱时序电路,所以有那么一个组合的always块有时也会产生warning,甚至不能运行。