Verilog和FPGA的自学笔记8——按键消抖与模块化设计
好几天不写文章了哈,真是不好意西~~
倒不是我偷懒,而是正在研读夏宇闻老师的《Verilog数字系统设计教程》。
收获是真大,我验证了一些自己之前的猜想,也纠正了自己的理解错误(tips:不少错误仍隐藏于之前的几篇笔记里……)
当然这也不能怪我,只能怪豆包Verilog水平太低哈哈(推卸责任ing……)
这几天在家自己实现了一个按键触发的宏观上的D触发器控制的led灯。简单点说。就是按一下,灯亮了,再看一下,灭了,再按一下,又亮了,再按一下,又(读者:无不无聊)
不是这有啥难的?这不现场敲都行?
always @(posedge key)led<= ~led;
哎,敲完就觉得不对了:咱做嵌入式开发时候,不得加个delay(5)来给key做消抖处理?
嗯,对,那正好上一次介绍了Verilog如何做延时(不知道的复习一下流水灯),那正好写一个就行了。
转念一想又不对了,Verilog咋知道按键按下再开始计时呢?这是一个过程,这不典型的软件编程思维思考硬件编程嘛?
哦,对,记得上次流水灯时写了个标志flag,那咱只要把key的边沿检测提取一下,此时立个flag(顺道立个FPGA的flag哈哈),然后让cnt开始计时就差不多啦~
0.模块化设计
Verilog最大的一个特点就是结构化,这样可以将庞大的工程分发给不同人完成,同时也使整个工程结构清晰。(个人感觉和C++里的函数作用挺像的。。。)
《Verilog数字系统设计教程》中讲到,一般RTL代码都是从上往下设计的,也就是先做顶级模块,之后再做下面的小模块。
这样做是有原因的。
讲点题外话,记得小学学画画,当时老师最多的教诲就是:XXX,别老盯着细节画!
这句话是有道理的,因为细节画好了,如果框架不合适,哎……重新来过吧您呐~~
而且模块化设计还有个好处。就是你可以一个模块一个模块的测试,做好一个确保没问题了,再做下一个,一步步来,最后项目成功的几率也更高。
所以先设计顶层模块(Top-module)!
不,应该先画画~

再一次露一手我优秀 稀烂 的画工(^_^)
可以看到,我们有一个顶层模块,内部包含了两个子模块,一个Debounce专门负责按键消抖,一个LED负责控制LED。通过这样的模块化设计,我们很容易在下次使用时直接搬运debounce,从而避免重复造轮子。
Top module如下:
(读者:哎啥是顶层模块?)
(作者:大概就是统领全局的模块,我这样理解应该没问题……)
module top_module(input sys_clk,input sys_rst,input key, output led
);wire key_posedge;led u_led(.key_posedge(key_posedge),.sys_clk (sys_clk),.sys_rst (sys_rst),.led (led)
);debounce u_debounce(.sys_clk (sys_clk),.sys_rst (sys_rst),.key (key),.key_d(key_posedge)
);endmodule
我们在设计顶层模块时,可以把整个大模块想象为一个黑箱,只考虑需要给它什么,以及我们需要得到什么,这样思路就会清晰许多~
至于下面的例化模块,我并不是一开始就写好的,而是现在其他两个.v文件里写好模块接口定义部分以后再回来写的。关于书写顺序,大家怎么舒服怎么来~
就是这么简单!
1.边沿检测!
关于边沿检测的实现,我们可以画个波形图来理解,假设我们的时钟clk和按键key的理想信号:

先把key寄存一下,并增加其取反信号:

我们取key和~key1的位与结果:

