前言

这学期报了 现代集成锁相环技术 这门课,还是新开的课。本来看考核方式是考试,有点头疼,谁知上两节课后老师说改成考察了。感谢我导。

但我依旧是数字方向,对锁相环一窍不通,也没啥学的欲望。期末的大作业还是得做的。

Verilog-A 仿真环境配置

期末三个大作业,第二个是进行行为级建模。MATLAB几百年没开过了,不想用,还是纯写代码吧。

OpenVAF

OpenVAF是一个开源的Verilog-A编译器,可以将代码编译成OSDI模型,供SPICE使用。

去官网下载: Downloads | OpenVAF

这里要注意下载v23.5.0的。新版本移除了各种依赖库,我是用不了。

放到Path下即可使用,用法为:

1
openvaf -o veriloga/dpll_behavior.osdi veriloga/dpll_behavior.va

编译没有问题就会生成OSDI文件,可以在NGSPICE内使用。

NGSPICE

Linux下直接安装发行包即可。最好看一下版本,需要是NGSPICE42。

随后导入OSDI模型,在最开头添加:

1
pre_osdi veriloga/dpll_behavior.osdi

即可导入OSDI。

Qucs-S

Qucs-S是一个开源的NGSPICE GUI。主要Linux下没啥好用的轻量化仿真软件了。直接在OpenSUSE下载编译好的deb即可,

这玩意有不少BUG,捏着鼻子用完了。

Verilog-A 模型设计

整体设计

锁相环本质上是一个负反馈系统。它通过比较参考信号和反馈信号的相位差,调节 VCO 的控制电压,使 VCO 输出频率最终稳定在目标频率。

对于整数分频 PLL,有:

fout=Nfreff_{out}=Nf_{ref}

本设计中参考频率为:

fref=100 MHzf_{ref}=100\ \text{MHz}

分频比为:

N=24N=24

因此锁定后的目标输出频率为:

fout=2.4 GHzf_{out}=2.4\ \text{GHz}

即PLL 会通过反馈环路自动调节 VCO,使其输出频率稳定在参考频率的 24 倍。

整体结构

整体结构如下:

flowchart TB
    REF["100 MHz 理想参考相位"] --> PD["相位误差检测"]
    FB["100 MHz 反馈相位"] --> PD

    PD --> LF["比例-积分环路滤波"]
    LF --> DCO["DCO 频率控制"]
    DCO --> OUT["2.4 GHz 输出相位/波形"]
    OUT --> DIV["÷24"]
    DIV --> FB

Verilog-A 模块

考虑到OpenVAF还不支持事件驱动型语句(如@cross),因此设计一个简单的数字锁相环,采用相位域建模方法,通过参考相位、输出相位、反馈相位和相位误差之间的连续关系描述锁相环的动态过程。

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
`include "constants.vams"
`include "disciplines.vams"

module dpll_behavior(
ref,
fb,
noise_in,
out,
ctrl,
phase_err
);
input ref, noise_in;
output fb, out, ctrl, phase_err;
electrical ref, noise_in, fb, out, ctrl, phase_err;
electrical phi_out, loop_int;

parameter real VDD = 1.2;
parameter real VTH = 0.6;
parameter integer DIVIDE = 24;
parameter real F_REF = 100e6;
parameter real F_CENTER = 2.4e9;
parameter real F_INIT = 2.0e9;
parameter real KDCO = 1.0e6;
parameter real KP = 125.0;
parameter real KI = 1.0e9;
parameter real CTRL_MIN = -600.0;
parameter real CTRL_MAX = 600.0;
parameter real CSTATE = 1.0e-12;
parameter real OUT_SHARPNESS = 20.0;
parameter real NOISE_GAIN = 1.0;

real pi2;
real ref_phase;
real out_phase;
real fb_phase;
real phase_error_rad;
real loop_integral;
real ctrl_unlimited;
real f_dco;
real ctrl_word;
real noise_phase;
real noisy_out_phase;

analog begin
@(initial_step) begin
pi2 = 6.2831853071795864769;
ctrl_word = (F_INIT - F_CENTER) / KDCO;
f_dco = F_INIT;
end

ref_phase = pi2 * F_REF * $abstime;
out_phase = V(phi_out);
noise_phase = NOISE_GAIN * V(noise_in);
noisy_out_phase = out_phase + noise_phase;
fb_phase = noisy_out_phase / DIVIDE;

