昨晚刚做了个呼吸灯,感觉挺不错的,给大家分享一下(打call打call)
呼吸灯还是很经典的,之前无论学什么嵌入式,在定时器PWM部分总是先拿这玩意开刀(doge)
先谈谈个人体会。
嵌入式定时器部分利用递增的计数器来调节占空比(即PWM),来实现呼吸灯,而再FPGA里,我们就是从头搭建一个计数器,然后类似的通过计数器中的值来递增或递减占空比。
1.原理分析
思路还是很清晰的哈,把大体模块先说下:
如果从全灭到全亮需要2s,我们可以设置1000个亮度,每个亮度显示2ms;接着把每个亮度分成1000等分,每个等分2us。这1000个2us有的需要led亮,有的需要led灭。这最小的1000等分就是PWM占空比的最小单位。
反正我看了好几次才明白啥意思,所以再给大家放张图解释一下:

大家看到下面的led脉冲比,可以发现占比的最小单位是2us,也就是说,再我们的系统中,2us是时间的最小单位。之后,2ms在2us的基础上计数,2s在2ms基础上计数。
而为什么要把2ms再次打散呢?因为你要想实现PWM,把每2ms作为一个亮度,那就不能在ms级上操作(因为每2ms中需要led有亮有灭),所以我们再把2ms分成1000分,在us级上操作led,这样对于更宏观的ms级就是不同亮度了。
2.敲代码......
我们先考虑一下顶层模块:
cpp
module breath(
input sys_clk,
input sys_rst,
output reg led
);
endmodule
很简单哈,除了时钟和复位的输入,就是led的输出了。
基于上面的原理,需要我们先把2us的块写出来。
cpp
reg [6:0] cnt;
parameter CNT = 7'd100;
always @(posedge sys_clk or negedge sys_rst) begin
if(!sys_rst)
cnt <= 7'd0;
else if(cnt < CNT - 7'd1)
cnt <= cnt + 7'd1;
else
cnt <= 7'd0;
end
打过流水灯和按键消抖这些新手村后,计数器这种小case对咱们这些大佬 小白 来说肯定时毫无难度了哈!不过有一点我觉得可以分享一下。
写C时的循环长这个德行:
- for(int i=0;i<CNT;i++)
这样就循环了CNT次。
但Verilog里不是这样啊!因为for语句里的i++是在循环结束后对i进行++,而always块里是立即执行,也就意味着如果你还这么(cnt < CNT)写,那就会执行CNT+1次,这肯定不是我们所期望的,所以我在if里多减了1。
还有,虽然我们现在可以在例化模块时传参,但把最大值定位为参数,可以使代码更简洁可读,推荐这样做。
接下来我们做ms级和s级模块。
ms和s的难点在于,你需要将cnt_2ms和cnt_2s在最大值时清零。相比之下cnt_2us好办,因为cnt_2us清零本来就是if当中的一种情况。但是!!
cnt_2ms的清零如果也像us级那样写,就像下面这样:
cpp
always @(posedge sys_clk or negedge sys_rst) begin
if(!sys_rst)
cnt2 <= 10'd0;
else if(cnt == CNT - 7'd1)
cnt2 <= cnt2 + 10'd1;
else
cnt2 <= 10'b0 ;
end
仿真出来就会这样:

会发现cnt_2ms压根连3都数不到!
所以cnt_2ms的清零是有额外条件的,不能简单把它扔到else里。
一种理解方式是:需要cnt_2ms数过 最大值时,才能允许清零。
所以我们可以这样写:
cpp
if(!sys_rst || (cnt2 > CNT2))
cnt2 <= 10'd0;
另一种理解方式是从波形图中来的:当cnt_2ms和cnt_2us都数到最大值时,才允许清零。
cpp
if(!sys_rst_n ||
(cnt_2ms == (CNT_2MS_MAX - 10'b1)&& (cnt_2us == (CNT_2US_MAX - 7'b1))))
cnt_2ms <= 10'b0;
我把相关字加粗了,大家仔细理解一下~
这两种方式实现的效果难道有区别吗??
别急~ 我们仿真看一下,第一种:

第二种:

区别很明显了吧......
(悄悄说一句其实第一种这种蹩脚的操作是我写的......)
咳咳,毋庸置疑,第一种也是可以完美实现的!!稀烂
事后我就开始琢磨,为啥我没想到第二种写法呢?
哎~ 其实我刚刚已经透露一点点了。
另一种理解方式是从波形图中来的
个人观点:我是按循环的思路来写的,第二种是按信号状态写的。或许在某种意义上,我仍深陷于软件编程的泥沼......
一起努力吧!
s级和ms级计数器一样一样的,就不废话啦,相信大家现在已经做出来从全灭到全亮的程序啦~
对于逆向过程,只需要设置一个标志位,然后根据标志位对led进行状态翻转即可,也没啥太大难度,大家自己改改调试一下就好。
之前看过不少程序思路分享,我就发现昂,大部分文章中的代码都是一截截的,这样做固然有道理,因为讲解得一段段讲。但对初学者就很不友好,因为我们大部分在学的时候几乎没有能力构建起程序框架,只会抄。所以在这个阶段,我们可能连拼出完整代码的机会都没有。
于是,我决定把整个代码给大家贴出来:
cpp
module breath(
input sys_clk,
input sys_rst,
output reg led
);
reg [6:0] cnt_2us;
reg [9:0] cnt_2ms;
reg [9:0] cnt_2s;
reg flag;
parameter CNT_2US = 7'd100;
parameter CNT_2MS = 10'd1000;
parameter CNT_2S = 10'd1000;
always @(posedge sys_clk or negedge sys_rst) begin
if(!sys_rst)
cnt_2us <= 7'd0;
else if(cnt_2us < CNT_2US - 7'd1)
cnt_2us <= cnt_2us + 7'd1;
else
cnt_2us <= 7'd0;
end
always @(posedge sys_clk or negedge sys_rst) begin
if(!sys_rst ||
((cnt_2ms == CNT_2MS - 10'd1) &&
(cnt_2us == CNT_2US - 7'd1)))
cnt_2ms <= 10'd0;
else if(cnt_2us == CNT_2US - 7'd1)
cnt_2ms <= cnt_2ms + 10'd1;
else
cnt_2ms <= cnt_2ms;
end
always @(posedge sys_clk or negedge sys_rst) begin
if(!sys_rst)begin
cnt_2s <= 10'd0;
flag <= 1'b1;
end
else if((cnt_2us == CNT_2US - 7'd1) &&
(cnt_2ms == CNT_2MS - 10'd1)&&
(cnt_2s == CNT_2S - 10'd1))begin
cnt_2s <= 10'd0;
flag <= ~flag;
end
else if(cnt_2ms == CNT_2MS - 10'd1 && cnt_2us == CNT_2US - 7'd1)
cnt_2s <= cnt_2s + 10'd1;
else
cnt_2s <= cnt_2s;
end
always @(posedge sys_clk or negedge sys_rst) begin
if(!sys_rst)
led <= 1'b0;
else if(cnt_2ms < cnt_2s)begin
if(flag)
led <= 1'b1;
else
led <= 1'b0;
end
else begin
if(flag)
led <= 1'b0;
else
led <= 1'b1;
end
end
endmodule
我在改代码的过程中,遇到了一些问题,在这里总结一下,也是给大家填填坑~
- 还是多驱动源的问题,不要在多个always块里给一个信号赋值,比如程序中flag的复位,本来放哪里都可以,但因为翻转逻辑在2s那里,所以我写在那。
- cnt_2s清零的条件注意一下,需要当cnt_2us、cnt_2ms和cnt_2s同时达到最大值-1时执行,所以 if 中需要三个条件
仿真波形也很好看:

看到板子上小灯丝滑过度,心里很开心的~~
本文不长,这次的分享也算是对计数器应用的进一步深入,也希望大家能通过练习这个小demo进一步获得水平的提升!
不知道后面会先写状态机还是IP核,可能又要闭关一段时间了,大家期待吧~~
如果有不明白或错误之处,也希望大家在评论区给出,帮助大家的同时也能再次提升自己对于FPGA和Verilog的理解,感谢大家!!
系列链接:
上一篇:Verilog和FPGA的自学笔记8------按键消抖与模块化设计
下一篇:码字ing......