第 1 节 项目背景
LED 灯的相关理论以及教学板的原理图已经在第一个案例《1 位闪烁灯》中进行了比较详细的讲解,这里就不再赘述,如果有需要可以返回上一章进行阅读学习。
第 2 节 设计目标
开始进行新的设计之前,依旧先要明确本设计的功能目标,这也是本书以及至简设计法的特别之处。通过对上一个案例的学习领悟也可以意识到明确设计目标的重要性,后续设计的每一个步骤都应该根据功能目标有进行针对性的逐步展开。至简设计法的这一方法让每一段代码每一个步骤都有其相应作用。反之,在设计中如果没有明确设计目标,而是按照自己的想法东一锤子、西一榔头,可能最终要花费成倍的时间才能做出同样的成果。所以再次强调,在设计目标确立这一步骤一定不要偷懒,要认真思考一下本次设计最终目标是什么,希望达到什么效果,明确目标后再进行设计实操才能起到事半功倍的效果。
本工程使用 4 个 LED 灯(LED1~LED4)来实现闪烁灯的功能。具体功能要求为:首先 LED1暗 1 秒,接着亮 1 秒后变暗;接着 LED2 暗 1 秒,亮 2 秒后变暗;随后 LED3 暗 1 秒,亮 3 秒后变暗;最后 LED4 暗 1 秒,亮 4 秒后变暗;至此一个循环结束,随后按照此规律进行循环往复。也就是说,四个 LED 灯依次循环闪烁,具体闪烁要求为:隔 1 秒,亮 N 秒,N 的变化为:1,2,3,4 秒,然后再次进入循环。闪烁灯变化功能图如图 3.2-1 所示,上板后的效果展示可参见图 3.2-2。可以登录至简设计法官方网站观看上板后的演示视频:www.mdy-edu.com/xxxx。
第 3 节 设计实现
与上个案例相同,下面会详细的按照步骤和思考逻辑进行工程的逐步分析。如果知识掌握比较牢靠、只想练习设计工程,可以跳过此部分,直接进入第五节“简化版步骤分享”进行实操练习。依旧建议初学者不要选择捷径,按照详细思路一步步学习分析打好基础,在掌握思想原理之后,再按照操作步骤反复进行实践。
3.1 顶层信号
新建目录:D:\mdy_book\mdyBookHuxiLed,并在该目录中,新建一个名为 mdyBookHuxiLed. v 的文件。用 GVIM 打开该文件后开始编写代码。这里再强调一下,建议初学者按照本书提供的文件路径以及文件名进行设置,避免不必要的错误。
首先应确定顶层信号。由设计目标可知控制相应 LED 灯亮或灭需要 FPGA 产生一个信号并将此信号传送至 LED 灯。想控制 LED 灯灭,则令 FPGA 输出信号为 1,想控制 LED 灯亮,则令 FPGA输出信号为 0。根据目标可知需要分别控制 4 个 LED 灯亮或灭,对应地需要 FPGA 产生 4 个信号。假定这 4 个信号分别为 led0、led1、led2 和 led3,将其分别与相应 LED 灯连接。操作中如果想控制LED0 亮,LED1~3 灭,则应控制 FPGA 输出的 led0 信号为 0,led1~3 信号都为 1。
硬件电路图的连接关系如下表所示,可以看到本工程中 LED0 连接的 FPGA 管脚为 AA4,对应
FPGA 工程信号为 led0;LED1 连接的 FPGA 管脚为 AB4,对应 FPGA 工程信号为 led1;LED2 连接的 FPGA 管脚为 AA5,对应 FPGA 工程信号为 led2;LED3 灯连接的 FPGA 管脚为 AB8,对应的FPGA 工程信号为 led3。工程的时钟管脚为 G1,对应 FPGA 工程信号为 clk,复位管脚为 AB12,对应 FPGA 工程信号为 rst_n。由此可见,本工程一共需要六个信号:时钟信号 clk、复位信号 rst_n 以及四个 LED 灯的控制信号 led0、led1、led2 和 led3。
基于此可以完成顶层信号代码的编写,将 module 的名称定义为 mdyBookHuxiLed。在顶层信号代码中需要将与外部相连接的输入/输出信号列出,从而实现信号与管脚的连接,其具体代码如下:
随后对信号的输入输出属性进行声明,指出对于 FPGA 来说这一信号属于输入还是输出,若为输入声明则为 input,若为输出声明则为 output。在本设计中由于 clk 是外部晶振输送给 FPGA 的,因此对于 FPGA 来说 clk 为输入信号 input。同样地,rst_n 是外部按键给 FPGA 的,在 FPGA 中也为输入信号 input。同时可知 led0、led1、led2、led3 四个信号是 FPGA 产生以实现对相应四个 LED灯的控制,因此 led0、led1、led2、led3 为输出信号 output。根据这些信息将输入输出端口定义补充完整,其代码如下:
3.2 信号设计
在进行架构设计前再次分析一下功能需求,首先 LED1 暗 1 秒,亮 1 秒变暗;接着 LED2 暗 1秒,亮 2 秒后变暗;随后 LED3 暗 1 秒,亮 3 秒后变暗;最后 LED4 暗 1 秒,亮 4 秒后变暗,如此循环往复。分析需求可以看出:当一个 LED 灯亮时,其余 LED 灯都属于灭的状态。如果将四个 LED 灯看成一个整体循环,可以把功能需求翻译成:LED0 复位后,灭 1 秒,亮 1 秒后灭 12 秒;LED1复位后,灭 3 秒,亮 2 秒后灭 9 秒;LED2 复位后,灭 6 秒,亮 3 秒后灭 5 秒;LED3,灭 10 秒后亮4 秒,四个 LED 灯以此为规律循环往复。
下面将以上逻辑翻译成信号来进行理解:
复位后,信号 led0=1 持续 1 秒,接着让 led0=0 持续 1 秒,然后让 led0=1 持续 12 秒,循环往复。
复位后,信号 led1=1 持续 3 秒,接着让 led1=0 持续 2 秒,然后让 led1=1 持续 9 秒,循环往复。
复位后,信号 led2=1 持续 6 秒,接着让 led2=0 持续 3 秒,然后让 led2=1 持续 5 秒,循环往复。
复位后,信号 led3=1 持续 10 秒,接着让 led3=0 持续 4 秒,循环往复。
以此为规律的信号波形图如下图所示。
从波形图中可看出信号 led0~led3 的变化单位最小是 1 秒,并且 4 个信号都是以 14 秒为一个周期进行循环往复。这样的情况下,至简设计法的思路是使用 2 个计数器,其中 1 个计数器用来确定 1秒的时间,另 1 个计数器用来计 1 秒的次数从而确定一个循环即 14 秒的时间。这两个计数器的使用为 led0~led3 的变化时间确定了标准。
本设计中将计算 1 秒次数的计数器命名为 cnt0。至简设计法的设计规则中有讲过,计数器的设计只考虑两个因素:加 1 条件和计数数量。只要确定好相应逻辑,就能完成计数器代码设计。本工程的工作时钟是 50MHz,时钟周期即 20ns,当计数器计数到1_000_000_000/20=50_000_000 个的时候就意味着 1 秒时间计时结束。该计数器始终不停地进行计数,因此可以认为其加 1 条件是一直有效的,可写成:assign add_cnt0==1。
可能会有同学提出疑问:加 1 条件的概念是什么?这里以停车位来进行比喻,一般情况下对每个停车位置会进行对应编号,但是如果某个位置上放置了一块石头无法作为停车位时,该位置就不能获得对应的编号。反之则可以认为停车位编号的加 1 条件就是:对应位置上没有石头,其可以继续进行编号,即 assign add_cnt0 = “没有石头”。因此如果在设计中计数器一直没有阻碍地进行计数工作,则可以认为加 1 条件是一直有效的。以往进行代码编写这一步骤时都是一行行的输入相应代码。至简设计法在这里提供一个小技巧,在节省编写代码的时间的同时在一定程度上降低了代码的出错率。至简设计法将日常代码中常用到的固定部分制作成模板,进行代码编程时可以调用相应模板后根据逻辑输入对应设计的变量将代码补充完整。此处就可以用模板编写计数器代码,下面带领同学们一起来学习一下这个炫酷的功能。
打开 GVIM 工具,在命令模式下输入“:Mdyjsq”,点击回车后调出了对应模板,如下图所示。之后再将本案例中的变量填到模板里面,就可以得到完整正确的计数器代码。
按照上文方法调出计数器模板后,将本案例中的变量填到模板里面,得到计数器 cnt0 的完整代码如下:
同样的,需要确定用于计时 14 秒计数器的加 1 条件和计数数量,将该计数器命名为 cnt1。该计数器的值每隔 1 秒进行加 1 操作,因此其加一条件为 cnt0 结束,用代码表示即为 end_cnt0。根据设计目标可知计数器 cnt1 用于确定周期时间,闪烁灯的一个周期是 14 秒,因此该计数器的计数数量应为 14。
此处可以继续调用代码模板,在 GVIM 的命令模式下输入“:Mdyjsq”,点击回车调出计数器模板后将“add_cnt1”和“end_cnt1”补充完整,得到代码如下:
确定好两个计数器的代码后请同学们思考一下四个 led 输出信号的变化。根据设计目标可知,LED 灯具有两种状态:亮和灭。对应地,四个 led 输出信号也两个变化点:变 0 和变 1。下面依次对四个 led 信号变化的原因进行判断,可以结合图 3.2-3 中的波形进行思考。首先来确定 led0,根据设计目标可以发现:led0 变 0 的原因是计数器完成了 1 秒计时,即当 add_cnt1 && cnt1==1-1 时,时间过去了一秒,led0 的值变为 0 来控制 LED0 点亮。而 led0 变 1 的原因则是计数器完成 2 秒时间计时,即 add_cnt1 && cnt1==2-1 时,led0 的值变为 1 从而控制 LED0 熄灭。在 GVIM 的编辑模式下输入“Shixu2”回车,调用至简设计法模板,将代码补充完整后得到 led0 信号的代码如下:
下面来思考输出信号 led1 的变化原因。同理,led1 的值变 0 的原因是计数到 3 秒时间,即 add_cnt1 && cnt1==3-1 时,led1 的值变为 0 从而控制 LED1 点亮。led1 变 1 的原因则是计数到 5 秒时间,即 add_cnt1 && cnt1==5-1 时,led1 的值变为 1 从而控制 LED1 熄灭。打开 GVIM,在编辑模式下输入“Shixu2”回车后调用至简设计法模板,将代码补充完整后得到 led1 信号的代码如图所示:
同样的逻辑来思考输出信号 led2 的变化。led2 变 0 的原因是计数到 6 秒时间,也就是 add_cnt1 && cnt1==6-1 时,led2 的值变为 0 从而控制 LED2 点亮。led2 变 1 的原因则是数到 9 秒时间,即add_cnt1 && cnt1==9-1 时,led2 的值变为 1 从而控制 LED2 熄灭。打开 GVIM,在编辑模式下输入“Shixu2”回车,调用至简设计法模板,将代码补充完整后得到 led2 信号的代码如下:
最后确定输出信号 led3 的变化。led3 变 0 的原因是计数到 10 秒时间,也就是 add_cnt1 && cnt1==10-1 时,led2 的值变为 0 从而控制 LED2 点亮。变 1 的原因是计数到 14 秒时间,即 add_cnt1 && cnt1==14-1,也就是 end_cnt1 时,led3 的值变为 1 从而控制 LED2 熄灭。在 GVIM 的编辑模式下输入“Shixu2”回车,调用至简设计法模板,将代码补充完整后得到 led0 信号的代码如下:
至此,主体程序已经完成。如果对四个输出信号代码的设计还存有疑虑,可以结合波形图再次着重了解一下设计目标,重新整理思路,完全吸收设计目标后再进行代码编写。反复领会吸收一定会有所收获。