phase_error_rad = sin(ref_phase - fb_phase);
loop_integral = V(loop_int);
ctrl_unlimited = (F_INIT - F_CENTER) / KDCO +
KP * phase_error_rad +
KI * loop_integral;

ctrl_word = 0.5 * (CTRL_MAX - CTRL_MIN) *
tanh(ctrl_unlimited / (0.5 * (CTRL_MAX - CTRL_MIN)));
f_dco = F_CENTER + KDCO * ctrl_word;

I(phi_out) <+ CSTATE * ddt(V(phi_out)) - CSTATE * pi2 * f_dco;
I(loop_int) <+ CSTATE * ddt(V(loop_int)) - CSTATE * phase_error_rad;

V(out) <+ 0.5 * VDD + 0.5 * VDD * tanh(OUT_SHARPNESS * sin(noisy_out_phase));
// V(fb) <+ 0.5 * VDD + 0.5 * VDD * tanh(OUT_SHARPNESS * sin(fb_phase));
V(fb) <+ 0.5 * VDD - 0.5 * VDD * tanh(OUT_SHARPNESS * sin(fb_phase));
V(ctrl) <+ ctrl_word;
V(phase_err) <+ phase_error_rad / (pi2 * F_REF) * 1.0e12;
end
endmodule

参考源

参考源根据仿真时间生成参考相位,并用连续函数生成接近方波的参考信号。这样既能提供相位信息,也能保持波形连续,有利于 SPICE 类仿真器收敛。

1
2
parameter real F_REF = 100e6;
ref_phase = pi2 * F_REF * $abstime;

PFD 与电荷泵

PFD/CP 模块直接计算参考相位和反馈相位之间的误差,并将其转换为电荷泵电流。

当参考相位领先反馈相位时,电荷泵会调节控制电压,使 VCO 频率升高;当反馈相位领先时,则降低 VCO 频率。这样就形成了 PLL 的负反馈调节过程。

1
phase_error_rad = sin(ref_phase - fb_phase);

接着接入电荷泵+环路滤波器:

1
2
3
4
5
6
7
8
loop_integral   = V(loop_int);

ctrl_unlimited = (F_INIT - F_CENTER) / KDCO
+ KP * phase_error_rad // 比例控制项
+ KI * loop_integral; // 积分控制项

ctrl_word = 0.5 * (CTRL_MAX - CTRL_MIN) *
tanh(ctrl_unlimited / (0.5 * (CTRL_MAX - CTRL_MIN)));

积分状态由下面这行实现:

1
I(loop_int) <+ CSTATE * ddt(V(loop_int)) - CSTATE * phase_error_rad;

VCO

VCO 的频率由控制电压决定:

fvco=f0+KvcoVctrlf_{vco}=f_0+K_{vco}V_{ctrl}

本设计中:

f0=1.6 GHzKvco=800 MHz/V\begin{aligned} f_0&=1.6\ \text{GHz}\\ K_{vco}&=800\ \text{MHz/V} \end{aligned}

锁定时目标频率为 2.4 GHz2.4\ \text{GHz},因此控制电压大约为:

Vctrl=2.41.60.8=1.0 VV_{ctrl}=\frac{2.4-1.6}{0.8}=1.0\ \text{V}

即在默认参数下,PLL 锁定时的控制电压约为 1 V。

首先定义参数:

1
2
3
parameter real F_CENTER = 2.4e9;
parameter real F_INIT = 2.0e9;
parameter real KDCO = 1.0e6;

接着定义VCO/DCO的调谐关系:

1
f_dco = F_CENTER + KDCO * ctrl_word;

随后进行相位积分,让 phi_out 节点电压表示输出相位:

1
I(phi_out) <+ CSTATE * ddt(V(phi_out)) - CSTATE * pi2 * f_dco;

最后由相位生成输出近似方波:

1
V(out) <+ 0.5 * VDD + 0.5 * VDD * tanh(OUT_SHARPNESS * sin(noisy_out_phase));

分频器

分频器采用相位域模型,不是真实的数字计数器。它直接将 VCO 相位除以分频比:

ϕfb=ϕvco24\phi_{fb}=\frac{\phi_{vco}}{24}

这样可以快速建立反馈相位与参考相位之间的关系,适合系统级验证。

1
fb_phase = noisy_out_phase / DIVIDE;

最后根据反馈相位生成反馈方波:

1
V(fb) <+ 0.5 * VDD - 0.5 * VDD * tanh(OUT_SHARPNESS * sin(fb_phase));

SPICE 封装

之后封装器件模型,便于后面可视化仿真:

1
2
3
4
5
6
7
8
9
.model DIGITALRING_DPLL dpll_behavior out_sharpness=20.0 noise_gain=1.0

.subckt DIGITALRING_DPLL_BEHAVIOR ref fb out ctrl phase_err
NDPLL ref fb 0 out ctrl phase_err DIGITALRING_DPLL
.ends DIGITALRING_DPLL_BEHAVIOR

.subckt DIGITALRING_DPLL_BEHAVIOR_NOISE ref fb noise_in out ctrl phase_err
NDPLL ref fb noise_in out ctrl phase_err DIGITALRING_DPLL
.ends DIGITALRING_DPLL_BEHAVIOR_NOISE

最后配套写一个SPICE网表用于测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
* Integer-N ADPLL behavioral simulation, 100 MHz reference to 2.4 GHz output.

VREF ref 0 PULSE(0 1.2 0 5p 5p 5n 10n)
VNOISE noise_in 0 0
.model DPLL dpll_behavior
NPLL ref fb noise_in out ctrl phase_err DPLL

.tran 10p 3u uic

.control
pre_osdi veriloga/dpll_behavior.osdi
set wr_singlescale
set wr_vecnames
run
wrdata results/veriloga_dpll.csv v(out) v(fb) v(ctrl) v(phase_err)
meas tran fb_20cycle_time TRIG v(fb) VAL=0.6 RISE=260 TARG v(fb) VAL=0.6 RISE=280
meas tran out_480cycle_time TRIG v(out) VAL=0.6 RISE=6240 TARG v(out) VAL=0.6 RISE=6720
let out_final_freq_hz = 480 / out_480cycle_time
let out_final_freq_ghz = out_final_freq_hz / 1e9
print fb_20cycle_time out_480cycle_time out_final_freq_hz out_final_freq_ghz
quit
.endc

.end

Qucs-S 可视化仿真

Qucs-S 网表与波形

可以看到仿真出的各种波形。下面简单分析一下。

结果分析

仿真时主要关注以下几个信号:

  • vctrl:控制电压是否逐渐收敛到约 1 V;
  • vout:VCO 输出频率是否接近 2.4 GHz;
  • fb:分频后反馈频率是否接近 100 MHz;
  • ref_phase - fb_phase:相位误差是否逐渐稳定。

如果 vctrl 长时间振荡,可能说明环路滤波器阻尼不足;如果控制电压达到上限仍无法锁定,则可能是 VCO 调谐范围不够。

输出频率

可以看到,输出频率是曲折上升的。

一开始,在还没有锁定时,参考相位和反馈相位之间存在频率差,所以 ref_phase - fb_phase 会不断变化。经过 sin() 之后,相位误差就是一个周期性摆动信号。这个周期性误差通过比例项 KP * phase_error_rad 直接进入控制量,所以 Vctrl 会有波纹,fout 也会有波纹。

随后,接近锁定时,fout 快速冲过 2.4 GHz。这是因为前面 fout 长时间低于 2.4 GHz,反馈相位已经累积落后很多。要把之前落后的相位追上,反馈时钟必须暂时跑得比参考时钟快, fout 短暂超过 2.4 GHz 是 PLL 在“追相位”。

最后,过冲后回落,稳定在 2.4 GHz,是因为相位误差符号改变,控制环路开始把 Vctrl 往回拉。

不足之处

全赖OpenVAF

  • PFD/CP 是平均相位模型,不会产生真实 UP/DN 窄脉冲;
  • 分频器不是实际数字计数器,不能反映触发器延迟和边沿量化误差;
  • 参考相位由时间生成,不能直接反映真实参考时钟抖动;
  • 环路滤波器需要在顶层电路中额外搭建;
  • 该模型更适合系统级分析,不适合精确研究参考杂散、相位噪声和电荷泵失配等问题。

SystemVerilog 数字锁相环

再来看数字模块。这下舒服了😋

基本频率相关计算公式依旧和上面一样。懒得写了。

整体结构

整体结构如下:

flowchart TB
    REF["ref_clk
100 MHz 参考时钟"] --> PD["dpll_phase_detector
相位/频率检测器"] FB["fb_clk
反馈时钟"] --> PD PD -->|phase_error_ps| LF["dpll_pi_filter
数字 PI 环路滤波器"] PD -->|phase_valid| LF PD -->|locked, lock_count| LOCK["锁定状态输出"] LF -->|ctrl_word_q16| DCO["dpll_dco_model
DCO 模型"] DCO -->|dco_clk
约 2.4 GHz| OUT["PLL 输出
dco_clk"] DCO -->|dco_clk| DIV["dpll_feedback_divider
/24 分频器"] DIV -->|fb_clk
约 100 MHz| FB

其中:

  • ref_clk 是 100 MHz 参考时钟;
  • 相位/频率检测器比较 ref_clkfb_clk 的时间差;
  • 数字 PI 滤波器根据相位误差生成控制字;
  • DCO 根据控制字调整输出频率;
  • 分频器将 DCO 输出除以 24,形成反馈时钟。

SystemVerilog 模块

顶层模块

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
`timescale 1ps / 1ps

module dpll_tb;
localparam integer REF_HALF_PERIOD_PS = 5000;
localparam integer DIVIDE = 24;

// 平均多少个 fb_clk 周期来计算平均输出频率
localparam integer AVG_N = 16;

reg ref_clk;
reg rst_n;

wire dco_clk;
wire fb_clk;
wire signed [31:0] phase_error_ps;
wire signed [31:0] ctrl_word_q16;
wire locked;
wire [15:0] lock_count;

// 方便在波形中观察分频计数器
wire [15:0] div_count_dbg;

integer csv_file;
integer sample_count;

time last_fb_edge;
time fb_period_ps;

time avg_start_fb_edge;
time avg_window_ps;
integer avg_count;

real output_freq_mhz;
real output_freq_avg_mhz;
real ctrl_word_lsb;

dpll_top_model #(
.DIVIDE(DIVIDE)
) dut (
.ref_clk (ref_clk),
.rst_n (rst_n),
.dco_clk (dco_clk),
.fb_clk (fb_clk),
.phase_error_ps(phase_error_ps),
.ctrl_word_q16 (ctrl_word_q16),
.locked (locked),
.lock_count (lock_count)
);

// 层次化引用内部计数器,仅用于仿真观察
assign div_count_dbg = dut.feedback_divider.div_count;

// 100 MHz reference clock
initial begin
ref_clk = 1'b0;
forever #(REF_HALF_PERIOD_PS) ref_clk = ~ref_clk;
end

initial begin
rst_n = 1'b0;
sample_count = 0;

last_fb_edge = 0;
fb_period_ps = 0;

avg_start_fb_edge = 0;
avg_window_ps = 0;
avg_count = 0;

output_freq_mhz = 0.0;
output_freq_avg_mhz = 0.0;
ctrl_word_lsb = 0.0;

csv_file = $fopen("results/systemverilog_dpll.csv", "w");
if (csv_file == 0) begin
$display("ERROR: could not open results/systemverilog_dpll.csv");
$finish;
end

$fwrite(
csv_file,
"time_ps,phase_error_ps,ctrl_word_lsb,fb_period_ps,output_freq_mhz,output_freq_avg_mhz,locked,lock_count,div_count_dbg\n"
);

$dumpfile("results/systemverilog_dpll.vcd");
$dumpvars(0, dpll_tb);

// reset 一段时间后释放
#30000 rst_n = 1'b1;

// 仿真 5 us
#5000000;

$display(
"SV final: t=%0t ps phase_error=%0d ps ctrl=%0f lsb fout_inst=%0f MHz fout_avg=%0f MHz locked=%0d",
$time, phase_error_ps, $itor(ctrl_word_q16) / 65536.0, output_freq_mhz,
output_freq_avg_mhz, locked);

$fclose(csv_file);
$finish;
end

always @(posedge fb_clk or negedge rst_n) begin
if (!rst_n) begin
last_fb_edge <= 0;
fb_period_ps <= 0;

avg_start_fb_edge <= 0;
avg_window_ps <= 0;
avg_count <= 0;

output_freq_mhz <= 0.0;
output_freq_avg_mhz <= 0.0;
ctrl_word_lsb <= 0.0;

sample_count <= 0;
end else begin
ctrl_word_lsb = $itor(ctrl_word_q16) / 65536.0;

// 单周期即时频率
if (last_fb_edge != 0) begin
fb_period_ps = $time - last_fb_edge;
output_freq_mhz = (DIVIDE * 1.0e6) / fb_period_ps;
end

last_fb_edge = $time;

// 多周期平均频率
if (avg_start_fb_edge == 0) begin
avg_start_fb_edge = $time;
avg_count = 0;
end else begin
avg_count = avg_count + 1;

if (avg_count == AVG_N) begin
avg_window_ps = $time - avg_start_fb_edge;
output_freq_avg_mhz = (DIVIDE * AVG_N * 1.0e6) / avg_window_ps;

avg_start_fb_edge = $time;
avg_count = 0;
end
end

// 从第二个 fb_clk 上升沿开始写 CSV,避免初始周期无效
if (fb_period_ps != 0) begin
$fwrite(csv_file, "%0t,%0d,%0f,%0t,%0f,%0f,%0d,%0d,%0d\n", $time, phase_error_ps,
ctrl_word_lsb, fb_period_ps, output_freq_mhz, output_freq_avg_mhz, locked,
lock_count, div_count_dbg);

if ((sample_count % 50) == 0) begin
$display(
"SV sample %0d: t=%0t ps phase=%0d ps ctrl=%0f lsb fb_period=%0t ps fout_inst=%0f MHz fout_avg=%0f MHz locked=%0d",
sample_count, $time, phase_error_ps, ctrl_word_lsb, fb_period_ps,
output_freq_mhz, output_freq_avg_mhz, locked);
end

sample_count = sample_count + 1;
end
end
end
endmodule

相位/频率检测器

相位/频率检测器模块名为 dpll_phase_detector,它的作用是在参考时钟上升沿采样最近一次反馈时钟上升沿的时间,从而得到时间域相位误差。为了提高初始频差较大时的捕获能力,代码中还加入了反馈周期误差辅助项。该检测器会把误差限制在 [-T_ref/2, T_ref/2] 范围内,并使用反馈周期误差辅助拉频。

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
module dpll_phase_detector #(
parameter integer REF_PERIOD_PS = 10000,
parameter integer FREQ_AID_GAIN = 2,
parameter integer LOCK_WINDOW_PS = 25,
parameter integer LOCK_CYCLES = 32
) (
input wire ref_clk,
input wire fb_clk,
input wire rst_n,
output reg signed [31:0] phase_error_ps,
output reg phase_valid,
output reg locked,
output reg [15:0] lock_count
);
time last_fb_edge;
reg signed [31:0] last_fb_period_ps;
reg signed [31:0] wrapped_error_ps;

always @(posedge fb_clk or negedge rst_n) begin
if (!rst_n) begin
last_fb_edge <= 0;
last_fb_period_ps <= REF_PERIOD_PS;
end else begin
if (last_fb_edge != 0) begin
last_fb_period_ps <= $time - last_fb_edge;
end
last_fb_edge <= $time;
end
end

always @(posedge ref_clk or negedge rst_n) begin
if (!rst_n) begin
phase_error_ps <= 0;
phase_valid <= 1'b0;
locked <= 1'b0;
lock_count <= 16'd0;
end else begin
wrapped_error_ps = $time - last_fb_edge;

if (wrapped_error_ps > (REF_PERIOD_PS / 2)) begin
wrapped_error_ps = wrapped_error_ps - REF_PERIOD_PS;
end else if (wrapped_error_ps < -(REF_PERIOD_PS / 2)) begin
wrapped_error_ps = wrapped_error_ps + REF_PERIOD_PS;
end

wrapped_error_ps = wrapped_error_ps +
FREQ_AID_GAIN * (REF_PERIOD_PS - last_fb_period_ps);

if (wrapped_error_ps > (REF_PERIOD_PS / 2)) begin
wrapped_error_ps = REF_PERIOD_PS / 2;
end else if (wrapped_error_ps < -(REF_PERIOD_PS / 2)) begin
wrapped_error_ps = -(REF_PERIOD_PS / 2);
end

phase_error_ps <= wrapped_error_ps;
phase_valid <= 1'b1;

if ((wrapped_error_ps <= LOCK_WINDOW_PS) && (wrapped_error_ps >= -LOCK_WINDOW_PS)) begin
if (lock_count < LOCK_CYCLES[15:0]) begin
lock_count <= lock_count + 16'd1;
end
end else begin
lock_count <= 16'd0;
end

locked <= (lock_count >= (LOCK_CYCLES - 1));
end
end
endmodule
1
2
3
4
5
6
7
8
9
10
11
always @(posedge fb_clk or negedge rst_n) begin
if (!rst_n) begin
last_fb_edge <= 0;
last_fb_period_ps <= REF_PERIOD_PS;
end else begin
if (last_fb_edge != 0) begin
last_fb_period_ps <= $time - last_fb_edge;
end
last_fb_edge <= $time;
end
end

这部分代码记录反馈时钟 fb_clk 的上升沿时间,并计算相邻反馈边沿之间的周期 last_fb_period_ps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
always @(posedge ref_clk or negedge rst_n) begin
if (!rst_n) begin
phase_error_ps <= 0;
phase_valid <= 1'b0;
locked <= 1'b0;
lock_count <= 16'd0;
end else begin
wrapped_error_ps = $time - last_fb_edge;

if (wrapped_error_ps > (REF_PERIOD_PS / 2)) begin
wrapped_error_ps = wrapped_error_ps - REF_PERIOD_PS;
end else if (wrapped_error_ps < -(REF_PERIOD_PS / 2)) begin
wrapped_error_ps = wrapped_error_ps + REF_PERIOD_PS;
end

wrapped_error_ps = wrapped_error_ps +
FREQ_AID_GAIN * (REF_PERIOD_PS - last_fb_period_ps);

这部分代码在参考时钟上升沿计算相位误差。$time - last_fb_edge 表示当前参考边沿与最近一次反馈边沿之间的时间差。后面的 FREQ_AID_GAIN * (REF_PERIOD_PS - last_fb_period_ps) 是频率辅助项,用于加快频率捕获。

1
2
3
4
5
6
7
8
9
10
11
12
        if ((wrapped_error_ps <= LOCK_WINDOW_PS) &&
(wrapped_error_ps >= -LOCK_WINDOW_PS)) begin
if (lock_count < LOCK_CYCLES[15:0]) begin
lock_count <= lock_count + 16'd1;
end
end else begin
lock_count <= 16'd0;
end

locked <= (lock_count >= (LOCK_CYCLES - 1));
end
end

这一段实现锁定判据:当相位误差连续多个参考周期都落在锁定窗口内时,locked 置为 1。

数字 PI 环路滤波器

环路滤波器模块名为 dpll_pi_filter,它使用 Q16 固定点格式实现数字 PI 控制。误差符号的约定是:如果反馈超前参考,则需要降低 DCO 频率;如果反馈滞后参考,则需要提高 DCO 频率。

关键参数如下:

1
2
3
4
5
parameter integer signed KP_Q16        = 512,
parameter integer signed KI_Q16 = 64,
parameter integer signed CTRL_INIT_Q16 = -250 * 65536,
parameter integer signed CTRL_MIN_Q16 = -1024 * 65536,
parameter integer signed CTRL_MAX_Q16 = 1024 * 65536

其中 KP_Q16 是比例系数,KI_Q16 是积分系数。CTRL_INIT_Q16 设置初始控制字为 -250 LSB,对应 DCO 初始频率低于目标频率,便于观察拉频过程。

积分和控制字更新代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
always @(posedge ref_clk or negedge rst_n) begin
if (!rst_n) begin
integrator_q16 <= CTRL_INIT_Q16;
ctrl_word_q16 <= CTRL_INIT_Q16;
end else if (phase_valid) begin
next_integrator_q16 = integrator_q16 - (KI_Q16 * phase_error_ps);

if (next_integrator_q16 > CTRL_MAX_Q16) begin
next_integrator_q16 = CTRL_MAX_Q16;
end else if (next_integrator_q16 < CTRL_MIN_Q16) begin
next_integrator_q16 = CTRL_MIN_Q16;
end

ctrl_calc_q16 = next_integrator_q16 - (KP_Q16 * phase_error_ps);
integrator_q16 <= next_integrator_q16;
ctrl_word_q16 <= clamp_q16(ctrl_calc_q16);
end
end

可以看到,PI 滤波器由两部分组成:

1
2
积分项:integrator_q16 - KI × phase_error_ps
比例项:- KP × phase_error_ps

最终输出 ctrl_word_q16,用于控制 DCO 输出频率。

DCO 行为模型

此模块无法综合。

DCO 模块名为 dpll_dco_model,它根据控制字计算当前输出频率,再用可变延迟产生时钟。报告中也说明该 DCO 使用 real 类型和可变延迟产生时钟,适合系统级验证,若用于芯片实现需要替换为真实 DCO 宏单元或振荡器阵列。

参数如下:

1
2
3
4
parameter real F_CENTER_HZ       = 2.4e9,
parameter real F_GAIN_HZ_PER_LSB = 1.0e6,
parameter real F_MIN_HZ = 1.8e9,
parameter real F_MAX_HZ = 3.0e9

频率控制关系为:

fDCO=FCENTER_HZ+FGAIN_HZ_PER_LSB×ctrl_word_lsbf_{DCO} = F_{CENTER\_HZ} + F_{GAIN\_HZ\_PER\_LSB} \times ctrl\_word\_lsb

对应代码:

1
2
3
4
5
6
7
8
ctrl_word_lsb = $itor(ctrl_word_q16) / 65536.0;
dco_freq_hz = F_CENTER_HZ + F_GAIN_HZ_PER_LSB * ctrl_word_lsb;

if (dco_freq_hz < F_MIN_HZ) begin
dco_freq_hz = F_MIN_HZ;
end else if (dco_freq_hz > F_MAX_HZ) begin
dco_freq_hz = F_MAX_HZ;
end

这部分先把 Q16 格式的控制字转换成 LSB 单位,再根据 DCO 增益计算实际频率,并限制在 1.8 GHz 到 3.0 GHz 之间。

DCO 输出时钟由半周期延迟生成:

1
2
half_period_ps = 5.0e11 / dco_freq_hz;
#(half_period_ps) dco_clk = ~dco_clk;

因为:

Thalf=0.5/fDCOT_{half} = 0.5 / f_{DCO}

单位换算到 ps 后,就是代码里的 5.0e11 / dco_freq_hz

/24 反馈分频器

反馈分频器模块名为 dpll_feedback_divider,它的作用是将 DCO 输出时钟除以 24,得到反馈时钟 fb_clk

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module dpll_feedback_divider #(
parameter integer DIVIDE = 24
) (
input wire dco_clk,
input wire rst_n,
output reg fb_clk
);
localparam integer HALF_DIVIDE = DIVIDE / 2;
reg [15:0] div_count;

always @(posedge dco_clk or negedge rst_n) begin
if (!rst_n) begin
div_count <= 16'd0;
fb_clk <= 1'b0;
end else if (div_count == (HALF_DIVIDE - 1)) begin
div_count <= 16'd0;
fb_clk <= ~fb_clk;
end else begin
div_count <= div_count + 16'd1;
end
end
endmodule

DIVIDE = 24 时,HALF_DIVIDE = 12。反馈时钟每 12 个 DCO 周期翻转一次,因此一个完整反馈周期为 24 个 DCO 周期:

ffb=fDCO/24f_{fb} = f_{DCO} / 24

当 DCO 输出为 2.4 GHz 时:

ffb=2.4GHz/24=100MHzf_{fb} = 2.4 \text{GHz} / 24 = 100 \text{MHz}

正好与参考时钟频率一致。

仿真结果

testbench 中参考时钟周期为 10000 ps,即 100 MHz;复位释放后,闭环运行 5 us。报告中的仿真结果显示,DPLL 能够从约 2.15 GHz 的初始频率逐渐拉入到 2.4 GHz 附近,并在约 3.325 us 时首次满足锁定判据。末段平均输出频率为 2399.973750 MHz,最终相位误差约为 15 ps。

锁定后的输出频率非常接近目标 2.4 GHz,误差约为:

1
2400 MHz - 2399.973750 MHz = 0.026250 MHz

相对误差约为 10.9 ppm,主要来自仿真时间精度和周期测量量化。