同步有限状态机(FSM)被称作是FPGA的灵魂,所谓状态机就是能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作、完成特定操作的控制中心。通过状态机的方法,可有效降低抽象难度,同时也能提高代码的可读性及维护性。
主要分为两大类:
米里(Mealy)状态机:输出不仅与状态有关,而且还与输入有关。
摩尔(moore)状态机:输出只和状态有关,与输入无关。
状态机通常包括组合逻辑和时序逻辑两部分,其中时序逻辑由一组触发器组成,用来记忆当前状态。组合逻辑又分为次态逻辑和输出逻辑两部分,次态逻辑的功能就是确定状态机的下一个状态,输出逻辑的功能就是确定状态机的输出。
一般设计步骤如下:
- 根据需求选择状态机结构,即摩尔型还是米里型(可省)。
- 根据实际情况列出状态机的状态,并对每个状态编码,一般采用独热码(one-hot),即一个状态用一位表示。
- 根据状态转移关系画出状态转移图。
- 根据状态转移图,用HDL语言对状态机进行描述。
状态机的代码描述风格有三种:一段式、两段式、三段式。
- 一段式状态机:将所有的逻辑写在一个always块中,它的可读性比较差,不利于维护。
- 两段式状态机:将组合逻辑和时序逻辑分开,具有较好的可读性,相对容易维护,不过其组合逻辑输出易出现毛刺、竞争冒险等问题。
- 三段式状态机:除具有两段式的优点外,它对状态输出进行了寄存,能有效消除毛刺,因此推荐使用三段式状态机。
下面的设计实例就是一个三段式状态机控制LED灯实现流水。
input wire sclk,
input wire reset_n,
output reg [7:0] led_state
);
端口定义不多说。
reg [3:0] clk_cnt;
reg clk_out;
always@(posedge sclk or negedge reset_n)
begin
if(!reset_n)
begin
clk_cnt<=4'd0;
clk_out<=1'b0;
end
else
begin
if(clk_cnt == count-1'b1)
begin
clk_cnt<=4'd0;
clk_out<=1'b1;
end
else
begin
clk_cnt<=clk_cnt+1'b1;
clk_out<=1'b0;
end
end
end
这就是一个10个时钟周期的
计数器,来实现延时,不作赘述,由于我用的是modelsim做的仿真,所以延时不宜过长
。
parameter state_1 = 8'b0000_0001;
parameter state_2 = 8'b0000_0010;
parameter state_3 = 8'b0000_0100;
parameter state_4 = 8'b0000_1000;
parameter state_5 = 8'b0001_0000;
parameter state_6 = 8'b0010_0000;
parameter state_7 = 8'b0100_0000;
parameter state_8 = 8'b1000_0000;
reg [7:0] current_state,next_state;
always@(posedge sclk or negedge reset_n) //state transition
begin
if(!reset_n)
current_state<=idle;
else if(clk_out)
current_state<=next_state;
else
current_state<=current_state;
end
这就是状态机的第一段,是一个同步时序电路,来实现状态切换,即先声明当前状态和下一时刻状态,然后在10个时钟周期后把下一时刻的状态赋予当前状态,若没到时间,即保持当前状态。注意复位只清除当前状态。本段描述的是状态切换的条件。
begin
case(current_state)
idle: next_state=state_1;
state_1: next_state=state_2;
state_2: next_state=state_3;
state_3: next_state=state_4;
state_4: next_state=state_5;
state_5: next_state=state_6;
state_6: next_state=state_7;
state_7: next_state=state_8;
state_8: next_state=idle;
default: next_state=idle;
endcase
end
这是状态机的第二段,本段为组合逻辑电路,采用阻塞赋值。case语句的条件是当前状态,根据当前状态找到下一状态,此段描述的是状态切换的内容。
begin
if(!reset_n)
led_state<=idle;
else if(clk_out)
case(next_state)
idle: led_state<=state_1;
state_1: led_state<=state_2;
state_2: led_state<=state_3;
state_3: led_state<=state_4;
state_4: led_state<=state_5;
state_5: led_state<=state_6;
state_6: led_state<=state_7;
state_7: led_state<=state_8;
state_8: led_state<=idle;
default: led_state<=idle;
endcase
else
led_state<=led_state;
end
endmodule
这是状态机的第三段,本段是同步时序逻辑,case语句的条件是下一时刻的状态,然后根据下一时刻的状态来确定输出。本段描述的是有下一时刻的状态来确定输出。
至此我们的三段式状态机已经设计完成。下面我们就要做一个testbench来测试状态机是否能正常工作,并实现相应的功能。
这是测试程序。
module tb_led_loop;
reg tb_sclk,tb_reset_n;
wire [7:0] tb_led_state;
initial
begin
tb_sclk=0;
tb_reset_n=0;
#100
tb_reset_n=1;
end
always #10 tb_sclk<=~tb_sclk; //crystal oscillator
led_loop led_loop_inst(
.sclk(tb_sclk),
.reset_n(tb_reset_n),
.led_state(tb_led_state)
);
endmodule
启动仿真是用tcl语言写的脚本自动仿真,尾缀为.do文件(如sim.do),当然你也可以是用GUI的方式。
.main clear
vlib work
vlog ../sim/*.v
vlog ../rtl/*.v
vsim -t ns -voptargs=+acc work.tb_led_loop
add wave tb_led_loop/led_loop_inst/sclk
add wave tb_led_loop/led_loop_inst/reset_n
add wave tb_led_loop/led_loop_inst/clk_out
add wave tb_led_loop/led_loop_inst/current_state
add wave tb_led_loop/led_loop_inst/next_state
add wave tb_led_loop/led_loop_inst/led_state
run 100us
然后再写一个批量处理文件,可以使.bat文件也可以是.cmd文件(如modelsim_ran.bat),不过要确保你的电脑环境变量设置里有modelsim.exe的目录,否则没法启动。
bat文件就一行内容 modelsim -do sim.do 。不过要确保sim.do与modelsim_ran.bat文件同一目录下。然后双击bat文件即可自动启动仿真了。
好像有点小bug,因为上电current_state默认是0,next_state默认是1,所以clk_out为高时,next_state就成了2,输出就变成了2,所以这就是为啥第一个周期led_state的第一个状态为2的原因。从第二个周期开始就正常了。
要是有什么不对的地方还请大家指正,谢谢……