实验九 有限状态机
9.1 实验目的
-
学习有限状态机的组成与类型;
-
掌握有限状态机的设计方式;
-
学习有限状态机的编码方式;
-
掌握使用有限状态机进行设计的方法。
9.2 原理介绍
9.2.1 有限状态机的基本概念
有限状态机(Finite State Machine,FSM)通常又称为状态机,是时序逻辑电路设计中经常采用的一种方式,也是数字系统设计的重要组成部分,尤其适用于设计数字系统的控制模块。在一些需要控制高速器件的场合,用状态机进行设计是解决问题的一种很好的方案,具有速度快、结构简单、可靠性高等优点。同时有限状态机也是时序电路的通用模型,任何时序电路都可以表示为有限状态机。
状态机的组成
有限状态机一般包括寄存器逻辑和组合逻辑两部分,寄存器用于存储状态,组合逻辑用于状态译码和产生输出信号,其电路的结构如图9.1所示。寄存器(存储电路)部分接收组合逻辑电路的内部输出信号,由时钟信号控制,存储的现态在时钟作用下变为次态。组合逻辑电路接收的是输入信号和存储电路的现态,在现态和输入信号的共同作用下产生次态和输出信号。
图9.1 有限状态机结构框图
状态机的分类
根据输出信号产生方式的不同,有限状态机可以分为米利型(Mealy)和摩尔型(Moore)两类。Mealy型状态机的输出不仅取决于电路当前状态,还取决于电路的输入信号,其输出是在输入变化后立即变化的,不依赖时钟信号的同步,其结构图如图9.2所示;Moore型状态机的输出仅依赖当前状态,而与输入信号无关,但输出发生变化时还需等待时钟的到来,必须等待状态发生变化时才能导致输出变化,因此比Mealy型要多等待一个时钟周期,其结构图如图9.3所示。
图9.2 Mealy型状态机结构图
图9.3 Moore型状态机结构图
状态机的状态图表示法
一般来说,状态机有三种表示方法:状态图、状态表和算法状态机图。实际上,这三种表示方法是等价的,相互之间可以进行任意转换。在用Verilog HDL描述状态机时,通常会用到状态图,下面介绍状态图的表示方法。
状态图是以信号流图方式表示出电路的状态转换过程。在状态图中,每个状态用一个圆圈(或者椭圆圈)表示,圆圈内有指示状态的符号。用带箭头的方向线指示状态转换的方向,当方向的起点和终点都在同一个圆圈上,则表示状态不变。标在方向线旁斜线左、右两侧的二进制数分别表示引起状态转移的输入信号以及当前输出信号。
图9.4是Mealy状态图的一个例子,其中A、B、C、D表示电路四个不同的状态,方向线旁边的X/Y表示引起状态转移的输入信号以及当前输出信号。一般来说,状态机中的状态转移有两种方式:无条件转移和有条件转移。在图9.4中,从状态A转移到状态B为无条件转移,其他状态之间的转移都是有条件要求的,例如,如果状态机的当前状态(现态)为B,当输入X=1时,状态机将从状态B转移到状态A;当X=0时,状态机将从状态B转移到状态C。引起状态发生改变的输入条件通常标在方向线的旁边,电路的输出结果也写在方向线的旁边,用斜线对输入和输出进行分隔,输入放在斜线左边,输出放在斜线右边。
需要强调的是,在Mealy状态图中,输出信号的表示方法容易引起读者的误解。当状态机处于所在的状态,并且在所示输入的作用下,就会产生输出,并非在状态机转移到下一状态时才会出现输出。例如,图9.4中,当状态机处于状态C时,输出Y只依赖于当前状态C和输入X,若X=1,则Y=0;若X=0,则Y=1。可见,输出信号Y是在状态转移之前产生的,与次态无关。
图9.4 Mealy状态图
由于Moore状态机的输出只依赖于状态机的当前状态,它的状态图与Mealy状态图略有不同,通常将输出写在圆圈的内部,即将输出和状态标注在一起,二者以斜线隔开,所以Moore状态图的圆圈内的内容为“状态/输出”。图9.5是Moore状态图的一个例子。若当前状态为C,那么当输入X=0时,下一状态转换为状态D;当输入X=1时,下一状态转换为状态B。
图9.5 Moore状态图
状态机的设计步骤
一般来说,状态机的设计步骤如下所示:
-
根据具体的设计原则,确定采用Moore状态机还是Mealy状态机。
-
分析设计要求,列出状态机的所有状态,并对每一个状态进行状态编码。
-
根据状态转移关系和输出函数,画出状态图。
-
根据所画的状态图,采用硬件描述语言对状态机进行描述。
在上面的设计步骤中,第3步是最困难也是最有创造性的一步。对同一个设计问题来说,不同的人可能会构造出不同的状态图。状态图直观地反映了状态机各个状态之间的转换关系以及转换条件,因而有利于理解状态机的工作机理,但此时要求设计的状态个数不能太多。对于状态个数较多的状态机,一般采用状态表的方法列出状态机的转移条件。如果输出信号较多,可以采用输出逻辑真值表进行表示。
9.2.2 有限状态机的Verilog HDL描述
下面通过一个例子来介绍状态机的设计过程。
【例子】
设计一个序列检测器电路,当检测到输入信号s出现二进制序列1010(自左至右的顺序)时,电路输出z=1,否则z=0。注意要考虑序列重叠的问题,如101010相当于出现两个1010序列。
【设计过程】
-
根据9.1小节介绍的状态机的设计步骤,首先要确定采用Mealy状态机还是Moore状态机。因为该电路在连续收到1010序列时,输出为1,否则输出0,即输出与输入信号有关,所以我们采用Mealy状态机。
-
第二步是列出状态机的所有状态,并进行状态编码。我们来分析电路的状态,根据设计要求,该电路必须能记忆收到的输入数据1、连续收到前两个数据10、连续收到前三个数据101、连续收到1010后的状态,可见该电路至少应该有四个状态,我们分别用S_1、S_2、S_3、S_4表示,若电路的初始状态用S_0表示,那么该电路有五个状态。
-
然后我们分析电路的状态转移关系,进而画出状态图。我们从第一个状态S_0开始分析,即从初始状态开始跳转。在分析时,我们也考虑到了序列重叠的可能性,例如101010相当于出现了两次1010序列。
① 开始时,电路处于S_0状态,此时会有两种情况,一种情况是收到第一个数据s=0时,电路仍旧处于S_0状态;另一种情况是当收到第一个有效数据s=1时,电路输出z=0,并且进入S_1状态,如图9.6所示。
图9.6 序列检测器状态跳转1
② 然后我们分析S_1状态的跳转情况,在S_1状态下同样有两种情况,一种情况是电路收到第二个有效数据s=0,即电路连续收到序列10,则电路进入S_2状态,输出z=0;另一种情况是电路收到第二个数据s=1时,即电路连续收到序列11,考虑序列重叠的情况,电路仍旧处于S_1状态,如图9.7所示。
图9.7 序列检测器状态跳转2
③ 接着我们分析S_2状态的跳转情况,在S_2状态下同样有两种情况,如果第三个数据s=0,即电路连续接收到序列100,那么电路的输出z=0,并且电路应该返回到初始状态S_0,重新开始检测;而如果第三个数据s=1,即电路连续接收到序列101,则电路转换到S_3状态,z=0,如图9.8所示。
图9.8 序列检测器状态跳转3
④ 下面我们该分析S_3状态的跳转情况了,在S_3状态下同样有两种情况,若s=0,那么电路已经连续收到四个有效数据1010,电路应输出z=1,此时若时钟信号的有效沿到来,则电路应转向S_4状态;而若s=1,即电路连续接收到序列1011,考虑序列重叠的情况,相当于电路连续接收到序列1,所以电路输出z=0,并且电路应该转入S_1状态,如图9.9所示。
图9.9 序列检测器状态跳转4
⑤ 最后我们看S_4状态的跳转,若s=0,电路连续收到序列10100,相当于电路检测完成一次序列1010后,又检测到0信号输入,即未检测到序列所需的有效数据,因此返回初始状态S_0,重新检测,并且z=0;而当s=1时,电路连续收到序列10101,考虑序列重叠的情况,相当于电路连续接收到序列101,所以电路应进入S_3状态,并且z=0,此时的状态跳转如图9.10所示。
图9.10 序列检测器状态跳转5
⑥ 至此,我们已经完成了序列检测器的状态跳转分析,图9.10为序列检测器的状态图。通过该图可以看出,当状态机处于S_2、S_4时,如果输入s=0,电路会转移到相同的次态S_0,并且输出z都为0;如果s=1,电路会转移到相同的次态S_3,输出z都为0。所以S_2、S_4为等价的状态,可以用S_2状态代替S_4,于是我们可以得到序列检测器的简化状态图,如图9.11所示。
实际上,可以直接用图9.10进行电路设计,不过此时会多用一个触发器。如果用内部触发器资源较多的FPGA器件实现该状态机,未简化的状态图是完全可以的。
图9.11 序列检测器简化状态图
-
状态图完成后,就可以使用硬件描述语言对状态图进行描述了。
利用Verilog HDL语言描述状态图主要包含四部分内容:
① 利用参数定义语句 parameter 描述状态机中各个状态的名称,并指定状态编码。例如,对序列检测器的状态分配可以使用最简单的自然二进制码,其描述如下:
parameter S0 = 2'b00, S1 = 2'b01, S2 = 2'b10, S3 = 2'b11;
② 用 always 块描述状态触发器,实现状态存储;
③ 使用 case 语句(也可以使用 if-else 语句)描述状态转移逻辑;
④ 描述状态机的输出逻辑。
常见的描述状态图的方法有三种:一段式状态机、两段式状态机、三段式状态机。接下来我们将分别使用这三种状态机来实现刚刚介绍的序列检测器。
一段式状态机
将整个状态机写到一个always模块内部,在该模块中既描述时钟控制的状态转移,又描述状态机的下一状态和输出,这种写法被称为一段式状态机。这种写法仅仅适用于非常简单的状态机设计,不符合将时序逻辑和组合逻辑分开描述的代码风格,而且在描述当前状态时还要考虑下一个状态的逻辑,整个代码的结构不清晰,不利于修改和维护,不利于时序约束条件的加入,也不利于综合器对设计的优化,所以我们不推荐这种写法。
上述检测1010序列的一段式状态机代码如下:
module detector_1(input clk,input rst_n,input s,output reg z );// 状态声明及编码 parameter S0 = 2'b00,S1 = 2'b01,S2 = 2'b10,S3 = 2'b11;// 变量声明 reg [1:0] state;always@(posedge clk, negedge rst_n) beginif(~rst_n)beginz <= 1'b0; // 异步复位,输出为0state <= S0; // 异步复位,状态跳变为S0endelsecase(state) // 状态跳转及输出S0:beginz = 1'b0;state = (s == 1) ? S1 : S0;endS1:beginz = 1'b0;state = (s == 0) ? S2 : S1;endS2:beginz = 1'b0;state = (s == 1) ? S3 : S0;endS3:beginif(s == 1'b0)beginz = 1'b1;state = S2;endelsebeginz = 1'b0;state = S1;end endendcase endendmodule
代码9.1 序列检测器一段式状态机
我们写的状态机综合器是可以识别出来的,点击Quartus Prime软件菜单栏的Tools --> Netlist Viewers --> State Machine Viewer,就会弹出如图9.12所示的状态图。另外,当我们打开RTL Viewer的时候看到的如图9.13所示的黄色块就是综合器自动识别出的状态机,双击进去也可以查看状态图,打开后显示的状态图与图9.12所示的状态图是一致的。
图9.12 代码9.1的State Machine Viewer
图9.13 代码9.1的RTL Viewer
我们再来看这段代码(代码9.1),严格地说,对序列检测器电路用单个always块进行描述存在着一个隐含的错误,即输出信号z的描述存在错误。本来信号z是由状态机的当前状态和输入信号共同决定的(该序列检测器是Mealy状态机),它是一个组合逻辑(具体可见图9.2),如果状态机的当前状态不变,而输入信号变了,信号z应该立即变化,但是按照上面的描述(代码9.1),信号z只有等到时钟上升沿到来时才会变化。在实际应用中,为了消除组合逻辑输出信号中的毛刺,在时序允许的情况下,通常允许Mealy状态机中输出信号通过寄存器输出。
总之,一段式状态机的写法仅仅适用于Moore状态机,它的电路结构可以用图9.14所示框图进行概述。
图9.14 单个always块描述的FSM的结构图
两段式状态机
所谓两段式状态机就是采用两个always模块来实现状态机的功能。其中一个always模块采用同步时序逻辑描述状态转移,而另一个always模块采用组合逻辑来判断状态转移条件,描述状态转移规律和电路的输出。两段式状态机是值得推荐的写法之一,它的电路结构可以用图9.15所示的框图进行概括,每个方框用一个always块描述。
图9.15 两个always块描述的FSM的结构图
上述检测1010序列的两段式状态机代码如下:
module detector_2(input clk,input rst_n,input s,output reg z );// 状态声明及编码 parameter S0 = 2'b00,S1 = 2'b01,S2 = 2'b10,S3 = 2'b11;// 变量声明 reg [1:0] cs, ns;// 时序逻辑,描述状态转移 always@(posedge clk, negedge rst_n) beginif(~rst_n)cs <= S0;elsecs <= ns; end// 组合逻辑,描述下一状态和输出 always@(cs, s) beginns = 2'bxx;z = 1'b0;case(cs)S0:beginz = 1'b0;ns = (s == 1) ? S1 : S0;endS1:beginz = 1'b0;ns = (s == 0) ? S2 : S1;endS2:beginz = 1'b0;ns = (s == 1) ? S3 : S0;endS3:beginif(s == 1'b0)beginz = 1'b1;ns = S2;endelsebeginz = 1'b0;ns = S1;endendendcase endendmodule
代码9.2 序列检测器两段式状态机
上述写法通过两个并行执行的always结构描述电路的功能,通过公共变量进行相互通信。第一个时序型always块采用非阻塞赋值,使用边沿触发事件描述了状态机的触发器部分;第二个组合逻辑性always块采用阻塞赋值,使用电平敏感事件描述了状态机下一个状态逻辑和输出逻辑部分。
第一个always块说明了异步复位到初始状态S_0和同步时钟完成的操作,语句 cs <= ns 仅在时钟的上升沿被执行,这意味着第二个always块内部 ns 的值变化会在时钟上升沿到来时被传送给 cs。
第二个always块把现态 cs 和输入数据 s 作为敏感变量,只要其中的任何一个变量发生变化,就会执行顺序语句块内部的case语句,跟在case语句后面的各分支项说明了图9.11中状态的转换以及输出信号。
使用Quartus Prime综合后,代码9.2的状态机如图9.16所示,该状态图与代码9.1的状态图(图9.12)一致。另外,我们也可以查看其RTL Viewer,如图9.17所示,与代码9.1的RTL Viewer(图9.13)相比,其输出z未通过寄存器输出,可能会有毛刺出现。
图9.16 代码9.2的State Machine Viewer
图9.17 代码9.2的RTL Viewer
另外一个值得注意的是,在第二个always块敏感列表下面一行应该写出下一状态 ns 的默认赋值,然后根据当前的状态和当前的输入由后面的case或if-else语句确定正确的转移。
... beginns = 2'bxx;z = 1'b0;case(cs)... end
代码9.3 两段式状态机中第二个always块中ns的默认值
对下一状态ns的默认赋值有三种方式:全部设置成不定状态(x)、设置成预先规定的初始状态、设置成FSM中的某一有效状态,推荐将敏感列表后面的默认状态设置成不定状态(x),它的优点有两点,一是在仿真时可以很好地考察所涉及的FSM的完备性,若设计的FSM不完备,则会进入任意状态,仿真时容易发现;二是综合器对代码进行逻辑综合时,会忽略没有定义的状态触发器向量。
三段式状态机
使用三个always块描述状态机的功能,这种写法被称为三段式状态机,它的结构图如图9.18所示。第一个always模块采用同步时序逻辑方式描述状态转移(中间方框),第二个always模块采用组合逻辑方式描述状态转移规律(第一个方框),第三个always模块描述电路的输出信号(第三个方框),并且在时序允许的情况下,通常让输出信号经过一个寄存器再输出,保证输出信号没有毛刺。
图9.18 三个always块描述的FSM的结构图
上述检测1010序列的三段式状态机代码如下:
module detector_3(input clk,input rst_n,input s,output reg z );// 状态声明及编码 parameter S0 = 2'b00,S1 = 2'b01,S2 = 2'b10,S3 = 2'b11;// 变量声明 reg [1:0] cs, ns;// 时序逻辑,描述状态转移 always@(posedge clk, negedge rst_n) beginif(~rst_n)cs <= S0;elsecs <= ns; end// 组合逻辑,描述下一状态 always@(cs, s) beginns = 2'bxx;case(cs)S0:ns = (s == 1) ? S1 : S0;S1:ns = (s == 0) ? S2 : S1;S2:ns = (s == 1) ? S3 : S0;S3:ns = (s == 0) ? S2 : S1;endcase end// 输出逻辑,让输出信号经过一个寄存器再送出,可以消除z信号中的毛刺 always@(posedge clk, negedge rst_n) beginif(~rst_n)z = 1'b0;elsebeginz = 1'b0;case(cs)S0:z = 1'b0;S1:z = 1'b0;S2:z = 1'b0;S3:z = (s == 1'b0) ? 1'b1 : 1'b0;endcaseend endendmodule
代码9.4 序列检测器三段式状态机
使用Quartus Prime综合后,代码9.4的状态机如图9.19所示,该状态图与代码9.1、9.2的状态图(图9.12、图9.16)一致。另外,我们也可以查看其RTL Viewer,如图9.20所示,可以看出其输出z通过寄存器输出,可以克服输出逻辑出现毛刺的问题,这在一些将输出信号作为控制逻辑的场合使用,可有效避免产生错误控制动作的可能性。
图9.19 代码9.4的State Machine Viewer
图9.20 代码9.4的RTL Viewer
9.2.3 有限状态机的状态编码
在状态机设计中,有一个重要的问题就是状态的编码,通常用 parameter 指定状态编码。状态机编码方式很多,由此产生的电路也不相同,常用的编码方式有三种:二进制编码、格雷码和独热(One-Hot)编码。表9.1给出了对六个状态进行编码的三种不同编码方式。
表9.1 三种编码方式的对比
状态 | 二进制编码 | 格雷编码 | 独热码 |
---|---|---|---|
state0 | 000 | 000 | 000001 |
state1 | 001 | 001 | 000010 |
state2 | 010 | 011 | 000100 |
state3 | 011 | 010 | 001000 |
state4 | 100 | 110 | 010000 |
state5 | 101 | 111 | 100000 |
二进制编码采用顺序的二进制数编码的每个状态。一个有 N 种状态的状态机至少需要 log_2{N} 个触发器来存储状态编码。例如,有8种状态的机器将至少需要3个触发器,这种方式使用的触发器最少。二进制编码的缺点是从一个状态转换到相邻状态时,有可能有多个比特位同时发生变化,如表9.1中,从state3转换到state4(从011变为100)时,三个比特位都需要同时发生变化,瞬变次数多,容易产生毛刺,引发逻辑错误。
格雷码可用相同的位数来实现编码,其特点是两个相邻码值之前仅有1位不同,用它来编码状态可减少瞬变的次数,也减少了产生毛刺和一些暂态的可能。
独热(One-Hot)编码使用n位状态触发器表示具有n个状态的状态机,每个状态与一个独立的触发器相对应,并且在任何时刻其中只有一个触发器有效(其值为1)。独热编码是一种流行的编码方式,虽然这种编码方案会使用较多的触发器,但这使得独热码状态机中的译码逻辑使用较少的门,因为它只是对寄存器中的一位进行译码,而不是一个矢量,所以独热码状态机可以有更快的速度,并且由于增加触发器而占用的面积可用简单的译码电路省下来的面积抵消。另外,修改独热码设计也非常容易,因为增加或去除一个状态不会影响到其余状态的编码和电路的性能。因此,推荐使用独热编码方式。
在设计过程中,可通过综合器指定编码方式,如在Quartus Prime软件中,选择菜单Assignments --> Settings,在Settings页面的Category栏中选Compiler Settings选项,单击Advanced Settings(Synthesis)...按钮,在出现的对话框的State Machine Processing栏中选择需要的编码方式,可选的编码方式有Auto、Gray、Johnson、Minimal Bits、One-Hot、Sequential、User-Encoded等几种,如图9.21所示,可以根据需要选择合适的编码方式。
图9.21 在Quartus Prime中选择编码方式
9.3 实验目标
-
使用Verilog HDL设计有限状态机,作为实验八实现的计算器的控制器;
9.4 设计实现
9.4.1 设计思路
系统框图
回顾实验8的系统框图:
图9.23 实验8计算器系统框图
图9.23中只画出了计算器的数据通路部分,而计算器的控制器是使用有限状态机来实现,图9.24是使用有限状态机作为控制器的计算器的系统框图。该计算器的输入和输出端口如下所述:
-
SW3 ~ SW0:计算器的数据输入;
-
*SW16:选择进行加法运算还是减法运算;
-
KEY2:用于使能数字钟,按下按键KEY2,数字钟停止计时,释放KEY2后,数字钟继续计时;
-
FPGA_CLK1_50:50MHz时钟,为寄存器提供时钟信号;
-
KEY0:系统复位信号;
-
KEY1:寄存器使能信号;
-
SW9 ~ SW7:功能选择信号;
-
LEDR7 ~ LEDR0:显示计算器的计算结果,以二进制形式显示在红色LED上;
-
HEX5 HEX0:显示计算器数字钟计时结果,HEX1 ~ HEX0显示计时结果的厘秒,HEX3 ~ HEX2显示计时结果的秒,HEX5 ~ HEX4显示计时结果的分钟。
图9.24 计算器系统框图
该计算器的功能如下:
-
拨动滑动开关SW9 ~ SW7,选择要实现的运算;
① *SW9 ~ SW7位置为"down、down、down",计算器实现与运算;
② SW9 ~ SW7位置为"down、down、up",计算器实现或运算;
③ SW9 ~ SW7位置为"down、up、down",计算器实现异或运算;
④ SW9 ~ SW7位置为"down、up、up",计算器实现非(位取反)运算;
⑤ SW9 ~ SW7位置为"up、down、down",计算器实现加/减运算;
⑥ SW9 ~ SW7位置为"up、down、up",计算器实现乘法运算;
⑦ SW9 ~ SW7位置为"up、up、down",计算器实现除2运算;
-
拨动滑动开关SW3 ~ SW0,输入第一个操作数;
-
按一次KEY1并释放;
-
对于 F = ~ A(位取反) 和 F = A / 2(除2) 运算,运算结果显示到LEDR7 ~ LEDR0;
-
对于其他运算,拨动滑动开关SW3 ~ SW0,输入第二个操作数;
-
按一次KEY1并释放,其他运算结果显示到LEDR7 ~ LEDR0上;
-
运算结果显示到LEDR7 ~ LEDR0上后,重新回到步骤1等待下一次运算;
-
数字钟实现计数功能,并将计数结果显示到HEX5 ~ HEX0上,按下KEY2不松开,数字钟停止计时;释放KEY2后,数字钟从它上次停止的时刻继续计时。
计算器控制器(状态机)的实现
计算器的数据通路我们已在前面的实验中实现,在这个实验中,我们重点来看怎样用有限状态机来实现计算器的控制器。从图9.24可以看出,该控制器的输入信号有时钟clk、复位rst_n、寄存器使能en和功能选择sel[2:0],输出信号有输入寄存器的使能信号en1、结果寄存器的使能信号en2和计算器所进行的运算选择信号ope[2:0]。根据计算器的功能,我们来分析控制器(状态机)的功能:
-
在初始状态S_0下,计算器不进行运算,状态机的输出信号en1=0、en2=0;
-
当输入信号en=1(按键KEY1被按下),状态机转向下一状态S_1,此时状态机的输出信号en1=1、en2=0;该状态会一直保持,直到输入信号en=0(按键KEY1被释放)后,会跳转到下一状态S_2;
-
在状态S_2下,根据输入信号sel[2:0],跳转到下一状态(S_3 ~ S_9);
-
对于 F = ~ A 和 F = A / 2 运算(仅需一个操作数),结果显示到LEDR7 ~ LEDR0上,故结果寄存器的使能信号en2=1,并且状态机跳转回初始状态S_0;
-
对于其他运算,需要等待第二个操作数的输入;
-
当输入信号en第二次等于1(按键KEY1第二次被按下)时,状态机跳转到下一状态S_{10},在该状态下,结果寄存器的使能信号en2=1,并且状态机跳转回初始状态S_0。
然后我们来进行状态机的设计,根据9.2.1小节介绍的状态机的设计步骤,该状态机的设计过程如下:
-
首先要确定采用Mealy状态机还是Moore状态机。从上述状态机的功能可以知道,输入信号en与两个输出信号en1、en2密切相关,故我们采用Mealy状态机;
-
第二步是列出状态机的所有状态,并进行状态编码。根据上述状态机的功能分析可知,该状态机共有十一个状态,我们分别使用 S_0 ~ S_{10}来表示。并且,由于我们使用的DE-Cloud开发板的FPGA资源丰富,我们采用独热编码方式,其Verilog HDL描述如下:
// 状态声明及编码,使能独热码 parameter S0 = 11'b000_0000_0001,S1 = 11'b000_0000_0010,S2 = 11'b000_0000_0100,S3 = 11'b000_0000_1000,S4 = 11'b000_0001_0000,S5 = 11'b000_0010_0000,S6 = 11'b000_0100_0000,S7 = 11'b000_1000_0000,S8 = 11'b001_0000_0000,S9 = 11'b010_0000_0000,S10 = 11'b100_0000_0000;
-
第三步是分析电路的状态转移关系,进而画出状态图。
① 开始时,电路处于初始状态S_0,此时会有两种情况,一种情况是输入信号en=0(按键KEY1未被按下)时,电路仍旧处于S_0状态,输出信号en1=0、en2=0;另一种情况是输入信号en=1(按键KEY1被按下)时,电路进入状态S_1;
② 然后我们分析S_1状态的跳转情况,在S_1状态下同样有两种情况,一种情况是输入信号en=1(即按键KEY1被按下后未释放),电路仍处于S_1状态,输出信号en1=1、en2=0;另外一种情况是输入信号en=0(即按键KEY1被按下后释放),电路进入状态S_2;
③ 在S_2状态下,根据输入信号sel[2:0](功能选择信号),跳转到不同的状态(S_3 ~ S_9);
④ 在S_6和S_9状态下,输出信号en1=0、en2=1,并且会跳转回初始状态S_0,注意这次状态转移为无条件跳转;
⑤ 在S_3、S_4、S_5、S_7、S_8状态下,会有两种情况,一种情况是输入信号en=0(按键KEY1未被按下)时,电路仍旧处于当前的状态;另一种情况是输入信号en=1(按键KEY1第二次被按下)时,电路进入状态S_{10};
⑥ 在S_{10}状态下,输出信号en1=0、en2=1,并且会跳转回初始状态S_0,注意这次状态转移也是无条件跳转。
根据上述状态转移关系,我们可以画出该状态机的状态图,如图9.25所示。
图9.25 计算器的状态机的状态图
-
状态图完成后,就可以使用Verilog HDL进行描述了。为了避免毛刺的出现,我们采用三段式状态机。
① 首先是状态机端口的声明,从图9.24可以看出,该控制器的输入信号有时钟clk、复位rst_n、寄存器使能en和功能选择sel[2:0],输出信号有输入寄存器的使能信号en1、结果寄存器的使能信号en2。
module fsmctrl(input clk, // 时钟信号input rst_n, // 复位信号,低电平有效input en, // 寄存器使能信号input [2:0] sel, // 功能选择信号,选择计算器进行的运算output reg en1, // 输入寄存器的使能信号output reg en2, // 结果寄存器的使能信号output reg [2:0] ope // 计算器进行的运算选择信号 );
代码9.5 状态机端口声明
② 然后是状态编码,我们使用独热编码方式;还有就是一些变量的声明,现态cs[10:0]、次态ns[10:0]。
// 状态声明及编码,使能独热码 parameter S0 = 11'b000_0000_0001,S1 = 11'b000_0000_0010,S2 = 11'b000_0000_0100,S3 = 11'b000_0000_1000,S4 = 11'b000_0001_0000,S5 = 11'b000_0010_0000,S6 = 11'b000_0100_0000,S7 = 11'b000_1000_0000,S8 = 11'b001_0000_0000,S9 = 11'b010_0000_0000,S10 = 11'b100_0000_0000;// 变量声明 reg [10:0] cs; reg [10:0] ns;
代码9.6 状态声明及变量声明
③ 然后就是三段式状态机的主体实现部分了,第一个always模块采用同步时序逻辑方式描述状态转移,在时钟信号的作用下,ns 的值会在时钟上升沿到来时被传送给 cs。
// 第一段状态机,描述状态转换 always@(posedge clk, negedge rst_n) beginif(~rst_n)cs <= S0;elsecs <= ns; end
代码9.7 三段式状态机第一个always块
④ 第二个always模块采用组合逻辑方式描述状态转移,
// 第二段状态机,组合逻辑,描述下一状态 always@(cs, en, sel) begincase(cs)S0:beginif(en == 1'b1) // 如果en=1,跳转到S1ns = S1; // 否则仍处于S0elsens = S0;endS1:beginif(en == 1'b0) // 如果en=0,跳转到S2ns = S2; // 否则仍处于S1elsens = S1;endS2:begincase(sel) // 根据sel的值,跳转到不同的状态S3~S9,进行不同的运算3'b000: ns = S3; // 与运算3'b001: ns = S4; // 或运算3'b010: ns = S5; // 异或运算3'b011: ns = S6; // 位取反运算3'b100: ns = S7; // 加/减法运算3'b101: ns = S8; // 乘法运算3'b110: ns = S9; // 除2运算default: ns = S3;endcaseendS3:beginif(en == 1'b1) // 与运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S3elsens = S3;endS4:beginif(en == 1'b1) // 或运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S4elsens = S4;endS5:beginif(en == 1'b1) // 异或运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S5elsens = S5;endS6:beginns = S0; // 位取反运算,计算完成,返回S0endS7:beginif(en == 1'b1) // 加/减法运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S7elsens = S7;endS8:beginif(en == 1'b1) // 乘法运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S8elsens = S8;endS9:beginns = S0; // 除2运算,计算完成,返回S0endS10:beginns = S0; // 计算完成,返回S0enddefault: ns = S0;endcase end
代码9.7 三段式状态机第二个always块
⑤ 第三个always模块描述电路的输出信号,这里我们使用了寄存器输出的方式。
// 第三段状态机,时序逻辑,描述输出 always@(posedge clk, negedge rst_n) beginif(~rst_n)beginen1 <= 1'b0;en2 <= 1'b0;ope <= 3'b000;endelsebegincase(ns)S0, S2:beginen1 <= 1'b0; // S0、S2状态,两个寄存器都不使能en2 <= 1'b0;endS1:beginen1 <= 1'b1; // 使能输入寄存器en2 <= 1'b0;endS3:beginope <= 3'b000; // 与运算选择信号endS4:beginope <= 3'b001; // 或运算选择信号endS5:beginope <= 3'b010; // 异或运算选择信号endS6:beginen1 <= 1'b0;en2 <= 1'b1; // 使能结果寄存器ope <= 3'b011; // 位取反运算选择信号endS7:beginope <= 3'b100; // 加/减法运算选择信号endS8:beginope <= 3'b101; // 乘法运算选择信号endS9:beginen1 <= 1'b0;en2 <= 1'b1; // 使能结果寄存器ope <= 3'b110; // 除2运算选择信号endS10:beginen1 <= 1'b0;en2 <= 1'b1; // 使能结果寄存器enddefault:beginen1 <= 1'b0;en2 <= 1'b0;ope <= 3'b000;endendcaseend end
代码9.7 三段式状态机第三个always块
9.4.2 代码实现
计算器控制器(状态机)fsmctrl.v
module fsmctrl(input clk, // 时钟信号input rst_n, // 复位信号,低电平有效input en, // 寄存器使能信号input [2:0] sel, // 功能选择信号,选择计算器进行的运算output reg en1, // 输入寄存器的使能信号output reg en2, // 结果寄存器的使能信号output reg [2:0] ope // 计算器进行的运算选择信号 );// 状态声明及编码,使能独热码 parameter S0 = 11'b000_0000_0001,S1 = 11'b000_0000_0010,S2 = 11'b000_0000_0100,S3 = 11'b000_0000_1000,S4 = 11'b000_0001_0000,S5 = 11'b000_0010_0000,S6 = 11'b000_0100_0000,S7 = 11'b000_1000_0000,S8 = 11'b001_0000_0000,S9 = 11'b010_0000_0000,S10 = 11'b100_0000_0000;// 变量声明 reg [10:0] cs; reg [10:0] ns;// 第一段状态机,描述状态转换 always@(posedge clk, negedge rst_n) beginif(~rst_n)cs <= S0;elsecs <= ns; end// 第二段状态机,组合逻辑,描述下一状态 always@(cs, en, sel) begincase(cs)S0:beginif(en == 1'b1) // 如果en=1,跳转到S1ns = S1; // 否则仍处于S0elsens = S0;endS1:beginif(en == 1'b0) // 如果en=0,跳转到S2ns = S2; // 否则仍处于S1elsens = S1;endS2:begincase(sel) // 根据sel的值,跳转到不同的状态S3~S9,进行不同的运算3'b000: ns = S3; // 与运算3'b001: ns = S4; // 或运算3'b010: ns = S5; // 异或运算3'b011: ns = S6; // 位取反运算3'b100: ns = S7; // 加/减法运算3'b101: ns = S8; // 乘法运算3'b110: ns = S9; // 除2运算default: ns = S3;endcaseendS3:beginif(en == 1'b1) // 与运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S3elsens = S3;endS4:beginif(en == 1'b1) // 或运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S4elsens = S4;endS5:beginif(en == 1'b1) // 异或运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S5elsens = S5;endS6:beginns = S0; // 位取反运算,计算完成,返回S0endS7:beginif(en == 1'b1) // 加/减法运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S7elsens = S7;endS8:beginif(en == 1'b1) // 乘法运算,如果en第二次为1,跳转到S10ns = S10; // 否则仍处于S8elsens = S8;endS9:beginns = S0; // 除2运算,计算完成,返回S0endS10:beginns = S0; // 计算完成,返回S0enddefault: ns = S0;endcase end// 第三段状态机,时序逻辑,描述输出 always@(posedge clk, negedge rst_n) beginif(~rst_n)beginen1 <= 1'b0;en2 <= 1'b0;ope <= 3'b000;endelsebegincase(ns)S0, S2:beginen1 <= 1'b0; // S0、S2状态,两个寄存器都不使能en2 <= 1'b0;endS1:beginen1 <= 1'b1; // 使能输入寄存器en2 <= 1'b0;endS3:beginope <= 3'b000; // 与运算选择信号endS4:beginope <= 3'b001; // 或运算选择信号endS5:beginope <= 3'b010; // 异或运算选择信号endS6:beginen1 <= 1'b0;en2 <= 1'b1; // 使能结果寄存器ope <= 3'b011; // 位取反运算选择信号endS7:beginope <= 3'b100; // 加/减法运算选择信号endS8:beginope <= 3'b101; // 乘法运算选择信号endS9:beginen1 <= 1'b0;en2 <= 1'b1; // 使能结果寄存器ope <= 3'b110; // 除2运算选择信号endS10:beginen1 <= 1'b0;en2 <= 1'b1; // 使能结果寄存器enddefault:beginen1 <= 1'b0;en2 <= 1'b0;ope <= 3'b000;endendcaseend endendmodule
代码9.8 fsmctrl.v
用Quartus Prime软件编译fsmctrl.v后,查看其State Machine Viewer如图9.26所示,这与我们画的状态图(图9.25)一致。
图9.26 fsmctrl.v的State Machine Viewer
计算器模块calculator.v
module calculator(input clk, // 时钟信号input rst_n, // 复位信号,低电平有效input [3:0] a, // 操作数,实验九修改为只有一个数据输入input carry_in, // 加/减法的进位input [2:0] sel, // 功能选择,选择计算器进行的运算input en, // 计算器中寄存器的使能控制信号input dig_clk_en, // 计算器数字钟功能的使能控制信号// 0: 数字钟停止计时 1: 数字钟继续计时output [ 7: 0] ledr_out, // 显示计算器的计算结果,以二进制显示output [ 6: 0] hex0_out, // hex5_out ~ hex0_out显示数字钟计时结果output [ 6: 0] hex1_out, // hex1_out ~ hex0_out显示计时结果的厘秒output [ 6: 0] hex2_out, // hex3_out ~ hex2_out显示计时结果的秒output [ 6: 0] hex3_out, // hex5_out ~ hex4_out显示计时结果的分钟output [ 6: 0] hex4_out, output [ 6: 0] hex5_out // output [ 6: 0] hex6_out,// output [ 6: 0] hex7_out );// 变量声明 // 状态机的输出信号 wire reg_en1; wire reg_en2; wire [2:0] operation_sel; // 两个操作数 wire [3:0] operator_a; wire [3:0] operator_b; // 各种运算的结果变量 wire [3:0] f1; wire [3:0] f2; wire [3:0] f3; wire [3:0] f4; wire [5:0] f5; // overflow, carryout, sum[3:0] wire [7:0] f6; // multiply wire [3:0] f7; // divide by 2 wire [3:0] m0; wire [3:0] m1; wire [3:0] s0; wire [3:0] s1; wire [3:0] cs0; wire [3:0] cs1; wire [7:0] f; wire [7:0] g;// 状态机做控制器 fsmctrl fsmctrl_inst(.clk (clk),.rst_n (rst_n),.en (en),.sel (sel),.en1 (reg_en1),.en2 (reg_en2),.ope (operation_sel) );// 输入信号赋值 assign operator_a = a; // 将操作数寄存 reg4bits reg4bits_inst(.clk (clk),.rst_n (rst_n),.en (~reg_en1),.d (a),.q (operator_b) );// 与运算 c1 c1_inst(.a (operator_a),.b (operator_b),.f (f1) ); // 或运算 c2 c2_inst(.a (operator_a),.b (operator_b),.f (f2) ); // 异或运算 c3 c3_inst(.a (operator_a),.b (operator_b),.f (f3) ); // 非运算 c4 c4_inst(.a (operator_a),.f (f4) ); // 加(减)运算 c5 c5_inst (.a (operator_a), .b (operator_b), .ci (carry_in), .f (f5) ); // 乘法运算 c6 c6_inst (.a (operator_a), .b (operator_b), .f (f6) ); // 除以2 c7 c7_inst (.clk (clk),.rst_n (rst_n),.a (operator_a),.f (f7) ); // 数字钟 c8 c8_inst (.clk (clk),.rst_n (rst_n),.en (dig_clk_en), .m1 (m1),.m0 (m0),.s1 (s1),.s0 (s0),.cs1 (cs1),.cs0 (cs0) );// 在f1,f2,f3,f4,f5,f6,f7七种运算结果中,选择一个运算结果f // 若位数不足8位,则需要高位补0 mux8x1 mux8x1_inst(.r ({4'd0, f1}),.t ({4'd0, f2}),.u ({4'd0, f3}),.v ({4'd0, f4}),.w ({2'd0, f5}),.x (f6), .y ({4'd0, f7}), .z (8'd0),.s (operation_sel),.m (f) );// 将选出的运算结果f存入寄存器中 reg8bits reg8bits_inst(.clk (clk),.rst_n (rst_n),.en (~reg_en2),.d (f),.q (g) );// 将g[7:0]以二进制的形式显示在ledr_out上 assign ledr_out = g;// 数字钟的显示 // hex1_out ~ hex0_out显示数字钟计时结果的厘秒 // hex3_out ~ hex2_out显示数字钟计时结果的秒 // hex5_out ~ hex4_out显示数字钟计时结果的分钟 decod7seg decod7seg_inst7 (.hex(m1), .display(hex5_out)); decod7seg decod7seg_inst6 (.hex(m0), .display(hex4_out)); decod7seg decod7seg_inst5 (.hex(s1), .display(hex3_out)); decod7seg decod7seg_inst4 (.hex(s0), .display(hex2_out)); decod7seg decod7seg_inst3 (.hex(cs1), .display(hex1_out)); decod7seg decod7seg_inst2 (.hex(cs0), .display(hex0_out));endmodule
代码9.9 calculator.v
calculator.v的RTL Viewer如图9.27所示,这与我们在9.4.1小节画的系统框图(图9.24)一致。
图9.27 calculator.v的RTL Viewer
9.5 实验步骤
9.5.1 创建工程和 共代码输入
-
点击电脑右下角的开始菜单找到Quartus软件,双击Quartus (Quartus Prime 17.1)打开Quartus Prime软件。
-
点击菜单File-->New Project Wizard弹出工程创建的对话框。在弹出的对话框中点击Next。
-
在您的DE1-SOC 工作文件夹下创建一个lab9的文件夹,并将工程路径指向该文件夹,且工程的名称也命名calculator。如图9.28所示。
图9.28-1 创建lab9 工程
图9.28-2 创建lab9 工程
-
创建Quartus工程报告如下。
图9.29 创建Quartus工程的报告
-
创建完Quartus工程后,左侧会打开实验步骤手册,右侧会在Quartus Prime中打开lab9工程,如图9.30所示。
图9.30 Quartus中打开lab9工程
-
浏览并打开lab9工程路径(默认为~/Desktop/UserDisk/Labs/Digital_Logic/lab9),点击右上侧的
按钮,然后选择New folder按钮,在弹出的New Folder对话框中输入v并点击Create按钮,这样就完成了在lab9文件夹下新建名为v的文件夹,如图9.31所示。
图9.31 在lab9文件夹下新建v文件夹
-
将实验八中的c1.v、c2.v、c3.v、c4.v、c5.v、c6.v、c7.v、c8.v、calculator.v、counter.v、decod7seg.v、fa.v、mux2x1.v、mux8x1.v、reg4bits.v、reg8bits.v文件拷贝至lab9的v文件夹中,如图9.32所示。
图9.32 拷贝文件至v文件夹
-
点击Quartus Prime工具栏的Assignments --> Settings --> Files,将v文件夹中的所有文件添加至本实验的Quartus工程,如图9.34所示。
图9.34 将v文件夹中的所有文件添加至Quartus工程
-
点击Quartus Prime工具栏的File --> New,在弹出的New窗口中选择Verilog HDL File,点击OK,新建一个空白的Verilog HDL文件,如图9.35所示。
图9.35 新建空白的Verilog HDL文件
-
拷贝代码9.8(fsmctrl.v)至新建的Verilog HDL文件,如图9.36所示。
图9.36 将代码9.8拷贝至新建的Verilog HDL文件
-
点击File --> Save As...,在弹出的Save As窗口中选择至lab9/v目录,在File name选项中输入fsmctrl.v,点击Save,将文件保存为fsmctrl.v,如图9.37所示。
图9.37 将文件保存为fsmctrl.v
-
在Quartus Prime的左上角,将Project Navigator由Hierarchy切换至Files,在Files栏中找到calculator.v,双击打开该文件;修改calculator.v文件的内容为代码9.9并保存,如图9.38所示。
图9.38 修改calculator.v
-
点击Quartus Prime工具栏的Processing --> Start --> Start Analysis & Synthesis或点击
按钮对Verilog HDL代码执行语法检查和综合。如果在该过程中提示有错误,请检查Verilog HDL代码语法,确保与上述代码块完全一致。
图9.40 对Verilog HDL代码进行分析和综合
9.5.2 仿真
-
点击Quartus Prime工具栏的File --> New --> Verilog HDL File,点击OK,新建一个空白Verilog HDL文件;
-
在新建的Verilog HDL文件中添加如下代码,如图9.41所示。
`timescale 1ns/1ps module calculator_tb();// 产生50MHz时钟信号 reg clk; localparam PERIOD = 20; // 50MHz,周期为20ns initial beginclk = 1'b0;forever #(PERIOD/2) clk = ~clk; // 每隔10ns,clk翻转一次 end// 产生复位信号,用作6位寄存器的异步复位 reg rst_n; initial beginrst_n = 1'b0;#(PERIOD) rst_n = 1'b1; end// 产生计算器的操作数a、运算操作选择sel和寄存器的使能信号en reg [3:0] a; reg carry_in; reg [2:0] sel; reg en; reg dig_clk_en; initial begin#0a = 4'd0;carry_in = 1'b0;en = 1'b0;dig_clk_en = 1'b1;sel = 3'b000; // 与运算,1010 & 0010 = 0010#(2 * PERIOD)a = 4'b1010;en = 1'b1; // 第一次en=1#(PERIOD)en = 1'b0;#(3*PERIOD)a = 4'b0010;en = 1'b1; // 第二次en=1#(PERIOD)en = 1'b0;#(5 * PERIOD)sel = 3'b001; // 或运算,1010 | 0010 = 1010#(PERIOD)a = 4'b1010;en = 1'b1; // 第一次en=1#(PERIOD)en = 1'b0;#(3*PERIOD)a = 4'b0010;en = 1'b1; // 第二次en=1#(PERIOD)en = 1'b0;#(5 * PERIOD)sel = 3'b010; // 异或运算,1010 ^ 0010 = 1000#(PERIOD)a = 4'b1010;en = 1'b1; // 第一次en=1#(PERIOD)en = 1'b0;#(3*PERIOD)a = 4'b0010;en = 1'b1; // 第二次en=1#(PERIOD)en = 1'b0;#(5 * PERIOD)sel = 3'b011; // 位取反运算#(PERIOD)a = 4'b1010; // ~1010 = 0101en = 1'b1; // 第一次en=1#(PERIOD)en = 1'b0;#(3*PERIOD)a = 4'b0010; // ~0010 = 1101en = 1'b1; // 第二次en=1#(PERIOD)en = 1'b0;#(5 * PERIOD)sel = 3'b100; // 加(减)运算,1010 + 0010 = 1100#(PERIOD)a = 4'b1010;en = 1'b1; // 第一次en=1#(PERIOD)en = 1'b0;#(3*PERIOD)a = 4'b0010;en = 1'b1; // 第二次en=1#(PERIOD)en = 1'b0;#(5 * PERIOD)sel = 3'b101; // 乘法运算,1010 * 0010 = 10100#(PERIOD)a = 4'b1010;en = 1'b1; // 第一次en=1#(PERIOD)en = 1'b0;#(3*PERIOD)a = 4'b0010;en = 1'b1; // 第二次en=1#(PERIOD)en = 1'b0;#(5 * PERIOD) sel = 3'b110; // 除2运算#(PERIOD)a = 4'b1010; // 1010 / 2 = 0101en = 1'b1; // 第一次en=1#(PERIOD)en = 1'b0;#(3*PERIOD)a = 4'b0010; // 0010 /2 = 0001en = 1'b1; // 第二次en=1#(PERIOD)en = 1'b0;#(8 * PERIOD) $stop(); end// 例化 calculator wire [7:0] ledr_out; wire [6:0] hex0_out; wire [6:0] hex1_out; wire [6:0] hex2_out; wire [6:0] hex3_out; wire [6:0] hex4_out; wire [6:0] hex5_out; //wire [6:0] hex6_out; //wire [6:0] hex7_out; calculator calculator_inst(.clk (clk),.rst_n (rst_n),.a (a),.carry_in (carry_in),.sel (sel),.en (en),.dig_clk_en (dig_clk_en),.ledr_out (ledr_out),.hex0_out (hex0_out),.hex1_out (hex1_out),.hex2_out (hex2_out),.hex3_out (hex3_out),.hex4_out (hex4_out),.hex5_out (hex5_out)// .hex6_out (hex6_out),// .hex7_out (hex7_out) );endmodule
代码9.12 calculator_tb.v
图9.41 在新建的Verilog HDL文件中添加代码9.12
-
点击File --> Save As...,在弹出的Save As窗口中选择至lab9/v目录,在File name选项中输入calculator_tb.v,点击Save,将文件保存为calculator_tb.v,如图9.42所示。
图9.42 将文件保存为calculator_tb.v
-
点击Quartus Prime工具栏的Assignments --> Settings,在弹出的Settings窗口中,在左侧选中Simulation栏,在右侧的Simulation页面下,Tool name选择ModelSim-Altera,如图9.43所示,Quartus Prime将会调用ModelSim进行仿真。
图9.43 设置仿真工具
-
依旧是在Simulation页面下,依次执行下面的步骤设置TestBench为calculator_tb:
① 选中Compile test bench,然后点击右侧的Test Benches...按钮,如图9.44所示;
图9.44 设置TestBench(步骤1)
② 在弹出的Test Benches窗口中点击New...;
图9.45 设置TestBench(步骤2)
③ 在弹出的New Test Bench Settings窗口中,在Test bench name栏输入calculator_tb;
④ 点击File name右侧的
图标;
图9.46 设置TestBench(步骤3、4)
⑤ 在弹出的Select File窗口中选择lab9/v目录下的calculator_tb.v;
⑥ 点击Open;
图9.47 设置TestBench(步骤5、6)
⑦ 然后在File name栏会出现选中的v/calculator_tb.v;
⑧ 点击File name右侧的Add按钮;
图9.48 设置TestBench(步骤7、8)
⑨ File name栏中的v/calculator_tb.v会出现在下面的框格中;
⑩ 点击OK,退出New Test Bench Settings窗口;
图9.49 设置TestBench(步骤9、10)
⑪ 返回Test Benches窗口,在Existing test bench settings框格中会出现前面设置的calculator_tb;
⑫ 点击OK,退出Test Benches窗口;
图9.50 设置TestBench(步骤11、12)
⑬ 返回Simulation页面,可以看到Compile test bench栏成功添加了calculator_tb仿真文件;
⑭ 点击OK完成设置。
图9.51 设置TestBench(步骤13、14)
-
点击Quartus Prime工具栏的Tools --> Run Simulation Tool --> RTL Simulation,Quartus会启动ModelSim进行仿真,仿真完成后,停在$stop()任务处,如图9.52所示。
图9.52 仿真完成后停在$stop()任务处
-
点击Wave切换至仿真波形窗口,如图9.53所示,再点击Wave窗口右上角如图9.54所示的Zoom/Unzoom按钮将将窗口最大化,方便观察信号。
图9.53 切换至仿真波形窗口
图9.54 Zoom/Unzoom按钮
-
在Wave窗口中,点击如图9.55所示的Zoom Full按钮显示完整的波形。
图9.55 Zoom Full按钮
-
完整的仿真波形如图9.56所示。
图9.56 完整的仿真波形
下面对仿真波形进行分析:
本次仿真的重点是仿真计算器控制器的功能,因此将两个操作数固定为 (1010)_B 和 (0010)_B,即a的值第一次为(1010)_B,第二次为(0010)_B。
⓪ 20ns处,复位信号rst_n由0变为1,复位完成,计算器正常工作;
① 40ns ~ 240ns,sel=(000)_B,做与运算,(1010)_B \ \& \ (0010)_B = (0010)_B;在连续两次en=1后,ledr\_out = (0000\_0010)_B;
② 240ns ~ 460ns,sel=(001)_B,做与运算,(1010)_B \ | \ (0010)_B = (1010)_B;在连续两次en=1后,ledr\_out = (0000\_1010)_B;
③ 460ns ~ 680ns,sel=(010)_B,做异或运算,(1010)_B \ \wedge \ (0010)_B = (1000)_B;在连续两次en=1后,ledr\_out = (0000\_1000)_B;
④ 680ns ~ 900ns,sel=(011)_B,做非运算,\sim(1010)_B = (0101)_B,\sim(0010)_B = (1101)_B;由于非运算只需要一个操作数,在第一次en=1后,ledr\_out = (0000\_0101)_B;在第二次en=1后,ledr\_out = (0000\_1101)_B;
⑤ 900ns ~ 1120ns,sel=(100)_B,carry\_in \ = \ (0)_B,做加法运算,(1010)_B \ + \ (0010)_B \ + \ (0)_B = (1100)_B;连续两次en=1后,ledr\_out = (0000\_1100)_B;
⑥ 1120ns ~ 1340ns,sel=(101)_B,做乘法运算,(1010)_B \ * \ (0010)_B \ = (1\_0100)_B;连续两次en=1后,$ledr_out = (0001_0100)_B
⑦ 1340ns ~ 仿真结束,sel=(110)_B,做除2运算,(1010)_B \ / \ 2 = (0101)_B,(0010)_B \ / \ 2 = (0001)_B;由于除2运算只需要一个操作数,在第一次en=1后,ledr\_out = (0000\_0101)_B;在第二次en=1后,ledr\_out = (0000\_0001)_B;
9.5.3 引脚分配、全编译与烧录
-
点击Quartus菜单Assignments——Pin Planner进行引脚分配。
图4.41
关于引脚分配信息可以查看DE1-SoC_v.5.1.3_HWrevF.revG_SystemCD\UserManual\DE1-SoC_User_manual.pdf第 22、25、26、28页或者E:\CD_Package\01-DE1-SoC\DE1-SoC_v.5.1.3_HWrevF.revG_SystemCD\Schematic\DE1-SoC.pdf的第3页
-
其实实验9的引脚信息跟实验8是一样的,没有改动。所以为了节省时间,我们可以打开lab8\calculator.qsf文件,拷贝如下内容到lab8\calculator.qsf文件的最后面:
set_location_assignment PIN_AF14 -to clk set_location_assignment PIN_AF10 -to a[3] set_location_assignment PIN_AF9 -to a[2] set_location_assignment PIN_AC12 -to a[1] set_location_assignment PIN_AE11 -to carry_in set_location_assignment PIN_AB12 -to a[0] set_location_assignment PIN_AA15 -to en set_location_assignment PIN_AA14 -to rst_n set_location_assignment PIN_AE12 -to sel[2] set_location_assignment PIN_AD10 -to sel[1] set_location_assignment PIN_AC9 -to sel[0] set_location_assignment PIN_W20 -to ledr_out[7] set_location_assignment PIN_Y19 -to ledr_out[6] set_location_assignment PIN_W19 -to ledr_out[5] set_location_assignment PIN_W17 -to ledr_out[4] set_location_assignment PIN_V18 -to ledr_out[3] set_location_assignment PIN_V17 -to ledr_out[2] set_location_assignment PIN_W16 -to ledr_out[1] set_location_assignment PIN_V16 -to ledr_out[0] set_location_assignment PIN_W15 -to dig_clk_en set_location_assignment PIN_AH28 -to hex0_out[6] set_location_assignment PIN_AG28 -to hex0_out[5] set_location_assignment PIN_AF28 -to hex0_out[4] set_location_assignment PIN_AG27 -to hex0_out[3] set_location_assignment PIN_AE28 -to hex0_out[2] set_location_assignment PIN_AE27 -to hex0_out[1] set_location_assignment PIN_AE26 -to hex0_out[0] set_location_assignment PIN_AD27 -to hex1_out[6] set_location_assignment PIN_AF30 -to hex1_out[5] set_location_assignment PIN_AF29 -to hex1_out[4] set_location_assignment PIN_AG30 -to hex1_out[3] set_location_assignment PIN_AH30 -to hex1_out[2] set_location_assignment PIN_AH29 -to hex1_out[1] set_location_assignment PIN_AJ29 -to hex1_out[0] set_location_assignment PIN_AC30 -to hex2_out[6] set_location_assignment PIN_AC29 -to hex2_out[5] set_location_assignment PIN_AD30 -to hex2_out[4] set_location_assignment PIN_AC28 -to hex2_out[3] set_location_assignment PIN_AD29 -to hex2_out[2] set_location_assignment PIN_AE29 -to hex2_out[1] set_location_assignment PIN_AB23 -to hex2_out[0] set_location_assignment PIN_AB22 -to hex3_out[6] set_location_assignment PIN_AB25 -to hex3_out[5] set_location_assignment PIN_AB28 -to hex3_out[4] set_location_assignment PIN_AC25 -to hex3_out[3] set_location_assignment PIN_AD25 -to hex3_out[2] set_location_assignment PIN_AC27 -to hex3_out[1] set_location_assignment PIN_AD26 -to hex3_out[0] set_location_assignment PIN_W25 -to hex4_out[6] set_location_assignment PIN_V23 -to hex4_out[5] set_location_assignment PIN_W24 -to hex4_out[4] set_location_assignment PIN_W22 -to hex4_out[3] set_location_assignment PIN_Y24 -to hex4_out[2] set_location_assignment PIN_Y23 -to hex4_out[1] set_location_assignment PIN_AA24 -to hex4_out[0] set_location_assignment PIN_AA25 -to hex5_out[6] set_location_assignment PIN_AA26 -to hex5_out[5] set_location_assignment PIN_AB26 -to hex5_out[4] set_location_assignment PIN_AB27 -to hex5_out[3] set_location_assignment PIN_Y27 -to hex5_out[2] set_location_assignment PIN_AA28 -to hex5_out[1] set_location_assignment PIN_V25 -to hex5_out[0] set_instance_assignment -name PARTITION_HIERARCHY root_partition -to | -section_id Top
-
Quartus Prime工具栏的Processing --> Start Compilation或点击
按钮编译工程,编译完成后,如图9.57所示。此外还可以看到在lab9/output_files文件夹中生成了calculator.sof文件,如图9.58所示。
图9.57 lab9编译完成
图9.58 lab9.sof
-
使用上一步生成的calculator.sof文件对FPGA进行编程。用USB Blaster线将DE1-SOC开发板和PC连接起来,给开发板上电。
-
点击Quartus Prime工具栏的Tools --> Programmer或点击
按钮打开Programmer窗口,如图9.59所示。
图9.59 Programmer窗口
-
在Programmer窗口中,点击Hardware Setup...打开Hardware Setup窗口,在Currently selected hardware的下拉框中选择"DE-SoC[USB-1]",点击Close,如图9.60所示。
图9.60 选择USB-Blaster
-
点击Auto Detect按钮,在弹出的Select Device窗口中,选择5CSEMA5(DE1-SoC开发板上的FPGA器件为Cyclone V SE 5CSEMA5F31C6),并点击OK,如图9.61所示;在弹出的Quartus Prime提示窗口中,点击Yes,如图9.62所示;然后在Programmer窗口会出现SOCVHPS和FPGA两个器件,如图9.63所示。
图9.61 选择5CSEBA6器件
图9.62 提示窗口
图9.63 Programmer窗口的SOCVHPS和FPGA两个器件
-
左键单击选中5CSEMA5器件,然后点击Change File按钮,如图9.64所示。
图9.64 点击Change File按钮
-
在弹出的Select New Programming File窗口中,选择lab9/output_files目录下的calculator.sof文件,点击Open,如图9.65所示,添加calculator.sof文件。
图9.65 添加lab9.sof文件
-
添加calculator.sof文件完成后会自动返回Programmer窗口,勾选Program/Configure,点击Start按钮,开始烧录calculator.sof文件,如图9.66所示。
图9.66 开始烧录lab9.sof文件
-
烧录完成后,Progress进度条显示100%,如图9.67所示。
图9.67 烧录lab9.sof完成
9.5.4 实验现象观察
-
通过切换滑动开关SW9 ~ SW7、SW3 ~ SW0到 up 或 down 位置,并按下按键KEY1,观察LEDR7~LEDR0的状态来测试计算器控制器的功能。
本实验重点验证计算器控制器的功能,因此将两个操作数固定为 (1010)_B 和 (0010)_B,即滑动开关SW3 ~ SW0只会有两种输入:"up、down、up、down"和"down、down、up、down",而我们是通过切换滑动开关SW9 ~ SW7控制计算器进行不同的运算。
① 先验证与运算:
-
切换滑动开关SW9~ SW7位置为"down、down、down",SW3 ~ SW0位置为"up、down、up、down",按下按键KEY1,LEDR7~LEDR0显示为全熄灭,如图9.71所示;
图9.71 与运算实验现象1
-
切换滑动开关SW3 ~ SW0位置为"down、down、up、down",按下按键KEY1,LEDR7 ~ LEDR0显示为熄灭、熄灭、熄灭、熄灭、熄灭、熄灭、点亮、熄灭,如图9.72所示;
即(1010)_B \ \& \ (0010)_B = (0010)_B = (2)_H;
即只有连续两次按下按键KEY1,LEDR7 ~ LEDR0 上才会显示与运算的结果。
图9.72 与运算实验现象2
② 我们继续验证或运算:
-
切换滑动开关SW9 ~ SW7位置为"down、down、up",SW3 ~ SW0位置为"up、down、up、down",按下按键KEY1,LEDR7 ~ LEDR0上仍旧显示上一步与运算的运算结果,即显示为熄灭、熄灭、熄灭、熄灭、熄灭、熄灭、点亮、熄灭,仍旧显示上一步与运算的运算结果,如图9.73所示;
图9.73 或运算实验现象1
-
切换滑动开关SW3 ~ SW0位置为"down、down、up、down",按下按键KEY1,LEDR7 ~ LEDR0显示发生变化,即显示为熄灭、熄灭、熄灭、熄灭、点亮、熄灭、点亮、熄灭,如图9.74所示;
即(1010)_B \ | \ (0010)_B = (1010)_B = (A)_H;
即只有连续两次按下按键KEY1,LEDR7 ~ LEDR0 上才会显示或运算的结果,而只按下一次按键KEY1,LEDR7 ~ LEDR0上依旧保持上一次与运算的运算结果。
图9.74 或运算实验现象2
③ 我们继续验证异或运算:
-
切换滑动开关SW9 ~ SW7位置为"down、up、down",SW3 ~ SW0位置为"up、down、up、down",按下按键KEY1,LEDR7 ~ LEDR0上仍旧显示上一步或运算的运算结果,即显示为熄灭、熄灭、熄灭、熄灭、点亮、熄灭、点亮、熄灭,,仍旧显示上一步或运算的运算结果,如图9.75所示;
图9.75 异或运算实验现象1
-
切换滑动开关SW3 ~ SW0位置为"down、down、up、down",按下按键KEY1,LEDR7 ~ LEDR0显示发生变化,即显示为熄灭、熄灭、熄灭、熄灭、点亮、熄灭、熄灭、熄灭,如图9.76所示;
即(1010)_B \ \wedge \ (0010)_B = (1000)_B = (8)_H;
即只有连续两次按下按键KEY1,LEDR7 ~ LEDR0 上才会显示异或运算的结果,而只按下一次按键KEY1,LEDR7 ~ LEDR0 上依旧保持上一次或运算的运算结果。
图9.76 异或运算实验现象2
④ 我们继续验证非(位取反)运算:
-
切换滑动开关SW9 ~ SW7位置为"down、up、up",SW3 ~ SW0位置为"up、down、up、down",按下按键KEY1,LEDR7 ~ LEDR0显示发生变化,即显示为熄灭、熄灭、熄灭、熄灭、熄灭、点亮、熄灭、点亮,如图9.77所示;
即\sim(1010)_B = (0101)_B = (5)_H;
由于非运算只需要一个操作数,所以在第一次按下按键KEY1后,非运算的运算结果就会显示到 LEDR7 ~ LEDR0;
图9.77 非运算实验现象1
-
切换滑动开关SW3 ~ SW0位置为"down、down、up、down",按下按键KEY1,LEDR7 ~ LEDR0显示发生变化,即显示为熄灭、熄灭、熄灭、熄灭、点亮、点亮、熄灭、点亮,如图9.78所示;
即\sim(0010)_B = (1101)_B = (d)_H;
即对于非运算在第一次按下按键KEY1后,非运算的运算结果就会显示到 LEDR7 ~ LEDR0 上。
图9.78 非运算实验现象2
⑤ 我们再来验证加/减法运算:
-
切换滑动开关SW9 ~ SW7位置为"up、down、down",SW3 ~ SW0位置为"up、down、up、down",SW11位置为"down",按下按键KEY1,LEDR7 ~ LEDR0上仍旧显示上一步非运算的运算结果,即显示为熄灭、熄灭、熄灭、熄灭、点亮、点亮、熄灭、点亮,仍旧显示上一步非运算的运算结果,即显示十六进制数字0d,如图9.79所示;
图9.79 加/减法运算实验现象1
-
切换滑动开关SW3 ~ SW0位置为"down、down、up、down",按下按键KEY1,LEDR7 ~ LEDR0显示发生变化,即显示为熄灭、熄灭、熄灭、熄灭、点亮、点亮、熄灭、熄灭,即显示十六进制数字0C,如图9.80所示;
即(1010)_B \ + \ (0010)_B \ + \ (0)_B = (1100)_B = (C)_H;
即只有连续两次按下按键KEY1,LEDR7 ~ LEDR0 和 HEX1 ~ HEX0 上才会显示加/减运算的结果,而只按下一次按键KEY1,LEDR7 ~ LEDR0 上依旧保持上一次非运算的运算结果。
图9.80 加/减法运算实验现象2
⑥ 在整个实验过程中,数字钟一直保持计数,HEX5 ~ HEX0上显示的数据会一直发生变化,如果按下KEY2不松开,数字钟停止计时;释放KEY2后,数字钟从它上次停止的时刻继续计时。
-
9.6 实验小结
通过本实验,我们学习了状态机的组成、分类、表示方法、状态编码以及设计步骤,同时还结合了常见的序列检测器的例子来介绍怎样设计状态机。最后,我们将状态机用作计算器的控制器,来控制计算器的整个运算过程,而利用状态机作为系统的控制模块,在数字系统设计中也是经常使用的方法。