This commit is contained in:
Steven Dan
2025-12-11 09:43:42 +08:00
commit d8b2974133
1822 changed files with 280037 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# Copyright 2023 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import matplotlib.pyplot as plt
import numpy as np
import soundfile
from scipy.io import wavfile # soundfile has some issues writing high Fs files
class audio_modulator:
"""
This test helper generates a wav file with a fixed sample rate and tone frequency
of a certain length.
A method then allows sections of it to be frequency modulated by a value in Hz.
The modulated signal (which uses cumultaive phase to avoid discontinuites)
may then be plotted as an FFT to understand the SNR/THD and may also be saved
as a wav file.
"""
def __init__(self, duration_s, sample_rate=48000, test_tone_hz=1000):
self.sample_rate = sample_rate
self.test_tone_hz = test_tone_hz
self.modulator = np.full(int(duration_s * sample_rate), test_tone_hz, dtype=np.float64)
def apply_frequency_deviation(self, start_s, end_s, delta_freq):
start_idx = int(start_s * self.sample_rate)
end_idx = int(end_s * self.sample_rate)
self.modulator[start_idx:end_idx] += delta_freq
def modulate_waveform(self):
# Now create the frequency modulated waveform
# this is designed to accumulate the phase so doesn't see discontinuities
# https://dsp.stackexchange.com/questions/80768/fsk-modulation-with-python
delta_phi = self.modulator * np.pi / (self.sample_rate / 2.0)
phi = np.cumsum(delta_phi)
self.waveform = np.sin(phi)
def save_modulated_wav(self, filename):
integer_output = np.int16(self.waveform * 32767)
# soundfile.write(filename, integer_output, int(self.sample_rate)) # This struggles with >768ksps
wavfile.write(filename, int(self.sample_rate), integer_output)
def plot_modulated_fft(self, filename, skip_s=None):
start_x = 0 if skip_s is None else int(skip_s * self.sample_rate) // 2 * 2
waveform = self.waveform[start_x:]
xf = np.linspace(0.0, 1.0/(2.0/self.sample_rate), waveform.size // 2)
N = xf.size
window = np.kaiser(N*2, 14)
waveform = waveform * window
yf = np.fft.fft(waveform)
fig, ax = plt.subplots()
# Plot a zoom in on the test
tone_idx = int(self.test_tone_hz / (self.sample_rate / 2) * N)
num_side_bins = 50
yf = 20 * np.log10(np.abs(yf) / N)
# ax.plot(xf[tone_idx - num_side_bins:tone_idx + num_side_bins], yf[tone_idx - num_side_bins:tone_idx + num_side_bins], marker='.')
# Plot the whole frequncy range from DC to nyquist
ax.plot(xf[:N], yf[:N], marker='.')
ax.set_xscale("log")
plt.xlim((10**1, 10**5))
plt.ylim((-200, 0))
plt.savefig(filename, dpi=150)
def load_wav(self, filename):
"""
Used for testing only - load a wav into self.waveform
"""
self.waveform, self.sample_rate = soundfile.read(filename)
if __name__ == '__main__':
"""
This module is not intended to be run directly. This is here for internal testing only.
"""
if 0:
test_len = 10
audio = audio_modulator(test_len)
for time_s in range(test_len):
modulation_hz = 10 * (time_s - (test_len) / 2)
audio.apply_frequency_deviation(time_s, time_s + 1, modulation_hz)
audio.modulate_waveform()
audio.save_modulated_wav("modulated.wav")
audio.plot_modulated_fft("modulated_fft.png")
else:
audio = audio_modulator(1)
audio.load_wav("modulated_tone_1000Hz_sd_ds.wav")
# audio = audio_modulator(1, sample_rate=3072000)
# audio.modulate_waveform()
audio.plot_modulated_fft("modulated_tone_1000Hz_sd_ds.png")
# audio.save_modulated_wav("modulated.wav")

View File

@@ -0,0 +1,287 @@
# Copyright 2023 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import subprocess
import re
from pathlib import Path
from sw_pll.pll_calc import print_regs
from contextlib import redirect_stdout
import io
register_file = "register_setup.h" # can be changed as needed. This contains the register setup params and is accessible via C in the firmware
class app_pll_frac_calc:
"""
This class uses the formulae in the XU316 datasheet to calculate the output frequency of the
application PLL (sometimes called secondary PLL) from the register settings provided.
It uses the checks specified in the datasheet to ensure the settings are valid, and will assert if not.
To keep the inherent jitter of the PLL output down to a minimum, it is recommended that R be kept small,
ideally = 0 (which equiates to 1) but reduces lock range.
"""
frac_enable_mask = 0x80000000
def __init__(self, input_frequency, F_init, R_init, f_init, p_init, OD_init, ACD_init, verbose=False):
"""
Constructor initialising a PLL instance
"""
self.input_frequency = input_frequency
self.F = F_init
self.R = R_init
self.OD = OD_init
self.ACD = ACD_init
self.f = f_init # fractional multiplier (+1.0)
self.p = p_init # fractional divider (+1.0)
self.output_frequency = None
self.fractional_enable = True
self.verbose = verbose
self.calc_frequency()
def calc_frequency(self):
"""
Calculate the output frequency based on current object settings
"""
if self.verbose:
print(f"F: {self.F} R: {self.R} OD: {self.OD} ACD: {self.ACD} f: {self.f} p: {self.p}")
print(f"input_frequency: {self.input_frequency}")
assert self.F >= 1 and self.F <= 8191, f"Invalid F setting {self.F}"
assert type(self.F) is int, f"Error: F must be an INT"
assert self.R >= 0 and self.R <= 63, f"Invalid R setting {self.R}"
assert type(self.R) is int, f"Error: R must be an INT"
assert self.OD >= 0 and self.OD <= 7, f"Invalid OD setting {self.OD}"
assert type(self.OD) is int, f"Error: OD must be an INT"
intermediate_freq = self.input_frequency * (self.F + 1.0) / 2.0 / (self.R + 1.0)
assert intermediate_freq >= 360000000.0 and intermediate_freq <= 1800000000.0, f"Invalid VCO freq: {intermediate_freq}"
# print(f"intermediate_freq: {intermediate_freq}")
assert type(self.p) is int, f"Error: r must be an INT"
assert type(self.f) is int, f"Error: f must be an INT"
# From XU316-1024-QF60A-xcore.ai-Datasheet_22.pdf
if self.fractional_enable:
# assert self.p > self.f, "Error f is not < p: {self.f} {self.p}" # This check has been removed as Joe found it to be OK in RTL/practice
pll_ratio = (self.F + 1.0 + ((self.f + 1) / (self.p + 1)) ) / 2.0 / (self.R + 1.0) / (self.OD + 1.0) / (2.0 * (self.ACD + 1))
else:
pll_ratio = (self.F + 1.0) / 2.0 / (self.R + 1.0) / (self.OD + 1.0) / (2.0 * (self.ACD + 1))
self.output_frequency = self.input_frequency * pll_ratio
return self.output_frequency
def get_output_frequency(self):
"""
Get last calculated frequency
"""
return self.output_frequency
def update_all(self, F, R, OD, ACD, f, p):
"""
Reset all App PLL vars
"""
self.F = F
self.R = R
self.OD = OD
self.ACD = ACD
self.f = f
self.p = p
return self.calc_frequency()
def update_frac(self, f, p, fractional=None):
"""
Update only the fractional parts of the App PLL
"""
self.f = f
self.p = p
# print(f"update_frac f:{self.f} p:{self.p}")
if fractional is not None:
self.fractional_enable = fractional
return self.calc_frequency()
def update_frac_reg(self, reg):
"""
Determine f and p from the register number and recalculate frequency
Assumes fractional is set to true
"""
f = int((reg >> 8) & ((2**8)-1))
p = int(reg & ((2**8)-1))
self.fractional_enable = True if (reg & self.frac_enable_mask) else False
return self.update_frac(f, p)
def get_frac_reg(self):
"""
Returns the fractional reg value from current setting
"""
# print(f"get_frac_reg f:{self.f} p:{self.p}")
reg = self.p | (self.f << 8)
if self.fractional_enable:
reg |= self.frac_enable_mask
return reg
def gen_register_file_text(self):
"""
Helper used to generate text for the register setup h file
"""
text = f"/* Input freq: {self.input_frequency}\n"
text += f" F: {self.F}\n"
text += f" R: {self.R}\n"
text += f" f: {self.f}\n"
text += f" p: {self.p}\n"
text += f" OD: {self.OD}\n"
text += f" ACD: {self.ACD}\n"
text += "*/\n\n"
# This is a way of calling a printing function from another module and capturing the STDOUT
class args:
app = True
f = io.StringIO()
with redirect_stdout(f):
# in pll_calc, op_div = OD, fb_div = F, f, p, ref_div = R, fin_op_div = ACD
print_regs(args, self.OD + 1, [self.F + 1, self.f + 1, self.p + 1] , self.R + 1, self.ACD + 1)
text += f.getvalue().replace(" ", "_").replace("REG_0x", "REG 0x").replace("APP_PLL", "#define APP_PLL")
return text
# see /doc/sw_pll.rst for guidance on these settings
def get_pll_solution(input_frequency, target_output_frequency, max_denom=80, min_F=200, ppm_max=2, fracmin=0.65, fracmax=0.95):
"""
This is a wrapper function for pll_calc.py and allows it to be called programatically.
It contains sensible defaults for the arguments and abstracts some of the complexity away from
the underlying script. Configuring the PLL is not an exact science and there are many tradeoffs involved.
See sw_pll.rst for some of the tradeoffs involved and some example paramater sets.
Once run, this function saves two output files:
- fractions.h which contains the fractional term lookup table, which is guarranteed monotonic (important for PI stability)
- register_setup.h which contains the PLL settings in comments as well as register settings for init in the application
This function and the underlying call to pll_calc may take several seconds to complete since it searches a range
of possible solutions numerically.
input_frequency - The xcore clock frequency, normally the XTAL frequency
nominal_ref_frequency - The nominal input reference frequency
target_output_frequency - The nominal target output frequency
max_denom - (Optional) The maximum fractional denominator. See/doc/sw_pll.rst for guidance
min_F - (Optional) The minimum integer numerator. See/doc/sw_pll.rst for guidance
ppm_max - (Optional) The allowable PPM deviation for the target nominal frequency. See/doc/sw_pll.rst for guidance
fracmin - (Optional) The minimum fractional multiplier. See/doc/sw_pll.rst for guidance
fracmax - (Optional) The maximum fractional multiplier. See/doc/sw_pll.rst for guidance
"""
input_frequency_MHz = input_frequency / 1000000.0
target_output_frequency_MHz = target_output_frequency / 1000000.0
calc_script = Path(__file__).parent/"pll_calc.py"
# input freq, app pll, max denom, output freq, min phase comp freq, max ppm error, raw, fractional range, make header
cmd = f"{calc_script} -i {input_frequency_MHz} -a -m {max_denom} -t {target_output_frequency_MHz} -p 6.0 -e {int(ppm_max)} -r --fracmin {fracmin} --fracmax {fracmax} --header"
print(f"Running: {cmd}")
output = subprocess.check_output(cmd.split(), text=True)
# Get each solution
solutions = []
Fs = []
regex = r"Found solution.+\nAPP.+\nAPP.+\nAPP.+"
matches = re.findall(regex, output)
for solution in matches:
F = int(float(re.search(r".+FD\s+(\d+.\d+).+", solution).groups()[0]))
solutions.append(solution)
Fs.append(F)
possible_Fs = sorted(set(Fs))
print(f"Available F values: {possible_Fs}")
# Find first solution with F greater than F
idx = next(x for x, val in enumerate(Fs) if val > min_F)
solution = matches[idx]
# Get actual PLL register bitfield settings and info
regex = r".+OUT (\d+\.\d+)MHz, VCO (\d+\.\d+)MHz, RD\s+(\d+), FD\s+(\d+.\d*)\s+\(m =\s+(\d+), n =\s+(\d+)\), OD\s+(\d+), FOD\s+(\d+), ERR (-*\d+.\d+)ppm.*"
match = re.search(regex, solution)
if match:
vals = match.groups()
output_frequency = (1000000.0 * float(vals[0]))
vco_freq = 1000000.0 * float(vals[1])
# Now convert to actual settings in register bitfields
F = int(float(vals[3]) - 1) # PLL integer multiplier
R = int(vals[2]) - 1 # PLL integer divisor
f = int(vals[4]) - 1 # PLL fractional multiplier
p = int(vals[5]) - 1 # PLL fractional divisor
OD = int(vals[6]) - 1 # PLL output divider
ACD = int(vals[7]) - 1 # PLL application clock divider
ppm = float(vals[8]) # PLL PPM error for requrested set frequency
assert match, f"Could not parse output of: {cmd} output: {solution}"
# Now get reg values and save to file
with open(register_file, "w") as reg_vals:
reg_vals.write(f"/* Autogenerated by {Path(__file__).name} using command:\n")
reg_vals.write(f" {cmd}\n")
reg_vals.write(f" Picked output solution #{idx}\n")
# reg_vals.write(f"\n{solution}\n\n") # This is verbose and contains the same info as below
reg_vals.write(f" Input freq: {input_frequency}\n")
reg_vals.write(f" F: {F}\n")
reg_vals.write(f" R: {R}\n")
reg_vals.write(f" f: {f}\n")
reg_vals.write(f" p: {p}\n")
reg_vals.write(f" OD: {OD}\n")
reg_vals.write(f" ACD: {ACD}\n")
reg_vals.write(f" Output freq: {output_frequency}\n")
reg_vals.write(f" VCO freq: {vco_freq} */\n")
reg_vals.write("\n")
for reg in ["APP PLL CTL REG", "APP PLL DIV REG", "APP PLL FRAC REG"]:
regex = rf"({reg})\s+(0[xX][A-Fa-f0-9]+)"
match = re.search(regex, solution)
if match:
val = match.groups()[1]
reg_name = reg.replace(" ", "_")
line = f"#define {reg_name} \t{val}\n"
reg_vals.write(line)
return output_frequency, vco_freq, F, R, f, p, OD, ACD, ppm
class pll_solution:
"""
Access to all the info from get_pll_solution, cleaning up temp files.
intended for programatic access from the tests. Creates a PLL setup and LUT and reads back the generated LUT
"""
def __init__(self, *args, **kwargs):
self.output_frequency, self.vco_freq, self.F, self.R, self.f, self.p, self.OD, self.ACD, self.ppm = get_pll_solution(*args, **kwargs)
from .dco_model import lut_dco
dco = lut_dco("fractions.h")
self.lut, min_frac, max_frac = dco._read_lut_header("fractions.h")
if __name__ == '__main__':
"""
This module is not intended to be run directly. This is here for internal testing only.
"""
input_frequency = 24000000
output_frequency = 12288000
print(f"get_pll_solution input_frequency: {input_frequency} output_frequency: {output_frequency}...")
output_frequency, vco_freq, F, R, f, p, OD, ACD, ppm = get_pll_solution(input_frequency, output_frequency)
print(f"got solution: \noutput_frequency: {output_frequency}\nvco_freq: {vco_freq}\nF: {F}\nR: {R}\nf: {f}\np: {p}\nOD: {OD}\nACD: {ACD}\nppm: {ppm}")
app_pll = app_pll_frac_calc(input_frequency, F, R, f, p, OD, ACD)
print(f"Got output frequency: {app_pll.calc_frequency()}")
p = 10
for f in range(p):
for frac_enable in [True, False]:
print(f"For f: {f} frac_enable: {frac_enable} got frequency: {app_pll.update_frac(f, p, frac_enable)}")

View File

@@ -0,0 +1,204 @@
# Copyright 2023 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from sw_pll.dco_model import lut_dco, sigma_delta_dco, lock_count_threshold
import numpy as np
class pi_ctrl():
"""
Parent PI(I) controller class
"""
def __init__(self, Kp, Ki, Kii=None, i_windup_limit=None, ii_windup_limit=None, verbose=False):
self.Kp = Kp
self.Ki = Ki
self.Kii = 0.0 if Kii is None else Kii
self.i_windup_limit = i_windup_limit
self.ii_windup_limit = ii_windup_limit
self.error_accum = 0.0 # Integral of error
self.error_accum_accum = 0.0 # Double integral of error (optional)
self.total_error = 0.0 # Calculated total error
self.verbose = verbose
if verbose:
print(f"Init sw_pll_pi_ctrl, Kp: {Kp} Ki: {Ki} Kii: {Kii}")
def _reset_controller(self):
"""
Reset any accumulated state
"""
self.error_accum = 0.0
self.error_accum_accum = 0.0
self.total_error = 0.0
def do_control_from_error(self, error):
"""
Calculate the LUT setting from the input error
"""
# clamp integral terms to stop them irrecoverably drifting off.
if self.i_windup_limit is None:
self.error_accum = self.error_accum + error
else:
self.error_accum = np.clip(self.error_accum + error, -self.i_windup_limit, self.i_windup_limit)
if self.ii_windup_limit is None:
self.error_accum_accum = self.error_accum_accum + self.error_accum
else:
self.error_accum_accum = np.clip(self.error_accum_accum + self.error_accum, -self.ii_windup_limit, self.ii_windup_limit)
error_p = self.Kp * error;
error_i = self.Ki * self.error_accum
error_ii = self.Kii * self.error_accum_accum
self.total_error = error_p + error_i + error_ii
if self.verbose:
print(f"error: {error} error_p: {error_p} error_i: {error_i} error_ii: {error_ii} total error: {self.total_error}")
return self.total_error
##############################
# LOOK UP TABLE IMPLEMENTATION
##############################
class lut_pi_ctrl(pi_ctrl):
"""
This class instantiates a control loop instance. It takes a lookup table function which can be generated
from the error_from_h class which allows it use the actual pre-calculated transfer function.
Once instantiated, the do_control method runs the control loop.
This class forms the core of the simulator and allows the constants (K..) to be tuned to acheive the
desired response. The function run_sim allows for a plot of a step resopnse input which allows this
to be done visually.
"""
def __init__(self, Kp, Ki, Kii=None, base_lut_index=None, verbose=False):
"""
Create instance absed on specific control constants
"""
self.dco = lut_dco()
self.lut_lookup_function = self.dco.get_lut()
lut_size = self.dco.get_lut_size()
self.diff = 0.0 # Most recent diff between expected and actual. Used by tests
# By default set the nominal LUT index to half way
if base_lut_index is None:
base_lut_index = lut_size // 2
self.base_lut_index = base_lut_index
# Set windup limit to the lut_size, which by default is double of the deflection from nominal
i_windup_limit = lut_size / Ki if Ki != 0.0 else 0.0
ii_windup_limit = 0.0 if Kii is None else lut_size / Kii if Kii != 0.0 else 0.0
pi_ctrl.__init__(self, Kp, Ki, Kii=Kii, i_windup_limit=i_windup_limit, ii_windup_limit=ii_windup_limit, verbose=verbose)
self.verbose = verbose
if verbose:
print(f"Init lut_pi_ctrl, Kp: {Kp} Ki: {Ki} Kii: {Kii}")
def get_dco_control_from_error(self, error, first_loop=False):
"""
Calculate the LUT setting from the input error
"""
self.diff = error # Used by tests
if first_loop:
pi_ctrl._reset_controller(self)
error = 0.0
dco_ctrl = self.base_lut_index - pi_ctrl.do_control_from_error(self, error)
return None if first_loop else dco_ctrl
######################################
# SIGMA DELTA MODULATOR IMPLEMENTATION
######################################
class sdm_pi_ctrl(pi_ctrl):
def __init__(self, mod_init, sdm_in_max, sdm_in_min, Kp, Ki, Kii=None, verbose=False):
"""
Create instance absed on specific control constants
"""
pi_ctrl.__init__(self, Kp, Ki, Kii=Kii, verbose=verbose)
# Low pass filter state
self.alpha = 0.125
self.iir_y = 0
# Nominal setting for SDM
self.initial_setting = mod_init
# Limits for SDM output
self.sdm_in_max = sdm_in_max
self.sdm_in_min = sdm_in_min
# Lock status state
self.lock_status = -1
self.lock_count = lock_count_threshold
def do_control_from_error(self, error):
"""
Run the control loop. Also contains an additional
low passs filtering stage.
"""
x = pi_ctrl.do_control_from_error(self, -error)
x = int(x)
# Filter noise into DCO to reduce jitter
# First order IIR, make A=0.125
# y = y + A(x-y)
# self.iir_y = int(self.iir_y + (x - self.iir_y) * self.alpha)
self.iir_y += (x - self.iir_y) >> 3 # This matches the firmware
sdm_in = self.initial_setting + self.iir_y
if sdm_in > self.sdm_in_max:
print(f"SDM Pos clip: {sdm_in}, {self.sdm_in_max}")
sdm_in = self. sdm_in_max
self.lock_status = 1
self.lock_count = lock_count_threshold
elif sdm_in < self.sdm_in_min:
print(f"SDM Neg clip: {sdm_in}, {self.sdm_in_min}")
sdm_in = self.sdm_in_min
self.lock_status = -1
self.lock_count = lock_count_threshold
else:
if self.lock_count > 0:
self.lock_count -= 1
else:
self.lock_status = 0
return sdm_in, self.lock_status
if __name__ == '__main__':
"""
This module is not intended to be run directly. This is here for internal testing only.
"""
Kp = 1.0
Ki = 0.1
Kii = 0.0
sw_pll = lut_pi_ctrl(Kp, Ki, Kii=Kii, verbose=True)
for error_input in range(-10, 20):
dco_ctrl = sw_pll.do_control_from_error(error_input)
mod_init = (sigma_delta_dco.sdm_in_max + sigma_delta_dco.sdm_in_min) / 2
Kp = 0.0
Ki = 0.1
Kii = 0.1
sw_pll = sdm_pi_ctrl(mod_init, sigma_delta_dco.sdm_in_max, sigma_delta_dco.sdm_in_min, Kp, Ki, Kii=Kii, verbose=True)
for error_input in range(-10, 20):
dco_ctrl, lock_status = sw_pll.do_control_from_error(error_input)

View File

@@ -0,0 +1,399 @@
# Copyright 2023 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from sw_pll.app_pll_model import register_file, app_pll_frac_calc
import matplotlib.pyplot as plt
import numpy as np
import os
import re
from pathlib import Path
"""
This file contains implementations of digitally controlled oscillators.
It uses the app_pll_model underneath to turn a control signal into a
calculated output frequency.
It currently contains two implementations of DCO:
- A lookup table version which is efficient in computation and offers
a range of frequencies based on a pre-calculated look up table (LUT)
- A Sigma Delta Modulator which typically uses a dedicated thread to
run the modulator but results in lower noise in the audio spectrum
"""
lock_status_lookup = {-1 : "UNLOCKED LOW", 0 : "LOCKED", 1 : "UNLOCKED HIGH"}
lock_count_threshold = 10
##############################
# LOOK UP TABLE IMPLEMENTATION
##############################
class lut_dco:
"""
This class parses a pre-generated fractions.h file and builds a lookup table so that the values can be
used by the sw_pll simulation. It may be used directly but is generally used a sub class of error_to_pll_output_frequency.
"""
def __init__(self, header_file = "fractions.h", verbose=False): # fixed header_file name by pll_calc.py
"""
Constructor for the LUT DCO. Reads the pre-calculated header filed and produces the LUT which contains
the pll fractional register settings (16b) for each of the entries. Also a
"""
self.lut, self.min_frac, self.max_frac = self._read_lut_header(header_file)
input_freq, F, R, f, p, OD, ACD = self._parse_register_file(register_file)
self.app_pll = app_pll_frac_calc(input_freq, F, R, f, p, OD, ACD)
self.last_output_frequency = self.app_pll.update_frac_reg(self.lut[self.get_lut_size() // 2] | app_pll_frac_calc.frac_enable_mask)
self.lock_status = -1
self.lock_count = lock_count_threshold
def _read_lut_header(self, header_file):
"""
read and parse the pre-written LUT
"""
if not os.path.exists(header_file):
assert False, f"Please initialize a lut_dco to produce a parsable header file {header_file}"
with open(header_file) as hdr:
header = hdr.readlines()
min_frac = 1.0
max_frac = 0.0
for line in header:
regex_ne = fr"frac_values_?\d*\[(\d+)].*"
match = re.search(regex_ne, line)
if match:
num_entries = int(match.groups()[0])
# print(f"num_entries: {num_entries}")
lut = np.zeros(num_entries, dtype=np.uint16)
regex_fr = r"0x([0-9A-F]+).+Index:\s+(\d+).+=\s(0.\d+)"
match = re.search(regex_fr, line)
if match:
reg, idx, frac = match.groups()
reg = int(reg, 16)
idx = int(idx)
frac = float(frac)
min_frac = frac if frac < min_frac else min_frac
max_frac = frac if frac > max_frac else max_frac
lut[idx] = reg
# print(f"min_frac: {min_frac} max_frac: {max_frac}")
return lut, min_frac, max_frac
def _parse_register_file(self, register_file):
"""
This method reads the pre-saved register setup comments from get_pll_solution and parses them into parameters that
can be used for later simulation.
"""
if not os.path.exists(register_file):
assert False, f"Please initialize a lut_dco to produce a parsable register setup file {register_file}"
with open(register_file) as rf:
reg_file = rf.read().replace('\n', '')
input_freq = int(re.search(r".+Input freq:\s+(\d+).+", reg_file).groups()[0])
F = int(re.search(r".+F:\s+(\d+).+", reg_file).groups()[0])
R = int(re.search(r".+R:\s+(\d+).+", reg_file).groups()[0])
f = int(re.search(r".+f:\s+(\d+).+", reg_file).groups()[0])
p = int(re.search(r".+p:\s+(\d+).+", reg_file).groups()[0])
OD = int(re.search(r".+OD:\s+(\d+).+", reg_file).groups()[0])
ACD = int(re.search(r".+ACD:\s+(\d+).+", reg_file).groups()[0])
return input_freq, F, R, f, p, OD, ACD
def get_lut(self):
"""
Return the look up table
"""
return self.lut
def get_lut_size(self):
"""
Return the size of look up table
"""
return np.size(self.lut)
def print_stats(self, target_output_frequency):
"""
Returns a summary of the LUT range and steps.
"""
lut = self.get_lut()
steps = np.size(lut)
register = int(lut[0])
min_freq = self.app_pll.update_frac_reg(register | app_pll_frac_calc.frac_enable_mask)
register = int(lut[steps // 2])
mid_freq = self.app_pll.update_frac_reg(register | app_pll_frac_calc.frac_enable_mask)
register = int(lut[-1])
max_freq = self.app_pll.update_frac_reg(register | app_pll_frac_calc.frac_enable_mask)
ave_step_size = (max_freq - min_freq) / steps
print(f"LUT min_freq: {min_freq:.0f}Hz")
print(f"LUT mid_freq: {mid_freq:.0f}Hz")
print(f"LUT max_freq: {max_freq:.0f}Hz")
print(f"LUT entries: {steps} ({steps*2} bytes)")
print(f"LUT average step size: {ave_step_size:.6}Hz, PPM: {1e6 * ave_step_size/mid_freq:.6}")
print(f"PPM range: {1e6 * (1 - target_output_frequency / min_freq):.6}")
print(f"PPM range: +{1e6 * (max_freq / target_output_frequency - 1):.6}")
return min_freq, mid_freq, max_freq, steps
def plot_freq_range(self):
"""
Generates a plot of the frequency range of the LUT and
visually shows the spacing of the discrete frequencies
that it can produce.
"""
frequencies = []
for step in range(self.get_lut_size()):
register = int(self.lut[step])
self.app_pll.update_frac_reg(register | app_pll_frac_calc.frac_enable_mask)
frequencies.append(self.app_pll.get_output_frequency())
plt.clf()
plt.plot(frequencies, color='green', marker='.', label='frequency')
plt.title('PLL fractional range', fontsize=14)
plt.xlabel(f'LUT index', fontsize=14)
plt.ylabel('Frequency', fontsize=10)
plt.legend(loc="upper right")
plt.grid(True)
# plt.show()
plt.savefig("lut_dco_range.png", dpi=150)
def get_frequency_from_dco_control(self, dco_ctrl):
"""
given a set_point, a LUT, and an APP_PLL, calculate the frequency
"""
if dco_ctrl is None:
return self.last_output_frequency, self.lock_status
num_entries = self.get_lut_size()
set_point = int(dco_ctrl)
if set_point < 0:
set_point = 0
self.lock_status = -1
self.lock_count = lock_count_threshold
elif set_point >= num_entries:
set_point = num_entries - 1
self.lock_status = 1
self.lock_count = lock_count_threshold
else:
set_point = set_point
if self.lock_count > 0:
self.lock_count -= 1
else:
self.lock_status = 0
register = int(self.lut[set_point])
output_frequency = self.app_pll.update_frac_reg(register | app_pll_frac_calc.frac_enable_mask)
self.last_output_frequency = output_frequency
return output_frequency, self.lock_status
######################################
# SIGMA DELTA MODULATOR IMPLEMENTATION
######################################
class sdm:
"""
Experimental - taken from lib_xua synchronous branch
Third order, 9 level output delta sigma. 20 bit unsigned input.
"""
# Limits for SDM modulator for stability
sdm_in_max = 980000
sdm_in_min = 60000
def __init__(self):
# Delta sigma modulator state
self.sdm_x1 = 0
self.sdm_x2 = 0
self.sdm_x3 = 0
# # generalized version without fixed point shifts. WIP!!
# # takes a Q20 number from 60000 to 980000 (or 0.0572 to 0.934)
# # This is work in progress - the integer model matches the firmware better
# def do_sigma_delta(self, sdm_in):
# if sdm_in > self.sdm_in_max:
# print(f"SDM Pos clip: {sdm_in}, {self.sdm_in_max}")
# sdm_in = self. sdm_in_max
# self.lock_status = 1
# elif sdm_in < self.sdm_in_min:
# print(f"SDM Neg clip: {sdm_in}, {self.sdm_in_min}")
# sdm_in = self.sdm_in_min
# self.lock_status = -1
# else:
# self.lock_status = 0
# sdm_out = int(self.sdm_x3 * 0.002197265625)
# if sdm_out > 8:
# sdm_out = 8
# if sdm_out < 0:
# sdm_out = 0
# self.sdm_x3 += int((self.sdm_x2 * 0.03125) - (sdm_out * 768))
# self.sdm_x2 += int((self.sdm_x1 * 0.03125) - (sdm_out * 16384))
# self.sdm_x1 += int(sdm_in - (sdm_out * 131072))
# return int(sdm_out), self.lock_status
def do_sigma_delta_int(self, sdm_in):
# takes a Q20 number from 60000 to 980000 (or 0.0572 to 0.934)
# Third order, 9 level output delta sigma. 20 bit unsigned input.
sdm_in = int(sdm_in)
sdm_out = ((self.sdm_x3<<4) + (self.sdm_x3<<1)) >> 13
if sdm_out > 8:
sdm_out = 8
if sdm_out < 0:
sdm_out = 0
self.sdm_x3 += (self.sdm_x2>>5) - (sdm_out<<9) - (sdm_out<<8)
self.sdm_x2 += (self.sdm_x1>>5) - (sdm_out<<14)
self.sdm_x1 += sdm_in - (sdm_out<<17)
return sdm_out
class sigma_delta_dco(sdm):
"""
DCO based on the sigma delta modulator
PLL solution profiles depending on target output clock
These are designed to work with a SDM either running at
1MHz:
- 10ps jitter 100Hz-40kHz with very low freq noise floor -100dBc
or 500kHz:
- 50ps jitter 100Hz-40kHz with low freq noise floor -93dBc.
"""
profiles = {"24.576_1M": {"input_freq":24000000, "F":int(147.455 - 1), "R":1 - 1, "f":5 - 1, "p":11 - 1, "OD":6 - 1, "ACD":6 - 1, "output_frequency":24.576e6, "mod_init":478151, "sdm_rate":1000000},
"22.5792_1M": {"input_freq":24000000, "F":int(135.474 - 1), "R":1 - 1, "f":9 - 1, "p":19 - 1, "OD":6 - 1, "ACD":6 - 1, "output_frequency":22.5792e6, "mod_init":498283, "sdm_rate":1000000},
"24.576_500k": {"input_freq":24000000, "F":int(278.529 - 1), "R":2 - 1, "f":9 - 1, "p":17 - 1, "OD":2 - 1, "ACD":17 - 1, "output_frequency":24.576e6, "mod_init":553648, "sdm_rate":500000},
"22.5792_500k": {"input_freq":24000000, "F":int(293.529 - 1), "R":2 - 1, "f":9 - 1, "p":17 - 1, "OD":3 - 1, "ACD":13 - 1, "output_frequency":22.5792e6, "mod_init":555326, "sdm_rate":500000}
}
def __init__(self, profile):
"""
Create a sigmal delta DCO targetting either 24.576 or 22.5792MHz
"""
self.profile = profile
self.p_value = 8 # 8 frac settings + 1 non frac setting
input_freq, F, R, f, p, OD, ACD, _, _, _ = list(self.profiles[profile].values())
self.app_pll = app_pll_frac_calc(input_freq, F, R, f, p, OD, ACD)
self.sdm_out = 0
self.f = 0
sdm.__init__(self)
def _sdm_out_to_freq(self, sdm_out):
"""
Translate the SDM steps to register settings
"""
if sdm_out == 0:
# Step 0
self.f = 0
return self.app_pll.update_frac(self.f, self.p_value - 1, False)
else:
# Steps 1 to 8 inclusive
self.f = sdm_out - 1
return self.app_pll.update_frac(self.f, self.p_value - 1, True)
def do_modulate(self, input):
"""
Input a control value and output a SDM signal
"""
# self.sdm_out, lock_status = sdm.do_sigma_delta(self, input)
self.sdm_out = sdm.do_sigma_delta_int(self, input)
frequency = self._sdm_out_to_freq(self.sdm_out)
return frequency
def print_stats(self):
"""
Returns a summary of the SDM range and steps.
"""
steps = self.p_value + 1 # +1 we have frac off state
min_freq = self._sdm_out_to_freq(0)
max_freq = self._sdm_out_to_freq(self.p_value)
target_output_frequency = self.profiles[self.profile]["output_frequency"]
ave_step_size = (max_freq - min_freq) / steps
print(f"SDM min_freq: {min_freq:.0f}Hz")
print(f"SDM max_freq: {max_freq:.0f}Hz")
print(f"SDM steps: {steps}")
print(f"PPM range: {1e6 * (1 - target_output_frequency / min_freq):.6}")
print(f"PPM range: +{1e6 * (max_freq / target_output_frequency - 1):.6}")
return min_freq, max_freq, steps
def plot_freq_range(self):
"""
Generates a plot of the frequency range of the LUT and
visually shows the spacing of the discrete frequencies
that it can produce.
"""
frequencies = []
for step in range(self.p_value + 1): # +1 since p value is +1 in datasheet
frequencies.append(self._sdm_out_to_freq(step))
plt.clf()
plt.plot(frequencies, color='green', marker='.', label='frequency')
plt.title('PLL fractional range', fontsize=14)
plt.xlabel(f'SDM step', fontsize=14)
plt.ylabel('Frequency', fontsize=10)
plt.legend(loc="upper right")
plt.grid(True)
# plt.show()
plt.savefig("sdm_dco_range.png", dpi=150)
def write_register_file(self):
with open(register_file, "w") as reg_vals:
reg_vals.write(f"/* Autogenerated SDM App PLL setup by {Path(__file__).name} using {self.profile} profile */\n")
reg_vals.write(self.app_pll.gen_register_file_text())
reg_vals.write(f"#define SW_PLL_SDM_CTRL_MID {self.profiles[self.profile]['mod_init']}\n")
reg_vals.write(f"#define SW_PLL_SDM_RATE {self.profiles[self.profile]['sdm_rate']}\n")
reg_vals.write("\n\n")
return register_file
if __name__ == '__main__':
"""
This module is not intended to be run directly. This is here for internal testing only.
"""
dco = lut_dco()
print(f"LUT size: {dco.get_lut_size()}")
dco.plot_freq_range()
dco.print_stats(12288000)
sdm_dco = sigma_delta_dco("24.576_1M")
sdm_dco.write_register_file()
sdm_dco.print_stats()
sdm_dco.plot_freq_range()
for i in range(30):
output_frequency = sdm_dco.do_modulate(500000)
# print(i, output_frequency)

View File

@@ -0,0 +1,60 @@
# Copyright 2023 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
class port_timer_pfd():
def __init__(self, nominal_output_hz, nominal_control_rate_hz, ppm_range=1000):
self.output_count_last = 0.0 # Integer value of last output_clock_count
self.first_loop = True
self.ppm_range = ppm_range
self.expected_output_clock_count_inc = nominal_output_hz / nominal_control_rate_hz
def get_error(self, output_clock_count_float, period_fraction=1.0):
"""
Calculate frequency error from the port output_count taken at the ref clock time.
Note it uses a floating point input clock count to make simulation easier. This
handles fractional counts and carries them properly.
If the time of sampling the output_count is not precisely 1.0 x the ref clock time,
you may pass a fraction to allow for a proportional value using period_fraction. This is optional.
"""
output_count_int = int(output_clock_count_float) # round down to nearest int to match hardware
output_count_inc = output_count_int - self.output_count_last
output_count_inc = output_count_inc / period_fraction
expected_output_clock_count = self.output_count_last + self.expected_output_clock_count_inc
error = output_count_inc - int(self.expected_output_clock_count_inc)
# Apply out of range detection so that the controller ignores startup or missed control loops (as per C)
if abs(error) > (self.ppm_range / 1e6) * self.expected_output_clock_count_inc:
# print("PFD FIRST LOOP", abs(error), (self.ppm_range / 10e6) * self.expected_output_clock_count_inc)
self.first_loop = True
else:
self.first_loop = False
self.output_count_last = output_count_int
return error, self.first_loop
if __name__ == '__main__':
"""
This module is not intended to be run directly. This is here for internal testing only.
"""
nominal_output_hz = 12288000
nominal_control_rate_hz = 93.75
expected_output_clock_inc = nominal_output_hz / nominal_control_rate_hz
pfd = port_timer_pfd(nominal_output_hz, nominal_control_rate_hz)
output_clock_count_float = 0.0
for output_hz in range(nominal_output_hz - 1000, nominal_output_hz + 1000, 10):
output_clock_count_float += output_hz / nominal_output_hz * expected_output_clock_inc
error = pfd.get_error(output_clock_count_float)
print(f"actual output Hz: {output_hz} output_clock_count: {output_clock_count_float} error: {error}")

View File

@@ -0,0 +1,270 @@
#!/usr/bin/env python3
# Copyright 2023 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
#======================================================================================================================
# Copyright XMOS Ltd. 2021
#
# Original Author: Joe Golightly
#
# File type: Python script
#
# Description:
# Stand-alone script to provide correct paramaters and registers settings to achieve desired clock frequency from
# xcore.ai PLLs
#
#
# Status:
# Released for internal engineering use
#
#======================================================================================================================
# PLL Structure + Final Output Divider
# PLL Macro
# +--------------------------------------------------------------------+
# | |
# ref clk input ---+--- ref_div --- PFC --- Filter --- VCO ---+--- /2 --- output_div ---+--- Final Output Div ---> output
# | | | |
# | +----- feedback_div -----+ |
# | | |
# +-----------------------------+--------------------------------------+
# |
# XMOS Fractional-N Control (Secondary PLL only)
# Terminology Note
# TruCircuits effectively incorporate the /2 into the feedback divider to allow the multiplication ratio to equal the feedback divider value.
# I find this terminology very confusing (particularly when talking about the fractional-n system as well) so I have shown the actual structure above.
# Specs of the above system, extracted from the TruCircuits specs.
# - Reference divider values (R) 1-64
# - Feedback divider values (F) 1-8192
# - Output divider values (OD) 1-8
# - VCO Frequency 720MHz - 3.6GHz
# PFC frequency = Divided ref clock frequency = Fpfc = Fin / R.
# VCO frequency = Fvco = Fpfc * F.
# PLL Output frequency = Fout = Fvco / (2 * OD).
# Overall PLL Fout = (Fin * F) / (2 * R * OD)
# After the PLL, the output frequency can be further divided by the final output divider which can divide by 1 to 65536.
# For the App Clock output, there is an additional divide by 2 at the output to give a 50/50 duty cycle.
# All frequencies are in MHz.
import math
from operator import itemgetter
import argparse
def print_regs(args, op_div, fb_div, ref_div, fin_op_div):
if args.app:
app_pll_ctl_reg = (1 << 27) | (((op_div)-1) << 23) | ((int(fb_div[0])-1) << 8) | (ref_div-1)
app_pll_div_reg = (1 << 31) | (fin_op_div-1)
app_pll_frac_reg = 0
if (fb_div[1] != 0): # Fractional Mode
#print(fb_div)
app_pll_frac_reg = (1 << 31) | ((fb_div[1]-1) << 8) | (fb_div[2]-1)
print('APP PLL CTL REG 0x' + '{:08X}'.format(app_pll_ctl_reg))
print('APP PLL DIV REG 0x' + '{:08X}'.format(app_pll_div_reg))
print('APP PLL FRAC REG 0x' + '{:08X}'.format(app_pll_frac_reg))
else:
pll_ctl_reg = (((op_div)-1) << 23) | ((int(fb_div[0])-1) << 8) | (ref_div-1)
pll_div_reg = fin_op_div-1
print('PLL CTL REG 0x' + '{:08X}'.format(pll_ctl_reg))
print('SWITCH/CORE DIV REG 0x' + '{:08X}'.format(pll_div_reg))
def print_solution(args, ppm_error, input_freq, out_freq, vco_freq, ref_div, fb_div, op_div, fin_op_div):
if (fb_div[1] != 0): # Fractional-N mode
fb_div_string = '{:8.3f}'.format(fb_div[0]) + " (m = " + '{:3d}'.format(fb_div[1]) + ", n = " + '{:3d}'.format(fb_div[2]) + ")"
else: # Integer mode
fb_div_string = '{:4d}'.format(int(fb_div[0]))
fb_div_string = '{:27}'.format(fb_div_string)
print("Found solution: IN " + '{:3.3f}'.format(input_freq) + "MHz, OUT " + '{:3.6f}'.format(out_freq) + "MHz, VCO " + '{:4.2f}'.format(vco_freq) + "MHz, RD " + '{:2d}'.format(ref_div) + ", FD " + fb_div_string + ", OD " + '{:2d}'.format(op_div) + ", FOD " + '{:4d}'.format(fin_op_div) + ", ERR " + str(round((ppm_error),3)) + "ppm")
print_regs(args, op_div, fb_div, ref_div, fin_op_div)
def print_solution_set(args, solutions):
if args.raw:
sol_str = ' Raw'
else:
sol_str = ' Filtered'
print('*** Found ' + str(len(solutions)) + sol_str + ' Solutions ***')
print('');
for solution in solutions:
print_solution(args, solution['ppm_error'], solution['input_freq'], solution['out_freq'], solution['vco_freq'], solution['ref_div'], solution['fb_div'], solution['op_div'], solution['fin_op_div'])
def find_pll():
parser = argparse.ArgumentParser(description='A script to calculate xcore.ai PLL settings to achieve desired output clock frequencies.')
parser.add_argument("-i", "--input", type=float, help="PLL reference input frequency (MHz)", default=24.0)
parser.add_argument("-t", "--target", type=float, help="Target output frequency (MHz)", default=600.0)
parser.add_argument("-e", "--error", type=int, help="Allowable frequency error (ppm)", default=0)
parser.add_argument("-m", "--denmax", type=int, help="Maximum denominator in frac-n config", default=0)
parser.add_argument("-p", "--pfcmin", type=float, help="Minimum phase frequency comparator frequency (MHz)", default=1.0)
parser.add_argument("-s", "--maxsol", type=int, help="Maximum number of raw solutions", default=200)
parser.add_argument("-a", "--app", help="Use the App PLL", action="store_true")
parser.add_argument("-r", "--raw", help="Show all solutions with no filtering", action="store_true")
parser.add_argument("--header", help="Output a header file with fraction option reg values", action="store_true")
parser.add_argument("--fracmax", type=float, help="Maximum fraction value to use", default=1.0)
parser.add_argument("--fracmin", type=float, help="Minimum fraction value to use", default=0.0)
args = parser.parse_args()
input_freq = args.input
output_target = args.target
ppm_error_max = args.error
# PLL Reference divider (R) 1-64
ref_div_list = list(range(1,65))
# PLL Output divider (OD) 1-8
op_div_list = list(range(1,9))
# Post PLL output divider 1-65536
fin_op_div_list = list(range(1,65537))
# To create the feedback divider list we need to create the list of fractions we can use for when using frac-n mode.
# den_max is the highest number we want to use as the denominator. This is useful to set as higher den_max values will have higher jitter so ideally this should be as low as possible.
if args.app:
pll_type = "App PLL"
den_max = args.denmax
else:
pll_type = "Core PLL"
den_max = 0
if (args.denmax != 0):
print("Core PLL does not have frac-n capability. Setting fracmax to 0")
# Fraction is m/n - m is numerator, n is denominator.
frac_list_raw = []
for m in range(1, den_max): # numerator from 1 to (den_max - 1)
for n in range(m+1, den_max+1): # denominator from (numerator+1) to den_max
frac = float(m)/float(n)
if (args.fracmin < frac < args.fracmax):
frac_list_raw.append([frac,m,n]) # We store the fraction as a float plus the integer numerator and denominator.
# Sort the fraction list based on the first element (the fractional value) then by the numerator of the fraction.
# This means we'll get the more desirable fraction to use first. So 1/2 comes before 2/4 even though they both produce the fraction 0.5
frac_list_sorted = sorted(frac_list_raw, key=itemgetter(0,1))
# Now we want to weed out useless fractional divide values.
# For example 1/2 and 2/4 both result in a divide ratio of 0.5 but 1/2 is preferable as the denominator is lower and so it will cause Phase Freq Comparator jitter to be at higher freq and so will be filtered more by the analogue loop filter.
frac_list = []
last_item = 0.0
for item in frac_list_sorted:
if (item[0] > last_item): # Fractional value has to be greater than the last value or not useful
#print("{0:.4f}".format(item[0]) + ", " + "{0:2d}".format(item[1]) + ", " + "{0:2d}".format(item[2]))
frac_list.append(item)
last_item = item[0]
# Output a header file containing the list of fractions as register values
if args.header:
with open("fractions.h", "w") as f:
print("// Header file listing fraction options searched", file=f)
print("// These values to go in the bottom 16 bits of the secondary PLL fractional-n divider register.", file=f)
print("short frac_values_" + str(den_max) + "[" + str(len(frac_list)) +"] = {", file=f)
for index, item in enumerate(frac_list):
#print(item[0],item[1],item[2])
frac_str = str(item[1]) + "/" + str(item[2])
frac_str_line = "Index: {:>3} ".format(index) + 'Fraction: {:>5}'.format(frac_str) + " = {0:.4f}".format(item[0])
print("0x" + format( (((item[1]-1) << 8) | (item[2]-1)),'>04X') + ", // " + frac_str_line, file=f)
print("};", file=f)
# Feedback divider 1-8192 - we store a total list of the integer and fractional portions.
fb_div_list = []
for i in range(1,8193):
fb_div_list.append([i,0,0]) # This is when not using frac-n mode.
for item in frac_list:
fb_div_list.append([i+item[0], item[1], item[2]])
# Actual Phase Comparator Limits
pc_freq_min = 0.22 # 220kHz
pc_freq_max = 1800.0 # 1.8GHz
# ... but for lower jitter ideally we'd use a higher minimum PC freq of ~1MHz.
if (0.22 <= args.pfcmin <= 1800.0):
pc_freq_min = args.pfcmin
# Actual VCO Limits (/1 output from PLL goes from 360 - 1800MHz so before the /2 this is 720 - 3600MHz)
vco_freq_min = 720.0 #720MHz
vco_freq_max = 3600.0 #3.6GHz
# New constraint of /1 output of PLL being 800MHz max (Both Core and App PLLs)
# So this doesn't constrain VCO freq becuase you have the output divider.
pll_out_max = 800.0 # 800MHz
# Print a summary of inputs
print("Using " + pll_type)
print("Input Frequency = " + str(input_freq) + "MHz")
print("Target Output Frequency = " + str(output_target) + "MHz")
print("Allowable frequency error = " + str(ppm_error_max) + "ppm")
print("Minimum Phase Frequency Comparator Frequency = " +str(pc_freq_min) + "MHz")
if (pll_type == "App PLL"):
print("Maximum denominator in frac-n config = " + str(den_max))
print("")
# Main loop
raw_solutions = []
for ref_div in ref_div_list:
pc_freq = input_freq/ref_div
if (pc_freq_min <= pc_freq <= pc_freq_max): # Check pc clock is in valid freq range.
for fb_div in fb_div_list:
vco_freq = pc_freq * fb_div[0]
if (vco_freq_min <= vco_freq <= vco_freq_max): # Check vco is in valid freq range.
for op_div in op_div_list:
pll_out_freq = vco_freq/(2*op_div)
if (pll_out_freq <= pll_out_max): # Check PLL out freq is in valid freq range.
for fin_op_div in fin_op_div_list:
if (len(raw_solutions) >= args.maxsol): # Stop when we've reached the max number of raw solutions
break
# See if our output freq is what we want?
out_freq = vco_freq/(2*op_div*fin_op_div)
if args.app:
out_freq = out_freq / 2 # fixed /2 for 50/50 duty cycle on app_clk output
# Calculate parts per million error
ppm_error = ((out_freq - output_target)/output_target) * 1000000.0
if (abs(ppm_error) <= (ppm_error_max+0.01)): # Hack a tiny additional error in to handle the floating point calc errors.
raw_solutions.append({'ppm_error':ppm_error, 'input_freq':input_freq, 'out_freq':out_freq, 'vco_freq':vco_freq, 'ref_div':ref_div, 'fb_div':fb_div, 'op_div':op_div, 'fin_op_div':fin_op_div})
if (out_freq < output_target):
break
# First filter out less desirable solutions with the same vco frequency and RD value. Keep the results with the highest PLL OD value.
# print_solution_set(raw_solutions)
solutions_sorted1 = sorted(raw_solutions, key=itemgetter('op_div'), reverse=True) # OD, higher first
solutions_sorted2 = sorted(solutions_sorted1, key=itemgetter('ref_div')) # Ref Div, lower first
solutions_sorted3 = sorted(solutions_sorted2, key=itemgetter('vco_freq'), reverse=True) # vco, higher first
# print_solution_set(solutions_sorted3)
filtered_solutions = []
for count, solution in enumerate(solutions_sorted3):
if count == 0:
filtered_solutions.append(solution)
else:
# Only keep solution If vco or ref_div values are different from last solution
if (solution['vco_freq'] != last_solution['vco_freq']) | (solution['ref_div'] != last_solution['ref_div']):
filtered_solutions.append(solution)
last_solution = solution
# print_solution_set(filtered_solutions)
# Final overall sort with lowest ref divider first
final_filtered_solutions = sorted(filtered_solutions, key=itemgetter('ref_div'))
if args.raw:
print_solution_set(args, raw_solutions)
else:
print_solution_set(args, final_filtered_solutions)
# When invoked as main program, invoke the profiler on a script
if __name__ == '__main__':
find_pll()

View File

@@ -0,0 +1,308 @@
# Copyright 2023 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from sw_pll.app_pll_model import get_pll_solution
from sw_pll.pfd_model import port_timer_pfd
from sw_pll.dco_model import lut_dco, sigma_delta_dco, lock_status_lookup
from sw_pll.controller_model import lut_pi_ctrl, sdm_pi_ctrl
from sw_pll.analysis_tools import audio_modulator
import matplotlib.pyplot as plt
import numpy as np
import sys
def plot_simulation(freq_log, target_freq_log, real_time_log, name="sw_pll_tracking.png"):
plt.clf()
plt.plot(real_time_log, freq_log, color='red', marker='.', label='actual frequency')
plt.plot(real_time_log, target_freq_log, color='blue', marker='.', label='target frequency')
plt.title('PLL tracking', fontsize=14)
plt.xlabel(f'Time in seconds', fontsize=10)
plt.ylabel('Frequency', fontsize=10)
plt.legend(loc="upper right")
plt.grid(True)
# plt.show()
plt.savefig(name, dpi=150)
##############################
# LOOK UP TABLE IMPLEMENTATION
##############################
class sim_sw_pll_lut:
"""
Complete SW PLL simulation class which contains all of the components including
Phase Frequency Detector, Controller and Digitally Controlled Oscillator using
a Look Up Table method.
"""
def __init__( self,
target_output_frequency,
nominal_nominal_control_rate_frequency,
Kp,
Ki,
Kii=None):
"""
Init a Lookup Table based SW_PLL instance
"""
self.pfd = port_timer_pfd(target_output_frequency, nominal_nominal_control_rate_frequency)
self.controller = lut_pi_ctrl(Kp, Ki, Kii=Kii, verbose=False)
self.dco = lut_dco(verbose=False)
self.target_output_frequency = target_output_frequency
self.time = 0.0
self.control_time_inc = 1 / nominal_nominal_control_rate_frequency
def do_control_loop(self, output_clock_count, period_fraction=1.0, verbose=False):
"""
This should be called once every control period.
output_clock_count is fed into the PDF and period_fraction is an optional jitter
reduction term where the control period is not exact, but can be compensated for.
"""
error, first_loop = self.pfd.get_error(output_clock_count, period_fraction=period_fraction)
dco_ctl = self.controller.get_dco_control_from_error(error, first_loop=first_loop)
output_frequency, lock_status = self.dco.get_frequency_from_dco_control(dco_ctl)
if first_loop: # We cannot claim to be locked if the PFD sees an error
lock_status = -1
if verbose:
print(f"Raw error: {error}")
print(f"dco_ctl: {dco_ctl}")
print(f"Output_frequency: {output_frequency}")
print(f"Lock status: {lock_status_lookup[lock_status]}")
return output_frequency, lock_status
def run_lut_sw_pll_sim():
"""
Test program / example showing how to run the simulator object
"""
# Example profiles to produce typical frequencies seen in audio systems. ALl assume 24MHz input clock to the hardware PLL.
profiles = [
# 0 - 12.288MHz with 48kHz ref (note also works with 16kHz ref), +-250PPM, 29.3Hz steps, 426B LUT size
{"nominal_ref_frequency":48000.0, "target_output_frequency":12288000, "max_denom":80, "min_F":200, "ppm_max":5, "fracmin":0.843, "fracmax":0.95},
# 1 - 12.288MHz with 48kHz ref (note also works with 16kHz ref), +-500PPM, 30.4Hz steps, 826B LUT size
{"nominal_ref_frequency":48000.0, "target_output_frequency":12288000, "max_denom":80, "min_F":200, "ppm_max":5, "fracmin":0.695, "fracmax":0.905},
# 2 - 24.576MHz with 48kHz ref (note also works with 16kHz ref), +-1000PPM, 31.9Hz steps, 1580B LUT size
{"nominal_ref_frequency":48000.0, "target_output_frequency":12288000, "max_denom":90, "min_F":140, "ppm_max":5, "fracmin":0.49, "fracmax":0.81},
# 3 - 24.576MHz with 48kHz ref (note also works with 16kHz ref), +-100PPM, 9.5Hz steps, 1050B LUT size
{"nominal_ref_frequency":48000.0, "target_output_frequency":24576000, "max_denom":120, "min_F":400, "ppm_max":5, "fracmin":0.764, "fracmax":0.884},
# 4 - 6.144MHz with 16kHz ref, +-200PPM, 30.2Hz steps, 166B LUT size
{"nominal_ref_frequency":16000.0, "target_output_frequency":6144000, "max_denom":40, "min_F":400, "ppm_max":5, "fracmin":0.635, "fracmax":0.806},
]
profile_used = 1
profile = profiles[profile_used]
nominal_output_hz = profile["target_output_frequency"]
# This generates the needed header files read later by sim_sw_pll_lut
# 12.288MHz with 48kHz ref (note also works with 16kHz ref), +-500PPM, 30.4Hz steps, 826B LUT size
get_pll_solution(24000000, nominal_output_hz, max_denom=80, min_F=200, ppm_max=5, fracmin=0.695, fracmax=0.905)
output_frequency = nominal_output_hz
nominal_control_rate_hz = profile["nominal_ref_frequency"] / 512
simulation_iterations = 100
Kp = 0.0
Ki = 1.0
Kii = 0.0
sw_pll = sim_sw_pll_lut(nominal_output_hz, nominal_control_rate_hz, Kp, Ki, Kii=Kii)
sw_pll.dco.print_stats(nominal_output_hz)
output_clock_count = 0
test_tone_hz = 1000
audio = audio_modulator(simulation_iterations * 1 / nominal_control_rate_hz, sample_rate=48000, test_tone_hz=test_tone_hz)
freq_log = []
target_freq_log = []
real_time_log = []
real_time = 0.0
period_fraction = 1.0
ppm_shift = +50
for loop in range(simulation_iterations):
output_frequency, lock_status = sw_pll.do_control_loop(output_clock_count, period_fraction=period_fraction, verbose=False)
# Now work out how many output clock counts this translates to
measured_clock_count_inc = output_frequency / nominal_control_rate_hz * (1 - ppm_shift / 1e6)
# Add some jitter to the output_count to test jitter compensation
jitter_amplitude = 100 # measured in output clock counts
clock_count_sampling_jitter = jitter_amplitude * (np.random.sample() - 0.5)
period_fraction = (measured_clock_count_inc + clock_count_sampling_jitter) * measured_clock_count_inc
output_clock_count += measured_clock_count_inc * period_fraction
real_time_log.append(real_time)
target_output_frequency = nominal_output_hz * (1 + ppm_shift / 1e6)
target_freq_log.append(target_output_frequency)
freq_log.append(output_frequency)
time_inc = 1 / nominal_control_rate_hz
scaled_frequency_shift = test_tone_hz * (output_frequency - target_output_frequency) / target_output_frequency
audio.apply_frequency_deviation(real_time, real_time + time_inc, scaled_frequency_shift)
real_time += time_inc
plot_simulation(freq_log, target_freq_log, real_time_log, "tracking_lut.png")
audio.modulate_waveform()
audio.save_modulated_wav("modulated_tone_1000Hz_lut.wav")
audio.plot_modulated_fft("modulated_fft_lut.png", skip_s=real_time / 2) # skip so we ignore the inital lock period
######################################
# SIGMA DELTA MODULATOR IMPLEMENTATION
######################################
class sim_sw_pll_sd:
"""
Complete SW PLL simulation class which contains all of the components including
Phase Frequency Detector, Controller and Digitally Controlled Oscillator using
a Sigma Delta Modulator method.
"""
def __init__( self,
target_output_frequency,
nominal_nominal_control_rate_frequency,
Kp,
Ki,
Kii=None):
"""
Init a Sigma Delta Modulator based SW_PLL instance
"""
self.pfd = port_timer_pfd(target_output_frequency, nominal_nominal_control_rate_frequency, ppm_range=3000)
self.dco = sigma_delta_dco("24.576_1M")
self.controller = sdm_pi_ctrl( (self.dco.sdm_in_max + self.dco.sdm_in_min) / 2,
self.dco.sdm_in_max,
self.dco.sdm_in_min,
Kp,
Ki,
Kii)
self.target_output_frequency = target_output_frequency
self.time = 0.0
self.control_time_inc = 1 / nominal_nominal_control_rate_frequency
self.control_setting = (self.controller.sdm_in_max + self.controller.sdm_in_min) / 2 # Mid way
def do_control_loop(self, output_clock_count, verbose=False):
"""
Run the control loop (which runs at a tiny fraction of the SDM rate)
This should be called once every control period.
output_clock_count is fed into the PDF and period_fraction is an optional jitter
reduction term where the control period is not exact, but can be compensated for.
"""
error, first_loop = self.pfd.get_error(output_clock_count)
ctrl_output, lock_status = self.controller.do_control_from_error(error)
self.control_setting = ctrl_output
if verbose:
print(f"Raw error: {error}")
print(f"ctrl_output: {ctrl_output}")
print(f"Lock status: {lock_status_lookup[lock_status]}")
return self.control_setting
def do_sigma_delta(self):
"""
Run the SDM which needs to be run constantly at the SDM rate.
See DCO (dco_model) for details
"""
frequncy = self.dco.do_modulate(self.control_setting)
return frequncy
def run_sd_sw_pll_sim():
"""
Test program / example showing how to run the simulator object
"""
nominal_output_hz = 24576000
nominal_control_rate_hz = 100
nominal_sd_rate_hz = 1e6
output_frequency = nominal_output_hz
simulation_iterations = 1000000
Kp = 0.0
Ki = 32.0
Kii = 0.25
sw_pll = sim_sw_pll_sd(nominal_output_hz, nominal_control_rate_hz, Kp, Ki, Kii=Kii)
sw_pll.dco.write_register_file()
sw_pll.dco.print_stats()
output_clock_count = 0
test_tone_hz = 1000
audio = audio_modulator(simulation_iterations * 1 / nominal_sd_rate_hz, sample_rate=6144000, test_tone_hz=test_tone_hz)
freq_log = []
target_freq_log = []
real_time_log = []
real_time = 0.0
ppm_shift = +50
# For working out when to do control calls
control_time_inc = 1 / nominal_control_rate_hz
control_time_trigger = control_time_inc
for loop in range(simulation_iterations):
output_frequency = sw_pll.do_sigma_delta()
# Log results
freq_log.append(output_frequency)
target_output_frequency = nominal_output_hz * (1 + ppm_shift / 1e6)
target_freq_log.append(target_output_frequency)
real_time_log.append(real_time)
# Modulate tone
sdm_time_inc = 1 / nominal_sd_rate_hz
scaled_frequency_shift = test_tone_hz * (output_frequency - target_output_frequency) / target_output_frequency
audio.apply_frequency_deviation(real_time, real_time + sdm_time_inc, scaled_frequency_shift)
# Accumulate the real number of output clocks
output_clock_count += output_frequency / nominal_sd_rate_hz * (1 - ppm_shift / 1e6)
# Check for control loop run ready
if real_time > control_time_trigger:
control_time_trigger += control_time_inc
# Now work out how many output clock counts this translates to
sw_pll.do_control_loop(output_clock_count)
real_time += sdm_time_inc
plot_simulation(freq_log, target_freq_log, real_time_log, "tracking_sdm.png")
audio.modulate_waveform()
audio.save_modulated_wav("modulated_tone_1000Hz_sdm.wav")
audio.plot_modulated_fft("modulated_fft_sdm.png", skip_s=real_time / 2) # skip so we ignore the inital lock period
if __name__ == '__main__':
if len(sys.argv) != 2:
assert 0, "Please select either LUT or SDM: sw_pll_sim.py <LUT/SDM>"
if sys.argv[1] == "LUT":
run_lut_sw_pll_sim() # Run LUT sim - generates "register_setup.h" and "fractions.h"
elif sys.argv[1] == "SDM":
run_sd_sw_pll_sim() # Run SDM sim - generates "register_setup.h"
else:
assert 0, "Please select either LUT or SDM: sw_pll_sim.py <LUT/SDM>"