问题的提法
您被说服为办公室设计一种茶点机。饮料部分由工会承保,因此仅售5卢布。机器接受1、2和5卢布的硬币。客户支付所需金额后,咖啡机将立即分配饮料并退还零钱。设计软饮料机的状态机。机器的输入是1、2和5卢布,即插入这些硬币中的哪一个。
假设对于每个时钟信号,仅插入一个硬币。机器的输出:倒苏打水,返还1卢布,返还2卢布,返还2换2卢布。一旦在机器中收集了5卢布(或更多),它将设置信号“ POUR GASING”,以及返回相应变化的信号。然后,机器必须准备好再次接受硬币。
理论
有限状态机或有限状态机(FSM)属于代表大多数数字电路的同步时序电路。这就是您应该如何实施项目的方法(至少在最初是这样)。该方法提供了电路的可重复性和验证,并且独立于各种电路元件的延迟关系。构造同步时序电路的规则规定,如果电路的元素满足以下条件,则该电路为同步时序电路:
- 每个电路元件可以是寄存器或组合电路。
- 至少一个模式元素是一个寄存器。
- 所有寄存器均由单个时钟信号提供时钟。
- 每个循环路径至少包含一个寄存器。
状态机具有几种状态,并将其存储在寄存器中。当时钟信号到达时,状态机可以更改其状态,并且状态将如何准确更改取决于输入信号和当前状态。在最简单的情况下,可能根本没有任何输入信号,因此分频器可以工作。有限状态机主要有两类:Moore自动机,其中输出信号仅取决于自动机的当前状态;和Mealy自动机,其中输出信号取决于当前状态和输入信号。原则上,任何有限状态机都可以根据Moore方案和Miley方案来实现,它们之间的区别在于Moore自动机将具有更多状态,并且将比Mily自动机落后一个时钟。对于纯碱机电路,我将使用Miles电路。让我们说明一下状态机:
符号 | 描述 |
---|---|
S 0 | 初始状态下,累计金额为0卢布。 |
小号1 | 累计金额为1卢布。 |
小号2 | 累计2卢布。 |
小号3 | 累计3卢布。 |
小号4 | 累计4卢布。 |
输入信号将是一个两位总线,并带有以下硬币面额编码:
符号 | 值 | 描述 |
---|---|---|
我1 | 01 | 1擦 |
我2 | 十 | 2卢布 |
我5 | 十一 | 5卢布 |
让我们绘制一个自动机的状态图(在Mealy自动机的状态图上,有必要在状态转换箭头上指示输出信号,我不会这样做,以免弄乱该图,所有输出信号将在下表中描述):
让我们写下状态和输出信号的变化表:
状态 | 输入信号 | |||||
---|---|---|---|---|---|---|
S | S' | insert | pour_water | C 1 . change1 | 2 . change2 | 2 2 . change22 |
S0 | S1 | I1 | 0 | 0 | 0 | 0 |
S0 | S2 | I2 | 0 | 0 | 0 | 0 |
S0 | S0 | I5 | 1 | 0 | 0 | 0 |
S1 | S2 | I1 | 0 | 0 | 0 | 0 |
S1 | S3 | I2 | 0 | 0 | 0 | 0 |
S1 | S0 | I5 | 1 | 1 | 0 | 0 |
S2 | S3 | I1 | 0 | 0 | 0 | 0 |
S2 | S4 | I2 | 0 | 0 | 0 | 0 |
S2 | S0 | I5 | 1 | 0 | 1 | 0 |
S3 | S4 | I1 | 0 | 0 | 0 | 0 |
S3 | S0 | I2 | 1 | 0 | 0 | 0 |
S3 | S0 | I5 | 1 | 1 | 1 | 0 |
S4 | S0 | I1 | 1 | 0 | 0 | 0 |
S4 | S0 | I2 | 1 | 1 | 0 | 0 |
S4 | S0 | I5 | 1 | 0 | 0 | 1 |
Quartus Prime
Quartus有一个免费的精简版,与专业版相比有一些限制,主要限制是用于项目仿真的源代码不超过10,000行。下载它,注册后,您可以单击链接,在编写最新版本为19.1的时候,我根据使用该版本的知识撰写了一篇文章。我们选择精简版19.1版Windows操作系统(应注意的是,有一个Quartus for Linux版本,它运行良好,ModelSim出现了问题,该版本为32位,并使用了旧版本的字体显示库,因此首先建议使用Windows版本),选择“合并文件”标签。下载的存档大小非常大-5.6 Gb,请记住这一点。我们扩展下载的档案并运行setup.bat。安装以标准方式进行,我们使用默认的组件选择。
项目创建
要创建一个新项目,请选择File-> New ... by Project Wizard。第一个向导窗口是信息性窗口,单击“ 下一步”,在第二个窗口中,选择项目所在的位置,其名称为“ soda_machine”,以及顶层设计元素“ soda_machine”,如图所示:
在下一个窗口中,选择“ Empty project”。用于添加文件的窗口“添加文件”,不添加任何内容。选择“ Family,Devices&Board Settings”设备的窗口对于真实项目非常重要,但是由于我们的项目远非真实,因此在此处保留默认设置,如图所示:
选择其他工具设置的窗口在“ EDA工具设置”中,选择要模拟的项目以使用“ ModelSim-Altera”和“系统Verilog HDL”格式,如图所示:
最后一个信息窗口“摘要”,单击“ 完成”。
编写源代码
我们将有两个带有源代码的主文件,这是soda_machine模块本身及其测试台,这两个文件都将使用insert_type数据类型,该数据类型描述了我们如何对硬币的面额进行编码,并且将其分离为一个单独的文件是合乎逻辑的。但是,Quartus和ModelSim的编译功能存在一些困难。 Quartus一次编译所有源文件,而ModelSim则分别编译每个文件,以编译Quartus'om产生的覆盖类型insert_type,我基于宏处理器的指令使用了C / C ++ include guard的技术。此外,为的ModelSim可以肯定的是,insert_type中使用soda_machine模块然后在测试平台中,将其描述放入soda_machine_types包中。鉴于这些要求,soda_machine_types.sv文件如下:
soda_machine_types.sv
`ifndef soda_machine_types_sv_quard
package soda_machine_types;
typedef enum logic [1:0] {I1=2'b01, I2=2'b10, I5=2'b11} insert_type;
endpackage
`define soda_machine_types_sv_quard
`endif
现在,soda_machine模块本身位于soda_machine.sv文件中:
苏打水机
`include "soda_machine_types.sv"
import soda_machine_types::*;
module soda_machine(
input logic clk, // Clock
input logic reset, // Active high level
input insert_type insert,
output logic pour_water,
output logic change1,
output logic change2,
output logic change22);
typedef enum logic [2:0] {S0, S1, S2, S3, S4} state_type;
(* syn_encoding = "default" *) state_type state, nextstate;
//
always_ff @(posedge clk, posedge reset)
if (reset)
state <= S0;
else
state <= nextstate;
//
always_comb
case (state)
S0:
case (insert)
I1:
nextstate = S1;
I2:
nextstate = S2;
I5:
nextstate = S0;
endcase
S1:
case (insert)
I1:
nextstate = S2;
I2:
nextstate = S3;
I5:
nextstate = S0;
endcase
S2:
case (insert)
I1:
nextstate = S3;
I2:
nextstate = S4;
I5:
nextstate = S0;
endcase
S3:
if (insert == I1)
nextstate = S4;
else
nextstate = S0;
S4:
nextstate = S0;
endcase
//
assign pour_water = (state == S4) | (insert == I5) | (state == S3) & (insert == I2);
assign change1 = (state == S1) & (insert == I5) | (state == S3) & (insert == I5) | (state == S4) & (insert == I2);
assign change2 = (state == S2) & (insert == I5) | (state == S3) & (insert == I5);
assign change22 = (state == S4) & (insert == I5);
endmodule
如何对状态机状态进行编码,我留给了Quartus处理。为了指示应该如何精确地进行编码,使用了属性(* syn_encoding =“ default” *),在这里可以看到其他编码选项。
应该注意的是,在实际项目中,Miles机器的组合逻辑的输出信号必须存储在寄存器中,并从寄存器的输出馈送到FPGA输出。必须使用同步器将输入信号与时钟频率同步,以避免陷入亚稳态。
要将文件添加到项目,请使用文件->新建“ SystemVerilog HDL文件”并在保存时提供适当的名称。添加这两个文件后,可以对项目进行编译处理->开始编译。成功编译后,您可以看到生成的方案Tools-> Netlist Viewers-> RTL Viewer:
RTL查看器
查看状态机statechart 工具->网表查看器->状态机查看器
状态机查看器
在“编码”选项卡上,您可以看到Quartus应用了“ one-hot”编码方案,这是当每个状态使用单独的D触发器,并且S 0状态被编码为0,而不是其他状态被编码为1时,这样做是为了将复位电路简化为初始状态。州。您可能会注意到RTL Viewer并没有完全显示原理图,而是一个概念。要查看原理图,请使用工具->网表查看器->技术地图查看器(调试后)
模拟
基本上,目前我们有一个用于汽水机的电路,但是您需要确保其正常工作,为此,我们编写了一个测试台并将其放置在soda_machine_tb.sv文件中:
soda_machine_tb.sv
`include "soda_machine_types.sv"
import soda_machine_types::*;
module soda_machine_tb;
insert_type insert;
logic [5:0] testvectors[10000:0];
int vectornum, errors;
logic clk, reset, pour_water, change1, change2, change22;
logic pour_water_expected, change1_expected, change2_expected, change22_expected;
//
soda_machine dut(
.clk(clk),
.reset(reset),
.insert(insert),
.pour_water(pour_water),
.change1(change1),
.change2(change2),
.change22(change22)
);
//
always
#5 clk = ~clk;
//
initial begin
//
$readmemb("../../soda_machine.tv", testvectors);
vectornum = 0;
errors = 0;
clk = 1;
//
reset = 1; #13; reset = 0;
end
//
always @(posedge clk) begin
#1; {insert, pour_water_expected, change1_expected, change2_expected, change22_expected} = testvectors[vectornum];
end
// ,
always @(negedge clk)
if (~reset) begin
if ((pour_water !== pour_water_expected) || (change1 !== change1_expected) || (change2 !== change2_expected) ||
(change22 !== change22_expected)) begin
$error("%3d test insert=%b\noutputs pour_water=%b (%b expected), change1=%b (%b expected), change2=%b (%b expected), change22=%b (%b expected)",
vectornum + 1, insert, pour_water, pour_water_expected, change1, change1_expected, change2, change2_expected, change22, change22_expected);
errors = errors + 1;
end
vectornum = vectornum + 1;
if (testvectors[vectornum] === 6'bx) begin
$display("Result: %3d tests completed with %3d errors", vectornum, errors);
$stop;
end
end
endmodule
为了测试我们的模块,使用了测试向量文件soda_machine.tv:
苏打水机
01_0_0_0_0
01_0_0_0_0
01_0_0_0_0
01_0_0_0_0
01_1_0_0_0
10_0_0_0_0
10_0_0_0_0
10_1_1_0_0
11_1_0_0_0
10_0_0_0_0
10_0_0_0_0
11_1_0_0_1
10_0_0_0_0
11_1_0_1_0
01_0_0_0_0
01_0_0_0_0
01_0_0_0_0
11_1_1_1_0
前两位是插入输入,后四位是我们等待输出的内容:pour_water,change1,change2,change22。例如,在文件开始处,连续五次插入卢布硬币,在第五个硬币上,我们等待pour_water信号出现,而更改信号无效。将soda_machine.tv文件添加到项目文件->新建“文本文件”中。
为了方便使用ModelSim,请添加具有以下内容的soda_machine_run_simulation.do文件:
soda_machine_run_simulation.do
add wave /soda_machine_tb/dut/clk
add wave /soda_machine_tb/dut/reset
add wave /soda_machine_tb/dut/insert
add wave /soda_machine_tb/dut/state
add wave /soda_machine_tb/dut/nextstate
add wave /soda_machine_tb/dut/pour_water
add wave /soda_machine_tb/dut/change1
add wave /soda_machine_tb/dut/change2
add wave /soda_machine_tb/dut/change22
view structure
view signals
run -all
wave zoom full
它将运行我们的仿真并在ModelSim中显示波形。将soda_machine_run_simulation.do文件添加到项目文件->新的“ Tcl脚本文件”中。
现在,我们将设置项目,以便自动开始模拟。选择菜单项分配->设置,选择类别EDA工具设置->模拟。在NativeLink设置中,选择“ 编译测试平台:”,然后单击“ 测试平台”按钮...在打开的“测试平台”窗口中,单击“ 新建” ...在打开的“ 新测试平台设置”窗口中,填写“ 测试平台名称”字段:soda_machine_tb并点击窗口底部的文件选择按钮...,选择我们的soda_machine_tb.sv文件,然后点击添加按钮。它看起来应该像这样:
在“ 新建测试平台设置”窗口中,单击“ 确定”。 “ 测试基准”窗口应如下所示:
在“ 测试基准”窗口中,单击“ 确定”。在NativeLink设置中,选中“ 使用脚本来设置仿真” 复选框,然后选择soda_machine_run_simulation.do文件。 “ 设置”窗口
应如下所示:
在“ 设置”窗口中,单击好的,我们编译项目Processing-> Start Compilation,运行仿真工具-> Run Simulation Tool-> RTL Simulation。ModelSim应该启动,项目将进行仿真。文字标签的外观:
ModelSim成绩单选项卡
测试台输出的已执行测试次数和检测到的错误均以红色突出显示。波形标签的外观:
ModelSim Wave选项卡
项目源代码
该项目的源代码位于github.com/igoral5/soda_machine克隆该项目,然后使用Quartus File-> Open Project ...
选择soda_machine.qpf文件来打开该项目。然后编译项目Processing-> Start Compilation并运行仿真工具-> Run Simulation Tool-> RTL Simulation。