16. 呼吸灯

16.1. 章节导读

在上一章节,我们使用板载LED等设计并实现了流水灯效果;在本章节,我们将带领读者设计并实现呼吸灯效果。

16.2. 理论学习

呼吸灯在我们的生活中很常见,在手机上多作为消息提醒指示灯而被广泛使用,其效果是小灯在一段时间内从完全熄灭的状态逐渐变到最亮,再在同样的时间段内逐渐达到完全熄灭的状态,并循环往复。这种效果就像“呼吸”一样,有张有弛,而且给人一种很舒服的感觉。其工作原理是利用PWM来控制小灯在相同时间段内的不同占空比, 即在同样小时间段内,小灯亮的时间依次增加到最大后再依次减小,从而实现渐亮到渐灭的“呼吸”效果。

16.3. 实战演练

16.3.1. 实验目标

使用板载LED灯D6实现呼吸灯效果。

16.3.2. 硬件资源

我们使用开发板上的一个LED灯去进行该实验的验证,如图 22‑1所示。

breath002

图 22‑1 硬件资源

由原理图可知,征途Pro开发板的led灯为低电平时点亮。如图 22‑2所示。

breath003

图 22‑2 led灯原理图

16.3.3. 程序设计

16.3.3.1. 模块框图

我们给模块取名为breath_led,同样本例我们也会用到计数器,所以时钟和复位信号也是必须有的,且输入只有时钟和复位信号。我们只需要控制一个led灯进行“呼吸”就可以了,所以输出是一个名为led_out的输出信号。根据上面的分析设计出的Visio框图如图 22‑3所示。

breath004

图 22‑3 呼吸灯模块框图

端口列表与功能总结如表格 22‑1所示。

表格 22‑1 呼吸灯模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

工作时钟,频率50MHz

sys_rst_n

1Bit

Input

复位信号,低电平有效

led_out

1Bit

Output

输出控制led灯

16.3.3.2. 波形图绘制

在画波形之前,我们先分析一下该如何才能让 led灯实现“呼吸”的效果。整个呼吸灯“呼吸”的效果分为两部分,一个过程是从灭到亮,另一个过程是从亮到灭。为了把复杂的问题简单化,我们把led整个“呼吸”的动作进行分解,先分析从灭到亮的过程,而从亮到灭则是与之相反的一个过程。

为了让“呼吸”的效果表达的比较完美,我们把从灭到亮的时间设置为1s,也就是led灯在1s的时间内完成从灭到亮的效果,同理从亮到灭也是一样的时间。我们知道在安全范围内给led灯供的电压越大led灯也就越亮,难道这里我们要控制led灯的电压渐渐变大吗?显然这是不现实的,所以我们用另一种方法,通过控制PW M的占空比来实现led灯越亮的效果。我们知道同一时间段内,如果供给led灯一个脉冲信号的低电平持续的时间越长(高电平持续的时间越短)led灯就越亮,我们就是通过调整PWM实现高低电平的占空来调控led灯的亮度,我们取n个相同的时间段,然后让低电平的持续时间按照相等的时间间隔逐渐增多,这样子我们看上去 的led灯就会越来越亮了。

知道了原理后我们就要进一步分析该如何具体实现,首先我们需要在1s的时间内产生很多个时间相等的小段,然后控制每个时间小段中低电平持续的时间渐渐增加就可以了。为了让“呼吸”效果显得更加细腻,我们把1s的时间分成1000个时间相等的时间小段,每个小段的时间就是1ms,这1000个1ms的时间小段中低电平持 续的时间还不能相等,要有一个从少到多渐渐递增的关系,为了让1000个1ms的时间小段都不一样,我们就需要将1ms再分成1000个时间小段,那么每一个时间小段就是1us, 在逐渐变亮的过程中我们可以让led灯在第1个1ms的时间小段内亮0us,即全灭;第2个1ms的时间小段亮1us;第3个1ms的时间 小段亮2us,……,第998个1ms的时间小段亮997us,第999个1ms的时间小段亮998us,第1000个1ms的时间小段亮999us。在逐渐变灭的过程中同理,我们划分的等级越多,“呼吸”的就越顺畅,看上去的效果就越好,但是人眼的分辨率的有限的,当大于这个分辨率人眼就看不出来这种差异了。

