写在前面的话
这一节呢,我们来实现一个流水灯驱动程序的编写,当然啦,点灯不是目的,最重要的是我们通过这个流水灯代码的实现可以掌握一些重要的规范。
项目需求
我们要求流水灯模式如下:当复位键按下时,灯全部熄灭,当复位键放开以后,首先,点亮第一个灯,然后第一个灯熄灭,同时点亮第二个灯,接着,第二个灯熄灭,同时点亮第三个灯,然后,第三个灯熄灭,同时点亮第四个灯,最后第四个灯熄灭,同时点亮第一个灯,如此循环往复,实现流水。
相关技术介绍
项目需求,我相信大家已经看清楚了,那么,接下来我们该怎么做呢?写代码?NO 我们来仔细的看看项目需求,这里面涉及到了按键,LED灯,还需要我们用按键控制流水灯的启动或停止。那么,在写代码之前,我们首先应该明确按键按下和放开有什么区别、LED是低电平点亮还是高电平点亮。只有清楚外设的性能,我们才可能编写代码正确地驱动这些外设。代码体现的是我们的思路,所以在写代码之前我们必须首先理顺自己的思路,否则盲目的编写代码,一定是徒劳的。
硬件设计
下图所示为轻触按键与FPGA的连接关系示意图
由上述电路图可知,当按键放开时,FPGA端口等于接到了上拉电阻,所以检测到的为高电平。当按键按下时,FPGA端子通过按键接到了地平面,检测到的为低电平。
下图所示为LED与FPGA的连接关系示意图
由上述电路图可知,LED正极全部接到了3.3V电源。那么,只有当FPGA端口给出低电平的时候,LED才会点亮。当FPGA端给高电平时,LED熄灭。
顶层架构设计
项目需求以及项目需求中所涉及到的所有外设都已经分析清楚了,那么接下来是不是可以开始编写代码了呢?答案还是—NO!哈哈,别着急,其实对于工程师而言,编写代码真的是小菜一碟,闭着眼睛都能敲几行哦。一个项目最重要的、最终决定成败的一般来说不是代码的具体实现,而是前期的架构设计,好的架构可以化简为易,将一个很复杂的工程逐步的拆分成很多简单的子模块,不但提高了设计效率和成功率,同时也比较适合团队作战,分工合作。如果在开始设计代码之前没有一个明朗的架构,做到最后甚至可能会发现原理根本无法通过,整体架构需要重新设计,那么这样就得不偿失啦。如下图所示为我们流水灯模块的一个基本架构,虽然很简单,甚至简单到了只有一个模块,但是呢,我们一定要养成这样的一个习惯。
端口描述
端口名 |
功能 |
clk |
系统时钟输入(50MHz) |
rst_n |
复位按键(低电平有效) |
pio_led |
4bit led驱动端口 |
代码解释
终于可以写代码了,具体代码实现步骤(先写什么,后写什么),大家可以观看梦翼师兄配套的视频。在这里,我们直接给出具体代码如下
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function:流水灯 *****************************************************/ 01 module led_learn( 02 clk, //系统时钟输入 03 rst_n, //系统复位 04 pio_led//LED驱动输出 05 ); 06 //系统输入 07 input clk; //系统时钟输入 08 input rst_n; //系统复位 09 //系统输出 10 output reg [3:0]pio_led;//LED驱动输出 11 //中间寄存器定义 12 reg [1:0]state;//状态寄存器定义 13 //LED驱动逻辑 14 always@(posedge clk or negedge rst_n) 15 begin 16 if(!rst_n) 17 begin 18 pio_led<=4'b1111;//LED全部熄灭 19 state<=0;//寄存器赋初值 20 end 21 else begin 22 case(state) 23 0:begin 24 pio_led<=4'b0111;//第一个灯点亮 25 state<=1;//状态跳转 26 end 27 1:begin 28 pio_led<=4'b1011;//第二个灯点亮 29 state<=2;//状态跳转 30 end 31 2:begin 32 pio_led<=4'b1101;//第三个灯点亮 33 state<=3;//状态跳转 34 end 35 3:begin 36 pio_led<=4'b1110;//第四个灯点亮 37 state<=0;//状态跳转 38 end 39 default:state<=0; 40 endcase 41 end 42 end 43 endmodule |
这里我们应用的就是状态机的概念,第一步进行某些操作,然后跳到下一步进行其他操作,直到所有操作完成以后再返回初始状态
那么我们现在想当然写出来的逻辑是不是可以正确实现我们的目标呢?这其实还是一个大大的问号,那么接下来,我们需要做的就是进行逻辑仿真,梦翼师兄在教学过程中常说的一句话就是“对不对,只有仿真说了算”。
接下来我们编写仿真代码如下
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function:流水灯测试模块 *****************************************************/ 01 `timescale 1ns/1ps //时间单位和精度定义 02 module tb; 03 04 //系统输入 05 reg clk; //系统时钟输入 06 reg rst_n; //系统复位 07 //系统输出 08 wire [3:0]pio_led;//LED驱动输出 09 10 initial 11 begin 12 clk=0; 13 rst_n=0; 14 #1000.1 rst_n=1; 15 end 16 17 always #10 clk=~clk;//50MHz时钟 18 19 led_learn led_learn( 20 .clk(clk), //系统时钟输入 21 .rst_n(rst_n), //系统复位 22 .pio_led(pio_led)//LED驱动输出 23 ); 24 endmodule |
运行仿真软件Modelsim 查看仿真波形(软件具体操作请观看梦翼师兄配套教学视频)
由仿真图,我们可以看到在每个时钟的上升沿pio_led的值发生一次切换,从第一个到第四个灯依次循环点亮,说明逻辑设计正确。
好了,波形仿真正确了,如果下载到开发板,流水灯可以实现了吗?答案还是NO!什么?梦翼师兄疯了吧,一会儿说逻辑对了,一会儿又说流水灯还不能实现,到底能不能愉快的玩耍啦?哈哈,这就是仿真和真实环境条件切换的一个误区。大家试想一下,我们现在每来一个时钟,LED就会切换一次状态,那么由于我们的时钟是50MHz,因此我们的时钟周期仅仅为20Ns,这么快的速度,人的眼睛是无法分辨的,那么在这种条件下,我们当然是看不到流水现象的。
那么如何才能看到流水呢?对,我们必须降低LED的切换速度。那么LED的切换速度和谁有关系呢?第一当然是时钟,假设时钟速度很慢,那么流水的速度自然也会很慢,第二个原因就是状态机,因为不管时钟速度如何,状态机只要控制自己的状态不要那么快切换,那么也可以实现我们的目的。
那么,首先让我们从状态机的角度去控制LED的切换速度,我们给出修改的代码如下
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function:流水灯 *****************************************************/ 01 module led_learn( 02 clk, //系统时钟输入 03 rst_n, //系统复位 04 pio_led//LED驱动输出 05 ); 06 //系统输入 07 input clk; //系统时钟输入 08 input rst_n; //系统复位 09 //系统输出 10 output reg [3:0]pio_led;//LED驱动输出 11 //中间寄存器定义 12 reg [1:0]state;//状态寄存器定义 13 reg [40:0]counter;//计数寄存器 14 //LED驱动逻辑 15 always@(posedge clk or negedge rst_n) 16 begin 17 if(!rst_n) 18 begin 19 pio_led<=4'b1111;//LED全部熄灭 20 state<=0;//寄存器赋初值 21 counter<=0;//计数器赋初值 22 end 23 else begin 24 case(state) 25 0:begin 26 pio_led<=4'b0111;//第一个灯点亮 27 if(counter<12)//延时时间不够则继续计数 28 counter<=counter+1; 29 else 30 begin 31 state<=1;//状态跳转 32 counter<=0;//计数器清零 33 end 34 end 35 1:begin 36 pio_led<=4'b1011;//第二个灯点亮 37 if(counter<12)//延时时间不够则继续计数 38 counter<=counter+1; 39 else 40 begin 41 state<=2;//状态跳转 42 counter<=0;//计数器清零 43 end 44 end 45 2:begin 46 pio_led<=4'b1101;//第三个灯点亮 47 if(counter<12)//延时时间不够则继续计数 48 counter<=counter+1; 49 else 50 begin 51 state<=3;//状态跳转 52 counter<=0;//计数器清零 53 end 54 end 55 3:begin 56 pio_led<=4'b1110;//第四个灯点亮 57 if(counter<12)//延时时间不够则继续计数 58 counter<=counter+1; 59 else 60 begin 61 state<=0;//状态跳转 62 counter<=0;//计数器清零 63 end 64 end 65 default:state<=0; 66 endcase 67 end 68 end 69 endmodule |
该段代码和上一段代码唯一的区别之处在于我们在状态机状态跳转的部分加入了延时计数器,只有累计时钟上升沿达到一定数量以后,状态才会发生跳转。这样就可以有效延长每个LED灯点亮的时间。当然啦,这里面计数值到12完全是梦翼师兄为了仿真方便而乱写的一个数,如果真的要实现流水灯,计数值应该很大才可以哦。接下来让我们看一下这段代码的仿真波形
对比两个波形图,我们可以看到加延时计数器之后,流水灯的切换速度明显变慢,可以实现我们的设计要求。通过该实验,我们同时也学到了计数器的用法。那么还剩下一种方式,那就是控制我们的频率了,我们设计代码如下
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function:流水灯 *****************************************************/ 01 module led_learn( 02 clk, //系统时钟输入 03 rst_n, //系统复位 04 pio_led//LED驱动输出 05 ); 06 //系统输入 07 input clk; //系统时钟输入 08 input rst_n; //系统复位 09 //系统输出 10 output reg [3:0]pio_led;//LED驱动输出 11 //中间寄存器定义 12 reg [1:0]state;//状态寄存器定义 13 reg [40:0]counter;//计数器定义 14 reg clk_slow;//慢时钟定义 15 //时钟分频电路 16 always@(posedge clk or negedge rst_n) 17 begin 18 if(!rst_n) 19 begin 20 counter<=0; 21 clk_slow<=0; 22 end 23 else 24 begin 25 if(counter<12) 26 counter<=counter+1; 27 else 28 begin 29 counter<=0; 30 clk_slow<=~clk_slow; 31 end 32 end 33 end 34 35 //LED驱动逻辑 36 always@(posedge clk_slow or negedge rst_n) 37 begin 38 if(!rst_n) 39 begin 40 pio_led<=4'b1111;//LED全部熄灭 41 state<=0;//寄存器赋初值 42 end 43 else begin 44 case(state) 45 0:begin 46 pio_led<=4'b0111;//第一个灯点亮 47 state<=1;//状态跳转 48 end 49 1:begin 50 pio_led<=4'b1011;//第二个灯点亮 51 state<=2;//状态跳转 52 end 53 2:begin 54 pio_led<=4'b1101;//第三个灯点亮 55 state<=3;//状态跳转 56 end 57 3:begin 58 pio_led<=4'b1110;//第四个灯点亮 59 state<=0;//状态跳转 60 end 61 default:state<=0; 62 endcase 63 end 64 end 65 endmodule |
以上代码16-33行为我们添加的计数器分频模块,通过计数器计数,利用快时钟分频得到一路慢时钟。代码36行
36 always@(posedge clk_slow or negedge rst_n) |
我们用分频得到的慢时钟作为状态机的切换时钟,这样也可以有效控制状态机切换的速度。查看仿真波形如下
由仿真图可知,第一:快时钟正确生产了慢时钟 第二:LED在慢时钟的控制下进行切换,说明我们的逻辑设计正确。通过该实验,我们也可以学会分频电路的设计方法。这也是梦翼师兄的初衷,尽可能通过一个实验项目多学东西,后面我们就可以利用这些知识点来解决其他问题啦。