Binary(二进制编码)、gray-code(格雷码)使用最少的触发器,较多的组合逻辑,one-hot(独热码)反之。CPLD组合逻辑资源多,使用gray-code;FPGA更多的触发器资源,使用one-hot。
一个完备的状态机(健壮性)应该具备初始化状态和默认状态。当芯片加电或者复位后,状态机应该能够自动将所有判断条件复位,并进入初始化状态。需要注明的一点是,大多是FPGA都有GSR(Globe Set/Reset)信号,当FPGA加电后,GSR信号拉高,对所有的寄存器、RAM等单元复位/置位,这时配置于FPGA的逻辑并未生效,所以不能保证正确进入初始化状态。所以使用GSR企图进入FPGA的初始化状态,常常会产生种种不必要的麻烦.一般方法是采用异步复位信号,当然也可以使用同步复位,但要注意同步复位逻辑的设计.解决这个问题的另一种方法是将默认的初始状态编码设置为全零,这样GSR复位后,状态机自动进入初始状态.
完整的状态机还应该包含一个默认(default)状态,当转移条件不满足,或者状态发生了突变,要保证逻辑不会陷入"死循环".这是对状态机健壮性的一个重要要求,也就是常说的"自恢复"功能.对if…else语句使用完备的条件判断语句.case语句要用default建立默认状态.可以添加一个额外的default状态,一旦进入这个状态就自动转入IDLE状态,重新启动状态机.
两段式FSM描述Mealy状态机,输出逻辑可以用"?"语句描述,或者使用case语句判断转移条件与输入信号即可.输出条件比较复杂,而且多个状态共用某些输出,则建议使用task/endtask将输出封装起来,达到模块复用的目的.
module state1 ( nrst,clk,
i1,i2,
o1,o2,
err
);
input nrst,clk;
input i1,i2;
output o1,o2,err;
reg o1,o2,err;
reg [2:0] NS; //NextState
parameter [2:0] //one hot with zero idle
IDLE = 3'b000,
S1 = 3'b001,
S2 = 3'b010,
ERROR = 3'b100;
//1 always block to describe state transition, state output, state input condition
always @ (posedge clk or negedge nrst)
if (!nrst)
begin
NS <= IDLE;
{o1,o2,err} <= 3'b000;
end
else
begin
NS <= 3'bx;
{o1,o2,err} <= 3'b000;
case (NS)
IDLE: begin
if (~i1) begin{o1,o2,err}<=3'b000;NS <= IDLE; end
if (i1 && i2) begin{o1,o2,err}<=3'b100;NS <= S1; end
if (i1 && ~i2) begin{o1,o2,err}<=3'b111;NS <= ERROR;end
end
S1: begin
if (~i2) begin{o1,o2,err}<=3'b100;NS <= S1; end
if (i2 && i1) begin{o1,o2,err}<=3'b010;NS <= S2; end
if (i2 && (~i1)) begin{o1,o2,err}<=3'b111;NS <= ERROR;end
end
S2: begin
if (i2) begin{o1,o2,err}<=3'b010;NS <= S2; end
if (~i2 && i1) begin{o1,o2,err}<=3'b000;NS <= IDLE; end
if (~i2 && (~i1))begin{o1,o2,err}<=3'b111;NS <= ERROR;end
end
ERROR: begin
if (i1) begin{o1,o2,err}<=3'b111;NS <= ERROR;end
if (~i1) begin{o1,o2,err}<=3'b000;NS <= IDLE; end
end
endcase
end
endmodule
module state2 ( nrst,clk,
i1,i2,
o1,o2,
err
);
input nrst,clk;
input i1,i2;
output o1,o2,err;
reg o1,o2,err;
reg [2:0] NS,CS;
parameter [2:0] //one hot with zero idle
IDLE = 3'b000,
S1 = 3'b001,
S2 = 3'b010,
ERROR = 3'b100;
//sequential state transition
always @ (posedge clk or negedge nrst)
if (!nrst)
CS <= IDLE;
else
CS <=NS;
//combinational condition judgment
always @ (nrst or CS or i1 or i2)
begin
NS = 3'bx;
ERROR_out;
case (CS)
IDLE: begin
IDLE_out;
if (~i1) NS = IDLE;
if (i1 && i2) NS = S1;
if (i1 && ~i2) NS = ERROR;
end
S1: begin
S1_out;
if (~i2) NS = S1;
if (i2 && i1) NS = S2;
if (i2 && (~i1)) NS = ERROR;
end
S2: begin
S2_out;
if (i2) NS = S2;
if (~i2 && i1) NS = IDLE;
if (~i2 && (~i1)) NS = ERROR;
end
ERROR: begin
ERROR_out;
if (i1) NS = ERROR;
if (~i1) NS = IDLE;
end
endcase
end
//output task
task IDLE_out;
{o1,o2,err} = 3'b000;
endtask
task S1_out;
{o1,o2,err} = 3'b100;
endtask
task S2_out;
{o1,o2,err} = 3'b010;
endtask
task ERROR_out;
{o1,o2,err} = 3'b111;
endtask
endmodule
三段式状态机的输出态可以是组合逻辑也可以是时序逻辑,一般使用时序逻辑,可避免电路毛刺。输出同步进程通常是case(现态),下例是书中例程,是case(次态),这两种方式的详细对比可以参考下文的《FPGA之道》中的内容。
module state3 ( nrst,clk,
i1,i2,
o1,o2,
err
);
input nrst,clk;
input i1,i2;
output o1,o2,err;
reg o1,o2,err;
reg [2:0] NS,CS;
parameter [2:0] //one hot with zero idle
IDLE = 3'b000,
S1 = 3'b001,
S2 = 3'b010,
ERROR = 3'b100;
//1st always block, sequential state transition
always @ (posedge clk or negedge nrst)
if (!nrst)
CS <= IDLE;
else
CS <=NS;
//2nd always block, combinational condition judgment
always @ (nrst or CS or i1 or i2)
begin
NS = 3'bx;
case (CS)
IDLE: begin
if (~i1) NS = IDLE;
if (i1 && i2) NS = S1;
if (i1 && ~i2) NS = ERROR;
end
S1: begin
if (~i2) NS = S1;
if (i2 && i1) NS = S2;
if (i2 && (~i1)) NS = ERROR;
end
S2: begin
if (i2) NS = S2;
if (~i2 && i1) NS = IDLE;
if (~i2 && (~i1)) NS = ERROR;
end
ERROR: begin
if (i1) NS = ERROR;
if (~i1) NS = IDLE;
end
endcase
end
//3rd always block, the sequential FSM output
always @ (posedge clk or negedge nrst)
if (!nrst)
{o1,o2,err} <= 3'b000;
else
begin
{o1,o2,err} <= 3'b000;
case (NS)
IDLE: {o1,o2,err}<=3'b000;
S1: {o1,o2,err}<=3'b100;
S2: {o1,o2,err}<=3'b010;
ERROR: {o1,o2,err}<=3'b111;
endcase
end
endmodule
看似是两段式两段式状态机,其实是将三段式状态机中时序逻辑部分放到一个always块中(《FPGA之道》书中推荐使用这种写法)
// -----------------------------------------------------------------------------
// KEYWORDS : AD7980
// -----------------------------------------------------------------------------
// PURPOSE : Driver for the AD7980 16-Bit, 1 MSPS PulSAR ADC in MSOP/QFN
// -----------------------------------------------------------------------------
// REUSE ISSUES
// Reset Strategy : Active low reset signal
// Clock Domains : 2 clocks - the system clock that drives the internal logic
// : and a clock for ADC conversions
// Critical Timing : N/A
// Test Features : N/A
// Asynchronous I/F : N/A
// Instantiations : N/A
// Synthesizable (y/n) : Y
// Target Device : AD7980
// Other : The driver is intended to be used for AD7980 ADCs configured
// : in /CS MODE, 3-WIRE, WITHOUT BUSY INDICATOR
// -----------------------------------------------------------------------------
`timescale 1ns/1ns //Use a timescale that is best for simulation.
//----------- Module Declaration -----------------------------------------------
module AD7980
//----------- Ports Declarations -----------------------------------------------
(
//clock and reset signals
input fpga_clk_i, //system clock
input adc_clk_i, //clock to be applied to ADC to read the conversions results
input reset_n_i, //active low reset signal
//IP control and data interface
output [15:0] data_o, //data read from the ADC
output reg data_rd_ready_o, //when set to high the data read from the ADC is available on the data_o bus
//ADC control and data interface
input adc_sdo, //ADC SDO signal
input adc_sdi, //ADC SDI signal (not used in 3-WIRE mode)
output adc_sclk_o, //ADC serial clock
output adc_cnv_o //ADC CNV signal
);
//----------- Registers Declarations -------------------------------------------
reg [ 3:0] adc_state; //current state for the ADC control state machine
reg [ 3:0] adc_next_state; //next state for the ADC control state machine
reg [ 3:0] adc_state_m1; //current state for the ADC control state machine in the ADC clock domain
reg [ 6:0] adc_tcycle_cnt; //counts the number of FPGA clock cycles to determine when an ADC cycle is complete
reg [ 6:0] adc_tcnv_cnt; //counts the number of FPGA clock cycles to determine when an ADC conversion is complete
reg [ 4:0] sclk_clk_cnt; //counts the number of clocks applied to the ADC to read the conversion result
reg adc_clk_en; //gating signal for the clock sent to the ADC
reg adc_cnv_s; //internal signal used to hold the state of the ADC CNV signal
reg [15:0] adc_data_s; //interal register used to store the data read from the ADC
//----------- Wires Declarations -----------------------------------------------
wire adc_sclk_s; //internal signal for the clock sent to the ADC
//----------- Local Parameters -------------------------------------------------
//ADC states
parameter ADC_IDLE_STATE = 4'b0001;
parameter ADC_START_CNV_STATE = 4'b0010;
parameter ADC_END_CNV_STATE = 4'b0100;
parameter ADC_READ_CNV_RESULT = 4'b1000;
//ADC timing
parameter real FPGA_CLOCK_FREQ = 100000000; //FPGA clock frequency [Hz]
parameter real ADC_CYCLE_TIME = 0.000001000; //minimum time between two ADC conversions (Tcyc) [s]
parameter real ADC_CONV_TIME = 0.000000670; //conversion time (Tcnvh) [s]
parameter [6:0] ADC_CYCLE_CNT = FPGA_CLOCK_FREQ * ADC_CYCLE_TIME - 1;
parameter [6:0] ADC_CNV_CNT = FPGA_CLOCK_FREQ * ADC_CONV_TIME;
//ADC serial clock periods
parameter ADC_SCLK_PERIODS = 5'd15; //number of clocks to be sent to the ADC to read the conversion result
//----------- Assign/Always Blocks ---------------------------------------------
assign adc_cnv_o = adc_cnv_s;
assign adc_sclk_s = adc_clk_i & adc_clk_en;
assign adc_sclk_o = adc_sclk_s;
assign data_o = adc_data_s;
//update the ADC timing counters
always @(posedge fpga_clk_i)
begin
if(reset_n_i == 1'b0)
begin
adc_tcycle_cnt <= 0;
adc_tcnv_cnt <= ADC_CNV_CNT;
end
else
begin
if(adc_tcycle_cnt != 0)
begin
adc_tcycle_cnt <= adc_tcycle_cnt - 1;
end
else if(adc_state == ADC_IDLE_STATE)
begin
adc_tcycle_cnt <= ADC_CYCLE_CNT;
end
if(adc_state == ADC_START_CNV_STATE)
begin
adc_tcnv_cnt <= adc_tcnv_cnt - 1;
end
else
begin
adc_tcnv_cnt <= ADC_CNV_CNT;
end
end
end
//read data from the ADC
always @(negedge adc_clk_i)
begin
if(adc_clk_en == 1'b1)
begin
adc_data_s <= {adc_data_s[14:0], adc_sdo};
sclk_clk_cnt <= sclk_clk_cnt - 1;
end
else
begin
sclk_clk_cnt <= ADC_SCLK_PERIODS;
end
end
//determine when the ADC clock is valid to be sent to the ADC
always @(negedge adc_clk_i)
begin
adc_state_m1 <= adc_state;
adc_clk_en <= ((adc_state_m1 == ADC_END_CNV_STATE) || (adc_state_m1 == ADC_READ_CNV_RESULT) && (sclk_clk_cnt != 0)) ? 1'b1 : 1'b0;
end
//update the ADC current state and the control signals
always @(posedge fpga_clk_i)
begin
if(reset_n_i == 1'b0)
begin
adc_state <= ADC_IDLE_STATE;
end
else
begin
adc_state <= adc_next_state;
case (adc_state)
ADC_IDLE_STATE:
begin
adc_cnv_s <= 1'b0;
data_rd_ready_o <= 1'b0;
end
ADC_START_CNV_STATE:
begin
adc_cnv_s <= 1'b1;
data_rd_ready_o <= 1'b1;
end
ADC_END_CNV_STATE:
begin
adc_cnv_s <= 1'b0;
data_rd_ready_o <= 1'b0;
end
ADC_READ_CNV_RESULT:
begin
adc_cnv_s <= 1'b0;
data_rd_ready_o <= 1'b0;
end
endcase
end
end
//update the ADC next state
always @(adc_state, adc_tcycle_cnt, adc_tcnv_cnt, sclk_clk_cnt)
begin
adc_next_state <= adc_state;
case (adc_state)
ADC_IDLE_STATE:
begin
if(adc_tcycle_cnt == 0)
begin
adc_next_state <= ADC_START_CNV_STATE;
end
end
ADC_START_CNV_STATE:
begin
if(adc_tcnv_cnt == 0)
begin
adc_next_state <= ADC_END_CNV_STATE;
end
end
ADC_END_CNV_STATE:
begin
adc_next_state <= ADC_READ_CNV_RESULT;
end
ADC_READ_CNV_RESULT:
begin
if(sclk_clk_cnt == 1)
begin
adc_next_state <= ADC_IDLE_STATE;
end
end
default:
begin
adc_next_state <= ADC_IDLE_STATE;
end
endcase
end
endmodule
module simple_fsm(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
input wire pi_money , //投币方式可以为:不投币(0)、投1元(1)
outputreg po_cola //po_cola为1时出可乐,po_cola为0时不出可乐
;
//只有三种状态,使用独热码
parameter IDLE =3'b001;
parameter ONE =3'b010;
parameter TWO =3'b100;
reg[2:0] state;
//第一段状态机,描述当前状态state如何根据输入跳转到下一状态
always@(posedge sys_clk ornegedge sys_rst_n)
if(sys_rst_n ==1'b0)
state <= IDLE; //任何情况下只要按复位就回到初始状态
else
case(state)
IDLE : if(pi_money ==1'b1)//判断输入情况
state <= ONE;
else
state <= IDLE;
ONE : if(pi_money ==1'b1)
state <= TWO;
else
state <= ONE;
TWO : if(pi_money ==1'b1)
state <= IDLE;
else
state <= TWO;
default: state <= IDLE; //如果状态机跳转到编码的状态之外也回到初始状态
endcase
//第二段状态机,描述当前状态state和输入pi_money如何影响po_cola输出
always@(posedge sys_clk ornegedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_cola <=1'b0;
else if((state == TWO)&&(pi_money ==1'b1))
po_cola <=1'b1;
else
po_cola <=1'b0;
endmodule
状态机中最为关键的部分,综合器能不能将RTL代码综合为状态机的样子主要看这部分代码如何来实现的。大家看到代码使用的是二段式状态机,但是又感觉怪怪的,这个状态机之所以和其他资料上的有所区别,其实是使用了新的写法。很多人都见过其他资料上总结的状态机代码写法有一段式、二段式、三段式(一段式指的是在一段状态机中使用时序逻辑既描述状态的转移,也描述数据的输出;二段式指在第一段状态机中使用时序逻辑描述状态转移,在第二段状态机中使用组合逻辑描述数据的输出;三段式指在第一段状态机中采用时序逻辑描述状态转移,在第二段在状态机中采用组合逻辑判断状态转移条件描述状态转移规律,在第三段状态机中描述状态输出,可以用组合电路输出,也可以时序电路输出)。这种一段式、二段式、三段式其实都是之前经典的老写法,也是一些老工程师仍然习惯用的写法,老方法是根据状态机理论建立的模型抽象后设计的,其实现的代码要严格按照固定的格式来写代码,否则综合器将无法识别出你写的代码是个状态机,因为早期的开发工具只能识别出固定的状态机格式,如果不按照标准格式写代码综合器最后无法综合成为状态机的样子。这样往往增加了设计的难度,很多人学习的时候还要去了解理论模型,反复学习理解很久才能够设计好的状态机,所以需要我们改进。
Moore型——状态机输出仅有现态决定,与输入无关
这种状态机的结构可以划分为两部分——状态转移部分和输出生成部分。
图中左半部分,输入和现态(现态寄存器的输出)通过组合逻辑共同作用产生次态,当下一次时钟有效沿到来的时候,现态寄存器发生更新,刚才产生的次态即成为新的现态,而新的现态和新的输入共同作用产生新的次态,如此往复。
图中右半部分,现态(现态寄存器的输出)直接通过组合逻辑产生当前的输出。有时候,不希望将这部分的组合逻辑延迟影响到后续模块的处理,可以添加输出寄存器来解决,需要注意一点,由于输出寄存器的更新需要等到下一次时钟的有效沿,因此经过输出寄存器寄存后的输出其实对应的是状态机的上一个状态。
Moore 2型状态机的结构仍划分为状态转移和输出两部分,对比Moore 1型状态机的原理结构框图,可以发现不同之处仅仅是输出生成部分的输入端由之前的现态(现态寄存器的输出)变为次态(由输入和现态通过组合逻辑产生)。这样一来,由于次态和次态决定的输出在同一个时钟周期内变得有效,那么在下一次时钟有效沿到来时,现态寄存器和输出寄存器将会同时进行更新,至此,次态变成新的现态,次态决定的输出变成新的输出。因此,Moore型状态机中经过寄存后的输出时对应于当前状态的。
Moore 2型相较于1型有其自身的缺点,那就是产生输出的组合逻辑延迟比较大,因为2型将产生次态的组合逻辑和产生输出的组合逻辑级联,这种将两级组合逻辑进行级联的方式会限制状态机本身的时钟工作频率。
Moore 1型和2型的本质区别在于“由状态产生输出”这部分的组合逻辑所处的位置。1型中将这部分逻辑置于现态寄存器之后,该组合逻辑的时间延迟会影响后续电路的工作,有时需要插入寄存器解决这个问题。2型中将这部分逻辑置于次态产生逻辑之后,这样这个组合逻辑级联会影响状态机机自身的工作频率。将这两种状态机的有点相结合,得到Moore 3型状态机:
Moore 3型将那些适合使用组合逻辑的输出使用1型中的方法来处理,而适合使用寄存器的输出采用2型的方式来处理。
如果后续模块为电平敏感的,那么自然选择寄存器形式的输出,不过FPGA中一般应该为全同步逻辑设计,所以绝大多数情况下,状态机机的前级输入、后级输出及本身是工作在一个时钟域内的,此时,假设由现态、输入产生次态的组合逻辑时间延迟为T1,由状态产生输出的逻辑组合的时间延迟为T2,而后续使用该输出的电路中,又会在输出信号到达下一个寄存器前引入时间延迟最大(考虑到会有多个地方使用该输出)为T3的组合逻辑。那个根据短板效应,若T1+T2 <= T2+T3,则该输出采用Moore 2型的寄存方式处理可以提升整个系统的性能;反之,该输出采用Moore 1型的组合方式可以提升系统性能。也就是看如何能使级联的组合逻辑延迟尽量小,现态寄存器和输出A寄存器可以打断这个延迟。
由于次态和输出均由现态和输入通过组合逻辑共同决定,因此可以将状态转移部分和输出生成部分合并成一个部分,兼并产生状态机的次态和输出。当下一次时钟有效沿到来后,现态寄存器完成刷新,次态成为了新的现态,而新的现态和新的输入共同作用产生新的次态和新的输出,如此往复。
域Moore 1型类似,Mealy 1型的输出也是直接通过组合逻辑产生的,若想通过添加输出寄存器来获得寄存输出,经过输出寄存器寄存后的输出其实对应的是状态机的上一个状态。
将Mealy 1型按照Moore 2型的原理结构框图进行一些简单的修改,可以得到Mealy 2型状态机。Mealy 2型状态机重新将状态转移部分和输出部分分开,并做成级联的形式,所以输出生成部分便由次态和输入共同作用产生产生次态对应的输出。这样,次态和次态决定的输出在同一个时钟周期内变得有效,那么在下一次时钟有效沿到来时,现态寄存器和输出寄存器将会同时进行更新,至此,次态变成新的现态,次态决定的输出变成新的输出。Mealy 2型经过i寄存后的输出是对应于当前状态的。
Mealy 1型和2型本质区别在于“由状态产生输出”这部分组合逻辑所处的位置。1型是将这部分逻辑与次态产生逻辑并联,那么该组合逻辑的时间延迟会影响后续电路的工作;2型中将这部分逻辑与次态产生逻辑串联,那么这部分组合逻辑的时间延迟会影响状态机本身的工作。因此将二者结合得到3型:
Mealy 3型状态机将适合使用组合逻辑的输出采用Mealy 1型的方式来处理,而适合使用寄存器的输出使用Mealy 2型的方式处理。所以,具体使用什么样的输出适合组合的形式,什么样的输出适合寄存的形式,需要根据具体的功能需求来确定。这与Moore型的不一样。主要因为:Mealy型状态机的输出除了与当前状态有关还与输出有关。对于相同的输入序列,Moore 1、2型状态机的状态变迁都是一样的,因此它们的输出也是完全一致的;但对于同样的输入序列,Mealy 1、2型状态机的状态变迁虽然是一样的,但Mealy 2型的输出实际上是由当前状态和上一次的输入得出的,这与Mealy 1型的输出是由当前状态和当前输入得出是不同的,故Mealy1、2之间不可随意替换。
Moore 3 + Mealy 3型得到下图的Mix型状态机,包含了四种类型的输出——Moore组合、Moore寄存、Mealy组合、Mealy寄存。