下面我们开始进行波形的绘制,首先画出输入时钟和复位的波形,根据分析我们至少需要设计三个计数器,分别为用于计数从灭到亮1s时间的cnt_1s计数器、用于划分1000个1ms时间小段的cnt_1ms计数器和用于划分1000个1us时间小段的cnt_1us计数器。然后就是要确定每个计数器计数开始和清零的条 件。我们先画出cnt_1us计数器的波形,根据50MHz的时钟计算得需要计50个数,即计数器从0计数到49,只要不复位则计数器一直计数,cnt_1us计数器计数满即可清零。接下来就是产生cnt_1ms计数器的波形,我们想是不是可以利用已经产生好的cnt_1us计数器来产生cnt_1ms计数器呢?答案 是当然可以的,我们完全没有必要再重新产生一个计数1ms的计数器,而是利用已经做好的1us计数器,根据cnt_1us计数器的时间计算得每当cnt_1us计数器计数到49时cnt_1ms计数器加1,cnt_1ms计数器计数到999且cnt_1us计数器同时也计数到49时cnt_1ms计数器才能清零,如果 少了cnt_1us计数器计数到49这个条件,仿真时会发现最后一个1ms的时间小段是不够1ms时间的。我们也用同样的方法产生cnt_1s计数器,每当cnt_1ms计数器计数到999且cnt_1us计数器计数到49时cnt_1s计数器加1,cnt_1s计数器计数到999且cnt_1ms计数器计数到999 且cnt_1us计数器计数到49三个条件同时满足时cnt_1s计数器清零(条件如此多的原因和cnt_1us计数器的分析是相同的,编写代码仿真时可以观察效果)。设计好的三个计数器的波形如图 22‑4所示。

breath005

图 22‑4 计数器产生波形图

我们产生了所需要的三个计数器后新的问题来了,我们该如何才能让每一个1ms的时间小段的低电平持续时间都不一样且渐渐增加呢?这是本实验的一个难点所在,之所以说是难点在于需要我们仔细观察、联想才能找到答案,而且还涉及到一个小小的技巧问题。如图 22‑5所示,我们先画出呼吸灯从灭到亮时间段预期的led_ou t波形图,结合三个计数器我们发现led_out为低电平时间时cnt_1s计数器的计数值总是小于cnt_1ms计数器的计数值,这简直就是太完美的发现了,我们让cnt_1s计数器的计数值大于cnt_1ms计数器的计数值就可以实现每一个1ms的时间小段的低电平持续时间都不一样且渐渐增加的效果。同理呼吸灯从 亮到灭正好是相反的过程,如图 22‑6所示,要使cnt_1s计数器的计数值总是大于cnt_1ms计数器的计数值才能实现每一个1ms的时间小段的低电平持续时间都不一样且渐渐减小的效果。

breath006

图 22‑5 呼吸灯波形图(一)

breath007

图 22‑6 呼吸灯波形图(二)

为了区分从灭到亮和从亮到灭两个过程我们使用一个标志信号,这个标志信号不是脉冲,而是一个电平,更准确的来讲应该叫使能信号,这里我们取名为cnt_1s_en,如图 22‑7和图 22‑8所示,cnt_1s_en使能信号为低电平时实现呼吸灯从灭到亮的过程,cnt_1s_en使能信号为高的时候实现呼吸灯从亮到灭的过程,cnt_1s_en使能信号就这样来回取反就可以了,取反的条件和cnt_1s清零的条件相同即可,这样才能够表示完整的1s时间。

breath008

图 22‑7 呼吸灯波形图(三)

breath009

图 22‑8 呼吸灯波形图(四)

16.3.3.3. 代码编写

为了后面仿真和修改呼吸灯“呼吸”的时间,我们将计数器的计数个数用parameter进行定义。呼吸灯模块参考代码如下。

