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中使用了除法或者乘法
这些问题我会在呼吸灯讲解(下)中解决,这篇文章也写了七千多字了,就暂时先这样吧~