这样就提取了完美的key上升沿信号key_posedge!
always@(posedge sys_clk or negedge sys_rst) beginif(!sys_rst)key1 <= 1'b0;elsekey1 <= key;
endassign key_posedge = key & ~key1; //取上升沿
我们一般通过以上的always语块来寄存目标信号,并通过下面的assign语句提取上升沿。
有些时候,我们也需要提取下降沿甚至是双沿(上升沿+下降沿),此时我们只要抄上always语句,再选择以下对应的assign就阔以了~
assign key_posedge = ~key & key1; //取下降沿
assign key_posedge = key ^ key1; //取双沿
至于原理嘛……懒得码字了,就靠大家自己啦哈哈!
2.Debounce模块设计
(就不写中文~)
根据上次流水灯的经验,咱最好把不同的功能实现放到不同的always块里。一个块对应一个功能,这样好~
所以类似的,一个采样块,一个计数块,一个……把按键信号送出去的块(哎呀第三个块我起不出俩字的名%#@&)
刚不久我们讨论了如何提取信号边沿(延时一拍),但现实中的key可不见得一定和clk同步,甚至有可能当你按下按键时,也是clk恰好到来之时。
(在一个系统中,这种不合群的信号也称为异步信号)
那这样就不太好办,时间太短可能导致信号提取不出来。所以我们在采样块中选择延时两拍的方式,确保信号采样正确:
always @(posedge sys_clk or negedge sys_rst)beginif(!sys_rst) beginkey1 <= 1'b1;key2 <= 1'b1;endelse beginkey1 <= key;key2 <= key1;end
end
key1延时一拍,key2延时两拍。这样确保了key1和key2中间严格间隔一个clk。
所以,在FPGA这种并行性很强的器件里,为了尽量让所有的信号都听从指挥,我们通常做成同步时序而不用异步时序,这样可以保证系统工作的稳定性。
那啥就是同步时序呢?
就是不管什么情况,你always块全由clk触发就行了!
就比如我这个按键消抖可以这样写:
always @(posedge sys_clk or negedge sys_rst)beginif(!sys_rst)cnt <= 18'b0;else if(key1 != key2)cnt <= CNT_MAX;else if(cnt > 18'b0)cnt <= cnt - 18'b1;else begincnt <= 18'b0;flag <= 1'b1;end
endalways @(posedge flag or negedge sys_rst)beginif(!sys_rst)led<= 1'b0;elseled <= ~led;
end
也可以这样写:
always @(posedge sys_clk or negedge sys_rst)beginif(!sys_rst)cnt <= 18'b0;else if(key1 != key2)cnt <= CNT_MAX;else if(cnt > 18'b0)cnt <= cnt - 18'b1;elsecnt <= 18'b0;
endalways @(posedge sys_clk or negedge sys_rst)beginif(!sys_rst)key_d <= 1'b1;else if(cnt == 18'b0)key_d <= key2;elsekey_d <= key_d;
end
可以清晰的看到,第一段代码的第二个always始终由flag的上升沿触发。
虽然不是不行,但你会发现所有always块都由clk触发的这段代码,它的整个同步感很强,那系统工作时出错概率自然就会低很多~
第一种的逻辑简单,更符合我们的认知。但为了效果更好,我们以后就强迫自己直接把(posedge sys_clk or negedge sys_rst)这个条件贴上,写完之后考虑如何用别的组合电路实现功能。
行啦,大家把消抖部分的模块自己写写改改,这次代码最难的部分也就差不多啦~~
关于仿真的initial语句今天多说两句。
关于消抖模块的仿真文件,如果按照我们以前的写法是这样的:
`timescale 1ns/1nsmodule tb_debounce();reg sys_clk;
reg sys_rst;
reg key;
wire key_d;initial beginsys_clk <= 1'b0;sys_rst <= 1'b0;key <= 1'b1;#20sys_rst <= 1'b1;#80key <= 1'b0;#5key <= 1'b1;#5key <= 1'b0;#7key <= 1'b1;#9key <= 1'b0;#300key <= 1'b1;#9key <= 1'b0;#7key <= 1'b1;#5key <= 1'b0;#5key <= 1'b1;endalways #1 sys_clk <= ~sys_clk;debounce #(18'd25) u_debounce (.sys_clk (sys_clk),.sys_rst (sys_rst),.key (key),.key_d(key_d)
);endmodule
这两天读《Verilog数字系统设计教程》,学了个fork-join语块,感觉挺适合某些人的思维(比如我),于是在这顺道分享分享~
fork-join语块与begin-end语块最大的区别是,前者是并行块,内部语句同时执行,后者是顺序块,内部语句依次执行。
书中有句话写的特别好,就是:fork像是一个分支的起点,打开各条路,而join则将所有分支收束起来。

而begin-end更像一串糖葫芦:

就比如上面写的让key翻来覆去的initial语句,也可以用fork-join语块改写成下面这样:
`timescale 1ns/1nsmodule tb_debounce();reg sys_clk;
reg sys_rst;
reg key;
wire key_d;initial fork#0 sys_clk <= 1'b0;#0 sys_rst <= 1'b0;#0 key <= 1'b1;#20 sys_rst <= 1'b1;#100 key <= 1'b0;#105 key <= 1'b1;#110 key <= 1'b0;#117 key <= 1'b1;#126 key <= 1'b0;#400 key <= 1'b1;#409 key <= 1'b0;#416 key <= 1'b1;#421 key <= 1'b0;#427 key <= 1'b1;#700 key <= 1'b0;#705 key <= 1'b1;#710 key <= 1'b0;#717 key <= 1'b1;#726 key <= 1'b0;#1000 key <= 1'b1;#1009 key <= 1'b0;#1016 key <= 1'b1;#1021 key <= 1'b0;#1027 key <= 1'b1;joinalways #1 sys_clk <= ~sys_clk;top_module u_top_module(.sys_clk(sys_clk),.sys_rst(sys_rst),.key(key),.led(led)
);endmodule
这样貌似再时间尺度上清晰了许多。
不过这个看个人习惯哈~ 没必要为了追求新颖就写这个……
哦对了,这里还需要介绍以下参数(parameter)的一个用法。
当模块被实例化时,可以通过 #(参数值) 这种方式更改模块内部的初始值。
比如上面测试用的代码最后,在原模块名称和例化模块名称中间写了个#(18’d25),这意思就是要让里面的CNT_MAX初始化时等于18’d25。
这样可就方便许多啦。之前因为延时时间过长不便于仿真,我们不得不把CNT的初值删一些0去,但下载验证时有时就忘了改回去……捂脸。这下我们只在仿真时通过这种方式进行初值重装,同时不影响vivado对rtl代码综合的那部分,理想!
注:关于多个参数传递等需要时我们再介绍~
下面是我的仿真波形,大家可以参考下~