代码清单 22‑1 呼吸灯模块参考代码(breath_led.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
module breath_led
#(
parameter CNT_1US_MAX = 6'd49 ,
parameter CNT_1MS_MAX = 10'd999 ,
parameter CNT_1S_MAX = 10'd999
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位

 output reg led_out //输出信号,控制led灯
 );

 ////
 //\* Parameter and Internal Signal \//
 ////
 //reg define
 reg [5:0] cnt_1us ;
 reg [9:0] cnt_1ms ;
 reg [9:0] cnt_1s ;
 reg cnt_1s_en ;

 ///
 //\* Main Code \//
 ////
 //cnt_1us:1us计数器
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_1us <= 6'b0;
 else if(cnt_1us == CNT_1US_MAX)
 cnt_1us <= 6'b0;
 else
 cnt_1us <= cnt_1us + 1'b1;

 //cnt_1ms:1ms计数器
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_1ms <= 10'b0;
 else if(cnt_1ms == CNT_1MS_MAX && cnt_1us == CNT_1US_MAX)
 cnt_1ms <= 10'b0;
 else if(cnt_1us == CNT_1US_MAX)
 cnt_1ms <= cnt_1ms + 1'b1;

 //cnt_1s:1s计数器
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_1s <= 10'b0;
 else if(cnt_1s == CNT_1S_MAX && cnt_1ms == CNT_1MS_MAX
 && cnt_1us == CNT_1US_MAX)
 cnt_1s <= 10'b0;
 else if(cnt_1ms == CNT_1MS_MAX && cnt_1us == CNT_1US_MAX)
 cnt_1s <= cnt_1s + 1'b1;

 //cnt_1s_en:1s计数器标志信号
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_1s_en <= 1'b0;
 else if(cnt_1s == CNT_1S_MAX && cnt_1ms == CNT_1MS_MAX
 && cnt_1us == CNT_1US_MAX)
 cnt_1s_en <= ~cnt_1s_en;

 //led_out:输出信号连接到外部的led灯
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 led_out <= 1'b0;
 else if((cnt_1s_en == 1'b1 && cnt_1ms < cnt_1s) \|\|
 (cnt_1s_en == 1'b0 && cnt_1ms > cnt_1s))
 led_out <= 1'b0;
 else
 led_out <= 1'b1;

 endmodule

16.3.3.4. 仿真验证

仿真文件编写

编写仿真文件对呼吸灯模块进行仿真验证,呼吸灯模块仿真参考代码如下。

代码清单 22‑2 呼吸灯模块仿真参考代码(tb_breath_led.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
\`timescale 1ns/1ns
module tb_breath_led();

////
//\* Parameter and Internal Signal \//
////
//wire define
wire led_out ;

 //reg define
 reg sys_clk ;
 reg sys_rst_n ;

 ///
 //\* Main Code \//
 ////
 //初始化系统时钟、全局复位
 initial begin
 sys_clk = 1'b1;
 sys_rst_n <= 1'b0;
 #20
 sys_rst_n <= 1'b1;
 end

 //sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50MHz
 always #10 sys_clk = ~sys_clk;

 ////
 //\* Instantiation \//
 ////
 //-------------------- breath_led_inst --------------------
 breath_led
 #(
 .CNT_1US_MAX(6'd4 ),
 .CNT_1MS_MAX(10'd9 ),
 .CNT_1S_MAX (10'd9 )
 )
 breath_led_inst
 (
 .sys_clk (sys_clk ), //input sys_clk
 .sys_rst_n (sys_rst_n ), //input sys_rst_n

 .led_out (led_out ) //output led_out
 );

 endmodule

仿真波形分析

打开ModelSim执行仿真,仿真出来的波形如图 22‑9所示,我们在仿真的时候通过修改参数的方式将计数器的值同比例缩小,所以让仿真运行了50us后就可以比较明显的观察到led_out的变化了,可以发现led_out信号在cnt_1s_en脉冲标志信号为低时高电平时序的时间是渐渐增加的,对应的led 灯会渐渐变亮;而在cnt_1s_en脉冲标志信号为低时高电平时序的时间是渐渐减小的,对应的led灯会渐渐变灭。是我们想要的结果,可以进行下板验证结果了。

breath010

图 22‑9 呼吸灯仿真波形图(一)

如图 22‑10所示,我们将三个计数器计数的关键位置处的波形放大,可以发现cnt_1us计数器计数到4时,cnt_1ms计数器加1,同时cnt_1us计数器清零;cnt_1us计数器计数到4时cnt_1ms计数器加1,cnt_1ms计数器计数到9且cnt_1us计数器计数到4时cnt_1ms计数器清 零;cnt_1us计数器计数到4且cnt_1ms计数器计数到9时cnt_1s计数器加1,cnt_1s计数器计数到9且cnt_1ms计数器计数到9且cnt_1us计数器计数到4时cnt_1s计数器清零,符合预先的设计思想。

breath011

图 22‑10 呼吸灯仿真波形图(二)

仿真验证通过后我们可以修改RTL代码中的条件来深刻体会计数器缺少条件带来的后果,可能在最后上板观察led灯“呼吸”的效果时差别不是很明显,但是仿真时可以观察到的很清晰,为了严谨一定不能缺少条件。

16.3.4. 上板验证

16.3.4.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 22‑2所示。

表格 22‑2 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

input

E1

时钟

sys_rst_n

input

M15

复位

led

output

L7

LED灯

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 22‑11所示。

breath012

图 22‑11 管脚分配

16.3.4.2. 结果验证

管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源与下载线与开发板连接好,如图 22‑12所示。

breath013

图 22‑12 下载连线图

打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下载,随后界面会显示下载成功,如图 22‑13所示。

breath014

图 22‑13 下载成功图

下载成功后即可以开始验证了,板卡上电后LED灯D6出现呼吸灯效果;则说明验证成功。

16.4. 章末总结

本章我们实现的是一个呼吸灯的例子,很多学习者感觉呼吸灯的例子是一个跨越式的内容,它虽然是一个简单的基础内容,没有新语法也没有太复杂的功能,但却给初学者带来不小的困难,让初学者一下子没有了思路,感觉很难实现。很多其他的教程都是采用的上帝手法,没有从细节上带着大家分析如果思考实现呼吸灯的例子,导致大家不 会去分析,以为呼吸灯是一个很难的例子,其实并不是这样的,我相信大家在看了前面详细的分析思考过程,一定能够感觉到呼吸灯的实现思路是非常清晰且简单的。

知识点总结

1、深刻体会呼吸灯的分析过程,如何把一个复杂的问题简单化,然后顺着解决问题的思路思考下去;

2、继续深入体会parameter参数化常数所带来的好处,以及用法;

3、多体会计数器如何控制,什么时候开始计数、什么时候清零,千万要考虑全面,不要忘记条件。

16.5. 拓展训练

本例中我们实现了一个led灯的“呼吸”,而板子上有4个led灯,我们可以修改代码让4个led灯的呼吸频率都不同步,大家可以尝试一下。