> 技术文档 > FPGA基础代码1——呼吸灯(上)(超详细,手把手教学!!!)

FPGA基础代码1——呼吸灯(上)(超详细,手把手教学!!!)


目录

引入

1.什么是PWM波

2.实现任意占空比PWM波

(1.)编写代码

(2)编写仿真代码

(3)仿真验证

3.实现最简单的呼吸灯

(1)实现思路

(2)最简单呼吸灯代码编写

(3)仿真验证

4.最简单的呼吸灯下板子验证 

5.总结


引入

        首先我们需要了解什么是呼吸灯,呼吸灯实际上在我们的日常生活中非常常见,就比如一些手机充电的时候,手机上就会有一闪一闪的LED灯珠,从灭到亮再从亮到灭,这就是呼吸灯。而如果我们想要使用Verilog开发FPGA从而实现呼吸灯的话,就需要学习PWM波,从而实现呼吸灯。

1.什么是PWM波

PWM是一种通过调节脉冲的​​占空比​​(高电平时间占整个周期的比例)来模拟不同电平的技术。

周期(Period)​​:一个完整PWM波的时间长度(如1ms)。

​占空比(Duty Cycle)​​:高电平时间占周期的百分比(如50%)

上图便是一个60%占空比的PWM波的波形图

其中每五个clk周期便是一个PWM周期,其中前两个clk时钟周期,PWM信号为低,后三个clk时钟周期PWM信号为高——3/5*100%=60%

PWM的周期和占空比讲解完了之后,我们再来讲解一下介绍中的这句话 “模拟不同电平的技术”

就拿LED举例子,假如我给这个LED灯5V电压,其亮度为100(只是假设的值),那么我给这个LED灯2.5V的电压,其亮度应该为50(理想情况)

但是我们都知道FPGA开发板的IO口的输出电压是不好调节的,并且就算调节输出电压的话也没有办法很连贯的调节,比如0.2V,0.015V这些都是做不到的

因此我们就需要PWM波来模拟这种电平,比如上面的60%占空比的PWM波,假如我们FPGA开发板的输出电压是5V,那么60%占空比的PWM波就能够模拟3V的输出(5V*60%),LED灯的亮度为60,就会比正常情况下的LED灯暗一点

综上,我们可以发现只要我们调节PWM波的占空比,就可以调节LED灯的亮度,只要将PWM波的占空比连贯的调节,那么我们就可以实现呼吸灯了!)

2.实现任意占空比PWM波

(1.)编写代码

在实现呼吸灯之前,我们先来实现输出任意占空比的PWM波,这样子才方便我们接下来去编写代码实现呼吸灯

首先我会将整体代码全部粘贴出来,然后分点的详细解释,将整体代码贴出来只是为了方便大家复制和查看,所以我推荐大家先粗略将整体代码看一眼,有一个印象,随后再查看下面的详细解释

`timescale 1ns / 1ps//输出任意占空比的PWM波module PWM_MAKER ( input clk, // 系统时钟(50MHz) input rst_n, // 低电平复位 output pwm // PWM输出);parameter CLK_FREQ = 5000_0000; // 50MHZ系统时钟parameter PWM_FREQ = 1000; // PWM周期为1000HZ/S,也就是1msparameter MS_CNT_MAX = CLK_FREQ / PWM_FREQ; //在50MHZ的系统时钟下,每50000个系统时钟周期等于1msparameter DUTY_CYCLE = 30; // 占空比30%(0~100可调)parameter DUTY_THRESHOLD = (MS_CNT_MAX * DUTY_CYCLE) / 100; // 计算阈值(比如在30%占空比情况下,那么我们就需要计算出来毫秒计数器计数最大值的30%)reg [15:0] ms_cnt; //毫秒计数器//毫秒计数器always @(posedge clk or negedge rst_n) begin if (!rst_n) begin ms_cnt = MS_CNT_MAX - 1) ms_cnt <= 0; else ms_cnt <= ms_cnt + 1; endend// PWM输出assign pwm = (ms_cnt < DUTY_THRESHOLD) ? 1 : 0; // 比较阈值然后输出PWMendmodule

首先,大部分的FPGA开发板的时钟都是50MHZ,因此本代码中使用的便是50MHZ,如果大家开发板的时钟频率不同的话,请自己在parameter中调整就可以!

parameter CLK_FREQ = 5000_0000; // 50MHZ系统时钟parameter PWM_FREQ = 1000; // PWM周期为1000HZ/S,也就是1msparameter MS_CNT_MAX = CLK_FREQ / PWM_FREQ; //在50MHZ的系统时钟下,每50000个系统时钟周期等于1ms

在开始编写PWM生成代码之前,我们先回忆一下PWM波的两个重要知识,分别是PWM周期,PWM占空比

周期的话是任意指定的,比如1ms一个周期,10ms一个周期都可以,在本次实验中我们使用的是1ms作为PWM周期

那么现在我们就需要计算出来多少个系统时钟周期等于一个PWM时钟周期了,这也就是这行代码的用处:parameter MS_CNT_MAX = CLK_FREQ / PWM_FREQ;

计算的结果是每50000个系统时钟周期等于一个PWM时钟周期,也就是1ms

接下来,我们需要开始指定PWM波的占空比:

parameter DUTY_CYCLE = 30; // 占空比30%(0~100可调)parameter DUTY_THRESHOLD = (MS_CNT_MAX * DUTY_CYCLE) / 100; // 计算阈值(比如在30%占空比情况下,那么我们就需要计算出来毫秒计数器计数最大值的30%)

在这里,我们设定占空比为30%,也就是一个PWM周期中,有30%的时间会输出高电平,剩下的70%时间输出为低电平

既然我们知道30%时间为高电平,那么我们就需要计算出来30%的时间(也就是30%个PWM周期)是多少个系统时钟周期,这也就是这行代码的用处:parameter DUTY_THRESHOLD = (MS_CNT_MAX * DUTY_CYCLE) / 100; 

到现在为止,我们已经计算出来了多少个系统时钟周期为一个PWM周期,以及计算出来了多少个系统时钟周期为PWM波中高电平的时间

有了这两个数值之后,我们终于可以开始编写代码了!

reg [15:0] ms_cnt; //毫秒计数器//毫秒计数器always @(posedge clk or negedge rst_n) begin if (!rst_n) ms_cnt = MS_CNT_MAX - 1) ms_cnt <= 0; else ms_cnt <= ms_cnt + 1;end

上面的代码是毫秒计数器的代码,也是我们一个PWM周期计数的代码

其的核心逻辑就是:如果复位信号有效,则直接清零,如果毫秒计数器记满(也就代表1ms过去了),那么我们也需要将计数器清零,这样才能持续且重复的计数

最后,如果既没有复位信号,也没有记满一个PWM周期(1ms),那么就自己加一,直到记满

是不是非常的简单!

毫秒计数器的代码编写完了之后,我们就需要开始编写PWM波输出的逻辑了

// PWM输出assign pwm = (ms_cnt < DUTY_THRESHOLD) ? 1 : 0; // 比较阈值然后输出PWM

这部分的代码也十分的简单,就只有一行

其的核心逻辑就是比较现在的毫秒计数器和“30%占空比所占的系统时钟数量”进行比较,如果毫秒计数器小于这个值,则输出高电平,反之输出低电平

(这部分我担心大家可能看不太懂,因此详细的讲解一下,如果你看懂了,那可以直接无视这个括号内的内容

ms_cnt的计数基准是clk,也就是系统时钟,每一个系统时钟周期其加一,当其计数了50000个系统时钟周期后代表1ms过去了,也就是一个PWM周期过去了

而DUTY_THRESHOLD这个常量的产生逻辑是:(MS_CNT_MAX * DUTY_CYCLE) / 100

也就是:毫秒计数器的计数最大值(也就是50000)*占空比(这里是30%,也就是0.3,但是Verilog中别用小数,因此我们先扩大一百倍,最后除以100倍)

故,DUTY_THRESHOLD这个常量的计数基准也是clk(其数值为15,000个clk)

综上,每50000个系统时钟周期(也就是一个PWM周期)中,前15000个系统时钟周期(也就是DUTY_THRESHOLD这个常量的值)输出高电平,其余时间输出低电平,从而实现了我们30%占空比的PWM波输出)

(2)编写仿真代码

`timescale 1ns / 1nsmodule tb_top();reg clk ;reg rst_n ;wire pwm ;initial begin rst_n = 0 ; clk = 0 ; #100 rst_n = 1 ;endalways #1 clk = ~clk ;PWM_MAKER u_PWM_MAKER( .clk (clk), .rst_n (rst_n), .pwm (pwm));endmodule

