diff --git a/Jenkinsfile b/Jenkinsfile index c644b7cc..1d1376cc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -30,29 +30,54 @@ pipeline { } stages { - stage('ci') { + stage('Build and tests') { agent { label 'linux&&64' } - steps { - sh 'mkdir lib_sw_pll' - // source checks require the directory - // name to be the same as the repo name - dir('lib_sw_pll') { - // checkout repo - checkout scm - installPipfile(false) - withVenv { - withTools(params.TOOLS_VERSION) { - sh './tools/ci/checkout-submodules.sh' - catchError { - sh './tools/ci/do-ci.sh' + stages{ + stage('Checkout'){ + steps { + sh 'mkdir lib_sw_pll' + // source checks require the directory + // name to be the same as the repo name + dir('lib_sw_pll') { + // checkout repo + checkout scm + installPipfile(false) + withVenv { + withTools(params.TOOLS_VERSION) { + sh './tools/ci/checkout-submodules.sh' + } } - zip archive: true, zipFile: "build.zip", dir: "build" - zip archive: true, zipFile: "tests.zip", dir: "tests/bin" - archiveArtifacts artifacts: "tests/bin/timing-report.txt", allowEmptyArchive: false + } + } + } + stage('Build'){ + steps { + dir('lib_sw_pll') { + withVenv { + withTools(params.TOOLS_VERSION) { + sh './tools/ci/do-ci-build.sh' + } + } + } + } + } + stage('Test'){ + steps { + dir('lib_sw_pll') { + withVenv { + withTools(params.TOOLS_VERSION) { + catchError { + sh './tools/ci/do-ci-tests.sh' + } + zip archive: true, zipFile: "build.zip", dir: "build" + zip archive: true, zipFile: "tests.zip", dir: "tests/bin" + archiveArtifacts artifacts: "tests/bin/timing-report.txt", allowEmptyArchive: false - junit 'tests/results.xml' + junit 'tests/results.xml' + } + } } } } diff --git a/python/sw_pll/analysis_tools.py b/python/sw_pll/analysis_tools.py new file mode 100644 index 00000000..a0f4faea --- /dev/null +++ b/python/sw_pll/analysis_tools.py @@ -0,0 +1,88 @@ +# 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: + 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") + diff --git a/python/sw_pll/app_pll_model.py b/python/sw_pll/app_pll_model.py new file mode 100644 index 00000000..c0506d42 --- /dev/null +++ b/python/sw_pll/app_pll_model.py @@ -0,0 +1,275 @@ +# 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. + """ + def __init__(self, input_frequency, F_init, R_init, f_init, p_init, OD_init, ACD_init, verbose=False): + 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.lock_status_state = 0 + self.fractional_enable = True + self.verbose = verbose + + self.calc_frequency() + + def calc_frequency(self): + 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): + 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=True): + """ + Update only the fractional parts of the App PLL + """ + self.f = f + self.p = p + 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)) + assert self.fractional_enable is True + + return self.update_frac(f, p) + + 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 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() + + 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 + + # Example profiles to produce typical frequencies seen in audio systems + 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 - 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}, + # 3 - 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}, + # 4 - 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}, + # 5 - 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}, + ] + """ + + + + 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)}") + diff --git a/python/sw_pll/controller_model.py b/python/sw_pll/controller_model.py new file mode 100644 index 00000000..92ef5a26 --- /dev/null +++ b/python/sw_pll/controller_model.py @@ -0,0 +1,169 @@ +# 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 +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 anu accumulated state + """ + self.error_accum = 0.0 + self.error_accum_accum = 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, lut_dco): + """ + 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, sigma_delta_dco): + def __init__(self, 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 = 478151 + + 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) + + # Filter some noise into DCO to reduce jitter + # First order IIR, make A=0.125 + # y = y + A(x-y) + self.iir_y = self.iir_y + (x - self.iir_y) * self.alpha + + return self.initial_setting + self.iir_y + + +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 + + sw_pll = lut_pi_ctrl(Kp, Ki, verbose=True) + for error_input in range(-10, 20): + dco_ctrl = sw_pll.do_control_from_error(error_input) + + Kp = 0.0 + Ki = 0.1 + Kii = 0.1 + + sw_pll = sdm_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) diff --git a/python/sw_pll/dco_model.py b/python/sw_pll/dco_model.py new file mode 100644 index 00000000..a8363560 --- /dev/null +++ b/python/sw_pll/dco_model.py @@ -0,0 +1,361 @@ +# 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"} + +############################## +# 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]) + self.lock_status = -1 + + 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(".+Input freq:\s+(\d+).+", reg_file).groups()[0]) + F = int(re.search(".+F:\s+(\d+).+", reg_file).groups()[0]) + R = int(re.search(".+R:\s+(\d+).+", reg_file).groups()[0]) + f = int(re.search(".+f:\s+(\d+).+", reg_file).groups()[0]) + p = int(re.search(".+p:\s+(\d+).+", reg_file).groups()[0]) + OD = int(re.search(".+OD:\s+(\d+).+", reg_file).groups()[0]) + ACD = int(re.search(".+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) + + register = int(lut[steps // 2]) + mid_freq = self.app_pll.update_frac_reg(register) + + register = int(lut[-1]) + max_freq = self.app_pll.update_frac_reg(register) + + 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) + 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 + elif set_point >= num_entries: + set_point = num_entries - 1 + self.lock_status = 1 + else: + set_point = set_point + self.lock_status = 0 + + register = int(self.lut[set_point]) + + output_frequency = self.app_pll.update_frac_reg(register) + 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. + """ + def __init__(self): + # Delta sigma modulator state + self.ds_x1 = 0 + self.ds_x2 = 0 + self.ds_x3 = 0 + + self.ds_in_max = 980000 + self.ds_in_min = 60000 + + self.lock_status = -1 + + # generalized version without fixed point shifts. WIP!! + # takes a Q20 number from 60000 to 980000 (or 0.0572 to 0.934) + def do_sigma_delta(self, ds_in): + if ds_in > self.ds_in_max: + print(f"SDM Pos clip: {ds_in}, {self.ds_in_max}") + ds_in = self. ds_in_max + self.lock_status = 1 + + elif ds_in < self.ds_in_min: + print(f"SDM Neg clip: {ds_in}, {self.ds_in_min}") + ds_in = self.ds_in_min + self.lock_status = -1 + + else: + self.lock_status = 0 + + sdm_out = int(self.ds_x3 * 0.002197265625) + + if sdm_out > 8: + sdm_out = 8 + if sdm_out < 0: + sdm_out = 0 + + self.ds_x3 += int((self.ds_x2 * 0.03125) - (sdm_out * 768)) + self.ds_x2 += int((self.ds_x1 * 0.03125) - (sdm_out * 16384)) + self.ds_x1 += int(ds_in - (sdm_out * 131072)) + + return sdm_out, self.lock_status + + +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 + 500kHz: + - 50ps jitter 100Hz-40kHz with low freq noise floor -93dBc. + or 1MHz: + - 10ps jitter 100Hz-40kHz with very low freq noise floor -100dBc + """ + def __init__(self, profile): + """ + Create a sigmal delta DCO targetting either 24.576 or 22.5792MHz + """ + profiles = {"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}, + "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}, + "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}, + "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}} + + self.profile = profile + self.p_value = 8 # 8 frac settings + 1 non frac setting + + input_freq, F, R, f, p, OD, ACD = list(profiles[profile].values()) + + self.app_pll = app_pll_frac_calc(input_freq, F, R, f, p, OD, ACD) + sdm.__init__(self) + + + def _sdm_out_to_freq(self, sdm_out): + """ + Translate the SDM steps to register settings + """ + if sdm_out == 0: + # Step 0 + return self.app_pll.update_frac(0, 0, False) + else: + # Steps 1 to 8 inclusive + return self.app_pll.update_frac(sdm_out - 1, self.p_value - 1) + + def do_modulate(self, input): + """ + Input a control value and output a SDM signal + """ + sdm_out, lock_status = sdm.do_sigma_delta(self, input) + + frequency = self._sdm_out_to_freq(sdm_out) + + return frequency, lock_status + + def print_stats(self, target_output_frequency): + """ + 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) + + + 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): # +1 since p value is +1 in datasheet and another +1 so we hit the max value + 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("\n\n") + + +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()}") + # # print(f"LUT : {dco.get_lut()}") + # 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(24576000) + sdm_dco.plot_freq_range() + for i in range(30): + output_frequency = sdm_dco.do_modulate(500000) + print(i, output_frequency) diff --git a/python/sw_pll/pfd_model.py b/python/sw_pll/pfd_model.py new file mode 100644 index 00000000..6ddddee3 --- /dev/null +++ b/python/sw_pll/pfd_model.py @@ -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}") + + diff --git a/python/sw_pll/sw_pll_sim.py b/python/sw_pll/sw_pll_sim.py index 30c92776..57f0de8a 100644 --- a/python/sw_pll/sw_pll_sim.py +++ b/python/sw_pll/sw_pll_sim.py @@ -1,654 +1,238 @@ -# Copyright 2022-2023 XMOS LIMITED. +# Copyright 2023 XMOS LIMITED. # This Software is subject to the terms of the XMOS Public Licence: Version 1. -import numpy as np +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 subprocess -import re -import os -from pathlib import Path -import soundfile - -header_file = "fractions.h" # fixed name by pll_calc.py -register_file = "register_setup.h" # can be changed as needed - - -class app_pll_frac_calc: - """ - This class uses the formula 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. - """ - def __init__(self, input_frequency, F_init, R_init, OD_init, ACD_init, f_init, r_init, verbose=False): - 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 = r_init # fractional fivider (+1.0) - self.output_frequency = None - self.lock_status_state = 0 - self.verbose = verbose - - self.calc_frequency() - - def calc_frequency(self): - 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" - - assert self.p > self.f, "Error f is not < p: {self.f} {self.p}" - - # From XU316-1024-QF60A-xcore.ai-Datasheet_22.pdf - self.output_frequency = self.input_frequency * (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)) - - return self.output_frequency - - def get_output_frequency(self): - return self.output_frequency - - def update_pll_all(self, F, R, OD, ACD, f, p): - self.F = F - self.R = R - self.OD = OD - self.ACD = ACD - self.f = f - self.p = p - self.calc_frequency() - - def update_pll_frac(self, f, p): - self.f = f - self.p = p - self.calc_frequency() - - def update_pll_frac_reg(self, reg): - """determine f and p from the register number and recalculate frequency""" - f = int((reg >> 8) & ((2**8)-1)) - p = int(reg & ((2**8)-1)) - self.update_pll_frac(f, p) - -class parse_lut_h_file(): - """ - 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, verbose=False): - 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}") - - self.lut_reg = lut - self.min_frac = min_frac - self.max_frac = max_frac - - def get_lut(self): - return self.lut_reg - - def get_lut_size(self): - return np.size(self.lut_reg) - -def get_frequency_from_error(error, lut, pll:app_pll_frac_calc): - """given an error, a lut, and a pll, calculate the frequency""" - num_entries = np.size(lut) - - set_point = int(error) # Note negative term for neg feedback - if set_point < 0: - set_point = 0 - lock_status = -1 - elif set_point >= num_entries: - set_point = num_entries - 1 - lock_status = 1 - else: - set_point = set_point - lock_status = 0 - - register = int(lut[set_point]) - pll.update_pll_frac_reg(register) - - return pll.get_output_frequency(), lock_status - -class error_to_pll_output_frequency(app_pll_frac_calc, parse_lut_h_file): - """ - This super class combines app_pll_frac_calc and parse_lut_h_file and provides a way of inputting the eror signal and - providing an output frequency for a given set of PLL configuration parameters. It includes additonal methods for - turning the LUT register settings parsed by parse_lut_h_file into fractional values which can be fed into app_pll_frac_calc. - - It also contains information reporting methods which provide the range and step sizes of the PLL configuration as well as - plotting the transfer function from error to frequncy so the linearity and regularity the transfer function can be observed. - """ - - def __init__(self, header_file, input_frequency, F_init, R_init, OD_init, ACD_init, f_init, p_init, verbose=False): - self.app_pll_frac_calc = app_pll_frac_calc.__init__(self, input_frequency, F_init, R_init, OD_init, ACD_init, f_init, p_init, verbose=False) - self.parse_lut_h_file = parse_lut_h_file.__init__(self, header_file, verbose=False) - self.verbose = verbose - - def reg_to_frac(self, register): - f = (register & 0xff00) >> 8 - p = register & 0xff - - return f, p - - def get_output_frequency_from_error(self, error): - lut = self.get_lut() - - return get_frequency_from_error(error, lut, self) - - def get_stats(self): - lut = self.get_lut() - steps = np.size(lut) - - register = int(lut[0]) - f, p = self.reg_to_frac(register) - self.update_pll_frac(f, p) - min_freq = self.get_output_frequency() - - register = int(lut[steps // 2]) - f, p = self.reg_to_frac(register) - self.update_pll_frac(f, p) - mid_freq = self.get_output_frequency() - - register = int(lut[-1]) - f, p = self.reg_to_frac(register) - self.update_pll_frac(f, p) - max_freq = self.get_output_frequency() - - return min_freq, mid_freq, max_freq, steps - - def plot_freq_range(self): - lut = self.get_lut() - steps = np.size(lut) - - frequencies = [] - for step in range(steps): - register = int(lut[step]) - f, p = self.reg_to_frac(register) - self.update_pll_frac(f, p) - frequencies.append(self.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("sw_pll_range.png", dpi=150) - -def parse_register_file(register_file): - """ - This helper function reads the pre-saved register setup comments from get_pll_solution and parses them into parameters that - can be used for the simulation. - """ - - with open(register_file) as rf: - reg_file = rf.read().replace('\n', '') - F = int(re.search(".+F:\s+(\d+).+", reg_file).groups()[0]) - R = int(re.search(".+R:\s+(\d+).+", reg_file).groups()[0]) - f = int(re.search(".+f:\s+(\d+).+", reg_file).groups()[0]) - p = int(re.search(".+p:\s+(\d+).+", reg_file).groups()[0]) - OD = int(re.search(".+OD:\s+(\d+).+", reg_file).groups()[0]) - ACD = int(re.search(".+ACD:\s+(\d+).+", reg_file).groups()[0]) - - return F, R, f, p, OD, ACD - - - - # 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_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(".+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" 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 - """ - def __init__(self, *args, **kwargs): - try: - 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) - self.lut = parse_lut_h_file("fractions.h") - finally: - Path("fractions.h").unlink(missing_ok=True) - Path("register_setup.h").unlink(missing_ok=True) - -class sw_pll_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. - """ - lock_status_lookup = {-1 : "UNLOCKED LOW", 0 : "LOCKED", 1 : "UNLOCKED HIGH"} - - def __init__(self, target_output_frequency, lut_lookup_function, lut_size, multiplier, ref_to_loop_call_rate, Kp, Ki, init_output_count=0, init_ref_clk_count=0, base_lut_index=None, verbose=False): - self.lut_lookup_function = lut_lookup_function - self.multiplier = multiplier - self.ref_to_loop_call_rate = ref_to_loop_call_rate - - self.ref_clk_count = init_output_count # Integer as we run this loop based on the ref clock input count - self.output_count_old = init_output_count # Integer - self.expected_output_count_inc_float = multiplier * ref_to_loop_call_rate - self.expected_output_count_float = 0.0 - - if base_lut_index is None: - base_lut_index = lut_size // 2 - self.base_lut_index = base_lut_index - - self.Kp = Kp - self.Ki = Ki - - self.diff = 0.0 #Most recent diff between expected and actual - self.error_accum = 0.0 #Integral of error - self.error = 0.0 #total error - - self.i_windup_limit = lut_size / Ki if Ki != 0.0 else 0.0 - - self.last_output_frequency = target_output_frequency - - self.verbose = verbose +import numpy as np - if verbose: - print(f"Init sw_pll_ctrl, target_output_frequency: {target_output_frequency} ref_to_loop_call_rate: {ref_to_loop_call_rate}, Kp: {Kp} Ki: {Ki}") - def get_expected_output_count_inc(self): - return self.expected_output_count_inc_float +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) - def get_error(self): - return self.error +############################## +# LOOK UP TABLE IMPLEMENTATION +############################## - def do_control_from_error(self, error): - """ Calculate the actual output frequency from raw input error term. - """ - self.diff = error # Used by tests +class sim_sw_pll_lut: + def __init__( self, + target_output_frequency, + nominal_nominal_control_rate_frequency, + Kp, + Ki, + Kii=None): - # clamp integral terms to stop them irrecoverably drifting off. - self.error_accum = np.clip(self.error_accum + error, -self.i_windup_limit, self.i_windup_limit) + self.pfd = port_timer_pfd(target_output_frequency, nominal_nominal_control_rate_frequency) + self.controller = lut_pi_ctrl(Kp, Ki, verbose=False) + self.dco = lut_dco(verbose=False) - error_p = self.Kp * error; - error_i = self.Ki * self.error_accum + 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 nominally + """ - self.error = error_p + error_i + 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 self.verbose: - print(f"diff: {error} error_p: {error_p}({self.Kp}) error_i: {error_i}({self.Ki}) total error: {self.error}") - print(f"expected output_count: {self.expected_output_count_inc_float} actual output_count: {output_count_inc} error: {self.error}") + 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]}") - actual_output_frequency, lock_status = self.lut_lookup_function(self.base_lut_index - self.error) + return output_frequency, lock_status - return actual_output_frequency, lock_status - def do_control(self, output_count_float, period_fraction=1.0): - """ Calculate the actual output frequency from the input output_count taken at the ref clock time. - 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. - """ - if 0 == output_count_float: - return self.lut_lookup_function(self.base_lut_index) - - output_count_int = int(output_count_float) - output_count_inc = output_count_int - self.output_count_old - output_count_inc = output_count_inc / period_fraction - - self.expected_output_count_float = self.output_count_old + self.expected_output_count_inc_float - self.output_count_old = output_count_int - - self.ref_clk_count += self.ref_to_loop_call_rate - - error = output_count_inc - int(self.expected_output_count_inc_float) - actual_output_frequency, lock_status = self.do_control_from_error(error) - - return actual_output_frequency, lock_status - -class audio_modulator: - def __init__(self, duration_s, sample_rate=48000, test_tone_hz=1000): - self.sample_rate = sample_rate - self.test_tone_hz = test_tone_hz - - # First generate arrays for FM modulation - self.each_sample_number = np.linspace(0, duration_s, int(sample_rate * duration_s)) - self.carrier = 2 * np.pi * self.each_sample_number * test_tone_hz - - # Blank array with 0Hz modulation - k = 2 * np.pi # modulation constant - amplitude of 1.0 = 1Hz deviation - self.modulator = k * self.each_sample_number - - 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] = self.modulator[start_idx:end_idx] + delta_freq - - - def get_modulated_waveform(self): - # Now create the frequency modulated waveform - waveform = np.cos(self.carrier + self.modulator) - - return waveform - - def save_modulated_wav(self, filename, waveform): - integer_output = np.int16(waveform * 32767) - soundfile.write(filename, integer_output, int(self.sample_rate)) - - def plot_modulated_fft(self, filename, waveform): - xf = np.linspace(0.0, 1.0/(2.0/self.sample_rate), self.each_sample_number.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.savefig(filename, dpi=150) - - -def run_sim(target_output_frequency, nominal_ref_frequency, lut_lookup_function, lut_size, verbose=False): - """ - This function uses the sw_pll_ctrl and passed lut_lookup_function to run a simulation of the response - of the sw_pll to changes in input reference frequency. - A plot of the simulation is generated to allow visual inspection and tuning. - """ - - # PI loop control constants +def run_lut_sw_pll_sim(): + nominal_output_hz = 12288000 + nominal_control_rate_hz = 93.75 + output_frequency = nominal_output_hz + simulation_iterations = 100 Kp = 0.0 Ki = 1.0 + Kii = 0.0 - ref_frequency = nominal_ref_frequency - sw_pll = sw_pll_ctrl(target_output_frequency, lut_lookup_function, lut_size, multiplier, ref_to_loop_call_rate, Kp, Ki, verbose=False) - - output_count_end_float = 0.0 - real_time = 0.0 - actual_output_frequency = target_output_frequency + sw_pll = sim_sw_pll_lut(nominal_output_hz, nominal_control_rate_hz, Kp, Ki, Kii=Kii) + output_clock_count = 0 - last_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_log = [] + target_freq_log = [] + real_time_log = [] + real_time = 0.0 + period_fraction = 1.0 - simulation_iterations = 1500 + ppm_shift = +50 - # Move the reference frequency about - iteration count, PPM change - ppm_shifts = ((250, 300), (500, 150), (800, -200), (1300, 0)) - # ppm_shifts = () # Straight run with no PPM deviation + for loop in range(simulation_iterations): + output_frequency, lock_status = sw_pll.do_control_loop(output_clock_count, period_fraction=period_fraction, verbose=False) - test_tone_hz = 1000 - audio = audio_modulator(simulation_iterations * ref_to_loop_call_rate / ref_frequency, sample_rate = ref_frequency, test_tone_hz = test_tone_hz) + # 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) - for count in range(simulation_iterations): - output_count_start_float = output_count_end_float - output_count_float_inc = actual_output_frequency / ref_frequency * ref_to_loop_call_rate - # Add some jitter to the output_count to test jitter compensation - # output_sample_jitter = 0 - output_sample_jitter = 100 * (np.random.sample() - 0.5) - output_count_end_float += output_count_float_inc + output_sample_jitter - # Compensate for the jitter - period_fraction = (output_count_float_inc + output_sample_jitter) / output_count_float_inc + 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: + def __init__( self, + target_output_frequency, + nominal_nominal_control_rate_frequency, + Kp, + Ki, + Kii=None): - # print(f"output_count_float_inc: {output_count_float_inc}, period_fraction: {period_fraction}, ratio: {output_count_float_inc / period_fraction}") + self.pfd = port_timer_pfd(target_output_frequency, nominal_nominal_control_rate_frequency, ppm_range=20000) + self.controller = sdm_pi_ctrl(Kp, Ki, Kii) + self.dco = sigma_delta_dco("24.576_1M") - actual_output_frequency, lock_status = sw_pll.do_control(output_count_end_float, period_fraction = period_fraction) - # lock_status = 0 + self.target_output_frequency = target_output_frequency + self.time = 0.0 + self.control_time_inc = 1 / nominal_nominal_control_rate_frequency - # Helpers for the tone modulation - time_in_s = lambda count: count * ref_to_loop_call_rate / ref_frequency - freq_shift = lambda actual_output_frequency, target_output_frequency, test_tone_hz: (actual_output_frequency / target_output_frequency - 1) * test_tone_hz - audio.apply_frequency_deviation(time_in_s(count), time_in_s(count + 1), freq_shift(actual_output_frequency, target_output_frequency, test_tone_hz)) - + self.control_setting = (self.dco.ds_in_max + self.dco.ds_in_min) / 2 # Mid way - # print(freq_shift(actual_output_frequency, target_output_frequency, test_tone_hz)) + + def do_control_loop(self, output_clock_count, verbose=False): + """ + Run the control loop which runs at a tiny fraction of the SDM rate + """ + + error, first_loop = self.pfd.get_error(output_clock_count) + ctrl_output = self.controller.do_control_from_error(error) + self.control_setting = ctrl_output if verbose: - print(f"Loop: count: {count}, time: {real_time}, actual_output_frequency: {actual_output_frequency}, lock_status: {sw_pll_ctrl.lock_status_lookup[lock_status]}") - + print(f"Raw error: {error}") + print(f"ctrl_output: {ctrl_output}") + print(f"Lock status: {lock_status_lookup[lock_status]}") - freq_log.append(actual_output_frequency) - target_log.append(ref_frequency * multiplier) + return self.control_setting - real_time += ref_to_loop_call_rate / ref_frequency + 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, lock_status = self.dco.do_modulate(self.control_setting) + return frequncy, lock_status - # A number of events where the input reference is stepped - ppm_adjust = lambda f, ppm: f * (1 + (ppm / 1000000)) - for ppm_shift in ppm_shifts: - (change_at_count, ppm) = ppm_shift - if count == change_at_count: - ref_frequency = ppm_adjust(nominal_ref_frequency, ppm) +def run_sd_sw_pll_sim(): + 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) + output_clock_count = 0 - plt.clf() - plt.plot(freq_log, color='red', marker='.', label='actual frequency') - plt.plot(target_log, color='blue', marker='.', label='target frequency') - plt.title('PLL tracking', fontsize=14) - plt.xlabel(f'loop_cycle {ref_to_loop_call_rate}', fontsize=14) - plt.ylabel('Frequency', fontsize=10) - plt.legend(loc="upper right") - plt.grid(True) - # plt.show() - plt.savefig("pll_step_response.png", dpi=150) - - # Generate fft of modulated test tone - audio.plot_modulated_fft(f"modulated_tone_fft_{test_tone_hz}Hz.png", audio.get_modulated_waveform()) - audio.save_modulated_wav(f"modulated_tone_{test_tone_hz}Hz.wav", audio.get_modulated_waveform()) - - -""" -ref_to_loop_call_rate - Determines how often to call the control loop in terms of ref clocks -xtal_frequency - The xcore clock 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 -""" - -ref_to_loop_call_rate = 512 -xtal_frequency = 24000000 -profile_choice = 0 - -# Example profiles to produce typical frequencies seen in audio systems -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 - 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}, - # 3 - 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}, - # 4 - 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}, - # 5 - 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}, - ] + 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, lock_status = 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__': - """ - This script checks to see if PLL settings have already been generated, if not, generates them. - It then uses these settings to generate a LUT and control loop instance. - A set of step functions in input reference frequencies are then generated and the - response of the sw_pll to these changes is logged and then plotted. - """ - - profile_used = profiles[profile_choice] - - # Make a list of the correct args for get_pll_solution - get_pll_solution_args = {"input_frequency":xtal_frequency} - get_pll_solution_args.update(profile_used) - del get_pll_solution_args["nominal_ref_frequency"] - get_pll_solution_args = list(get_pll_solution_args.values()) - - # Extract the required vals from the profile - target_output_frequency = profile_used["target_output_frequency"] - nominal_ref_frequency = profile_used["nominal_ref_frequency"] - multiplier = target_output_frequency / nominal_ref_frequency - # input_frequency, target_output_frequency, max_denom=80, min_F=200, ppm_max=2, fracmin=0.65, fracmax=0.95 - - - # Use pre-caclulated saved values if they exist, otherwise generate new ones - if not os.path.exists(header_file) or not os.path.exists(register_file): - output_frequency, vco_freq, F, R, f, p, OD, ACD, ppm = get_pll_solution(*get_pll_solution_args) - print(f"output_frequency: {output_frequency}, vco_freq: {vco_freq}, F: {F}, R: {R}, f: {f}, p: {p}, OD: {OD}, ACD: {ACD}, ppm: {ppm}") - else: - F, R, f, p, OD, ACD = parse_register_file(register_file) - print(f"Using pre-calculated settings read from {header_file} and {register_file}:") - - print(f"PLL register settings F: {F}, R: {R}, OD: {OD}, ACD: {ACD}, f: {f}, p: {p}") - - # Instantiate controller - error_from_h = error_to_pll_output_frequency(header_file, xtal_frequency, F, R, OD, ACD, f, p, verbose=False) - error_from_h.plot_freq_range() - - min_freq, mid_freq, max_freq, steps = error_from_h.get_stats() - step_size = ((max_freq - min_freq) / steps) - - print(f"min_freq: {min_freq:.0f}Hz") - print(f"mid_freq: {mid_freq:.0f}Hz") - print(f"max_freq: {max_freq:.0f}Hz") - print(f"average step size: {step_size:.6}Hz, PPM: {1e6 * 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}") - print(f"LUT entries: {steps} ({steps*2} bytes)") - - run_sim(target_output_frequency, nominal_ref_frequency, error_from_h.get_output_frequency_from_error, error_from_h.get_lut_size(), verbose=False) + run_lut_sw_pll_sim() + run_sd_sw_pll_sim() + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 097d778e..c8599821 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ pytest pandas soundfile - +scipy diff --git a/tests/test_lib_sw_pll.py b/tests/test_lib_sw_pll.py index 2fe002d4..b4d43e11 100644 --- a/tests/test_lib_sw_pll.py +++ b/tests/test_lib_sw_pll.py @@ -14,13 +14,10 @@ import numpy as np import copy +from sw_pll.app_pll_model import pll_solution, app_pll_frac_calc +from sw_pll.sw_pll_sim import sim_sw_pll_lut + from typing import Any -from sw_pll.sw_pll_sim import ( - pll_solution, - app_pll_frac_calc, - sw_pll_ctrl, - get_frequency_from_error, -) from dataclasses import dataclass, asdict from subprocess import Popen, PIPE from itertools import product @@ -53,21 +50,17 @@ def __init__(self, args: DutArgs, pll): self.pll = pll self.args = DutArgs(**asdict(args)) # copies the values self.lut = self.args.lut - self.args.lut = len(self.lut.get_lut()) - self.ctrl = sw_pll_ctrl( + self.args.lut = len(self.lut) + nominal_control_rate_hz = args.target_output_frequency / args.pll_ratio / args.loop_rate_count + self.ctrl = sim_sw_pll_lut( args.target_output_frequency, - self.lut_func, - len(self.lut.get_lut()), - args.loop_rate_count, - args.pll_ratio, + nominal_control_rate_hz, args.kp, - args.ki, - base_lut_index=args.nominal_lut_idx, - ) + args.ki, ) def lut_func(self, error): """Sim requires a function to provide access to the LUT. This is that""" - return get_frequency_from_error(error, self.lut.get_lut(), self.pll) + return get_frequency_from_error(error, self.lut, self.pll) def __enter__(self): """support context manager""" @@ -80,17 +73,18 @@ def do_control(self, mclk_pt, _ref_pt): """ Execute control using simulator """ - f, l = self.ctrl.do_control(mclk_pt) + f, l = self.ctrl.do_control_loop(mclk_pt) - return l, f, self.ctrl.diff, self.ctrl.error_accum, 0, 0 + return l, f, self.ctrl.controller.diff, self.ctrl.controller.error_accum, 0, 0 def do_control_from_error(self, error): """ Execute control using simulator """ - f, l = self.ctrl.do_control_from_error(error) + dco_ctl = self.ctrl.controller.get_dco_control_from_error(error) + f, l = self.ctrl.dco.get_frequency_from_dco_control(dco_ctl) - return l, f, self.ctrl.diff, self.ctrl.error_accum, 0, 0 + return l, f, self.ctrl.controller.diff, self.ctrl.controller.error_accum, 0, 0 class Dut: """ @@ -102,8 +96,8 @@ def __init__(self, args: DutArgs, pll, xe_file=DUT_XE): self.args = DutArgs(**asdict(args)) # copies the values self.args.kp = self.args.kp self.args.ki = self.args.ki - lut = self.args.lut.get_lut() - self.args.lut = len(args.lut.get_lut()) + lut = self.args.lut + self.args.lut = len(args.lut) # concatenate the parameters to the init function and the whole lut # as the command line parameters to the xe. list_args = [*(str(i) for i in asdict(self.args).values())] + [ @@ -139,7 +133,7 @@ def do_control(self, mclk_pt, ref_pt): locked, reg, diff, acum, first_loop, ticks = self._process.stdout.readline().strip().split() - self.pll.update_pll_frac_reg(int(reg, 16)) + self.pll.update_frac_reg(int(reg, 16)) return int(locked), self.pll.get_output_frequency(), int(diff), int(acum), int(first_loop), int(ticks) def do_control_from_error(self, error): @@ -151,7 +145,7 @@ def do_control_from_error(self, error): locked, reg, diff, acum, first_loop, ticks = self._process.stdout.readline().strip().split() - self.pll.update_pll_frac_reg(int(reg, 16)) + self.pll.update_frac_reg(int(reg, 16)) return int(locked), self.pll.get_output_frequency(), int(diff), int(acum), int(first_loop), int(ticks) @@ -217,7 +211,7 @@ def basic_test_vector(request, solution_12288, bin_dir): loop_rate_count = 1 # Generate init parameters - start_reg = sol.lut.get_lut()[0] + start_reg = sol.lut[0] args = DutArgs( target_output_frequency=target_mclk_f, kp=0.0, @@ -234,15 +228,15 @@ def basic_test_vector(request, solution_12288, bin_dir): # directly into the lut index. therefore the "ppm_range" or max # allowable diff must be at least as big as the LUT. *2 used here # to allow recovery from out of range values. - ppm_range=int(len(sol.lut.get_lut()) * 2), + ppm_range=int(len(sol.lut) * 2), lut=sol.lut, ) - pll = app_pll_frac_calc(xtal_freq, sol.F, sol.R, sol.OD, sol.ACD, 1, 2) + pll = app_pll_frac_calc(xtal_freq, sol.F, sol.R, 1, 2, sol.OD, sol.ACD) frequency_lut = [] - for reg in sol.lut.get_lut(): - pll.update_pll_frac_reg(reg) + for reg in sol.lut: + pll.update_frac_reg(reg) frequency_lut.append(pll.get_output_frequency()) frequency_range_frac = (frequency_lut[-1] - frequency_lut[0])/frequency_lut[0] @@ -251,7 +245,7 @@ def basic_test_vector(request, solution_12288, bin_dir): plt.savefig(bin_dir/f"lut-{name}.png") plt.close() - pll.update_pll_frac_reg(start_reg) + pll.update_frac_reg(start_reg) input_freqs = { "perfect": target_ref_f, @@ -368,6 +362,7 @@ def test_lock_lost(basic_test_vector, test_f): this_df = df[df["ref_f"] == input_freqs[test_f]] not_locked_df = this_df[this_df["locked"] != 0] + assert not not_locked_df.empty, "Expected lock to be lost when out of range" first_not_locked = not_locked_df.index[0] after_not_locked = this_df[first_not_locked:]["locked"] != 0 @@ -419,7 +414,7 @@ def test_locked_values_within_desirable_ppm(basic_test_vector, test_f): def test_low_level_equivalence(solution_12288, bin_dir): """ Simple low level test of equivalence using do_control_from_error - Feed in random numbers into but C and Python DUTs and see if we get the same results + Feed in random numbers into C and Python DUTs and see if we get the same results """ _, xtal_freq, target_mclk_f, sol = solution_12288 @@ -430,8 +425,8 @@ def test_low_level_equivalence(solution_12288, bin_dir): target_ref_f = 48000 # Generate init parameters - start_reg = sol.lut.get_lut()[0] - lut_size = len(sol.lut.get_lut()) + start_reg = sol.lut[0] + lut_size = len(sol.lut) args = DutArgs( target_output_frequency=target_mclk_f, @@ -444,20 +439,14 @@ def test_low_level_equivalence(solution_12288, bin_dir): ref_clk_expected_inc=0, app_pll_ctl_reg_val=0, app_pll_div_reg_val=start_reg, - nominal_lut_idx=0, # start low so there is some control to do - # with ki of 1 and the other values 0, the diff value translates - # directly into the lut index. therefore the "ppm_range" or max - # allowable diff must be at least as big as the LUT. *2 used here - # to allow recovery from out of range values. + nominal_lut_idx=lut_size//2, ppm_range=int(lut_size * 2), lut=sol.lut, ) - pll = app_pll_frac_calc(xtal_freq, sol.F, sol.R, sol.OD, sol.ACD, 1, 2) - - frequency_lut = [] + pll = app_pll_frac_calc(xtal_freq, sol.F, sol.R, 1, 2, sol.OD, sol.ACD) - pll.update_pll_frac_reg(start_reg) + pll.update_frac_reg(start_reg) input_errors = np.random.randint(-lut_size // 2, lut_size // 2, size = 40) print(f"input_errors: {input_errors}") diff --git a/tools/ci/checkout-submodules.sh b/tools/ci/checkout-submodules.sh index bd16abb5..e51e6427 100755 --- a/tools/ci/checkout-submodules.sh +++ b/tools/ci/checkout-submodules.sh @@ -22,11 +22,11 @@ rm -rf modules mkdir -p modules pushd modules -clone fwk_core git@github.com:xmos/fwk_core.git 9e4f6196386995e2d7786b376091404638055639 -clone fwk_io git@github.com:xmos/fwk_io.git 6b3275cbe4e39abce3b6e822655732767a62740d +clone fwk_core git@github.com:xmos/fwk_core.git v1.0.2 +clone fwk_io git@github.com:xmos/fwk_io.git v3.3.0 clone infr_scripts_py git@github.com:xmos/infr_scripts_py.git 1d767cbe89a3223da7a4e27c283fb96ee2a279c9 -clone infr_apps git@github.com:xmos/infr_apps.git 8bc62324b19a1ab32b1e5a5e262f40f710f9f5c1 +clone infr_apps git@github.com:xmos/infr_apps.git 8bc62324b19a1ab32b1e5a5e262f40f710f9f5c1 pip install -e infr_apps -e infr_scripts_py popd diff --git a/tools/ci/do-ci.sh b/tools/ci/do-ci-build.sh similarity index 67% rename from tools/ci/do-ci.sh rename to tools/ci/do-ci-build.sh index 8c9e91e2..cdbeefee 100755 --- a/tools/ci/do-ci.sh +++ b/tools/ci/do-ci-build.sh @@ -1,14 +1,8 @@ #! /usr/bin/env bash # -# build and test stuff +# build stuff set -ex cmake -B build -DCMAKE_TOOLCHAIN_FILE=modules/fwk_io/xmos_cmake_toolchain/xs3a.cmake cmake --build build --target all --target test_app --target test_app_low_level_api --target simple --target i2s_slave -j$(nproc) - -pushd tests -pytest --junitxml=results.xml -rA -v --durations=0 -o junit_logging=all -ls bin -popd - diff --git a/tools/ci/do-ci-tests.sh b/tools/ci/do-ci-tests.sh new file mode 100755 index 00000000..773b094b --- /dev/null +++ b/tools/ci/do-ci-tests.sh @@ -0,0 +1,11 @@ +#! /usr/bin/env bash +# +# test stuff + +set -ex + +pushd tests +pytest --junitxml=results.xml -rA -v --durations=0 -o junit_logging=all +ls bin +popd +