文章目录
1 章节导读
在前面的几章节当中,我们实现了我们的机械按键和触摸按键控制我们的 LED 灯的一个小实验。那么在本章节当中,我们来一次 LED 灯小实验的一个进阶,我们利用开发板板载的 4 个 LED 灯完成一个流水灯的实验。
2 理论学习
那么如果大家之前玩过单片机的肯定知道,流水灯实验绝对是一个经典的例程,它的效果是让排列成一排的 LED 灯依次闪亮,那么像流水一样循环不止,看上去特别的舒服。
它的原理就是依次控制每个连接到 LED 灯的 I/O 口的高低电平的变化。
那么我们本次的实验是让我们的 LED 灯依次点亮,点亮间隔是 0.5s,换句话说就是:让排列成一排的 LED 灯每次只点亮一个,每次点亮的时间是 0.5s。那么这样就实现了流水灯的一个效果,而且我们的肉眼还能够分辨出来。
本讲视频的理论知识是比较简单的,我们直接开始实战演练。
3 实战演练
3.1 实验目标
在实战演练部分我们将使用实验工程,设计并实现一个流水灯电路。
3.2 硬件资源
使用板载的 4 个 LED 灯 D6、D7、D8、D9 验证实验工程
3.3 程序设计
那么首先先来搭建一下文件体系
然后打开 doc 文件夹建立一个 Visio 文件,绘制模块框图和波形图
3.3.1 模块框图
那么首先是我们模块框图的绘制。那么在前边我们已经提到了,我们的 LED 灯每次点亮的时间是 0.5s 那么肯定需要一个计数器来对 0.5s 进行计时,那么需要计数器就肯定需要时钟信号和复位信号
我们的输入信号除了时钟信号和复位信号之外,不需要其他的输入信号了。那么下面就是我们的输出信号
因为我们要控制 4 个 LED 灯,所以说输出信号把它的位宽设为 4 位宽;它的每一个比特位控制一个 LED 灯,刚好是控制 4 个 LED 灯。
那么这样,模块框图绘制完成,那么下面开始波形图的绘制。
3.3.2 波形绘制
首先我们还是先将时钟和复位信号的波形画出
那么时钟信号和复位信号波形绘制完成。
因为我们的 LED 灯依次闪亮的时间间隔是 0.5s 这儿需要一个计数器来计数 0.5s 的时间,我们要声明一个内部的变量。
已知我们的时钟频率是 50 MHz 50\text{MHz} 50MHz 计数时间是 0.5 s 0.5\text{s} 0.5s 那么计数的最大值应该是 M = ( 0.5 s ) / ( 1 / 50 MHz ) = ( 0.5 s ) / ( 2 × 1 0 − 8 s ) = 2.5 × 1 0 7 \text{M} = (0.5\text{s}) / (1/50\text{MHz}) = (0.5\text{s}) / (2\times10^{-8}\text{s}) = 2.5\times10^7 M=(0.5s)/(1/50MHz)=(0.5s)/(2×10−8s)=2.5×107
那么首先我们的计数器它的初值是 0,那么计数器每个时钟周期自加一;那么经过计算,在系统时钟 50 MHz 50\text{MHz} 50MHz 的频率下计 0.5 s 0.5\text{s} 0.5s 的时间需要计数的个数是 2.5 × 1 0 7 2.5\times10^7 2.5×107,这就表示我们的计数器需要从 0 计数到 2.5 × 1 0 7 − 1 = 24999999 2.5\times10^7 - 1 = 24999999 2.5×107−1=24999999。我们这儿采取一个省略的画法;当我们的计数器计数到最大值就对它进行清零,开始下一个周期的计数
那么这样我们计数器的波形绘制完成。
那么计数器的波形绘制完成之后,我们还需要一个脉冲信号。为什么需要这个脉冲信号呢?
我们可以用脉冲信号作为条件,控制我们的 LED 灯它的一个流水的一个效果。
当我们的复位信号有效时,给我们脉冲信号赋一个初值低电平;当我们的计数器计数到最大值减一的时候,让它保持一个高脉冲,也就是一个时钟周期的高电平;那么其他时刻,让它依然保持低电平
那么这样脉冲信号的波形已经绘制完成。
下面就可以开始绘制我们的输出信号。当我们的复位信号有效时,给我们的输出信号赋一个初值就是 4'b1110
。为什么要赋这个初值呢?
因为我们的 LED 灯是低电平点亮,这样就表示点亮第一个灯,其他的灯就保持一个熄灭的状态;然后让它在计数器的第一个计数周期内也保持这个值,也就是说前 0.5s 点亮第一个 LED 灯。那么当它检测到我们的脉冲标志信号时,把它的值变为 4'b1101
也就是说点亮第二个 LED 灯,其他的灯熄灭。然后在下一个脉冲信号有效时,把它变为 4'b1011
那么第三个灯就点亮,其他灯熄灭。下一个周期就变成了 4'b0111
。那么再下一个周期,又回到了我们的初值 4'b1110
那么这个波形就是输出信号的波形。
那么这样就实现了按照顺序每次只点亮一个 LED 灯,而且它的点亮时间是 0.5s 就是一个计数周期。它对应的效果图应该是这个样子
那么第一次是将从右往左的第一个灯点亮,点亮时间是 0.5s;那么第二个周期这个灯熄灭,然后点亮从右往左的第二个 LED 灯,让它点亮 0.5s;那么第三次就是将从右往左的第三个 LED 灯点亮,其他灯处于熄灭状态;那么第四次就是将从右往左的第四个 LED 灯点亮,其他灯处于熄灭状态。每次点亮单个灯的时间为 0.5s,然后第五个状态就回到初始的一个状态,那么这样依次往下循环,就实现了一个流水灯的一个效果,与上面绘制的波形是对应的。
3.3.3 代码编写
那么下面参照这个波形图就开始代码的编写
那么首先是模块开始、模块名称、端口列表,然后是模块结束;我们这儿先加一个参数,什么参数呢?就是我们的 LED 灯它的计数最大值,那么加在这儿方便我们的实例化,也方便我们的仿真验证;那么下面是端口列表,有两路输入、一路输出,这儿要注意我们的输出信号是有位宽的,位宽是 4 那么这样端口列表编写完成
声明两个寄存器:一个是计数器,一个是脉冲信号。那么计数器的位宽是 25 位宽,如果说我们在使用计数器的时候位宽不确定,可以使用计算器来计算一下:先输入我们十进制的最大值,然后在这个二进制部分就可以看到它的位宽。下面是我们的脉冲标志信号
那么下面开始赋值,我们使用 always 语句、使用异步复位。当我们的复位信号有效时,我们的计数器赋一个初值,初值是 0 与我们的波形图要对应;然后当它计数到最大值的时候,对它进行一个清零,那么这儿就可以用到我们定义的参数了,那么参数的定义它的好处前面已经讲过了,这儿就不再进行叙述,当计数器计数到最大值归零;那么当我们的复位信号无效且计数器没有计数到最大值,那么每个时钟周期让它自加一
那么这样就完成了计数器的赋值。
那么下面是我们的脉冲信号的赋值,同样使用 always 语句。当我们的复位信号有效时,我们的脉冲信号初值为 0 与这个位置是对应的;当我们的计数器计数到最大值减一的时候,让我们的脉冲信号保持一个时钟周期的高电平,也就是一个高脉冲;那么其他时刻,我们的脉冲信号依然保持我们的低电平
那么这样参照波形图,我们的脉冲信号的代码也编写完成。
下面开始输出信号的赋值。输出信号的赋值我们仍然使用 always 语句,当我们的复位信号有效时给它一个初值,就是 4’b1110
那么到了这里,怎么实现我们 LED 灯的流水效果呢?
我们就想到了,在 Verilog 语法当中我们讲到了两个运算符就是左移运算符和右移运算符,那么左移运算符它是两个小于号 <<
,那么右移运算符是两个大于号 >>
这个很容易记住,那个箭头指向哪儿就是往哪个方向移动。
参照这个图我们看到
我们要使用左移的符号,而且每次是左移一位。
那么这儿代码就可以这样编写:让它自己左移一位,然后再赋给它自己
那么这样就实现了一个流水灯的一个效果。这儿大家还要考虑一点是什么呢?
当左移到这个位置,开始下一个循环的流水的时候,这个值要怎么给?
所以说这儿还需要再加一个条件,这儿要加一个什么样的条件呢?就是在这个位置,当我们的输出信号是 4’b0111 而且我们的 cnt_flag 信号有效时,再把这个初值赋给我们的输出信号,就是这个位置,我们这样编写:当我们的输出信号等于 4’b0111 而且我们的标志信号有效,那这个位置就给它一个初值。那么这儿大家还要考虑,这个位置还需要加一个条件,什么条件呢?当我们的标志信号有效时才能进行移位,要不然它就一直在移位,那么其他时刻让它保持原来的值不变
那么这样我们的代码编写完成
water_led.v
module water_led
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
output reg [3:0] led_out
);
reg [24:0] cnt;
reg cnt_flag;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 25'd0;
else if (cnt == CNT_MAX)
cnt <= 25'd0;
else
cnt <= cnt + 25'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if (cnt == (CNT_MAX-1))
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
led_out <= 4'b1110;
else if ((led_out == 4'b0111)
&&(cnt_flag == 1'b1))
led_out <= 4'b1110;
else if (cnt_flag == 1'b1)
led_out <= led_out << 1;
else
led_out <= led_out;
endmodule
下面进行代码的编译。
3.3.4 代码编译
回到我们的桌面
然后添加我们编写的代码,然后进行一次全编译,检查语法错误
3.3.5 仿真验证
那么下面就开始编写我们的仿真代码
那么首先是时间参数,然后是我们的模块开始、模块名称,然后端口列表为空,模块结束,然后声明变量,将我们的输出信号引出,它的位宽是 4 位宽
然后使用 initial 语句赋初值,然后是时钟信号的频率,然后是我们的实例化,然后重定义我们的参数,我们这里就把它定义为一个 25’d24 我们随便定义的,你也可以选择其他的数字,然后保存
这样我们的仿真文件编写完成
tb_water_led.v
`timescale 1ns/1ns
module tb_water_led();
reg sys_clk;
reg sys_rst_n;
wire[3:0]led_out;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
water_led
#(
.CNT_MAX (25'd24)
)
water_led_inst
(
.sys_clk (sys_clk ),
.sys_rst_n(sys_rst_n),
.led_out (led_out )
);
endmodule
那么回到我们的实验工程,添加我们的文件,然后再次全编译,检查我们的仿真文件当中的错误,点击 OK
然后是仿真设置,然后开始仿真
那么仿真编译完成之后,打开 sim 窗口添加我们的模块波形;然后回到波形界面,全选、分组、消除前缀;然后点击 Restart 我们这儿先设置一个 3us 然后运行一次试一下,然后全局视图;那么这儿,调整一下波形的位置,切换一下进制
这儿就发现了两个问题,什么问题呢?第一个是给输出信号的赋值和波形图不对应,第二个是完成一个周期的流水灯循环后没有重新赋初值。
第一个我们忽略的一个问题,那么第一个周期内点亮的是第一个 LED 灯,第二个是点亮的后两个,第三个是点亮了后三个,那么第四次就全部点亮了。这是为什么呢?因为我们忽略了一点:我们的左移和右移,会在后面的空位是补 0 而不是补高电平,如果补高电平的话这儿就对了;但是它是补 0,所以说这儿是有问题的,而且与我们的波形图是不对应的。那么这儿怎么修改呢?
我们想到了一个方法,再添加一个变量。什么意思呢?大家来看一下
我们还需要添加一个新的变量 led_out_reg[3:0] 是对输出信号的一个寄存,它既然左移和右移那么空位是补 0 的话,我们就这样进行绘制:我们这儿初值给它什么呢?初值给它一个 4’b0001 然后让它在第一个周期也保持 4’b0001,也就是说到这个位置它不是左移补 0 吗?那就让它补 0。那么后面就应该这样来写了:那么既然是左移是补 0 那么就是 4’b0010 左移一位后面补 0 嘛,没问题;那么这儿 4’b0100;那么这儿又是补得 0,没问题,那么这个位置应该是 4’b1000 然后这儿又回到了一个初值 4’b0001
那么输出信号对我们新加的这个变量进行一个取反,就得到了 4’b1110、4’b1101、4’b1011、4’b0111、4’b1110
那么这样就应该能解决这个问题。
下面参照这个新的波形图修改一下代码。
那么首先声明一个新的变量 led_out_reg
它的位宽也是 4 位宽,那么这儿直接是对我们的变量进行赋值;然后替换。那么输出信号既然这儿没有一个延迟一个时钟周期的一个效果,那肯定是用组合逻辑,这儿使用 assign 语句,使用 assign 语句,然后对它进行一个取反
那么修改完成之后保存
water_led.v
module water_led
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
output reg [3:0] led_out
);
reg [24:0] cnt;
reg cnt_flag;
reg [3:0] led_out_reg;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 25'd0;
else if (cnt == CNT_MAX)
cnt <= 25'd0;
else
cnt <= cnt + 25'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if (cnt == (CNT_MAX-1))
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
led_out_reg <= 4'b0001;
else if ((led_out_reg == 4'b1000)
&&(cnt_flag == 1'b1))
led_out_reg <= 4'b0001;
else if (cnt_flag == 1'b1)
led_out_reg <= led_out_reg << 1;
else
led_out_reg <= led_out_reg;
assign led_out = ~led_out_reg;
endmodule
回到我们的仿真文件,然后回到 Library 对修改的代码进行一个重新的编译;然后回到这个波形界面,点击 Restart 清除已存在波形,然后再运行一次、全局视图
那么这样我们参照我们绘制波形图来看一下这个波形。那么首先先看一下计数器,那么切换一下它的格式,这样我们的计数器它的初值为 0 然后每个时钟周期自加一,加到最大值 24 归零,没有问题
然后看我们的脉冲信号。最大值减一的时候,拉高一个时钟周期高电平,那么因为是时序逻辑,延迟一个时钟,刚好与最大值对应,这儿是没有问题的
然后最后看我们最重要的信号——输出信号。那么输出信号的初值 4’b1110 没问题;那么下一个计数周期变为 4’b1101 也没有问题;然后是 4’b1011 没有问题;然后是 4’b0111 也没有问题;那么一个流水过去之后,下一个流水的初值是 4’b1110 那么这儿也是没有问题的
我们的仿真波形与我们绘制波形图是一致的,那么这样仿真验证成功。
3.4 上板验证
3.4.1 引脚绑定
下面绑定我们的管脚,那么这里绑入 L7 这儿是 M6 那么下面是 P3 这里是 N3;然后我们的时钟信号是 E1;复位按键是 M15。那么绑定完成之后进行一次全编译,全编译完成,点击 OK
3.4.2 结果验证
那么同样的按照下图所示连接我们的下载器和我们的电源,那么下载器的另一端连接到电脑,开发板上电
回到工程,那么点击 Programmer 这个位置打开下载界面,添加我们的 SOF 文件,然后点击开始,下载程序
然后看一下运行结果
那么这儿可以明显的看出我们的 LED 灯是呈现了一个流水的一个效果,那么大家也可以调整这个保持时间,我们的保持时间是 0.5s 那么大家通过调整保持时间,我们让它看起来更流畅,那么各位朋友可以自己试一下。
那么上板验证成功。
那么以上就是本章节的全部内容。
我们的流水灯的实现其实和我们前面的计数器和我们的分频器没有多大的差别,差别之处就在于:我们的 LED 灯是怎么实现流水的一个效果,就用到了我们前面讲到的左移、右移的语法,那么希望大家能够掌握这个语法。
参考资料: