Verilog各类分频器设计详解

分频器是时序电路的基本器件,它的功能是对系统时钟或其他时钟进行分频产生所需要的时钟信号。分频有两种方式:一是通过HDL语言建模产生所需要的时钟信号,二是利用开发工具的PLL进行分频。前者分频灵活,需编写代码实现;后者使用场景受限,因为有的低端FPGA没有PLL,但PLL的分频效果更好,而且在进行小数分频时也比较容易实现。本文首先尝试用HDL语言建模方式设计各种类型的分频器,最后给大家简单介绍一下PLL的使用。如有不足之处还望大家批评指正。

偶数分频器

我们先从最简单的偶数分频器切入,慢慢分析各种分频器的实现。

若要实现二分频,则只需要在原时钟的上升沿进行输出时钟状态的翻转即可,如下图所示

1

若实现四分频呢?则需要一个计数器,每次在原时钟的上升沿计数,当计数器记到2个上升沿时输出时钟状态进行翻转,如下图所示

2

现在对一般情况进行分析,对时钟进行N分频,N为偶数;则计数器每次在原时钟的上升沿计数,计数器的范围为0~(N-1),我们可以在0—(N-1)这N个数中分出两个范围选择输出时钟的状态,如当cnt在0—M范围时输出时钟为低电平,当cnt在(M+1)—(N-1)范围时输出时钟为高电平,则我们可以动态调整输出时钟的占空比,输出时钟的占空比为(N-M-1)/N;

以8分频为例,则N=8,取M=3,则此时输出的时钟应当是50%占空比,如下图所示

3

若取M=1,则此时输出时钟的占空比应当是75%,如下图所示

4

偶数分频器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 偶数分频器示例,可调占空比
module clk_div_even(
input wire clk, //系统时钟
input wire rst_n, //异步低电平复位
input wire [7:0] clkperiod, //分频系数,N(偶数)分频时clkperiod=N
input wire [7:0] clklow, //低电平占用系统时钟的周期数
//占空比为50%时clklow=N/2
output reg clk_out //输出时钟
);

reg [7:0] cnt;

