偶分频
为什么先说偶分频,因为其实现方式是比较不同的(相对奇偶降频来说),也是比较简单和易于理解的。学习一门技术,弄清楚其中的原理非常重要,俗语有云:基础不牢,地动山摇,等到发现自己缺少相关基础后,往往已经为时已晚。
频率定义:f=1/T,假定我们使用的系统时钟的频率为50MHz,即周期T=20ns,即相邻最近的一组高低脉冲间隔为20ns。
我们知晓,分频模块的实现依赖于计数器,对于计数器,有两点必须掌握:
- 计数器何时开始计数
- 计数器何时清零
因此,只要你能精准的控制它计数、清零的时刻,实现一个分频或者降频模块岂不是手到拈来!
我们假如要实现一个N分频的模块(N为偶数),计数器cnt必不可少,复位信号sys_rst_n低电平有效时,cnt为0,clk_out也为0,但复位信号无效,且处于时钟上升沿时,cnt开始计数(注:计数器0也占用一个周期),数值累计加1,直到cnt等于N/2-1时,对输出信号取反,便实现了一个简单的偶分频模块的功能。
为什么要取反?
假定我们需要计数1s的计数器,其cnt要计数到10亿,才得以开始下一个循环,取反的讨巧之处在于,我实际计数的cnt变为了理论上的一半,不管你是0还是1,我只要计数到N/2-1,就取反,等效于其后取反的部分的cnt个数也和前面一致,而不需计数到10亿这个数目,同时,也节省了大量寄存器资源,绿色环保。
module divider
#(
parameter DIVIDER = 'd6 // 使用parameter定义参数,它可在模块之间传递
)
(
input wire sys_clk, // 使用分频方式的可参数化的偶分频模块
input wire sys_rst_n,
output reg clk_out
);
reg [$clog2(DIVIDER):0] cnt; // $clog2是对2取对数,为向上取整
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 'd0;
else if(cnt == ((DIVIDER >> 1'b1) - 1))
cnt <= 'd0;
else
cnt <= cnt + 1'b1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0 )
clk_out <= 1'b0;
else if(cnt == (DIVIDER >> 1'b1) - 1)
clk_out <= ~clk_out;
else
clk_out <= clk_out;
endmodule
复制代码
`timescale 1ns/1ns
module tb_divider();
reg sys_clk;
reg sys_rst_n;
wire clk_out;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
divider
#(
.DIVIDER(10)
)
divider_inst(
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.clk_out(clk_out)
);
endmodule
复制代码
分析仿真结果,cnt从0开始计数,直到4清零,以此循环,而输出信号clk_out在cnt为4时,发生反转,刚好是50MHz的十分频,即clk_out的频率为5MHz,仿真结果契合了我们的设计。
奇分频
奇分频的解决通路,整个理下来非常有趣。
假设我们要取时钟的五分频,若采用取反的方法,发现会行不通:带入上面的公式,计数器cnt反转的值为2.5!!!要知道,cnt的个数只能整数个,这时候,我们就只能退而求其次,计数到接近2.5的往下取整的2,什么?你问我为什么不向上取整?我只能说我们站在巨人的肩膀上。
步骤一:我们在sys_clk上升沿且cnt为2的时候,拉高信号clk_out_r,cnt为4时,拉低信号clk_out_r,其余的时刻信号保持不变。在sys_clk为下降沿时,重复步骤一,得到的信号即为clk_out_f,示意图如下:
步骤二:对采集到的clk_out_r和clk_out_f进行与操作,便可得到”完美”的占空比为50%的系统时钟5分频,上图绿色部分即为所求,相关代码如下:
module divider_five
#(
parameter DIVIDER = 'd5
)
(
input wire sys_clk,
input wire sys_rst_n,
output wire clk_out
);
reg [$clog2(DIVIDER) : 0] cnt;
reg clk_out_r;
reg clk_out_f;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 'd0;
else if(cnt == (DIVIDER-1'b1))
cnt <= 'd0;
else
cnt <= cnt + 1'b1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
clk_out_r <= 1'b0;
else if(cnt == ((DIVIDER -1'b1) >> 1'b1))
clk_out_r <= 1'b1;
else if(cnt ==(DIVIDER-1'b1))
clk_out_r <= 1'b0;
else
clk_out_r <= clk_out_r;
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
clk_out_f <= 1'b0;
else if(cnt == ((DIVIDER -1'b1) >> 1'b1))
clk_out_f <= 1'b1;
else if(cnt ==(DIVIDER-1'b1))
clk_out_f <= 1'b0;
else
clk_out_f <= clk_out_f;
assign clk_out = clk_out_f | clk_out_r;
endmodule
复制代码
`timescale 1ns/1ns
module tb_divider_five();
reg sys_clk;
reg sys_rst_n;
wire clk_out;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
divider_five
#(
.DIVIDER(11)
)
divider_five_inst(
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.clk_out(clk_out)
);
endmodule
复制代码
从仿真结果很容易得知cnt和输出信号clk_out符合预期设计。
奇偶降频
奇偶降频可以放在一块,其实质还是计数器的应用。要实现一个N分频的模块(严格来说,应称之为降频),我们直接可以让计数器cnt计数到N-1,在系统时钟上升沿拉高一个周期的高电平,此时得到的就是降频之后的信号。这里笔者就不做重复了。
看到这里,那长得帅的朋友可能会问了:既然有两种”分频”方式,我们在实际开发中该如何选择呢?
- 后级模块调用两种时钟语法不同
always@(posedge clk_out or negedge sys_rst_n) // 此为使用分频时钟
复制代码
always@(posedge sys_clk or negedge sys_rst_n) // 此为使用降频时钟
if(sys_rst_n == 1'b0)
else if(clk_flag == 1'b1)
A <= A + 1'b1;
复制代码
- 高速系统设计中,尽量采用降频方式,其中涉及到一个全局时钟树的概念,简单地说,降频可以保证更低的时钟偏斜(Skew)和抖动(Jitter)。抖动很好理解,所谓的时钟偏移,指的是从同一时钟源发出的时钟脉冲,通过不同的路径到达每个触发器的时间不同而产生的偏差成为时钟偏移。
千言万语汇成一句话,降频方式好于分频方式。
最后的思考
文中笔者使用了parameter定义了一个分频参数,在判断计数器cnt时使用了移位运算符,到最后modelsim仿真阶段,才发现有漏网之鱼——————要是我定义的DIVIDER数值比较小,会发现cnt并没有任何的数据输出,这也是日后有待完善的地方,笔者也在进一步思考,如何将奇偶分频模块整合到一起,合二为一,使其输入某一个分频数,实现任意分频,只能说学习使我快乐,码农永远在路上!
。。。。。。未完待续
复制代码