Verilog HDL的for循环与其余语言的for循环含义完全不一样,Verilog HDL的for循环一般都是为了简化书写而存在的,下面以一个按键消抖的模块进行说明,其实按键消抖并且检测按键是否被按下的原理很简单,源码一般只需要写一位按键的检测消抖就行,NUM个按键,实际上同时例化NUM个该模块就行了,如果手动把例化代码写NUM遍,会造成代码篇幅较长,并且容易出现错误,此时就可以使用for循环简化代码书写来达到相同效果。
注意:for循环只是简化代码,最终综合生成的电路是相同的。
信号 | 位宽 | I/O | 含义 |
---|---|---|---|
clk | 1 | I | 系统时钟,默认50MHz |
rst_n | 1 | I | 系统复位,低电平有效 |
key_in | 1 | I | 按键输入,默认低电平有效 |
key_out | 1 | O | 按键输出,一个时钟宽度的高电平表示按键被按下一次。 |
机械按键一般会存在如下图所示的抖动,为了防止误触发,会对其进行消抖,使得输入信号变得平滑,一般认为按键抖动持续的时间是20ms左右,但是为了代码的通用性,其实可以用TIME_20MS参数代替这个数据,便于修改。
由于本模块除了完成消抖之外,还需要检测按键是否被按下,设计思路就是使用一个计数器cnt对按键按下时的有效电平进行计数,当计数到TIME_20MS时,表示按键被按下了,此时计数器的值应该保持不变。只要检测到按键输入信号无效时,计数器cnt清零,重新计数,从而达到消抖的目的。
按键信号是外来信号,为减小亚稳态出现的机率,在使用之前应该先延迟两个时钟防止亚稳态的产生。
综上:按键输入延迟信号key_in_ff[1:0] = {key_in_ff[0],key_in}。
计数器cnt:初始值为0,当key_in_ff==1时cnt=0,计数器cnt加一条件add_cnt=~key_i n_ff,计数器cnt的结束条件end_cnt=add_cnt && cnt == TIME_20MS-1。
通过检测计数器cnt的结束条件的上升沿确定按键是否被按下。end_cnt_ff[1:0] = {end_cnt_ff[0], end_cnt}。
按键检测输出信号:key_out = ~end_cnt_ff[1] && end_cnt_ff。
为了增加代码的参数化设计,本模块其实就是参数TIME_20MS以及时钟频率,所以增加时钟周期的参数TIME_CLK,默认等于20,表示时钟周期默认20ns(频率50MHz),通过TIME_20MS_NUM参数计算出TIME_20MS对应的系统时钟个数,也是计数器cnt计数的最大值。此时通过自动计算位宽函数clogb2计算出TIME_20MS_NUM对应的位宽TIME_20MS _NUM_W 作为计数器cnt的位宽。
自此,所有信号位宽要么已经固定不可能发送变化,要么就会根据TIME_20MS和TIME _CLK的值进行变化,从而实现参数化设计,使用时只需要配置这两个参数就行,无需修改模块内其余信号的位宽,减小错误的可能。
//用于按键消抖,并且检测按键是否被按下,当按下时,key_out输出一个时钟宽度的高电平,表示按键被按下一次;
module key#(
parameter TIME_20MS = 20_000_000,//按键抖动持续的最长时间,默认最长持续时间为20ms。
parameter TIME_CLK = 20 //系统时钟周期,默认20ns。
)(
input clk ,//系统时钟,50MHz。
input rst_n ,//系统复位,低电平有效。
input key_in ,//待输入的按键输入信号,默认低电平有效;
output reg key_out //按键消抖后输出信号,当按键按下一次时,输出一个时钟宽度的高电平;
);
localparam TIME_20MS_NUM = TIME_20MS / TIME_CLK ;//按键抖动时间对应的系统时钟数;
localparam TIME_20MS_NUM_W = clogb2(TIME_20MS_NUM-1);//利用自动计算位宽函数,自动计算出TIME_20MS_NUM对应的数据位宽;
reg [1 : 0] key_in_ff;
reg [TIME_20MS_NUM_W - 1 : 0] cnt ;
reg [1 : 0] end_cnt_ff;
wire add_cnt ;
wire end_cnt ;
//自动计算位宽的函数;
function integer clogb2(input integer depth);begin
if(depth==0)
clogb2=1;
else if(depth!=0)
for(clogb2=0; depth>0;clogb2=clogb2+1)
depth=depth>>1;
end
endfunction
//将按键输入信号延迟两个时钟,减小亚稳态出现的机率。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_in_ff <= 2'b11;
end
else begin
key_in_ff <= {key_in_ff[0],key_in};
end
end
//按键按下有效时间的计数器;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin//初始值为0;
cnt <= {{TIME_20MS_NUM_W}{1'b0}};
end
else if(key_in_ff[1])begin//当按键没有被按下时,计数器清零;
cnt <= {{TIME_20MS_NUM_W}{1'b0}};
end
else if(add_cnt)begin
if(end_cnt)//当计数器计数到最大值时,计数器保持不变;
cnt <= cnt;
else//当按键没有被按下,并且没有计数到最大值时,计数器加一;
cnt <= cnt + {{{TIME_20MS_NUM_W-1}{1'b0}},1'b1};
end
end
assign add_cnt = ~key_in_ff[1];
assign end_cnt = add_cnt && cnt == TIME_20MS_NUM - 1;
//检测计数器结束条件,满足的上升沿,表示按键被按下一次;
//将按键计数器cnt结束条件延迟两个时钟。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
end_cnt_ff <= 2'b0;
end
else begin
end_cnt_ff <= {end_cnt_ff[0],end_cnt};
end
end
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_out <= 1'b0;
end
else begin//检测按键计数器结束条件的上升沿,表示按键被按下一次,将key_out拉高一次;
key_out <= (~end_cnt_ff[1] & end_cnt_ff[0]);
end
end
endmodule
测试代码(可以用循环这些语法优化,但是没必要):
`timescale 1 ns/1 ns
module test();
//时钟和复位
reg clk ;
reg rst_n;
reg key_in;
wire key_out;
parameter CYCLE = 20;//时钟周期,单位为ns,可在此修改时钟周期。
parameter RST_TIME = 3 ;//复位时间,此时表示复位3个时钟周期的时间。
//为加快仿真速度,将按键消抖时间设置为10_000ns,即500个时钟周期
key#(.TIME_20MS(10000)) uut_key(
.clk (clk ),
.rst_n (rst_n ),
.key_in (key_in ),
.key_out (key_out )
);
//生成本地时钟50M
initial begin
clk = 0;
forever
#(CYCLE/2)
clk=~clk;
end
//产生复位信号
initial begin
rst_n = 1;
#2;
rst_n = 0;
#(CYCLE*RST_TIME);
rst_n = 1;
end
//输入信号key_in赋值方式
initial begin
#1;//赋初值
key_in = 1'b1;
#(10*CYCLE);
key_in = 1'b0;
#(30*CYCLE);
key_in = 1'b1;
#(20*CYCLE);
key_in = 1'b0;
#(40*CYCLE);
key_in = 1'b1;
#(10*CYCLE);
key_in = 1'b0;
#(700*CYCLE);
key_in = 1'b1;
#(10*CYCLE);
key_in = 1'b0;
#(30*CYCLE);
key_in = 1'b1;
#(40*CYCLE);
key_in = 1'b0;
#(50*CYCLE);
key_in = 1'b1;
#(20*CYCLE);
key_in = 1'b0;
#(750*CYCLE);
key_in = 1'b1;
#(10*CYCLE);
key_in = 1'b0;
#(30*CYCLE);
key_in = 1'b1;
#(10*CYCLE);
key_in = 1'b0;
#(30*CYCLE);
key_in = 1'b1;
#(600*CYCLE);
$stop;
end
endmodule
为了节约仿真时间,将仿真时将TIME_20MS设置为10_000,总体结果如下:
当按键输入低电平持续500个时钟时,计数器cnt保持计数结果不变,并且输出信号key_out拉高一个时钟,表示按键被按下一次。
介绍了一位按键的消抖,那NUM位呢?先看传统方式:
想法很简单,就是把前面的key模块单独例化NUM个就行了,就是将例化代码抄写NUM遍,如下面代码是对NUM=3的按键进行消抖:
module key_top#(
parameter TIME_20MS = 20_000_000,//按键抖动持续的最长时间,默认最长持续时间为20ms。
parameter TIME_CLK = 20 ,//系统时钟周期,默认20ns。
parameter NUM = 3 //按键个数;
)(
input clk ,//系统时钟,50MHz。
input rst_n ,//系统复位,低电平有效。
input [NUM-1 : 0] key_in ,//待输入的按键输入信号,默认低电平有效;
output [NUM-1 : 0] key_out //按键消抖后输出信号,当按键按下一次时,输出一个时钟宽度的高电平;
);
//例化按键消抖并检测0模块
key #(
.TIME_20MS (TIME_20MS ),
.TIME_CLK (TIME_CLK )
)uut_key0(
.clk (clk ),
.rst_n (rst_n ),
.key_in (key_in[0] ),
.key_out (key_out[0] )
);
//例化按键消抖并检测1模块
key #(
.TIME_20MS (TIME_20MS ),
.TIME_CLK (TIME_CLK )
)uut_key1(
.clk (clk ),
.rst_n (rst_n ),
.key_in (key_in[1] ),
.key_out (key_out[1] )
);
//例化按键消抖并检测2模块
key #(
.TIME_20MS (TIME_20MS ),
.TIME_CLK (TIME_CLK )
)uut_key2(
.clk (clk ),
.rst_n (rst_n ),
.key_in (key_in[2] ),
.key_out (key_out[2] )
);
endmodule
利用quartus对上面代码进行综合,得到的RTL视图如下所示,就是将key模块例化3个,一位按键输入一个模块即可。
这种方法是最常见的,但是例化太繁琐,如果要例化100个该模块,一不小心就可能出错。
C语言的for循环就是按循序将循环体执行i遍,但是Verilog的for循环并不会按顺序将循环体执行i遍,而是直接将循环体对应的电路复制i个,复制的电路并行运行。
使用for循环完成对按键模块key的NUM次例化的代码如下(只需要改变参数NUM就可以实现例化模块的个数):
module key_top#(
parameter TIME_20MS = 20_000_000,//按键抖动持续的最长时间,默认最长持续时间为20ms。
parameter TIME_CLK = 20 ,//系统时钟周期,默认20ns。
parameter NUM = 3 //按键个数;
)(
input clk ,//系统时钟,50MHz。
input rst_n ,//系统复位,低电平有效。
input [NUM-1 : 0] key_in ,//待输入的按键输入信号,默认低电平有效;
output [NUM-1 : 0] key_out //按键消抖后输出信号,当按键按下一次时,输出一个时钟宽度的高电平;
);
genvar bit_num;
//例化NUM个按键消抖模块;
generate
for(bit_num = 0 ; bit_num < NUM ; bit_num = bit_num + 1)begin:KEY
key #(
.TIME_20MS (TIME_20MS ),
.TIME_CLK (TIME_CLK )
)uut_key(
.clk (clk ),
.rst_n (rst_n ),
.key_in (key_in[bit_num] ),
.key_out (key_out[bit_num] )
);
end
endgenerate
endmodule
Quartus综合以上代码得到的RTL电路如下所示,对比前面可知,例化得到的电路结构完全一致,而代码却少很多,并且可以实现任意数量的例化。
Verilog HDL的for循环其实就是将循环体对应的电路复制 n 遍,并不是其它语言for循环的含义将循环体的代码按顺序执行n遍。要注意for循环可以在always块中进行使用,使用generate时,begin后面一定记得跟:和名称,不然综合工具会报错。