always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 8'b0;
clk_out <= 1'b0;
end
else begin
if(cnt == clklow - 1'b1) begin
clk_out <= 1'b1;
cnt <= cnt + 8'd1;
end
else if(cnt == clkperiod - 1'b1) begin
clk_out <= 1'b0;
cnt <= 8'd0;
end
else begin
cnt <= cnt + 8'd1;
end
end
end

endmodule

奇数分频器

我们先以三分频模块切入,分析奇数分频器的思想:整体思路是产生两路上升沿和下降沿触发信号,然后对这两路信号进行操作得到最终分频时钟。

当分频系数N为奇数时,使用一个计数器在0~(N-1)循环进行计数,控制输出(N-1)/2个高电平,(N+1)/2个低电平,称为clk_1;然后将此clk_1电平信号延迟半个周期称为clk_2,最后输出clk_out = clk_1 | clk_2,即为占空比为50%的奇数分频器;另一种思路是产生输出(N+1)/2个高电平,(N-1)/2个低电平的clk_1,输出的分频时钟为clk_out = clk_1 & clk_2

clk_out = clk_1 & clk_2为例,将clk_1延时半个时钟周期的方法有两种,法1是直接使用下降沿的锁存器对clk_1锁存得到clk_2,法2得到clk_2的原理与clk_1相同,不过是在下降沿检测。

5

若采用法一,则我们用一个下降沿触发的D触发器锁存clk_1的结果clk_2,然后将clk_1clk_2信号做逻辑“或”就得到了占空比50%的分频时钟信号clk_out

结合上面的偶数分频器,如果N为偶数,则clk_1就是我们所需要的分频结果,如果N为奇数,则clk_1 & clk_2就是我们所需的分频结果,所以我们可以把偶数分频和奇数分频结合,实现N分频器设计,其中N为正整数;通过N[0]选择输出,N[0]=1为奇数分频,N[0]=0为偶数分频。

5分频结果如下:

6

6分频结果如下:

7

正整数分频器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
module clk_div_integer #(
parameter N = 6, //N分频,N为整数
parameter WIDTH = 3 //计数器位宽
) (
input wire clk,
input wire rstn,
output wire clk_out
);

reg [WIDTH-1:0] cnt;
reg clk_1,clk_2;
wire clk_odd;

always @(posedge clk or negedge rstn) begin
if(~rstn) begin
cnt <= 0;
end
else begin
if(cnt == N-1) begin
cnt <= 0;
end
else begin
cnt <= cnt + 1'b1;
end
end
end

always @(posedge clk or negedge rstn) begin
if(~rstn) begin
clk_1 <= 1'b0;
end
else begin
if(cnt == ((N-1) >> 1)) begin
clk_1 <= 1'b1;
end
else if(cnt == (N-1))begin
clk_1 <= 1'b0;
end
else begin
clk_1 <= clk_1;
end
end
end

//法1:在时钟下降沿锁存clk_1得到clk_2
always @(negedge clk or negedge rstn) begin
if(~rstn) begin
clk_2 <= 1'b0;
end
else begin
clk_2 <= clk_1;
end
end

//法2:clk2与clk_1产生的方式相同,差别是clk_2是下降沿触发
// always @(negedge clk or negedge rstn) begin
// if(~rstn) begin
// clk_2 <= 1'b0;
// end
// else begin
// if(cnt == ((N-1) >> 1)) begin
// clk_2 <= 1'b0;
// end
// else if(cnt == (N-1)) begin
// clk_2 <= 1'b1;
// end
// else begin
// clk_2 <= clk_2;
// end
// end
// end

assign clk_odd = clk_1 | clk_2;
assign clk_out = N[0] ? clk_odd : clk_1;

endmodule

半整数分频器

1、占空比非50%

网上比较多的分频思路是:半整数分频多出来的那半个周期为高电平,其余为低电平。以5.5分频为例,以原时钟的半周期为单位,可以分频输出1高10低。原理是用计数器循环记数0~10即11个周期,控制输出clk_1前6周期高电平,后5周期低电平,然后再使用该计数器得到一下降沿触发的5低6高的输出clk_2,最后输出clk_out = clk_1 & clk_2。波形图如下图所示

8

现在我们虽然得到了5.5分频后的信号,但占空比不是很理想,是否可以在其基础上进行改进来实现占空比近似50%的分频信号呢?

2、占空比近似50%

由上面的波形图我们可以看到,如果clkxclkycnt的其它状态(稍微偏大的值)进行状态翻转的话,可能输出的clk_out = clk_1 & clk_2就能达到近似50%的占空比。在尝试后发现,clkxcnt等于N+M2N时进行状态翻转,clkycnt等于NM时进行状态翻转,此时输出的clk_out = clk_1 & clk_2近似50%占空比。其中N为不超过分频系数的最大整数,如5.5分频时N=5,当N为奇数时M=(N-1)/2,当N为偶数时M=(N+1)/2

占空比近似50%的4.5分频的波形图如下所示

9

半整数分频器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
`timescale 1ns/1ns
module clk_div_half #(
parameter N = 5 //5.5分频时N=5,以此类推
) (
input wire clk,
input wire rstn,
output wire clk_div
);

//localparam M = 0; //占空比非50%,高电平仅有半周期
localparam M = N[0] ? ((N - 1) >> 1) : ((N + 1) >> 1); //占空比近似50%
reg [31:0] cnt;
reg clkx,clky;

always @(posedge clk or negedge rstn) begin
if(~rstn) begin
cnt <= 32'd0;
end
else if(cnt == (N<<1)) begin
cnt <= 32'd0;
end
else begin
cnt <= cnt + 1'b1;
end
end

always @(posedge clk or negedge rstn) begin
if(~rstn) begin
clkx <= 1'b0;
end
else if(cnt == N + M) begin
clkx <= 1'b0;
end
else if(cnt == (N << 1))begin
clkx <= 1'b1;
end
end

always @(negedge clk or negedge rstn) begin
if(~rstn) begin
clky <= 1'b0;
end
else if(cnt == N) begin
clky <= 1'b1;
end
else if(cnt == M) begin
clky <= 1'b0;
end
end

assign clk_div = clkx & clky;

endmodule

小数分频器

以8.7分频为例来分析小数分频器的设计。因为无法用计数器表示0.7这种数字,所以我们用一个等效的概念来实现8.7分频,原时钟87个周期的总时间等于分频后的时钟10个周期的总时间。

因为8.7分频在8分频和9分频之间,所以我们用8分频和9分频来组合生成8.7分频的时钟。可以列方程组,设8分频共x个周期,9分频共y个周期,则

  • x+y=10 (1)
  • 8*x + 9 *y = 87 (2)

解得x=3,y=7。

即通过3次8分频和7次9分频可得到8.7分频。但如果是按序先输出3个8分频再输出7次9分频的时钟用处不大,我们还得乱序使其均匀输出,不然会造成时钟频率均匀性不好,相位抖动大的问题

此处我们介绍脉冲删除小数分频,该方法相对比较简单。什么意思呢?就是说我在87个输入时钟里删掉77个时钟周期,这样不就输出了10个时钟周期了吗?也就实现了8.7分频,那么该怎么删呢?查阅论文后得到结论:

  1. 设置寄存器cnt位宽自定,初始值为0;
  2. clk的上升沿cnt=cnt+分母,并判断cnt是否大于分子,若大于分子则在下一周期减去分子;
  3. cnt小于分子时,输出脉冲信号为0,cnt大于分子时,输出脉冲信号为1;

说起来比较乱,我们以7/3分频为例来看

时钟序号 cnt 输出脉冲
0 3 0
1 6 0
2 9 1
3 (12->)5 0
4 8 1
5 (11->)4 0
6 (7->)0 1

从表中可以看到每7个周期输出3个脉冲,刚好满足分频要求。

小数分频器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
`timescale 1ns/1ns
module clk_div_decimal #(
parameter fraction = 16'd87, //分频的分子
parameter denominator = 16'd10, //分频的分母
parameter cnt_width = 8 //计数器的位宽
) (
input wire clk,
input wire rstn,
output reg clk_out
);

