1.设计规划及波形绘制
1.1 有限状态机FSM原理
FSM 工作原理:
在任何给定的时刻,有限状态机只能处于有限个状态中的一个。当某些条件满足或某些事件发生时,状态机会根据当前状态和事件类型进行状态转换, 这种变化被称为状态转移。
"有限"表示状态的个数是有限的。
FSM分类:
Mealy: 输出取决于 状态+输入(可以看到引了state和input两条线到输出逻辑。)

Moore: 输出仅取决于输入(可以看到只引了state到输出逻辑。)

对应的,我们数电中学的状态图的画法也不同。
Mealy:
- 圆圈内表示状态的编码。(状态名称可标可不标,下图没有)
- 箭头上标出输入+输出,表示不仅状态会影响输出,输入也会影响。

Moore:
- 圆圈内表示状态的编码(S0~S9为状态名称,可标可不标)
- 箭头上只标输出。

PS:Mealy机比较常用。
1.2 波形绘制及实验目标
实验目标:投币可乐贩卖机,可乐2.5元一瓶,投币只有1元和0.5元,贩卖机可找零。
模块框图如下:
- 输入:投币是输入,有未投币,投0.5元,投1元三种情况,故二进制需要两位表示输入。
- 输出:有不出可乐不找零,出可乐不找零,出可乐找零三种情况,也需要两位二进制

- 状态:有投入0元(IDLE),已投入0.5元(s1),已投入1元(s2),已投入1.5元(s3),已投入2元(s4),已投入3元(s5)。用独热码表示,故需二进制六位。
(PS:投入2.5元这个状态可以不用画出来,因为满了2.5直接回到IDLE就可以了,看下图。其实投入3元也可以不用画,参考野火代码27-第二十讲-状态机(三)_哔哩哔哩_bilibili。 而且一般FSM状态机用独热码编状态,其实状态机的状态编码可选择,不同编码各有优缺点。这个也不是特别懂,以后回来填坑。)

对应实验目标,状态图,可绘制波形如下:

2.代码编写
2.1rtl
module complex_cola_FSM
(
input sys_clk ,
input rst_n ,
input [1:0] in_money , //投入0元:00 投入0.5元:01 投入1元:10
output reg [1:0] Out //第一位表示是否找零(1找零,0不找零),第二位表示是否出可乐(1出可乐,0不出可乐)
);
reg [5:0] state; //IDLE:投了0元(000001),s1:投了0.5元(000010),s2:投了1元(000100),s3:投了1.5元(001000),s4:投了2元(010000),s5:投了3元(100000)
//(Mealy机状态图投入2.5元状态可以省略)
parameter IDLE = 6'b000001;
parameter s1 = 6'b000010;
parameter s2 = 6'b000100;
parameter s3 = 6'b001000;
parameter s4 = 6'b010000;
parameter s5 = 6'b100000;
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
state <= IDLE;
else case (state)
// IDLE:;
IDLE: state <= (in_money == 2'b01) ? s1 : ( (in_money == 2'b10) ? s2 : IDLE );
s1 : state <= (in_money == 2'b01) ? s2 : ( (in_money == 2'b10) ? s3 : s1 );
s2 : state <= (in_money == 2'b01) ? s3 : ( (in_money == 2'b10) ? s4 : s2 );
s3 : state <= (in_money == 2'b01) ? s4 : ( (in_money == 2'b10) ? IDLE : s3 );
s4 : state <= (in_money == 2'b01) ? IDLE : ( (in_money == 2'b10) ? s5 : s4 );
s5 : state <= (in_money == 2'b01) ? s1 : ( (in_money == 2'b10) ? s2 : IDLE );
default:state <= IDLE;
endcase
end
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
Out <= 2'b00;
else if( (state == s3) && (in_money == 2'b10) || (state == s4) && (in_money == 2'b01))
Out <= 2'b01;
else if(state == s5)
Out <= 2'b11;
// else if((state == s4) && (in_money == 2'b10) )
// Out <= 2'b11;
else
Out <= 2'b00;
end
endmodule
其中代码这一块,这两种判断输出为11的写法功能上是一样的,只是第一种写法(红色框)比第二种写法输出11慢一拍(即慢一个时钟周期)。(PS:总感觉哪里怪怪的...有没有大神能够解答一下这里...)

