When You Want to Get Cycle-to-Cycle Jitter at PLL Output Due to the Reference Oscillator

Caution - the script does not provide the correct result. You expect cycle-to-cycle to be a high-frequency phenomenon - and therefore, to not depend on the oscillator phase-noise or jitter. But the script will show a large value for c2c jitter on the PLL output. If you can figure it out, please post/comment.

Designers Guide : Phase Noise and Jitter prediction, by Ken Kundert

Converting Phase Noise to Cycle-to-Cycle Jitter in a PLL

When designing or analyzing phase-locked loops (PLLs), one key metric of interest is the RMS cycle-to-cycle jitter at the output. Given phase noise data (typically in dBc/Hz) for the reference oscillator, how do we estimate the jitter after the PLL processes it?

🎯 Goal

Estimate RMS cycle-to-cycle jitter (in seconds or picoseconds) at the output of a PLL, based on:

  • Measured or simulated phase noise data (dBc/Hz vs. offset frequency)
  • PLL dynamics: natural frequency (fn), damping ratio (ζ), divider ratio (N)
  • Carrier frequency (fc)

🧭 Step-by-Step Breakdown

1. Read Phase Noise Data

Extract two columns from a CSV file:

  • f_offset: Offset frequencies (e.g., 1 kHz, 10 kHz)
  • L_dbc: Phase noise values in dBc/Hz

2. Convert dBc/Hz to Linear Phase Noise PSD

Use:

S_phi_in = 2 * 10 ** (L_dbc / 10)
This converts logarithmic SSB noise into linear power spectral density (rad²/Hz). The factor 2 accounts for the total noise power from both sidebands.

3. Apply the PLL Transfer Function

The PLL acts as a low-pass filter for reference phase noise. The closed-loop transfer function from reference to output (in the s-domain) is:

    H(s) = N / (1 + 2ζ·s/ωₙ + s²/ωₙ²)

where:

  • N is the divider ratio
  • ζ is the damping factor
  • ωₙ = 2Ï€·fn is the natural frequency in rad/s

In the frequency domain, we compute the squared magnitude:

|H(j2Ï€f)|² = ωₙ⁴ / [ (ωₙ² - ω²)² + (2ζωₙω)² ]

This determines how phase noise at each offset frequency is shaped by the PLL. Importantly:

  • Low-frequency noise (inside loop bandwidth) is passed to the output.
  • High-frequency noise (outside bandwidth) is suppressed.

4. Convert Phase PSD to Time PSD

To obtain timing jitter, convert phase PSD to time PSD using:

S_tau = S_phi_out / (2Ï€·fc)²
This gives the jitter power spectral density in units of seconds squared per Hz (s²/Hz).

5. Apply Cycle-to-Cycle Jitter Filter

Cycle-to-cycle jitter captures variations from one clock period to the next. The filtering effect is modeled by:

H_ccj(f)² = 4·sin²(Ï€fT)
where T = 1 / fc is the clock period. This emphasizes jitter components near the Nyquist frequency and suppresses slow variations.

6. Integrate Over Frequency

Finally, integrate the filtered PSD to obtain total jitter power:

rms_ccj = sqrt( ∫ S_ccj(f) df )
The square root converts from variance to RMS jitter.

✅ Final Output

The script computes and prints the RMS cycle-to-cycle jitter in picoseconds:

RMS Cycle-to-Cycle Jitter:  X.XXX ps

🧠 Summary

StageDescription
dBc/Hz → rad²/HzConvert log-scale phase noise to linear PSD
Apply PLL filterPass low-frequency reference noise, suppress high-frequency
Convert to time PSDTranslate phase noise to equivalent timing noise
Cycle-to-cycle filterExtract only variations from one clock period to the next
IntegrateSum jitter energy across frequency
SqrtConvert jitter power to RMS jitter

This methodology connects frequency-domain measurements (phase noise) to time-domain performance (jitter), enabling precise prediction of clock behavior in high-speed systems.


Here is a script that you can use as (fn is the 3dB bandwidth of the PLL in Hz)

$ python3 pll_jitter.py phase_noise_saved.csv --N 256 --zeta 0.9 --fn 2e3 --fc 32768

import numpy as np
import argparse
import matplotlib.pyplot as plt
from scipy.integrate import simpson

import pandas as pd

def read_phase_noise_file(filename):
    with open(filename, 'r') as f:
        first_line = f.readline()

    # Try to detect whether the first row is a header
    try:
        float(first_line.split(',')[0])
        # It's numeric → no header
        df = pd.read_csv(filename, header=None)
    except ValueError:
        # Not numeric → has header
        df = pd.read_csv(filename)

    freq = df.iloc[:, 0].values
    L_dbc_per_hz = df.iloc[:, 1].values
    return freq, L_dbc_per_hz


def dBc_to_rad2(L_dbc):
    """Convert dBc/Hz to rad^2/Hz."""
    return 2 * 10 ** (L_dbc / 10)

def H_squared(f, fn, zeta):
    """|H(j2Ï€f)|^2 for a second-order PLL closed-loop TF."""
    wn = 2 * np.pi * fn
    omega = 2 * np.pi * f
    num = wn**2
    denom = (wn**2 - omega**2)**2 + (2 * zeta * wn * omega)**2
    return num**2 / denom

def phase_to_time_psd(S_phi, f_carrier):
    """Convert phase noise PSD (rad^2/Hz) to time jitter PSD (s^2/Hz)."""
    return S_phi / (2 * np.pi * f_carrier) ** 2

def ccj_filter(f, T):
    """Cycle-to-cycle jitter filter: 4*sin^2(pi*f*T)"""
    return 4 * np.sin(np.pi * f * T) ** 2

def main():
    parser = argparse.ArgumentParser(description="Estimate RMS cycle-to-cycle jitter from phase noise")
    parser.add_argument("filename", help="Phase noise file (2 columns: offset frequency [Hz], L(f) [dBc/Hz])")
    parser.add_argument("--N", type=int, default=1, help="PLL divider ratio (N)")
    parser.add_argument("--zeta", type=float, default=0.707, help="Damping ratio ζ")
    parser.add_argument("--fn", type=float, required=True, help="PLL natural frequency in Hz")
    parser.add_argument("--fc", type=float, required=True, help="Carrier frequency (Hz)")
    args = parser.parse_args()

    f_offset, L_dbc = read_phase_noise_file(args.filename)
    S_phi_in = dBc_to_rad2(L_dbc)
    H2 = H_squared(f_offset, args.fn, args.zeta)
    S_phi_out = (args.N ** 2) * S_phi_in * H2
    S_tau = phase_to_time_psd(S_phi_out, args.fc)

    T = 1 / args.fc
    S_ccj = ccj_filter(f_offset, T) * S_tau

    rms_ccj = np.sqrt(simpson(S_ccj, f_offset))  # result in seconds
    print(f"RMS Cycle-to-Cycle Jitter: {rms_ccj * 1e12:.3f} ps")

if __name__ == "__main__":
    main()

Comments

Popular posts from this blog

Plotting Device Parameters (gm, vdsat, etc) Vs Time or Temp

HIdden States in Your Behavioral Model