reg [cnt_width-1:0] cnt;
always @(posedge clk or negedge rstn) begin
if(~rstn) begin
cnt <= 0;
clk_out <= 1'b0;
end
else if(cnt < fraction) begin
cnt <= cnt + denominator;
clk_out <= 1'b0;
end
else begin
cnt <= cnt + denominator - fraction;
clk_out <= 1'b1;
end
end

endmodule

8.7分频结果如下

10

3.25分频结果如下

11

PLL分频

这里就给大家简单介绍一下如何在Vivado中对PLL进行例化。

首先打开vivado,新建一个RTL项目,点击Flow Navigator窗口中的IP Catalog,在search处搜索自己想要的IP核的名字,例如输入clock就会找到Clocking Wizard这个IP核,如下图所示

12

双击Clocking Wizard 这个IP核,就能弹出配置窗口;我们简单配置一些信息,输入时钟为100MHz,分频输出两个时钟,一个是30MHz,一个是18MHz,查看分频效果;

13

14

点击Generate,生成IP核,然后在source窗口就会出现一个文件

15

16

下面对这个生成的IP进行例化,测试分频效果

17

将这个例化模块的例程添加到自己的顶层仿真代码中,就可以查看分频结果了。


本次的博客就讲到这里,其中PLL的使用讲的很浅显,大家要想深入学习PLL的IP使用的话可以学习官方文档。若文章中存在任何错误或不足欢迎大家指正,欢迎大家在博客下方留言交流。