仿真代码没什么好说的,在本项目中我们没有引入外部输入,比如按键之类的,因此我们只需要不断产生一个时钟信号就可以(之所以使用#1 clk = ~clk,而不是#10,是为了更快的仿真出结果罢了)

(如果你不是很懂仿真的话,我简单的讲解一下,initial代表一开始会发生什么,然后#x代表延迟多少ns,always表示一直去做什么事情

在这个仿真代码中,我们initial也就是一开始让复位信号为0,时钟为0,等待100ns后复位信号为1

然后我们always也就是一直让clk每1ns翻转一次,从而产生了时钟信号)

(3)仿真验证

通过仿真波形图我们可以清晰的看出来,我们成功的实现了30%占空比的PWM波的输出

3.实现最简单的呼吸灯

(1)实现思路

在上面,我们成果的实现了任意占空比(只需要修改常量DUTY_CYCLE即可)的PWM生成代码,现在我们可以开始编写一个最简单的呼吸灯了

至于实现的思路也很简单,就是让输出的PWM波的占空比连续改变,比如:1%,2%,3%……98%,99%,100%这样子

也就是说,我们要编写代码来让PWM波的占空比自己改变!

好了,咱们现在自己来给自己布置一个任务,实现一个最简单的呼吸灯,让其每秒钟从灭到亮的变化

(2)最简单呼吸灯代码编写

和之前一样,先将全部代码粘贴出来,然后再详细的讲解

`timescale 1ns / 1ps//在一秒钟内逐渐变亮的呼吸灯代码module PWM_LED( input clk ,//输入的系统时钟 input rst_n ,//输入的系统复位 output led//输出的led灯);parameter CLK_FREQ = 5000_0000 ;//50MHZ系统时钟parameter PWM_FREQ = 1000 ;//PWM周期为1000HZ/S,也就是1msparameter MS_CNT_MAX = CLK_FREQ/PWM_FREQ ;//在50MHZ的系统时钟下,每50000个系统时钟周期等于1msparameter S_CNT_MAX = PWM_FREQ ;//秒计数器是基于毫秒计数器进行工作的,毫秒计数器记满1000次等于一秒钟过去了parameter CNT_MULTI = MS_CNT_MAX / S_CNT_MAX ;//毫秒计数器和秒计数器之间,计数最大值的倍数关系reg [9:0] s_cnt ;//秒计数器reg [15:0] ms_cnt;//毫秒计数器//毫秒计数器always@(posedge clk or negedge rst_n)begin if(!rst_n)begin ms_cnt = MS_CNT_MAX - 1\'b1)begin ms_cnt <= 16\'b0 ; end else begin ms_cnt <= ms_cnt + 1\'b1 ; endend//秒计数器always@(posedge clk or negedge rst_n)begin if(!rst_n)begin s_cnt = S_CNT_MAX - 1\'b1 && (ms_cnt >= MS_CNT_MAX - 1\'b1))begin s_cnt <= 10\'b0 ; end else if(ms_cnt == MS_CNT_MAX - 1\'b1)begin s_cnt = s_cnt)?0:1;//将毫秒计数器的值除以50(倍率),这样子除下来的值最大值就为1000,和秒计数器的最大值相同endmodule

在第一部分PWM波代码编写当中,我们都知道PWM波最终输出的代码是只有一行的:

assign pwm = (ms_cnt < DUTY_THRESHOLD) ? 1 : 0; // 比较阈值然后输出PWM

其中的DUTY_THRESHOLD就是当前占空比(比如30%占空比)输出高电平的时间(计数基准是系统时钟,30%占空比为15000个clk;不过请注意,这些数值都是建立在PWM周期为1ms的,如果PWM周期改变,DUTY_THRESHOLD也要改变,相信聪明的你一定知道为什么!) 

而我们前面又说了,实现呼吸灯实际上就是修改PWM的占空比

因此,我们实际上需要改变的就是DUTY_THRESHOLD的值,那么在编写呼吸灯的代码中,这个值就不能和之前一样是一个常量了,而是需要代码自己改变的

所以我们定义了一个新的寄存器:

reg [9:0] s_cnt ;//秒计数器parameter PWM_FREQ = 1000 ;//PWM周期为1000HZ/S,也就是1msparameter S_CNT_MAX = PWM_FREQ ;//秒计数器是基于毫秒计数器进行工作的,毫秒计数器记满1000次等于一秒钟过去了

这个秒计数器的计数最大值为1000,其的计数基准是ms_cnt

用人话来说就是:当ms_cnt每记满一次,s_cnt加一,代表1ms过去了

如果s_cnt记满了,则清零,代表1s钟过去了(也代表着呼吸灯从灭到亮结束了)

而这个计数器,就将代替我们之前定义的常量DUTY_THRESHOLD

(至于为什么能够代替,听我接下来详细讲解)

首先,我们将秒计数器的逻辑写出来:

//秒计数器always@(posedge clk or negedge rst_n)begin if(!rst_n)begin s_cnt = S_CNT_MAX - 1\'b1 && (ms_cnt >= MS_CNT_MAX - 1\'b1))begin s_cnt <= 10\'b0 ; end else if(ms_cnt == MS_CNT_MAX - 1\'b1)begin s_cnt <= s_cnt + 1\'b1 ; endend

秒计数器的实现比较简单,只需要注意秒计数器清零的时候,一定是其本身已经最大了,且ms计数器也最大了

而毫秒计数器和PWM生成部分一样,就不再次重复了,直接看最关键的代码部分,也就是呼吸灯输出的部分:

//呼吸灯assign led = ((ms_cnt / CNT_MULTI) >= s_cnt)?0:1;//将毫秒计数器的值除以50(倍率),这样子除下来的值最大值就为1000,和秒计数器的最大值相同parameter CNT_MULTI = MS_CNT_MAX / S_CNT_MAX ;//毫秒计数器和秒计数器之间,计数最大值的倍数关系

核心逻辑就是,如果ms_cnt的值除以一个常数后,还大于s_cnt的值,那么就输出0

(这个常数比较关键,因为ms_cnt的最大值为50000,而s_cnt的值为1000,两者想要相互比较后再生成任意占空比的PWM波的话,就需要将最大值统一一下,为什么要统一一下,下面会讲

这里他们两者之间的最大值相差为50倍,故将ms_cnt的值除以50,当然也可以将s_cnt的值乘以50,而乘消耗的资源是比除要小的,不过这种超级简单的工程就不用考虑什么资源占用了,随便写就行)

(带入数值举例:

当ms_cnt记满了3次之后,s_cnt的值就为3,那么会在ms_cnt的值为0-149的时候,输出高电平,占空比是0.3%

同理,如果s_cnt为100的话,那么在ms_cnt的值为0-4999的时候输出高电平,占空比是10%

相信聪明的你一定看出来了,s_cnt的值改变,成功的将PWM输出的占空比也改变了

这也就是为什么要把ms_cnt的最大值和s_cnt的最大值统一一下,只有它们两个最大值想等的情况下,才能输出最高为100%占空比的PWM波

如果不统一的话,ms_cnt的最大值比s_cnt的最大值大50倍,那么就会导致最多只能输出2%占空比的PWM波(就是0-2%占空比)

大家可以在不统一到1000的情况下,带入一些数据计算一下,就会发现最多只能输出2%占空比的PWM波了)

(3)仿真验证

仿真代码和PWM波生成的仿真代码是一样的,不需要改变的(只有例化部分改动一下就可以,核心代码不变)

`timescale 1ns / 1nsmodule tb_top();reg clk ;reg rst_n ;wire led ;initial begin rst_n = 0 ; clk = 0 ; #100 rst_n = 1 ;endalways #1 clk = ~clk ;PWM_LED u_PWM_LED( .clk (clk), .rst_n (rst_n), .led (led));endmodule

通过上面的仿真波形图可以看出,PWM的生成逻辑和我们预期的一样,当s_cnt为100的时候,ms_cnt为4999的时候,输出的PWM波(led信号)才会从高电平变为低电平

通过上面的仿真波形图可以看出,当s_cnt == 100的时候,PWM波的占空比为10%(可以肉眼估测,也可以算时间) 

4.最简单的呼吸灯下板子验证 

绑管脚,综合,生成比特流最后下板(如何下板属于基础中的基础了,我这个系列只讲代码部分,如果很多人都不懂如何下板,甚至不懂如何创建工程,乃至不知道如何下载vivado或者FPGA开发工具的话,我会看情况出文章讲解)

开发板板载LED灯成功的在每秒钟内从灭到亮不断变化

(不过请注意,由于该变化是线性变化的,而人眼对亮度的感知是非线性的(近似对数关系),因此如果你也下板了后,就会发现这玩意亮度变化好像有点不均匀啊

这是正常现象,代码是正确的)

5.总结

现在我们成功的实现了一个最简单的呼吸灯,不过相信聪明的你一定发现了许多问题,就比如:

一般的呼吸灯不都是从灭到亮,再从亮到灭吗?

在verilog中使用了除法或者乘法

这些问题我会在呼吸灯讲解(下)中解决,这篇文章也写了七千多字了,就暂时先这样吧~