PLL LOOP

← all examples · interactive version of sun_pll_sky130nm/jupyter/pll.ipynb · phase locked loops
PFD/CP K_pd
LOOP FILTER Z(s)
VCO K_vco/s
DIVIDE BY N
LOOP
LOOP FILTER
C2 suppresses the ripple the charge pump leaves on the control line. It also adds a pole, and that pole is what eats your phase margin.
VIEW
The notebook's own comment reads #- Fix PLL / Set Q=0.1 / Choose w3db=0.1wpll. Its shipped values give Q ≈ 0.9, so that TODO is still open. Try to close it.
UNITY GAIN
PHASE MARGIN
w_pll
w_z
Q = w_z/w_pll
PEAKING
Open loop L(s)
Closed loop L/(1+L)
VCO noise 1/(1+L)
Unity gain
w_pll

WHAT YOU ARE LOOKING AT

A charge pump PLL, linearised around lock and written as one open-loop gain. The phase detector turns phase error into current, the loop filter turns current into voltage, the VCO turns voltage into frequency, and frequency is the integral of phase — which is where the second integrator, and all the difficulty, comes from:

L(s) = K_vco · K_pd · Z(s) / (N · s),   Z(s) = (1 + sRC1) / ( s(C1+C2)(1 + sτ_p) )

Two poles at the origin means −180° of phase before anything else happens, so without the zero at 1/(RC1) the loop would be exactly marginally stable. The zero is not an optimisation; it is the only thing holding the loop up. C2 then adds a pole that takes some of that phase back — the ripple filtering is not free.

The bottom left panel is the pair that matters for noise. The closed loop L/(1+L) is a low pass: reference and divider noise get through inside the loop bandwidth and are filtered outside it. Its complement 1/(1+L) is a high pass: VCO noise is suppressed inside the loop bandwidth and untouched outside. The two cross at the loop bandwidth, and choosing that crossover is most of PLL design — you are picking which noise source you would rather have.

THINGS WORTH TRYING

THE NOTEBOOK

Kvco = 2*np.pi*1.6e9
Kpd = 1e-6/(2*np.pi)        # current divided by 2 pi
R = 32e3*2
C1 = 6.024e-12
C2 = 0.33e-12
N = 32

wpll = np.sqrt(Kpd*Klp*Kvco/N)
wz = 1/(R*C1)
w3db = wpll**2/wz
Q = wz/wpll

KlpHlp = 1/np.multiply((C1 + C2),s)* \
    (1 + np.multiply(s,(R*C1)))/(1 + np.multiply(s,R*(C1*C2)/(C1 + C2)))

Ls = np.divide(Kvco*Kpd*KlpHlp, N*s)
Cs = np.divide(Ls,1 + Ls)

Source: sun_pll_sky130nm/jupyter/pll.ipynb, from the SUN PLL in sky130. The sliders start on the notebook's values. The step response is not in the notebook; it comes from integrating the same closed-loop expression, so the ringing you see and the phase margin you read are two views of one thing. The companion pfd.ipynb is not here because it plots real SPICE output rather than a model.