2.2 tb
`timescale 1ns/1ns
module tb_complex_cola_FSM();
reg sys_clk ;
reg rst_n ;
reg [1:0] in_money; //投入0元:00 投入0.5元:01 投入1元:10
wire [1:0] Out ;
initial begin
sys_clk <= 1'b1;
rst_n <= 1'b0;
in_money <= 2'b00;
#52
rst_n <= 1'b1;
end
//50MHZ时钟
always #10 sys_clk <= ~sys_clk;
//随机生成输入
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
in_money <= 2'b00;
else
in_money <= {$random} % 4;
end
complex_cola_FSM u_complex_cola_FSM(
.sys_clk (sys_clk ),
.rst_n (rst_n ),
.in_money (in_money ),
.Out (Out )
);
endmodule
写代码时的错误及注意点:
注意点:
1. 最好给状态起名称,在代码中用名称来编写,可读性比直接看编码高。
如下图1直接使用状态编码,有时候直接看00不知道对应哪个状态,如果状态比较多不容易记住。

图1
可用局部参数parameter先定义好状态名称对应的编码,然后在代码编写过程中就可以直接使用状态名称了。如图2所示。

图2 优化后代码
2. 使用case语句别忘了加default,否则工具综合会额外产生Latch。
Verilog中case语句不加default的影响_verilog case default-CSDN博客
https://blog.csdn.net/weixin_39520719/article/details/106433787 见这个博主的文章,写的很清楚。
错误点:
1. 下图画线代码冗余,只用判断少数的输出01和11的情况即可,其他情况均输出00。

2. tb文件生成输入时,如果用延时#来描述输入波形,最好不要对准clk的沿变化。
以另一个代码工程为例。rtl代码为:
module cola_FSM
(
input sys_clk ,
input rst_n ,
input in_money, //0表示未投入,1表示投入一元
output reg out_cola //0表示不出可乐,1表示出可乐
);
reg [2:0] state; //IDLE:00 s1:01 s2:10
parameter IDLE = 3'b001; //IDLE:初始未投币,s1:投入一元,s2:投入两元
parameter s1 = 3'b010;
parameter s2 = 3'b100;
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
state <= IDLE;
else case (state)
IDLE:
if(in_money == 1'b1)
state <= s1;
else
state <= IDLE;
s1: if(in_money == 1'b1)
state <= s2;
else
state <= s1;
s2: if(in_money == 1'b1)
state <= IDLE;
else
state <= s2;
default: state <= IDLE;
endcase
end
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
out_cola <= 'b0;
// else if( (state == IDLE) || (state == s1) || ( (state == s2) && (in_money == 'b0)) )
// out_cola <= 'b0;
else if( (state == s2) && (in_money == 1'b1) )
out_cola <= 'b1;
else
out_cola <= 'b0;
end
endmodule

使用延时# 未错开输入
tb文件,当用延时#的方法来设置输入时,输入变化刚好与时钟周期上升沿对齐(如上图),此时输出的波形如下,

使用延时# 未错开输入波形
可以看到此时光标对准的地方,state在输入in_money跳变时同时跳变到下一状态。这与rtl代码逻辑不符,按照rtl的逻辑应该是state在in_money的下一拍才进行状态转移。
这是因为采用延时的方法设置输入,看似对准了sys_clk,但可能没对准,导致仿真器在时钟上升沿可能采样到上一个时刻或者是下一个跳变时刻的值,那么输出就会不确定。

使用同步随机输入
但使用always块来进行输入赋值时(如上图),这个时候,输入的变化就是和时钟严格对齐的,那么仿真器就会在时钟上升沿检测到旧值。

使用同步随机输入波形
当使用延时#来进行输入时,最好就与时钟错来,比如下图错开了1ns。

3 逻辑仿真及波形验证
与上面波形图一致。
(本贴仅是个人经验,如有侵权请联系我~)