第六节 功能描述-时序逻辑
6.1 always语句
时序逻辑的代码一般有两种:同步复位的时序逻辑和异步复位的时序逻辑。在同步复位的时序逻辑中复位不是立即有效,而在时钟上升沿时复位才有效。其代码结构如下:
always@(posedge clk) begin if(rst_n==1’b0) 代码语句; else begin 代码语句; end end
在异步复位的时序逻辑中复位立即有效,与时钟无关。其代码结构如下:
always@(posedge clk or negedge rst_n) begin if(rst_n==1’b0) 代码语句; else begin 代码语句; end end
针对时序逻辑的 verilog 设计提出以下建议:
为了教学的方便代码统一采用异步时钟逻辑,建议同学们都采用此结构,这样设计时只需考虑是用时序逻辑还是组合逻辑结构来进行代码编写即可。在实际工作中请遵从公司的相应规范进行代码设计。
使用 GVim 软件打开代码后,输入“Zuhe”命令后回车可得到组合逻辑的代码结构,输入“Shixu”命令后回车可得到时序逻辑的代码结构。
没有复位信号的时序逻辑代码设计是不规范的,建议不要这样使用。
6.2 D触发器
数字电路中介绍了多种触发器,如 JK 触发器、D 触发器、RS 触发器、T 触发器等。在 FPGA中使用的是最简单的触发器——D 触发器
6.2.1 D触发器结构
图 1.3- 37 是 D 触发器的结构图,可以将其视为一个芯片,该芯片拥有 4 个管脚,其中 3 个是输入管脚:时钟 clk、复位 rst_n、信号 d;1 个是输出管脚:q。
该芯片的功能如下:当给管脚 rst_n 给低电平(复位有效),即赋值为 0 时,输出管脚 q 处于低电平状态。如果管脚 rst_n 为高电平,则观察管脚 clk 的状态,当 clk 信号由 0 变 1 即处于上升沿的时候,将此时 d 的值赋给 q。若 d 是低电平,则 q 也是低电平;若 d 是高电平,则 q 也是高电平。
6.2.2 D触发器波形
图 1.3- 38 为 D 触发器的功能波形图,该波形图反映了 D 触发器各个信号的变化情况,从左到右表示时间的走势。从图中可以看到时钟信号有规律地进行高低变化。
按照从左向右的顺序观察波形图可以发现:开始状态下,rst_n 等于 1,d 等于 0,q 等于 1
随后 rst_n 由 1 变 0,此时输出信号 q 立即变成 0。对应的功能是:当给管脚 rst_n 低电平,也就是赋值为 0 时,输出管脚 q 处于低电平状态。
在 rst_n 为 0 期间,即使在有时钟或信号 d 发生变化的情况下 q 仍然保持为低电平。
在 rst_n 由 0 变成 1 撤消复位后,q 没有立刻发生变化。
在第 4 个时钟上升沿时,此时 rst_n 等于 1,而 d 等于 1,因此 q 变成了 1。
第 5 个时钟上升沿,仍然是同样情况,rst_n=1,d=1,因此 q=1。
在第 6 个时钟上升沿,rst_n=1,d=0,因此 q=0。
第 7~10 个时钟沿也是按同样方式判断。对应的功能是:如果管脚 rst_n 为高电平,则观察管脚 clk,在 clk 由 0 变 1 即上升沿的时候,将现在 d 的值赋给 q。若 d 是低电平,q 也是低电平;若 d 是高电平,q也是高电平。
6.2.3 D 触发器代码
首先,观察如下这段时序逻辑的代码:
从语法上分析该段代码的功能为:该段代码总是在“时钟 clk 上升沿或者复位 rst_n 下降沿”的时候执行一次。具体执行方式如下:
如果复位 rst_n=0,则 q 的值为 0;
如果复位 rst_n=1,则将 d 的值赋给 q(注意,前提条件是时钟上升沿的时候)。
上例的功能与本案例的功能是相同的:当给管脚 rst_n 给低电平,也就是赋值为 0 时,输出管脚q 就处于低电平状态。如果管脚 rst_n 为高电平则观察管脚 clk,在 clk 由 0 变 1 即上升沿的时候,将现在 d 的值赋给 q,d 是低电平,q 也是低电平,d 是高电平,q 也是高电平。
因此可以看出这段代码的功能与 D 触发器的功能是一样的,即该代码其实就是在描述一个 D 触发器,也就是 D 触发器的代码。前文中已经讲过在 FPGA 设计中可以用原理图的形式来设计,也可以用硬件描述语言来设计。
当用原理图来设计时几个 D 触发器还可以忍受,但如果出现几千几万个 D 触发器则必定是头晕眼花,而用硬件描述语言 Verilog 则不存在这一问题。
6.2.4 怎么看FPGA波形
下面来讨论如下图所示的波形,先观察在第 4 个时钟上升沿的时刻,思考一下此时看到的信号 q的值是多少?是 0 还是 1?或者观察到的是 q 的上升沿?
首先明确一点:Verilog 代码对应的是硬件,因此应该从硬件的角度来分析这个问题。再来理清一下代码的因果关系:先有时钟上升沿,此为因,然后再将 d 的值赋给 q,这才是结果。这个因果是有先后关系的,对于硬件来说这个“先后”无论是多么地迅速,也一定会占用一定时间,所以 q 的变化会稍后于 clk 的上升沿。例如下图就是硬件的实际变化情况。
图 1.3- 40 中就很容易看出,第 4 个时钟上升沿时刻对应的 q 值为 0,也就是变化前的值。上面的波形虽然更将近于实际,但这样画图使这一过程非常复杂,且非必要操作。因此建议只需掌握这种看波形规则,即时钟上升沿看信号,是看到变化之前的值。
所以第 4 个时钟上升沿时,看到 q 值为 0;在第 6 个时钟上升沿时,看到 q 值为 1;在第 7 个时钟上升沿时,看到 q 值为 0;在第 8 个时钟上升沿时,看到 q 值为 1;在第 10 个时钟上升沿时,看到 q 值为 0。注意一下,复位信号是在系统开始时刻或者出现异常时才使用,一般上电后就不会再次进行复位,也可以认为复位是一种特殊情况。
下面考虑正常使用的情况:**无论是从功能上还是波形上,都可以看到信号 q 只在时钟上升沿才变化,而绝对不会在中间发生变化。在一般的数字系统中大部分信号之间的传递都是在同一个时钟下进行的,即大部分都是同步电路。跨时钟的电路占比非常小,属于特殊的异步电路。**在本教材中,如果没有提前说明所有的案例、练习都默认为同步电路。
下面具体分析每个时钟下 q 信号的情况:
在 rst_n 由 1 变 0 时,q 立刻变成 0。
在第 2 个时钟上升沿,看到 rst_n 为 0。按代码功能,q 仍然为 0。
在第 3 个时钟上升沿,看到 rst_n 为 0。按代码功能,q 仍然为 0。
在第 4 个时钟上升沿,看到 rst_n 为 1,d 值为 1,q 值为 0。按代码功能,q 变成 1。
在第 5 个时钟上升沿,看到 rst_n 为 1,d 值为 1,q 值为 1。按代码功能,q 变成 1。
在第 6 个时钟上升沿,看到 rst_n 为 1,d 值为 0,q 值为 1。按代码功能,q 变成 0。
在第 7 个时钟上升沿,看到 rst_n 为 1,d 值为 1,q 值为 0。按代码功能,q 变成 1。
在第 8 个时钟上升沿,看到 rst_n 为 1,d 值为 0,q 值为 1。按代码功能,q 变成 0。
在第 9 个时钟上升沿,看到 rst_n 为 1,d 值为 0,q 值为 0。按代码功能,q 变成 0。
在第 10 个时钟上升沿,看到 rst_n 为 1,d 值为 1,q 值为 0。按代码功能,q 变成 1。
6.3 时钟
时钟信号是每隔固定时间上下变化的信号。本次上升沿和上一次上升沿之间占用的时间就是时钟周期,其倒数为时钟频率。高电平占整个时钟周期的时间,被称为占空比。
FPGA 中时钟的占空比一般是 50%,即高电平时间和低电平时间一样。其实占空比在 FPGA 内部没有太大的意义,因为 FPGA 使用的是时钟上升沿来触发,设计师们更加关心的是时钟频率。
如果时钟的上升沿每秒出现一次,说明时钟的时钟周期为 1 秒,时钟频率为 1Hz。如果时钟的上升沿每 1 毫秒出现一次,说明时钟的时钟周期为 1 毫秒,时钟频率为 1000Hz,或写成 1kHz。
现在普通 FPGA 器件所支持的时钟频率范围一般不超过 150M,高端器件一般不超过 700M(注意,该值为经验值,实际时钟的频率与其具体器件和设计电路有关),所对应的时钟周期在纳秒级范围。因此在本教材中所有案例的时钟频率一般选定范围是几十至一百 M 左右。
下面列出本教材常用到的时钟频率以及所对应的时钟周期,方便进行换算。
时钟是 FPGA 中最重要的信号,其他所有信号在时钟的上升沿统一变化,这就像军队里的令旗, 所有军队在看到令旗到来的时刻执行已经设定好的命令。
时钟这块令旗影响着整体电路的稳定。首先,时钟要非常稳定地进行跳动。就如军队令旗,如果时快时慢就会让人无所适从,容易出错。而如果令旗非常稳定,每个人都知道令旗的指挥周期,就可以判断令旗到来前是否可以完成任务,如果无法完成则进行改正(修改代码),从而避免系统出错。其次,一个高效的军队中令旗越少越好,如果不同部队对标不同的令旗,那么部队协作就容易出现问题,整个军队无法高效的完成工作,容易出现错误。同样的道理,FPGA 系统的时钟必定是越少越好,最好只存在一个时钟。以上就是要求不要把信号放在时序逻辑敏感列表的原因。
6.4 时序逻辑代码和硬件
先来分析一下下面这段代码:
仍然从语法上分析该段代码的功能。该段代码总是在“时钟 clk 上升沿或者复位 rst_n 下降沿”的时候执行一次。具体执行方法如下:
如果复位 rst_n=0,则 q 的值为 0;
如果复位 rst_n=1,则将(a+d)的结果赋给 q(注意,前提条件是时钟上升沿的时候)。
假设用信号 c 表示 a+d 的结果,则第 2 点可改为:如果复位 rst_n=1,则将 c 的值赋给 q(注意,前提条件是时钟上升沿的时刻)。很明显这是一个 D 触发器,输入信号为 d,输出为 q,时钟为 clk,复位为 rst_n,其电路示意图如下图所示:
可知 c 是 a+d 的结果,因此其自然是通过一个加法器实现,画出上面代码所对应的电路结构图,可以看出在 D 触发器的基础上增加了一个加法器。
很容易分析出上面电路的功能:信号 a 和信号 b 相加得到 c,c 连到 D 触发器的输入端。当 clk出现上升沿时,将 c 的值传给 q。这与代码功能是一致的。下面是代码和硬件所对应的波形图。
先看信号 c 的波形:c 的产生只有与 a 和 d 有关,与 rst_n 和 clk 无关。c 是 a+d 的结果,按照二进制加法:0+0=0,0+1=1,1+1=0 可以画出 c 的波形。
在第 1 个时钟期间,a=0,d=0,所以 c=0+0=0;
在第 2 个时钟期间,a=1,d=0,所以 c=1+0=1;
在第 3 个时钟期间,a=1,d=1,所以 c=1+1=0;
在第 4 个时钟期间,a=0,d=1,所以 c=0+1=1;
在第 5 到第 6 个时钟期间,a=0,d=0,所以 c=0+0=0;
在第 7 个时钟期间,a=1,d=1,所以 c=1+1=0;
在第 8 个时钟期间,a=0,d=1,所以 c=0+1=1;
在第 9 个时钟期间,a=0,d=0,所以 c=0+0=0;
在第 10 个时钟期间,a=0,d=1,所以 c=0+1=1。
再看信号 q 的波形:q 是 D 触发器的输出,其只在 rst_n 的下降沿或者 clk 的上升沿才变化,其他时刻不变化,即 a、d、c 发生变化时,q 不会立刻发生改变。
下面具体分析每个时钟下 q 信号的情况:
在 rst_n 由 1 变 0 时,q 立刻变成 0。
在第 2 个时钟上升沿,看到 rst_n 为 0。按代码功能,q 仍然为 0。
在第 3 个时钟上升沿,看到 rst_n 为 0。按代码功能,q 仍然为 0。
在第 4 个时钟上升沿,看到 rst_n 为 1,c 值为 0,q 值为 0。按代码功能,q 变成 0;
在第 5 个时钟上升沿,看到 rst_n 为 1,c 值为 1,q 值为 0。按代码功能,q 变成 1;
在第 6 个时钟上升沿,看到 rst_n 为 1,c 值为 0,q 值为 1。按代码功能,q 变成 0;
在第 7 个时钟上升沿,看到 rst_n 为 1,c 值为 0,q 值为 0。按代码功能,q 变成 0;
在第 8 个时钟上升沿,看到 rst_n 为 1,c 值为 0,q 值为 0。按代码功能,q 变成 0;
在第 9 个时钟上升沿,看到 rst_n 为 1,c 值为 1,q 值为 0。按代码功能,q 变成 1;
在第 10 个时钟上升沿,看到 rst_n 为 1,c 值为 0,q 值为 1。按代码功能,q 变成 0;
在第 11 个时钟上升沿,看到 rst_n 为 1,c 值为 1,q 值为 0。按代码功能,q 变成 1。
在讨论时序逻辑的加法器时 对加法器的输出 c 和 D 触发器的输出 q 分开进行讨论,就像两块独立的电路。同样的道理,在设计 Verilog 代码时也可以将其分开来进行编写。先将下面的硬件电路用 Verilog 描述出来
该电路对应的电路可以写成:
上面的两段代码,都是描述同一加法器硬件电路。接着用 Verilog 对触发器进行描述:
其代码的写法如下:
最后可以看到,两段代码都有信号 c,说明这两段代码是相连的,利用硬件连接起来可以变成如
下图所示的电路。
由此可见,下面两段代码所对应的硬件电路是一模一样的。
那么这两种代码哪一种比较好呢?答案是这两段代码并无区别,因为两者的硬件是相同的。由此也可以得知评估 verilog 代码好坏的最基本标准,即不是看代码行数而是看硬件。
6.5 阻塞语句和非阻塞赋值
在 always 语句块中,Verilog 语言支持两种类型的赋值:阻塞赋值和非阻塞赋值。阻塞赋值使用“=”语句;非阻塞赋值使用“<=”语句。
阻塞赋值:在一个“begin…end”的多行赋值语句,先执行当前行的赋值语句,再执行下一行的赋值语句。
非阻塞赋值:在一个“begin…end”的多行赋值语句,在同一时间内同时赋值。
上面两个例子中,1 到 4 行部分是阻塞赋值,程序会先执行第 2 行,得到结果后再执行第 3 行。6 至 9 行这一段是非阻塞赋值,第 7 行和第 8 行的赋值语句是同时执行的。
具体分析一下这两段代码这件的区别:假设当前 c 的值为 0,d 的值为 0,a 的新值为 1。
阻塞赋值的执行过程和结果为:程序先执行第 2 行,此时 c 的值将更新为 1,然后再执行 3 行,此时 c+a 也就是相当于 1+1=2,即 d 的值为 2。
非阻塞赋值的执行过程和结果为:程序同时执行第 7 行和 8 行。需要特别注意是,在执行第 8行的时候,第 7 行还并未执行,这也就意味着 c 的值还没有发生变化,即此时 c 的值为 0。同时执行的结果是,c 的值为 1,d 的值为 1。
根据规范要求,组合逻辑中应使用阻塞赋值“=”,时序逻辑中应使用非阻塞赋值“<=”。可以将这个规则牢牢记住,按照这一规则进行设计绝对不会发生错误。制定这个规范的原因并不是考虑语法需要,而是为了正确的进行硬件描述。