数字集成电路设计-12-状态机的四种写法

2019-04-13 14:33发布

引言

在实际的数字电路设计中,状态机是最常用的逻辑,而且往往是全部逻辑的核心部分,所以状态机的质量,会在比较大的程度上影响整个电路的质量。本小节我们通过一个简单的例子(三进制脉动计数器)来说明一下状态机的4中写法。

1,模块功能

由于我们的目的在于说明状态机的写作方式,所以其逻辑越简单有利于理解。就是一个简单的脉动计数器,每个三个使能信号输出一个标示信号。

2,一段式

状态机的写法,一般有四种,即一段式,两段式,三段式,四段式。对于一段式的写法,整个状态机的状态转移、转移条件、对应状态的输出都写在一个always块里,故称‘一段式’。那么,脉动计数器状态机的一段式写法该怎么写呢?如下所示:

/* * file : fsm1.v * author: Rill * date : 2014-05-11 */ module Mfsm1 ( clk, rst, enable, done ); input wire clk; input wire rst; input wire enable; output reg done; parameter s_idle = 4'd0; parameter s_1 = 4'd1; parameter s_2 = 4'd2; parameter s_3 = 4'd3; reg [3:0] state; always @(posedge clk) begin if(rst) begin done <=1'b0; state <= s_idle; end else begin case(state) s_idle: begin if(enable) state <= s_1; done <= 1'b0; end s_1: begin if(enable) state <= s_2; done <= 1'b0; end s_2: begin if(enable) begin state <= s_3; done <= 1'b1; end else begin done <= 1'b0; end end s_3: begin state <= s_idle; done <= 1'b0; end default: begin state <= s_idle; done <= 1'b0; end endcase end end endmodule

3,两段式

状态机的另外一种写法是‘两段式’的。两段式的写法,整个状态机由两个always块组成,第一个块只负责状态转移,第二个块负责转移条件和对应状态的输出。其中第一个块是时序逻辑,第二个块是组合逻辑。脉动计数器状态机的两段式写法又是怎样的呢?

/* * file : fsm2.v * author: Rill * date : 2014-05-11 */ module Mfsm2 ( clk, rst, enable, done ); input wire clk; input wire rst; input wire enable; output reg done; parameter s_idle = 4'd0; parameter s_1 = 4'd1; parameter s_2 = 4'd2; parameter s_3 = 4'd3; reg [3:0] current_state; reg [3:0] next_state; always @(posedge clk) begin if(rst) begin current_state <= s_idle; end else begin current_state <= next_state; end end always @(*) begin case(current_state) s_idle: begin if(enable) next_state = s_1; done = 1'b0; end s_1: begin if(enable) next_state = s_2; done = 1'b0; end s_2: begin if(enable) next_state = s_3; done = 1'b0; end s_3: begin next_state = s_idle; done = 1'b1; end default: begin next_state = s_idle; done = 1'b0; end endcase end endmodule


4,三段式

从上面可以看出,两段式的写法是从一段式发展而来的,将一段式的写法中将状态转移部分提取出来,作为一个独立的always块,就变成了两段式。按照这个思路继续推进,如果将两段式的第二个块中的转移条件提取出来,也作为一个独立的块,就变成了‘三段式’,三段式的写法中,状态转移块是时序逻辑,转移条件块是组合逻辑,对应状态的输出是时序逻辑。那么,脉动计数器状态机的三段式写法是怎样的呢?

/* * file : fsm3.v * author: Rill * date : 2014-05-11 */ module Mfsm3 ( clk, rst, enable, done ); input wire clk; input wire rst; input wire enable; output reg done; parameter s_idle = 4'd0; parameter s_1 = 4'd1; parameter s_2 = 4'd2; parameter s_3 = 4'd3; reg [3:0] current_state; reg [3:0] next_state; always @(posedge clk) begin if(rst) begin current_state <= s_idle; end else begin current_state <= next_state; end end always @(*) begin case(current_state) s_idle: begin if(enable) next_state = s_1; end s_1: begin if(enable) next_state = s_2; end s_2: begin if(enable) next_state = s_3; end s_3: begin next_state = s_idle; end default: begin next_state = s_idle; end endcase end always @(posedge clk) begin if(rst) begin done <= 1'b0; end else begin case(next_state) s_idle: begin done <= 1'b0; end s_1: begin done <= 1'b0; end s_2: begin done <= 1'b0; end s_3: begin done <= 1'b1; end default: begin done <= 1'b0; end endcase end end endmodule


5,四段式

上面的三种状态机的写法是我们经常提到的,也是经典的三种。这三种写法在逻辑上是完全等价的,也就是是说,无论采用哪种写法,模块的功能都是一样的,但前两种一般只出现在教科书中,在实际的项目中是很少见到的。原因在于生成网表的综合器,由于目前的综合器还不够智能,其优化算法对三种写法的敏感度不同,造成最终生成的电路有所区别,有时候区别较大,尤其是对于复杂的状态机。无数血与泪的实践证明,使用前面两种写法生成的电路在时序、性能、功耗和面积等方面的表现都不如三段式的写法,所以即使三段式的写法会让你多敲几次键盘,在实际的电路设计中尽量采用三段式的写法来描述状态机,多敲的那几次键盘换来的电路质量的提高是完全值得的。
俗话说,“没有最好,只有更好”。三段式的写法是不是最好的呢?我认为不见得如此。上面说到,如果采用三段式的写法,代码会变长,如果是大的状态机,结果会更明显。那么,有没有一种写法,既能产生优质的电路,又能少敲几次键盘呢?答案是肯定的。
仔细观察上面三种写法,你会发现,无论是哪种写法,都会使用case语句,case语句不仅占用的代码行数最多,而且综合器对case语句还有不同的解析(full case和parallel case),如果我们将三段式的写法中的case语句换成assign语句,并将状态转移块进一步将当前状态和下一个状态拆分开,就变成了“四段式”,四段式的写法由状态识别,状态转移,转移条件和对应状态的输出四部分组成。那么,脉动计数器状态机四段式的写法又是如何实现的呢?

