(原创)用Verilog实现一个参数化的呼吸灯(Verilog,CPLD/FPGA)
1.Abstract
观察到一个有趣的现象,每当把Apple笔记本合上的时候,那个白色的呼吸灯就会反复地由暗渐明,然后又由明渐暗,乍一看就像Apple笔记本在打盹休息一样,十分可爱!于是突发奇想,要不用Verilog也写一个吧,资源也不需要太多,一个LED灯就可以了。为了使用方便,可以把它做成参数化的,可以根据时常进行参数调节;深睡、浅睡跟清醒的时候呼吸频率似乎是不一样的…
下面就来分析和实践一下。
2.Content
2.1 理论分析
根据上述描述的现象,仔细分析一下,就是对LED灯亮的占空比设置,画张图来表示更直观一些。
为了表达得更明晰一些,将LED这根信号线用红色凸显出来。从左边往右边看吧,在RST的复位信号之前是一个不确定的逻辑(前一个状态,可能是0,也可能是1),所以用黑色块加深表示,复位后LED输出低电平(将电路设计为LED信号为1时驱动灯管亮)。过一段时间以后,输出一个占空比为1%的高电平,再过一段时间,输出为2%的高电平,依次往后直到输出为100%的高电平,这样现象就是灯管逐渐变亮;要是灯管逐渐变暗,直接将逻辑反过来就可以了。
2.2 整体设计
纵观整个灯的变化过程,可以归结为4个过程,依次为逐渐变亮、亮保持、逐渐变灭、灭保持,依次用S0,S1,S2,S3表示;若有复位信号产生,则所有状态立刻转到S0;若没有复位信号产生,那么就在这个状态机里边循环。用状态转换表和状态图来表示就容易理解得多。
TAB2.1 状态转换表
当前状态 | 跳转条件 | 下一个状态 |
S0 | RST / !S0_end | S0 |
S0 | S0_end | S1 |
S1 | RST | S0 |
S1 | !S0_end | S1 |
S1 | S0_end | S2 |
S2 | RST | S0 |
S2 | !S0_end | S2 |
S2 | S0_end | S3 |
S3 | RST / S3_end | S0 |
S3 | !S3_end | S3 |
用状态图来表示。
虽然转换表与状态转换图表达的功能都是一样的,乍一看上去,还是觉得状态装换图更加容易接受一些,红色表示非正常状态下的转换,绿色表示正常循环。
状态转换就设计完了,每一个状态的时长比较长,都是秒为单位的,下面就是关于怎样点灯了。灯的亮灭跟状态有关,每一个状态下灯的变化是不同的。在S0下,灯是随着占空比的增加而逐步变亮的,这样的重复多次,才能达到柔和平缓的效果(只重复一次的话,变换节凑就太明显了,效果生硬不堪);在S1下,是灯亮的至高点,衔接灯逐步变亮到开始变暗的过度;在S2下,灯开始逐步变灭,和S1一样,灯随着占空比的增加而逐步变暗,这样的过程需要重复多次,达到一个柔和平缓的效果;在S3下,灯完全熄灭,衔接灯逐步变暗到开始逐步变亮的过度。
展开S0或S2下的一个灯逐步变化的过程,以S0为例。S0从占空比为0逐步变化到100%,对应的每个占空比下也应该分多个循环,进行控制LED灯。这样看的话,就好像有一个层次结构,主层次占空比是S0逐步从0增加至100%,频率比较小,次级层次则是对应当前主层次的占空比进行比较,也是从1逐步增加到100,若小于当前占空比,则灯亮,否则,灯灭,它的频率比较大。分三种层次来看,从整体上看,就是四种状态的切换,频率最小;从主层次上的某个占空比看,LED灯亮的程度取决于当前主层次上这个占空比;从次级层次上来看,如果在主层次的占空比内,则亮灯,否则就灭灯。
整个逻辑描述就完成了,下面根据这个思路开始描述电路。因为对复位没有太严格的要求,所以做成同步时序逻辑电路。
/* ----------------------------------------------- Module Name: breath Module Function: 参数化的呼吸灯 Module Input: CLK 时钟, RST 复位 Module Output: LED LED灯 Module Reference: None Note: 参数设定 ####################################### CLK_50M 时钟设定,默认是50M Hz UP_TIME 上升时间,单位为秒 HD_TIME 最亮保持时间,单位为秒 H2_TIME 熄灭保持时间,单位为秒 DN_TIME 下降时间,单位为秒 ####################################### ---------------------------------------------*/ module breath(LED,CLK,RST); output reg LED = 0; input CLK; input RST; // 时钟参数化 parameter CLK_50M = 28'd50_000_000; // 时间参数化 parameter UP_TIME = 1; parameter HD_TIME = 1; parameter H2_TIME = 1; parameter DN_TIME = 3; parameter CNT_MAIN_UP_FULL = CLK_50M / 200 * UP_TIME; // 1/2 时间百分比 parameter CNT_MAIN_DN_FULL = CLK_50M / 200 * DN_TIME; parameter CNT_SUB_UP_FULL = CNT_MAIN_UP_FULL / 10000; // 占空比内分为100个子份 parameter CNT_SUB_DN_FULL = CNT_MAIN_DN_FULL / 10000; parameter CNT_HD_FULL = CLK_50M * HD_TIME; // 保持时间 parameter CNT_H2_FULL = CLK_50M * H2_TIME; // 状态机 上升-高亮保持-下降-熄灭保持-上升 // S0 -> S1 -> S2 -> S3 -> S0 parameter S0 = 2'd0, S1 = 2'd1, S2 = 2'd2, S3 = 2'd3; // 伪时钟 reg main_clk = 0; reg sub_clk = 0; // 计数器 reg [27:0] cnt_main_clk = 28'd0; reg [27:0] cnt_sub_clk = 28'd0; reg [27:0] cnt_hold = 28'd0; reg [7:0] cnt_main = 8'd0; reg [7:0] cnt_sub = 8'd0; reg [1:0] status = 2'd0; // 状态结束标志 reg up_end = 1'b0; reg hd_end = 1'b0; reg h2_end = 1'b0; reg dn_end = 1'b0; // 记录上一个时钟状态 reg last_main_clk = 1'b0; reg last_sub_clk = 1'b0; always @(posedge CLK) begin if(!RST) // 复位 begin cnt_main_clk <= 28'd0; cnt_sub_clk <= 28'd0; cnt_hold <= 28'd0; main_clk <= 1'b0; sub_clk <= 1'b0; cnt_main <= 8'd0; cnt_sub <= 8'd0; status <= 2'd0; LED <= 1'b0; up_end <= 1'b0; hd_end <= 1'b0; h2_end <= 1'b0; dn_end <= 1'b0; end else begin // 当前状态确定 case(status) S0: begin // 上升状态 if(cnt_main_clk > CNT_MAIN_UP_FULL - 1) // 产生主时钟 begin cnt_main_clk <= 28'd0; main_clk <= ~main_clk; end else begin cnt_main_clk <= cnt_main_clk + 1; end if(cnt_sub_clk > CNT_SUB_UP_FULL - 1) // 产生子时钟 begin cnt_sub_clk <= 28'd0; sub_clk <= ~ sub_clk; end else begin cnt_sub_clk <= cnt_sub_clk + 1; end end S1: begin // 高亮保持 if(cnt_hold > CNT_HD_FULL - 1) begin cnt_hold <= 28'd0; hd_end <= 1'b1; end else begin cnt_hold <= cnt_hold + 1; end end S2: begin // 下降 if(cnt_main_clk > CNT_MAIN_DN_FULL - 1) // 产生主时钟 begin cnt_main_clk <= 28'd0; main_clk <= ~main_clk; end else begin cnt_main_clk <= cnt_main_clk + 1; end if(cnt_sub_clk > CNT_SUB_DN_FULL - 1) // 产生子时钟 begin cnt_sub_clk <= 28'd0; sub_clk <= ~ sub_clk; end else begin cnt_sub_clk <= cnt_sub_clk + 1; end end S3: begin // 熄灭保持 if(cnt_hold > CNT_H2_FULL - 1) begin cnt_hold <= 28'd0; h2_end <= 1'b1; end else begin cnt_hold <= cnt_hold + 1; end end endcase // 主沿处理 if(last_main_clk != main_clk) begin last_main_clk <= main_clk; if(main_clk) //模拟上升沿 begin cnt_main <= cnt_main + 1; if(cnt_main > 8'd99) begin cnt_main <= 8'd0; if(status == S0) up_end <= 1'b1; if(status == S2) dn_end <= 1'b1; end end end // 子沿处理 if(last_sub_clk != sub_clk) begin last_sub_clk <= sub_clk; if(sub_clk) // 模拟上升沿 begin if(cnt_sub == 8'd99) begin cnt_sub <= 8'd0; LED <= 1'b1; end else begin cnt_sub <= cnt_sub + 1; if(status == S0) begin if(cnt_sub < cnt_main)LED <= 1'b1; //逻辑正 else LED <= 1'b0; end if(status == S2) begin if(cnt_sub < cnt_main)LED <= 1'b0; //逻辑负 else LED <= 1'b1; end end end end // 状态机切换 if(up_end) beginup_end <= 1'b0;cnt_main_clk <= 28'd0;cnt_sub_clk <= 28'd0;status <= S1;
LED <= 1'b1;end if(hd_end) beginhd_end <= 1'b0;cnt_hold <= 28'd0;status <= S2;end if(dn_end) begindn_end <= 1'b0;cnt_main_clk <= 28'd0;cnt_sub_clk <= 28'd0;status <= S3;
LED <= 1'b0;end if(h2_end) beginh2_end <= 1'b0;cnt_hold <= 28'd0;status <= S0;end end end endmodule
源码看起来挺多的,主要分成三大部分,时钟产生、沿检测和数据处理、状态切换。设计的是上升和下降时间分割成100份,更符合占空比设计,每一个占空比下循环检测100次,达到平缓柔和切换的效果。看看生成出来的逻辑网表,生成的RTL视图下的逻辑网表很大,最后以附件的方式完整给出,这里就截取局部的看一下吧。
红色的线是指CLK时钟,状态机输入均与触发器连接,保证电路是沿触发操作。中间的黄色部分是一个状态机,表示的是上述理论设计中状态的切换,打开看一下。
注意观察下红色线,这样的状态是在预先的设计中没有的,但是状态转换图将所有状态都列出来了,可以说是对手工设计的一种补充吧,为确定设计的状态机是正确的,一般都会生成出一个与TAB2.1相似的表格,可以对照检验一下。
2.3 电路验证
验证电路最重要的方法还是功能验证和时序逻辑验证,一般时序逻辑验证要求比较高一点,在功能验证成功而时序验证不成功的情况下,可以采用换更高速率的器件或者降低时钟频率,最为关键的是功能验证需要正确。
因为测试的时间要求很长,软件自带的仿真工具受限,所以采用modelsim来辅助验证,在测试之前,首先要写好测试程序。
module breath_tb; reg CLK = 1'b0; reg RST = 1'b1; wire LED; initial begin CLK = 1'b0; #30 RST = 1'b0; #50 RST = 1'b1; #50000000 $stop; end always #10 CLK = ~CLK; breath M0 (.LED(LED), .CLK(CLK), .RST(RST)); endmodule
加入测试波形以后的仿真结果。
限于屏幕,测试仿真图只能截取部分,两根红色的标尺表明了状态从S0转换到S1再到S2的分界线,在S0下(最左边),LED的频率变化逐步变高(第一个信号),S1时是高亮保持,LED信号为高电平,S2下,LED的频率变化逐步变小(可能看得不太明显),再截取局部看看。
S0下占空比是100的标尺,在sub_clk的上升沿,次级层次占空比加1,满100后变为0重新开始计数。
逻辑验证正确以后,最后将生成的数据烧录到FPGA芯片上,观察板子上LED的现象。
视频地址:http://v.youku.com/v_show/id_XODI5NjA3MjA4.html
2.4 小结
电路设计和验证做完了,可以说是达到了相应的效果,LED灯慢慢地呼吸起来,跟开始描述的一样,像是打盹睡觉一般,十分可爱!
觉得唯一的不足就是描述的程序没有模块化,整段就在一个always块内,看起来特别庞杂。检测时钟边沿用的是伪上升沿。尝试过将这些模块分开开来,但是综合和适配的时候产生了问题,时序电路的一个寄存器不能再多个块内进行赋值操作,受于水平有限吧,暂时还没有想到更好的方法来替代,希望有经验的朋友能够指点一二,提前先谢谢了!
3.Conclusion
虽然只是一个呼吸灯小程序,用文字的方式把它们记录下来确实花了不少功夫;把设计从草稿纸上有条理地搬到电子版上来,多个软件都需要熟练操作。值得肯定的是,用电子版的方式记录这些,既可以理清逻辑思维,也方便以后查看和完善,很是方便。
4.Platform
1. Quartus II Version 9.1 Build 222
2. ModelSim SE PLUS 6.5
3. TimeGen 3.1
4.MicroSoft Office Visio Professional 2003 SP3
5.Reference
[1] Verilog 数字系统设计教程 夏宇闻
6. Attachment
呼吸灯RTL视图文件:https://files.cnblogs.com/hechengfei/%E5%91%BC%E5%90%B8%E7%81%AFRTL%E8%A7%86%E5%9B%BE.pdf
7.Notes
附加一点说明吧,其实很不愿意用写程序这个词儿来说写硬件电路,尽管编程跟描述电路的前期这个用文本写相应的语句过程是一样的,某些朋友就会立即严正地指出,这样说是完全不对的、不科学、甚至错误!首先我想非常感谢这些朋友,严格上来说,用程序这个字眼确实有些不对,不科学,感谢你们的细心批评指正,不过也容我稍微说明一下,这里使用程序字眼,是指广泛意义上程序,是使用文本编辑器书写语句、进行语法检查等这样通用的过程,后期逻辑综合和逻辑验证抽象起来看也是一个结果生成和验证的过程,它们的过程类似,所以用了程序这个字眼。恳请这些朋友能接受我的这样理解。