#- 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.
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.
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.