/* * file : fsm4.v * author: Rill * date : 2014-05-11 */ module Mfsm4 ( clk, rst, enable, done ); input wire clk; input wire rst; input wire enable; output done; parameter s_idle = 4'd0; parameter s_1 = 4'd1; parameter s_2 = 4'd2; parameter s_3 = 4'd3; reg [3:0] current_state; wire c_idle = (current_state == s_idle); wire c_1 = (current_state == s_1); wire c_2 = (current_state == s_2); wire c_3 = (current_state == s_3); wire n_idle = c_3; wire n_1 = c_idle & enable; wire n_2 = c_1 & enable; wire n_3 = c_2 & enable; wire [3:0] next_state = {4{n_idle}} & s_idle | {4{n_1}} & s_1 | {4{n_2}} & s_2 | {4{n_3}} & s_3; always @(posedge clk) begin if(rst) current_state <= s_idle; else if(n_idle | n_1 | n_2 | n_3) current_state = next_state; end assign done = c_3; endmodule


6,验证

通过对比,我们很容易就会发现,采用四段式写法写出来的状态机,代码数量会减少很多,不仅如此,由于使用的语句类型减少了(只有赋值语句),生成电路的质量也会有所改善。那是否在进行电路设计的时候采用四段式的写法就没有缺点了呢?还有句俗话叫“金无足赤,人无完人”,由于四段式的写法将状态机拆分的过于零散,以至于综合器都识别不出来它是一个状态机了,所以在做覆盖率(coverage)分析的时候,分析工具只会按一般的逻辑进行分析,各个状态之间的转换概率就分析不出来了。
既然状态机有这么多种写法,在实际工作中采用哪一种呢?我认为三段式和四段式都是可以接受的(我个人习惯四段式的写法)。如果将来有一天综合器对四种写法综合出来的电路都差不多,那读者就可以根据自己的喜好来任意选择了。 
上面提到,无论采用哪种写法,模块实现的功能都是完全相同的,倒底是不是呢?我们需要写一个简单的测试激励(testbench)来验证一下。

/* * file : tb.v * author: Rill * date : 2014-05-11 */ module tb; reg clk; reg rst; reg enable; wire done1; wire done2; wire done3; wire done4; Mfsm1 fsm1 ( .clk(clk), .rst (rst), .enable(enable), .done(done1) ); Mfsm2 fsm2 ( .clk(clk), .rst (rst), .enable(enable), .done(done2) ); Mfsm3 fsm3 ( .clk(clk), .rst (rst), .enable(enable), .done(done3) ); Mfsm4 fsm4 ( .clk(clk), .rst (rst), .enable(enable), .done(done4) ); always #1 clk = ~clk; integer loop; initial begin clk = 0; rst = 0; enable = 0; loop = 0; repeat (10) @(posedge clk); rst = 1; repeat (4) @(posedge clk); rst = 0; repeat (100) @(posedge clk); for(loop=1;loop<10;loop=loop+1) begin enable = 1; @(posedge clk); enable = 0; @(posedge clk); end repeat (100) @(posedge clk); $stop; end endmodule

7,modelsim下的波形




8,ncsim的波形

上面是用windows下的modelsim得到的仿真波形,如果我们用ncsim(IUS),并且在Linux下,我们最好写一个简单的脚本来进行仿真,提高工作效率。
#! /bin/bash # # fsm.sh # usage: ./fsm.sh c/w/r # Rill create 2014-09-03 # TOP_MODULE=tb tcl_file=run.tcl if [ $# != 1 ];then echo "args must be c/w/r" exit 0 fi if [ $1 == "c" ]; then echo "compile lib..." ncvlog -f ./vflist -sv -update -LINEDEBUG; ncelab -delay_mode zero -access +rwc -timescale 1ns/10ps ${TOP_MODULE} exit 0 fi if [ -e ${tcl_file} ];then rm ${tcl_file} -f fi touch ${tcl_file} if [ $1 == "w" ];then echo "open wave..." echo "database -open waves -into waves.shm -default;" >> ${tcl_file} echo "probe -shm -variable -all -depth all;" >> ${tcl_file} echo "run" >> ${tcl_file} echo "exit" >> ${tcl_file} fi if [ $1 == "w" -o $1 == "r" ];then echo "sim start..." ncsim ${TOP_MODULE} -input ${tcl_file} fi echo "$(date) sim done!"
运行脚本: 
./fsm.sh c ./fsm.sh w
执行:
simvision wave/wave.trn
即可得到仿真波形,如下所示:


从中可以看出,ncsim和modelsim得到的仿真波形有所不同。原因在于前面的三种三段式写法是寄存器输出,第四种是组合逻辑输出。