状态机设计

来源:互联网 发布:轩辕世界神兽数据 编辑:程序博客网 时间:2024/05/02 02:20

状态机设计


FSM简介:

http://bbs.ednchina.com/BLOG_ARTICLE_258884.HTM


FSM 分两大类:米里型和摩尔型,组成要素有输入(包括复位),状态(包括当前状态的操作),状态转移条件,状态的输出条件,图1为状态机结构图。设计FSM的方法和技巧多种多样,但是总结起来有两大类:第一种,将状态转移和状态的操作和判断等写到一个模块(processblock)中。另一种是将状态转移单独写成一个模块,将状态的操作和判断等写到另一个模块中(在Verilog代码中,相当于使用两个“always” block)。其中较好的方式是后者。其原因如下:


首先FSM 和其他设计一样,最好使用同步时序方式设计,好处不再赘述。而状态机实现后,状态转移是用寄存器实现的,是同步时序部分。状态的转移条件的判断是通过组合逻辑判断实现的,之所以第二种比第一种编码方式合理,就在于第二种编码将同步时序和组合逻辑分别放到不同的程序块(processblock)中实现。这样做的好处不仅仅是便于阅读、理解、维护,更重要的是利于综合器优化代码,利于用户添加合适的时序约束条件,利于布局布线器实现设计


图1为状态机结构图


显式的FSM 描述方法可以描述任意的FSM(参考Verilog 第四版P181 有限状态机的说明)。两个always 模块。其中一个是时序模块,一个为组合逻辑。时序模块设计与书上完全一致,表示状态转移,可分为同步与异步复位。


同步:


always @(posedge clk)


if (!reset)


…………


异步:


always @(posedge clk or negedge reset)


if (!reset)


…………




 


组合逻辑用case语句,sensitive list 包括当然状态(current state)和输入(a,b,c…)。


编者注:以下是编者从State Machine Coding Styles for Synthesis一文中摘取的程序代码,是一个简单状态机的示例,采用两个always块的方法:


module bm1_s (err, n_o1, o2, o3, o4,i1, i2, i3, i4, clk, rst);


output err, n_o1, o2, o3, o4;


input i1, i2, i3, i4, clk, rst;


reg err, n_o1, o2, o3, o4;


parameter [2:0] //可以在此处添加综合约束属性来限定状态机的编码:binary,one-hot,gray,etc


IDLE = 3'd0,


S1 = 3'd1,


S2 = 3'd2,


S3 = 3'd3,


ERROR = 3'd4;


reg [2:0] state, next;


always @(posedge clk or posedge rst)//异步复位,时序逻辑


if (rst) state <= IDLE;


else state <= next;


always @(state or i1 or i2 or i3 or i4) begin //组合逻辑,敏感列表包含当前状态以及所有的状态机输入


next = 3'bx;//设置默认值,以便防止因为if或者case语句不完整综合生成锁存器


err = 0; n_o1 = 1;


o2 = 0; o3 = 0; o4 = 0;


case (state)


IDLE: begin


next = ERROR;//如果下面所有的if条件都不符合,则对next赋该默认值


if (!i1) next = IDLE;


if (i1 & i2) next = S1;


if (i1 & !i2 & i3) next = S2;


end


S1: begin


next = ERROR;


if (!i2) next = S1;


if (i2 & i3) next = S2;


if (i2 & !i3 & i4) next = S3;


n_o1 = 0;


o2 = 1;


end


S2: begin


next = ERROR;


if (i3) next = S2;


if (!i3 & i4) next = S3;




 


o2 = 1;


o3 = 1;


end


S3: begin


next = S3;


if (!i1) next = IDLE;


if (i1 & i2) next = ERROR;


o4 = 1;


end


endmodule


对于状态机的输出可以通过寄存器寄存一下,消除毛刺,这将另外需要一个always块,也就是状态机三个always块的写法。


编码风格:


1.避免生成锁存器


一个完备的状态机(健壮性强)应该具备初始化(reset)状态和默认(default)状态。当芯片加电或者复位后,状态机应该能够自动将所有判断条件复位,并进入初始化状态。需要注


明的一点是,大多数FPGA有GSR(Global Set/Reset)信号,当FPGA 加电后,GSR 信号拉高,对所有的寄存器,RAM 等单元复位/置位,这时配置于FPGA的逻辑并未生效,所以不能保证正确的进入初始化状态。所以使用GSR 进入FPGA 的初始化状态,常常会产生种种不必一定的麻烦。一般简单方便的方法是采用异步复位信号,当然也可以使用同步复位,但是要注意同步复位的逻辑设计。状态机也应该有一个默认(default)状态,当转移条件不满足,或者状态发生了突变时,要能保证逻辑不会陷入死循环。这是对状态机健壮性的一个重要要求,也就是常说的要具备自恢复功能。对应于编码就是对case,if-else 语句要特别注意,要写完备的条件判断语句。VHDL 中,当使用CASE 语句的时候,要使用When Others建立默认状态。使用IF...THEN...ELSE语句的时候,要在ELSE指定默认状态。Verilog中,使用case语句的时候要用default建立默认状态,使用if...else语句的注意事项相似。


