简单状态机设计(1)
1.0 序列检测器电路设计
设计一个序列检测器电路,功能是:检测出串行输入数据Sin中的4位二进制序列0101(自左至右输入),当检测到该序列时,输出Out=1;没有检测到该序列时,输出Out=0(注意考虑序列重叠的可能性,如010101,相当于出现两个0101序列)。
经过分析,首先可以确定采用米利型状态机设计该电路。因为该电路在连续收到信号0101时,输出为1,其他情况下输出为0,所以采用米利型状态机。
其次,确定状态机的状态图,该电路必须能记忆所收到的输入数据0、连续收到前两个数据01...可见至少要是个状态,分别用S1,S2,S3,S4,再加上电路初始态S0。根据要求可以画出状态图:
观察该图可以看出,当状态机处以S2、S4的时候,如果输入Sin = 1,则电路会转移到相同的次态S0,如果输入Sin = 0,则电路会转移到相同的次态S3,且两种情况下输出Out都为0。所以,S2、S4为等价状态,可用S2代替S4,于是得到简化的状态图:
如果用CPLD/FPGA器件实现状态机,则逻辑综合器会自动化简状态机。
利用Verilog HDL描述状态图主要包括:
(1) 利用参数定义语句parameter描述状态机中各个状态的名称,并指定状态编码。
(2) 用时序的always块描述状态触发器实现的状态存储。
(3) 使用敏感表和case语句(也可以采用if-else语句)描述状态转换逻辑。
(4) 描述状态机的输出逻辑。
这个电路我试着用3种方式来描述,看看他们的差别在哪里。
(1) 单个always块描述状态机方法(应该避免的写法)
1 module Sequence_detection(
2 rst,clk,iSin,
3 oOut
4 );
5 input rst,clk,iSin;
6 output oOut;
7 reg oOut;
8 reg[1:0] State;
9
10 parameter[1:0] S0 = 2'b00,
11 S1 = 2'b01,
12 S2 = 2'b10,
13 S3 = 2'b11;
14
15 always @(posedge clk or negedge rst)
16 begin
17 if(~rst)
18 State <= S0; //异步复位
19 else
20 case(State)
21 S0:begin oOut = 1'b0; State = (iSin == 1'b1) ? S0 : S1; end
22 S1:begin oOut = 1'b0; State = (iSin == 1'b1) ? S2 : S0; end
23 S2:begin oOut = 1'b0; State = (iSin == 1'b1) ? S0 : S3; end
24 S3:if(iSin == 1'b1) begin oOut = 1'b1; State = S2; end
25 else begin oOut = 1'b0; State = S1; end
26 endcase
27 end
28
29 endmodule
生成的RTL视图:
State模块里面其实就是一个状态机:
仿真结果有时会出错:
在每个clk上升沿读取数据,可以看到是0101,在285ps时输出oOut应该是高电平,但不是,这是为什么了?再看下面一张图
这张是对的,为什么有时对又是错了,如果是这样那就失去了它的意义了。
对序列检测器电路用单个always块的描述的逻辑存在一个隐含的错误,即输出信号oOut的描述存在错误。其原因是:oOut信号是由状态机的当前状态和输入信号共同决定的,它是一个纯组合逻辑电路,如果当前状态不变,而输入信号变了,oOut信号应立即发生变化,而不是等到时钟上升沿来了才变化。因此,单个always块描述状态机的写法仅仅适用于穆尔型状态机。这虽然是个问题,但是跟我出现的问题貌似没有什么直接关系,郁闷。。。继续思考。
在实际应用中,为了消除组合逻辑输出信号中的毛刺,在时序允许的情况下,通常允许米利型状态机中输出信号通过寄存器输出。但是单个的always块的描述方法将状态转换判断的组合逻辑和状态触发器转移的时序逻辑混合编写在同一个always块中,不符合将时序和组合逻辑分开描述的代码风格(Coding Style),而且在描述当前状态时还要考虑下一个状态的逻辑,整个代码的结构不清晰,不利于修改和维护,不利于时序约束条件的加入,不利于综合器对设计的优化。所以不推荐使用单个always块的描述方式。
必须为自己的粗心大意买单,还说输出有时会出错原来状态图都是错了,不经意的一眼,看来不能随便啊。case语句后的 S1:当条件满足时(iSin == 1'b1)时会跳到S2否则还会停留在S1。改过来看看正确的状态机就不会出错了。太粗心了!
正确的状态机:
(2) 两个always块描述状态机的方法(推荐写法)
1 module Sequen_detect_2(
2 rst,clk,iSin,
3 oOut
4 );
5 input rst,clk,iSin;
6 output oOut;
7 reg oOut;
8 reg[1:0] Current_state,Next_state;
9
10 parameter S0 = 2'b00,
11 S1 = 2'b01,
12 S2 = 2'b10,
13 S3 = 2'b11;
14 //状态转换,时序电路
15 always @ (posedge clk or negedge rst)
16 begin
17 if(~rst) Current_state <= S0;
18 else Current_state <= Next_state;
19 end
20
21 //下一状态产生和输出信号,组合逻辑
22 always @ (Current_state or iSin)
23 begin
24 Next_state = 2'bxx;
25 oOut = 1'b0;
26 case(Current_state)
27 S0:begin oOut = 1'b0;Next_state = (iSin == 1'b1)? S0 : S1; end
28 S1:begin oOut = 1'b0;Next_state = (iSin == 1'b1)? S2 : S1; end
29 S2:begin oOut = 1'b0;Next_state = (iSin == 1'b1)? S0 : S3; end
30 S3:if(iSin == 1'b1)
31 begin oOut = 1'b1;Next_state = S2; end
32 else
33 begin oOut = 1'b0;Next_state = S1; end
34 endcase
35 end
36
37 endmodule
值得注意的是Next_state = 2'bxx;对状态的默认赋值有3种方式:(1) 全部设置成不定状态(x); (2) 设置成预先规定的初始状态; (3) 设置成FSM中的某一有效状态。设置成不定状态(x)的好处是:(1) 在仿真时可以很好地考察所设计的FSM的完备性,若设计的FSM不完备,则进入任意状态,仿真时容易发现;(2) 综合器对代码进行综合时,会忽略没有定义的状态触发器向量。
生成的RTL视图:
和上面的比较了一下,少了个D触发器,这是为什么呢?并且仿真的结果也不一样了。仿真图:
当然新的问题也就随之而来:
我们可以明显的看到这种写法和上面写法的区别,最重要的是:输出oOut不在是在clk上升沿来的时候才能发生变化,它是一个纯组合逻辑电路只要输入条件满足输出就立马发生改变。随着毛刺的出现,毛刺是一个很重要的问题,很值得我们认真思考。为什么会出现毛刺了,我们可以清楚的看到这时次态是完全由组合逻辑电路决定的,时序电路做的事情仅仅是在clk上升沿来的时候把,把次态赋给现态。在445ps时在S1状态,输出为0、在455ps时在S2状态,输出为0、在465ps时在S3状态,此时就算clk的posedge不来,只要iSin == 1,输出同样会为1,正如上图,但在这里,clk 的上升沿和iSin 的高电平是同时来的,所以,在 iSin 的高电平来的那一瞬间,输出oOut = 1,就在同时状态装换到S2,输出oOut = 0;
这就导致了上面毛刺的产生。怎么解决这个问题呢?我们来看看第三种方法。
(3) 3个always块描述状态机(推荐写法)
1 module Sequen_detect_3(
2 rst,clk,iSin,
3 oOut
4 );
5 input rst,clk,iSin;
6 output oOut;
7 reg oOut;
8 reg[1:0] Current_state,Next_state;
9
10 parameter S0 = 2'b00,
11 S1 = 2'b01,
12 S2 = 2'b10,
13 S3 = 2'b11;
14
15 //状态转换,时序逻辑
16 always @ (posedge clk or negedge rst)
17 begin
18 if(~rst) Current_state <= S0; //异步清零
19 else Current_state <= Next_state; //在clk上升沿触发器状态翻转
20 end
21
22 //下一个状态的产生,组合逻辑
23 always @ (Current_state or iSin)
24 begin
25 Next_state = 2'bxx;
26 case(Current_state)
27 S0:begin Next_state = (iSin == 1'b1) ? S0 : S1; end
28 S1:begin Next_state = (iSin == 1'b1) ? S2 : S1; end
29 S2:begin Next_state = (iSin == 1'b1) ? S0 : S3; end
30 S3:if(iSin == 1'b1) begin Next_state = S2;end
31 else begin Next_state = S1;end
32 endcase
33 end
34
35 //输出:让输出信号经过一个寄存器再输出,可以消除oOut毛刺
36 always @ (posedge clk or negedge rst)
37 begin
38 if(~rst) oOut = 1'b0;
39 else begin
40 oOut = 1'b0;
41 case(Current_state)
42 S0,S1,S2:oOut = 1'b0;
43 S3:if(iSin == 1) oOut = 1'b1;
44 else oOut = 1'b0;
45 endcase
46 end
47 end
48
49 endmodule
分析一下代码,我们很容易看出:第一个always块采用同步时序逻辑方式描述状态转移(在电路框图的中间框),第二个always块采用组合逻辑方式描述状态转移规律(一般是第一个方框),第三个always块描述电路的输出信号,在时序允许的情况下,通常让输出信号经过一个寄存器再输出,保证输出信号中没有毛刺。
综合后的RTL视图,就跟第一版程序是一样的了,又有了D触发器,仿真结果和第一版也是一样的,但是两版代码的风格(Coding Style)是不一样的。