可以看到debounce模块是成功的。
3.LED控制
这里和边沿检测一毛一样,自己怎么喜欢怎么来~
很多按键的操作,我看市面上很多都是上升沿触发,就是它得等你松开按键之后才有反应(while(!key)),但我就不喜欢,我喜欢让它一按下去就有反应!
所以在我的程序中,提取了下降沿:
module led(input key_posedge,input sys_clk,input sys_rst,output reg led
);reg key;
wire led_filp;always @(posedge sys_clk or negedge sys_rst)beginif(!sys_rst)key <= 1'b0;elsekey <= ~key_posedge;
endalways @(posedge sys_clk or negedge sys_rst)beginif(!sys_rst)led <= 1'b0;else if(led_filp)led <= ~led;else led <= led;
endassign led_filp = ~key & ~key_posedge;endmodule
尽管消抖模块那边我们已经打了两拍,但这里为了提取边沿,我们仍需再打一拍,这样才能弄个脉冲出来~
4.总体仿真

字有点小哈,大体凑合看看形状就得了……
今天这篇真是累啊,足足6000多字,也是我这段时间几乎全部收获了……
这个工程算是第一次采用多文件模块化设计,第一次引入顶层模块,对Verilog的层次感又有了新的体会……
不知道后面会先写状态机还是IP核,可能又要闭关一段时间了,大家期待吧~~
如果有不明白或错误之处,也希望大家在评论区给出,帮助大家的同时也能再次提升自己对于FPGA和Verilog的理解,感谢大家!!
系列链接:
上一篇:Verilog和FPGA的自学笔记7——流水灯与时序约束(XDC文件的编写)
下一篇:码字ing……