另外有一个技巧:大多数综合器都支持Verilog编码状态机的完备状态属性--full case。这个属性用于指定将状态机综合成完备的状态,如Synplicity的综合工具(Synplify/Synplify Pro)支持的命令格式如下:


case (current_state) // synthesis full_case


2b00 : next_state <=2b01;


2b01 : next_state <=2b11;


2b11 : next_state <=2b00;


//这两段代码等效


case (current_state)


2b00 : next_state <=2b01;


2b01 : next_state <=2b11;


2b11 : next_state <=2b00;


default : next_state <= 2bx;


Synplicity还有一个关于状态机的综合属性,叫// synthesis parallel_case其功能是检查所有的状态是并行的(parallel),也就是说在同一时间只有一个状态能够成立。


编者注:使用上述两个综合约束属性会造成综合前后仿真结果的不一致,请慎重使用。




 


2.参数定义用parameter


状态的定义用parameter定义,不推荐使用`define 宏定义的方式,因为define宏定义在编译时自动替换整个设计中所定义的宏,而parameter仅仅定义模块内部的参数,定义的参数不会与模块外的其他状态机混淆。


3.时序电路中一定要使用<=非阻塞赋值方式


Verilog的非阻塞行赋值模拟的是实际硬件中串行寄存器的行为,排除了很多潜在的竞争冒险。在使用非阻塞赋值的时候,很多设计者采用"intra-assignment timing delay"(在非阻塞赋值前人为加入一个很小单位的延时)。如下例:


always @(posedge clk or posedge rst)


if (rst)


state <= #1 IDLE;


else


state <= #1 nextstate;


关于这种写法的阐释有下面几点:


I.首先,这种描述是行为级描述方式,仅仅在仿真时起作用在综合时会自动被综合器忽略。也就是通常所说的延时描述是不可综合的。


II.这种描述的好处之一是:它简单模拟了赋值过程寄存器内部的clock-to-output的延时,在做行为级功能仿真的时候,也可以发现一些由于寄存器固有延迟造成的时序和功能问题。


III.避免了由RTL级代码向门级描述转变过程中的一些潜在错误,如保持时间带来的问题。


4不论是二进制编码的FSM,还是独热码FSM,复位时状态寄存器应该赋值为IDLE状态,如下:


always@( posedge clk or posedge rst )


if( rst ) state <= #1 IDLE;


else state <= #1 next;


如果实现简化的独热码FSM,复位时状态寄存器首先赋值为全零,然后再立即置位状态寄存器的IDLE位。注意:两个非阻塞性赋值作用在同一位上。这完全符合Verilog标准


规定,在此情况下,最后的非阻塞性赋值会代替前面所有的非阻塞性赋值,即更新状态机的IDLE位,如下所示:


always@( posedge clk or posedge rst )


if( rst ) begin


state <= 5'b0;


state[IDLE] <= 1'b1;


end


else state <= next;


5状态机中组合逻辑块的赋值


在组合always块中只能使用阻塞性赋值。来自同步always块的状态寄存器和状态机的所有输入触发组合always块,该块更新下一状态。应该在组合always块的最前面为下一状态赋一个默认值,块内的case语句会更新该默认值。通常下一状态有三种默认值:全x;预定的恢复状态,如IDLE状态寄存器的当前值。如果默认值为全x,那么前仿真模型会使状态机在没有任何明确的状态转移时,输出未知。这不仅有利于状态机的调试,还有利于综合,因为综合工具将x作为"无关"。一些设计(如卫星应用,媒体应用,需要使用FSM触发器作为检测扫描链的设计和使用正规验证工具做等价检测的设计)要求下一状态是已知的,而不是全x。通常这些设计下一状态的默认值应该是IDLE或全零,才能满足设计的要求。将下一状态




 


初始化为默认值,可能比在每个case语句中指定明确的状态转移更简单。


6 Synplify中状态机设计:


可以在Synplify中添加在state定义时添加如下约束属性来限定状态机的编码:


reg [2:0] state /* synthesis syn_encoding = "onehot" */;


Synplify中包含一个强大的FSM编辑器,可以产生在时间和面积上均得到优化的状态机设计,但这将忽略一些状态机中未定义的状态(invalid state),如果必须在状态机进入了未定义的状态后能自动回到有效状态,可以在状态机生成时添加一个安全属性(safe),使得到达无效状态时能回到初始状态,这对电路的时间和面积产生很小的影响:reg [2:0] state /* synthesis syn_encoding = "safe,onehot" */;


这种方法可能与源代码中实际描述的不一致,对于大多数设计来说这不会产生问题,但如果必须与源代码中描述的状态机流程图相吻合,可以通过约束属性关掉FSM编辑器:reg [4:0] state /* synthesis syn_preserve=1 */;但这将严重影响电路的时间和面积特性,下表是将三种方法应用在Altera Flex10k 和 Xilinx Virtex上的比较:


编者注:对第5条和第6条在Synplicity公司的Application NoteDesigning Safe Verilog State Machines with Synplify中有更详细的说明。


7几种状态机编码比较:


状态机编码有gray、binary、one-hot 等,其中Binary、gray-code 编码使用最少的触发器,较多的组合逻辑。而one-hot 编码反之。由于CPLD 更多的提供组合逻辑资源,而FPGA 更多的提供触发器资源,所以CPLD 多使用gray-code,而FPGA 多使用one-hot 编码。另一方面,对于小型设计使用gray-code 和binary 编码更有效,而大型状态机使用one-hot更高效。看synplicity 的文档,推荐在24 个状态以上会用格雷码,在5~24 个状态会用独热码,在4 个状态以内用二进制码,肯定独热码比二进制码在实现FSM 部分会占更多资源,但是译码输出控制简单,所以如果状态不是太多,独热码较好。状态太少译码不会太复杂,二进制就可以了。状态太多,前面独热码所占资源太多,综合考虑就用格雷码了。在代码中添加综合器的综合约束属性或者在图形界面下设置综合约束属性可以比较方便的改变状态的编码。在synplify综合时,把FSM compile 勾上,就算用binary 表示,综合器会自动综合成one-hot 模式。


也可在coding时直接添加描述:


VHDL


attribute TYPE_ENCODING_style of <typename> : type is ONEHOT;


Verilog


Reg[2:0] state; // synthesis syn_encoding = "value"(onehot)



 


其中one-hot编码又分为几种:verbose one-hot 、simplified one-hot、one-hot with zero-idle,关于其具体区别请参考State Machine Coding Styles for Synthesis一文。


8同步状态机输入变量的处理


所有的输入变量都必须与状态机时钟同步,否则,状态机的实际运行可能出现奇怪的现


象,会莫名其妙地出现非法状态。所有的状态机都不可避免地可能死锁,在运行中间歇地中止。为什么呢?这是因为,实际上所有的物理状态机是用物理门实现的,而物理门的传播延时不为0。输入信号经过不同的物理门组合到不同的状态触发器输入端D的传播延时有细微的差别。如果输入信号正好在错误的时间改变,那么较快的通路会探测到变化,而较慢的通路则不会。时钟是不变的,所以必定有一个触发器会出现错误的电平。这样就导致状态转移错


误,整个状态机电路就会误动作。这种设计错误通常不被人注意,因为误动作的几率很小。所以,一定要注意异步输入。解决异步输入的办法是同步器,通常二级同步器能够预防错误的状态入口。虽然可能性很小,还是必须注意一个统计问题,即异步输入还是有可能传播通过同步器。因为两个同步阶段的出现,这种可能性被大大地减小了。一些空间或军事设计需要使用三级同步器,但是对于商业和工业级设计,二级同步器已经足够了。为了最大的减小延迟,第一级使用下降沿触发,第二级使用上升沿触发。


9 FSM输出产生


FSM的输出逻辑可以编码为一个独立的连续赋值块,也可以编码为一个组合逻辑always块。如果输出赋值编码到组合always块中,那么输出赋值就可以放到Verilog的task中,每


个case语句调用task。将输出赋值与组合always块分开,不仅有利于在需要更改时改变输出逻辑,还可以避免综合工具产生意外的锁存器。双always块状态机中,如果将输出赋值放在组合always块内,那么应该在always块的顶部初始化输出为默认值,任何case语句将输出改变为合适值。通常,这种方法使用的代码比在每个case语句中为输出赋值更少,并且可以突出输出的变化。Mealy和寄存输出使用连续赋值或在组合always块中,很容易添加Mealy输出,具体如下:


assign rd_out = ( state == READ ) & !rd_strobe_n;


case( state )


...


READ: if( ! rd_strobe_n ) rd_out = 1'b1;


需要注意的是经过状态转移组合输出会出现很窄的毛刺。由于时钟的倾斜,不等的时钟到输出延时Tco和不等的组合逻辑传播延时,毛刺会不断出现。这种毛刺可能很窄,也可能在大多数逻辑分析仪和一些示波器上看见。被这些输出驱动的电路的敏感沿肯定有毛刺,并且可能导致电路错误。更糟糕的是,毛刺的宽度随着温度和功耗变化。为此我们可以在时序always块内使用非阻塞性赋值实现寄存输出。FSM可以使用一个时序always块,也可以在第二个时序always块添加相应的代码。此外如果状态比较简单,还可以定义这样一种状态机,它的输出就是状态位:assign {OUT3, OUT2, OUT1, OUT0} = state[3:0];


这样每个输出的时序都可预测,并且传播延时最小。这样的输出是没有毛刺的,因为假设输入的建立时间Ts和保持时间Th是可观测的,那么每个时钟最多只有一个触发器变化。如果一个输出对于两个连续的状态都是逻辑1,并且输入采取同步方法,那么就可以保证输出的下降沿不会出现毛刺。


原创粉丝点击