来自:数电和Verilog-时序逻辑实例四:状态机(过程分析)_数电状态机_程序员Marshall的博客-CSDN博客
1. 题目
要求设计一个自动饮料售卖机,饮料10分钱,硬币有5分和10分两种,并考虑找零。
(1)画出状态转移图,公式和卡诺图分析
(2)使用Verilog实现
(3)搭建测试平台做简单验证
2. 两段式描述-传统公式
2.1 什么叫做两段式描述的状态机?
两段,可以理解为两个always程序块。
(1)第一个always程序块
采用同步时序逻辑电路描述状态转移。
(2)第二个always程序块
采用组合逻辑电路判断状态转移条件并描述状态转移规律,同时组合逻辑输出结果。
2.2 设计过程
第一步
确定输入输出。
A=1表示投入5分钱,B=1表示投入10分钱,Y=1表示弹出饮料,Z=1表示找零。
第二步
确定电路状态。
S0表示售卖机里还没有钱币,S1表示已经投了5分钱。
注意这里不存在其他的情况,比如已经投了10分钱,那已经足够完成本次交易,电路应该回归初始的S0状态,所以只会有S0和S1这两种状态。
第三步
画状态转移图。
S0状态时:
(1)不投钱,则AB=00,此时肯定不会弹出饮料,也不会找零,因此YZ=00,此时状态也不会跳转,保持为S0。
(2)投入5分钱,则AB=10,此时还不够10分钱,因此不会弹出饮料,也不会找零,因此YZ=00,此时状态将跳转为S1。
(3)投入10分钱,则AB=01,此时售卖机中已经达到10分钱,因此会弹出饮料,但不会找零,因此YZ=10,完成交易后回到初始状态S0,等待进行下一次交易。
(4)同时投入5分钱和10分钱的情况假设不会发生(投币口只支持一次投一个币),因此AB=11的情况,默认YZ=00,并回到初始态S0。
S1状态时:
(1)不投钱,则AB=00,此时肯定不会弹出饮料,也不会找零,因此YZ=00,此时状态也不会跳转,保持为S1。
(2)投入5分钱,则AB=10,此时售卖机中已经达到10分钱,因此会弹出饮料,但不会找零,因此YZ=10,完成交易后回到初始状态S0,等待进行下一次交易。
(3)投入10分钱,则AB=01,此时售卖机中已经达到15分钱,因此会弹出饮料,同时会找零,因此YZ=11,完成交易后回到初始状态S0,等待进行下一次交易。
但说实话,一般人手中有两枚硬币,肯定直接投10分钱就好了,哪有先投5分钱再投10分钱的呢?是不是傻?
但做设计需要考虑这种特殊的情况,那可能是这个顾客忘了自己有两枚硬币,也可能是上个顾客投了5分钱后,发现身上只有5分钱就走了,然后第二个顾客过来捡了便宜。
所以只要有可能发生的情况,在设计时都必须考虑到,尤其是实际的项目中比这复杂的情况都要尽量考虑到,而这些地方恰恰是容易发生问题,产生bug的地方,需要尤其认真仔细。
(4)同时投入5分钱和10分钱的情况假设不会发生(投币口只支持一次投一个币),因此AB=11的情况,默认YZ=00,并回到初始态S0。
第四步
可以对照上面的状态转换图来画卡诺图。
第五步
卡诺图化简并输出计算公式。
2.3 设计代码
/***
* Author: Stephen Dai
* Date: 2023-07-26 20:57:34
* LastEditors: Stephen Dai
* LastEditTime: 2023-07-26 21:14:27
* FilePath: CodeProjectAutoMachineautomachine.v
* Description:
*
*/
module automachine(
input clk,
input rst,
input A,
input B,
output reg Y,
output reg Z
);
parameter S0 = 1'b0;
parameter S1 = 1'b1;
reg current_state;
reg next_state;
always @(posedge clk or negedge rst ) begin
if(!rst)
current_state <= S0;
else
current_state <= next_state;
end
always @(current_state or A or B) begin
next_state = (~B) & (A^current_state);
Y = ((~A) & B) || (A & (~B) & current_state);
Z = (~A) & B & next_state;
end
endmodule
2.4 测试代码
module top;
logic clk;
logic rst_n;
logic a,b;
logic y,z;
automachine DUT(.clk(clk),.rst(rst_n),.A(a),.B(b),.Y(y),.Z(z));
initial begin
clk = 0;
forever begin
#10;
clk = ~clk;
end
end
initial begin
rst_n = 0;
#50;
rst_n = 1;
end
initial begin
$display("%10t -> Start!!!",$time);
a = 0;
b = 0;
#80;
$display("------------------------------------------");
$display("%10t -> Round 1",$time);
$display("%10t -> insert 5 cents",$time);
a = 1;
b = 0;
#20;
$display("%10t -> insert 5 cents",$time);
a = 1;
b = 0;
#20;
a = 0;
b = 0;
#100;
$display("------------------------------------------");
$display("%10t -> Round 2",$time);
$display("%10t -> insert 5 cents",$time);
a = 1;
b = 0;
#20;
$display("%10t -> insert 10 cents",$time);
a = 0;
b = 1;
#20;
a = 0;
b = 0;
#100;
$display("------------------------------------------");
$display("%10t -> Round 3",$time);
$display("%10t -> insert 10 cents",$time);
a = 0;
b = 1;
#20;
a = 0;
b = 0;
#100;
$display("%10t -> Finish!!!",$time);
$finish;
end
endmodule : top
这里可以制造了以下三种实际投币购买饮料的场景来做测试:
Round 1
先投5分钱,然后再投5分钱。
此时期望的结果是弹出饮料,但不会找零,即Y和Z输出分别为1和0,即至少Y会有一段高电平的状态。
Round 2
先投5分钱,然后再投10分钱。
此时期望的结果是弹出饮料,并找零,即Y和Z输出都为1,即Y和Z都会有一段高电平的状态。
Round 3
投10分钱。
此时期望的结果是弹出饮料,但不会找零,即Y和Z输出分别为1和0,即至少Y会有一段高电平的状态。
2.5 仿真结果
3. 两段式描述-行为级
这里依然采用两段式描述,即:
(1)第一个always程序块
采用同步时序逻辑电路描述状态转移。
(2)第二个always程序块
采用组合逻辑电路判断状态转移条件并描述状态转移规律,同时组合逻辑输出结果。
但是,这一次,我们不用像之前那样,又画状态转移图,又画卡诺图真值表的,这次我们简单点。
3.1 设计代码
/***
* Author: Stephen Dai
* Date: 2023-07-26 22:24:31
* LastEditors: Stephen Dai
* LastEditTime: 2023-07-26 22:39:23
* FilePath: CodeProjectAutoMachineautomachine2.0.v
* Description:
*
*/
module automachine2(
input clk,
input rst,
input A,
input B,
output reg Y,
output reg Z
);
parameter S0 = 1'b0;
parameter S1 = 1'b1;
reg current_state;
reg next_state;
always @(posedge clk or negedge rst ) begin
if(!rst)
current_state <= S0;
else
current_state <= next_state;
end
always @(current_state or A or B) begin
Y = 1'b0;
Z = 1'b0;
case(current_state)
S0:begin
if (A == 1'b1 && B == 1'b0) //AB=10,投入5分钱,YZ=00,不出货不找零,跳转至S1
next_state = S1;
else if (A == 1'b0 && B == 1'b1) begin//AB=01,投入10分钱,YZ=10,出货不找零,跳转至S0
Y = 1'b1;
next_state = S0;
end
else
next_state = S0; //AB=00或者11保持S0不跳转
end
S1:begin
if (A == 1'b0 && B == 1'b0) //AB=00,不投币,保持现状S1
next_state = S1;
else if (A == 1'b1 && B == 1'b0) begin//AB=10,再投入5分钱,YZ=10,出货不找零,跳转至S0
Y = 1'b1;
next_state = S0;
end
else if (A == 1'b0 && B == 1'b1) begin//AB=01,再投入10分钱,YZ=11,出货找零,跳转至S0
Y = 1'b1;
Z = 1'b1;
next_state = S0;
end
else
next_state = S0; //AB=00或者11跳转S0
end
endcase
end
endmodule
3.2 验证代码
module top;
logic clk;
logic rst_n;
logic a,b;
logic y,z;
automachine2 DUT(.clk(clk),.rst(rst_n),.A(a),.B(b),.Y(y),.Z(z));
initial begin
clk = 0;
forever begin
#10;
clk = ~clk;
end
end
initial begin
rst_n = 0;
#50;
rst_n = 1;
end
initial begin
$display("%10t -> Start!!!",$time);
a = 0;
b = 0;
#80;
$display("------------------------------------------");
$display("%10t -> Round 1",$time);
$display("%10t -> insert 5 cents",$time);
a = 1;
b = 0;
#20;
$display("%10t -> insert 5 cents",$time);
a = 1;
b = 0;
#20;
a = 0;
b = 0;
#100;
$display("------------------------------------------");
$display("%10t -> Round 2",$time);
$display("%10t -> insert 5 cents",$time);
a = 1;
b = 0;
#20;
$display("%10t -> insert 10 cents",$time);
a = 0;
b = 1;
#20;
a = 0;
b = 0;
#100;
$display("------------------------------------------");
$display("%10t -> Round 3",$time);
$display("%10t -> insert 10 cents",$time);
a = 0;
b = 1;
#20;
a = 0;
b = 0;
#100;
$display("%10t -> Finish!!!",$time);
$finish;
end
endmodule : top
3.3 仿真结果
同之前的
4. 三段式描述
4.1 什么叫做三段式描述的状态机?
三段,可以理解为三个always程序块。
(1)第一个always程序块
采用同步时序逻辑电路描述状态转移。
(2)第二个always程序块
采用组合逻辑电路判断状态转移条件并描述状态转移规律。
(3)第三个always程序块
采用同步时序逻辑将结果寄存后输出。
两者的区别是将原先第二个always程序块中对y和z的组合逻辑输出改为了第三个always块的时序逻辑的寄存输出。
其实就这么简单,不少网络以及相关书籍上把它讲复杂了,甚至还给讲错了。
网络上随便搜索“三段式状态机”,基本给出的第三段always块的例子基本都是基于next_state输出的,很少看到有基于current_state输出的,这就形成了一种思维定势,认为三段式的第三段只能基于next_state描述,其实这是不对的。
应该说不管基于current_state还是next_state,目的都是要将最后输出的结果进行时钟同步后寄存器输出,并不拘泥于实现的形式,比如本节给出的例子中的第三个always块就并不是像书上和网络上那样都基于next_state来描述实现的。
这种错误的地方,除了网上以外,书本上要么没讲,要么提到的地方存在问题,至少我看到的地方有出现这种类似的错误,比如:EDA先锋工作室的《设计与验证Verilog HDL》就出现了这种错误。
下面用二段式改三段式的过程来给大家说明这两种描述方式的区别。
4.2两段式改三段式过程
第一步
将第二个always程序块中的输出y和z部分挪到第三个always块中,注意第三个always块为时序逻辑,采用非阻塞赋值。
第二步
删除第二个always块中的y和z部分。
第三步
化简合并第二个always块中的逻辑即可得到最终的三段式状态机描述代码。
注意这里用二段式改三段式的过程只是为了给大家说明这两种描述方式的区别,并不是让你通过先写两段式,再写三段式的过程来做设计实现。
实际上, 你完全可以直接编写实现三段式的Verilog代码,比如就上面的这个代码,我们来好好思考一下内部的转换逻辑就知道该怎么写了。
可以发现,三段式状态机描述通过将结果y和z寄存器后输出,使得其与时钟进行同步,输出电平以时钟周期为单位进行了整型。
简单说,之前采用组合逻辑输出,因此输出结果是会立刻变化的,而现在采用了与时钟同步的寄存器对结果进行寄存后再输出,因此输出的y和z波形是以时钟周期为电平单位变化的,只有在时钟的跳变沿才会产生变化,即不像组合逻辑那样会立刻产生变化。
这样一来的好处主要是改善了时序条件,便于后期满足电路的时序要求,消除了组合逻辑带来的毛刺。
但是三段式要相对复杂一点,多写了一个always块来将结果寄存器输出的时序逻辑,从而综合后的电路面积可能会(不一定)相对更多一些。
但为了提高设计的稳定性,推荐采用三段式描述进行状态机的设计。
4.3 设计代码
module automachine3(
input clk,
input rst,
input A,
input B,
output reg Y,
output reg Z
);
parameter S0 = 1'b0;
parameter S1 = 1'b1;
reg current_state;
reg next_state;
always @(posedge clk or negedge rst ) begin
if(!rst)
current_state <= S0;
else
current_state <= next_state;
end
always @(current_state or A or B) begin
case(current_state)
S0:begin
if (A == 1'b1 && B == 1'b0) //AB=10,投入5分钱,YZ=00,不出货不找零,跳转至S1
next_state = S1;
else if (A == 1'b0 && B == 1'b1) begin//AB=01,投入10分钱,YZ=10,出货不找零,跳转至S0
next_state = S0;
end
else
next_state = S0; //AB=00或者11保持S0不跳转
end
S1:begin
if (A == 1'b0 && B == 1'b0) //AB=00,不投币,保持现状S1
next_state = S1;
else if (A == 1'b1 && B == 1'b0) begin//AB=10,再投入5分钱,YZ=10,出货不找零,跳转至S0
next_state = S0;
end
else if (A == 1'b0 && B == 1'b1) begin//AB=01,再投入10分钱,YZ=11,出货找零,跳转至S0
next_state = S0;
end
else
next_state = S0; //AB=00或者11跳转S0
end
endcase
end
always @(posedge clk or negedge rst) begin
if (!rst)begin
Y <= 1'b0;
Z <= 1'b0;
end
else begin
case(current_state)
S0:begin
if (A == 1'b0 && B == 1'b1)begin //AB=01,投入10分钱,YZ=10,出货不找零
Y <= 1'b1;
Z <= 1'b0;
end
else begin
Y <= 1'b0;
Z <= 1'b0;
end
end
S1:begin
if (A == 1'b1 && B == 1'b0) begin//AB=10,再投入5分钱,YZ=10,出货不找零,跳转至S0
Y <= 1'b1;
Z <= 1'b0;
end
else if (A == 1'b0 && B == 1'b1) begin//AB=01,再投入10分钱,YZ=11,出货找零,跳转至S0
Y <= 1'b1;
Z <= 1'b1;
end
else begin
Y <= 1'b0;
Z <= 1'b0;
end
end
endcase
end
end
endmodule
4.4 验证代码
同上