init
This commit is contained in:
96
lib_sw_pll/python/sw_pll/analysis_tools.py
Normal file
96
lib_sw_pll/python/sw_pll/analysis_tools.py
Normal 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")
|
||||
|
||||
287
lib_sw_pll/python/sw_pll/app_pll_model.py
Normal file
287
lib_sw_pll/python/sw_pll/app_pll_model.py
Normal 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)}")
|
||||
|
||||
204
lib_sw_pll/python/sw_pll/controller_model.py
Normal file
204
lib_sw_pll/python/sw_pll/controller_model.py
Normal 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)
|
||||
399
lib_sw_pll/python/sw_pll/dco_model.py
Normal file
399
lib_sw_pll/python/sw_pll/dco_model.py
Normal 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)
|
||||
60
lib_sw_pll/python/sw_pll/pfd_model.py
Normal file
60
lib_sw_pll/python/sw_pll/pfd_model.py
Normal 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}")
|
||||
|
||||
|
||||
270
lib_sw_pll/python/sw_pll/pll_calc.py
Normal file
270
lib_sw_pll/python/sw_pll/pll_calc.py
Normal 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()
|
||||
308
lib_sw_pll/python/sw_pll/sw_pll_sim.py
Normal file
308
lib_sw_pll/python/sw_pll/sw_pll_sim.py
Normal 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>"
|
||||
Reference in New Issue
Block a user