init
This commit is contained in:
44
lib_sw_pll/lib_sw_pll/CMakeLists.txt
Normal file
44
lib_sw_pll/lib_sw_pll/CMakeLists.txt
Normal file
@@ -0,0 +1,44 @@
|
||||
## Source files
|
||||
file(GLOB_RECURSE LIB_C_SOURCES src/*.c )
|
||||
file(GLOB_RECURSE LIB_CXX_SOURCES src/*.cc)
|
||||
file(GLOB_RECURSE LIB_XC_SOURCES src/*.xc)
|
||||
file(GLOB_RECURSE LIB_ASM_SOURCES src/*.S )
|
||||
|
||||
## cmake doesn't recognize .S files as assembly by default
|
||||
set_source_files_properties(LIB_ASM_SOURCES PROPERTIES LANGUAGE ASM)
|
||||
|
||||
## Assume all asm is XS3A for now
|
||||
set(XCORE_XS3A_SOURCES ${LIB_ASM_SOURCES})
|
||||
|
||||
## Set any local library compile options
|
||||
set(LIB_COMPILE_FLAGS "-Os" "-g")
|
||||
|
||||
## Includes files
|
||||
set(LIB_PUBLIC_INCLUDES api src)
|
||||
set(LIB_PRIVATE_INCLUDES src)
|
||||
|
||||
## Gather library sources
|
||||
set(LIB_PUBLIC_SOURCES "")
|
||||
set(LIB_PRIVATE_SOURCES ${LIB_C_SOURCES} ${LIB_CXX_SOURCES} ${LIB_XC_SOURCES})
|
||||
|
||||
## Append platform specific sources
|
||||
list(APPEND LIB_PRIVATE_SOURCES ${${CMAKE_SYSTEM_NAME}_SOURCES})
|
||||
|
||||
## Create library target
|
||||
add_library(lib_sw_pll STATIC)
|
||||
target_sources(lib_sw_pll
|
||||
PUBLIC
|
||||
${LIB_PUBLIC_SOURCES}
|
||||
PRIVATE
|
||||
${LIB_PRIVATE_SOURCES}
|
||||
)
|
||||
target_include_directories(lib_sw_pll
|
||||
PUBLIC
|
||||
${LIB_PUBLIC_INCLUDES}
|
||||
PRIVATE
|
||||
${LIB_PRIVATE_INCLUDES}
|
||||
)
|
||||
target_compile_options(lib_sw_pll
|
||||
PRIVATE
|
||||
${LIB_COMPILE_FLAGS}
|
||||
)
|
||||
317
lib_sw_pll/lib_sw_pll/api/sw_pll.h
Normal file
317
lib_sw_pll/lib_sw_pll/api/sw_pll.h
Normal file
@@ -0,0 +1,317 @@
|
||||
// Copyright 2022-2024 XMOS LIMITED.
|
||||
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <xccompat.h>
|
||||
|
||||
#ifdef __XC__
|
||||
#define _Bool uint8_t
|
||||
#else
|
||||
#include <xcore/hwtimer.h>
|
||||
#include <xcore/port.h>
|
||||
#include <xcore/clock.h>
|
||||
#include <xcore/channel.h>
|
||||
#include <xcore/assert.h>
|
||||
#endif
|
||||
|
||||
// SW_PLL Component includes
|
||||
#include "sw_pll_common.h"
|
||||
#include "sw_pll_pfd.h"
|
||||
#include "sw_pll_sdm.h"
|
||||
|
||||
|
||||
/**
|
||||
* \addtogroup sw_pll_lut sw_pll_lut
|
||||
*
|
||||
* The public API for using the Software PLL.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* sw_lut_pll initialisation function.
|
||||
*
|
||||
* This must be called before use of sw_pll_lut_do_control.
|
||||
* Call this passing a pointer to the sw_pll_state_t stuct declared locally.
|
||||
*
|
||||
* \param sw_pll Pointer to the struct to be initialised.
|
||||
* \param Kp Proportional PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param Ki Integral PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param Kii Double integral PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param loop_rate_count How many counts of the call to sw_pll_lut_do_control before control is done.
|
||||
* Note this is only used by sw_pll_lut_do_control. sw_pll_lut_do_control_from_error
|
||||
* calls the control loop every time so this is ignored.
|
||||
* \param pll_ratio Integer ratio between input reference clock and the PLL output.
|
||||
* Only used by sw_pll_lut_do_control for the PFD. Don't care otherwise.
|
||||
* Used to calculate the expected port timer increment when control is called.
|
||||
* \param ref_clk_expected_inc Expected ref clock increment each time sw_pll_lut_do_control is called.
|
||||
* Pass in zero if you are sure the mclk sampling timing is precise. This
|
||||
* will disable the scaling of the mclk count inside sw_pll_lut_do_control.
|
||||
* Only used by sw_pll_lut_do_control. Don't care otherwise.
|
||||
* \param lut_table_base Pointer to the base of the fractional PLL LUT used
|
||||
* \param num_lut_entries Number of entries in the LUT (half sizeof since entries are 16b)
|
||||
* \param app_pll_ctl_reg_val The setting of the app pll control register.
|
||||
* \param app_pll_div_reg_val The setting of the app pll divider register.
|
||||
* \param nominal_lut_idx The index into the LUT which gives the nominal output. Normally
|
||||
* close to halfway to allow symmetrical range.
|
||||
* \param ppm_range The pre-calculated PPM range. Used to determine the maximum deviation
|
||||
* of counted mclk before the PLL resets its state.
|
||||
* Note this is only used by sw_pll_lut_do_control. sw_pll_lut_do_control_from_error
|
||||
* calls the control loop every time so this is ignored.
|
||||
*
|
||||
*/
|
||||
void sw_pll_lut_init( sw_pll_state_t * const sw_pll,
|
||||
const sw_pll_15q16_t Kp,
|
||||
const sw_pll_15q16_t Ki,
|
||||
const sw_pll_15q16_t Kii,
|
||||
const size_t loop_rate_count,
|
||||
const size_t pll_ratio,
|
||||
const uint32_t ref_clk_expected_inc,
|
||||
const int16_t * const lut_table_base,
|
||||
const size_t num_lut_entries,
|
||||
const uint32_t app_pll_ctl_reg_val,
|
||||
const uint32_t app_pll_div_reg_val,
|
||||
const unsigned nominal_lut_idx,
|
||||
const unsigned ppm_range);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* sw_pll LUT version control function.
|
||||
*
|
||||
* It implements the PFD, controller and DCO output.
|
||||
*
|
||||
* This must be called periodically for every reference clock transition.
|
||||
* Typically, in an audio system, this would be at the I2S or reference clock input rate.
|
||||
* Eg. 16kHz, 48kHz ...
|
||||
*
|
||||
* When this is called, the control loop will be executed every n times (set by init) and the
|
||||
* application PLL will be adjusted to minimise the error seen on the mclk count value.
|
||||
*
|
||||
* If the precise sampling point of mclk is not easily controlled (for example in an I2S callback)
|
||||
* then an additional timer count may be passed in which will scale the mclk count. See i2s_slave
|
||||
* example to show how this is done. This will help reduce input jitter which, in turn, relates
|
||||
* to reduced output jitter.
|
||||
*
|
||||
* \param sw_pll Pointer to the sw_pll state struct.
|
||||
* \param mclk_pt The 16b port timer count of mclk at the time of calling sw_pll_lut_do_control.
|
||||
* \param ref_pt The 16b port timer ref ount at the time of calling sw_pll_lut_do_control. This value
|
||||
* is ignored when the pll is initialised with a zero ref_clk_expected_inc and the
|
||||
* control loop will assume that mclk_pt sample timing is precise.
|
||||
*
|
||||
* \returns The lock status of the PLL. Locked or unlocked high/low. Note that
|
||||
* this value is only updated when the control loop has run.
|
||||
* The type is sw_pll_lock_status_t.
|
||||
*/
|
||||
sw_pll_lock_status_t sw_pll_lut_do_control(sw_pll_state_t * const sw_pll, const uint16_t mclk_pt, const uint16_t ref_pt);
|
||||
|
||||
/**
|
||||
* low level sw_pll control function for use as pure PLL control loop.
|
||||
*
|
||||
* This must be called periodically.
|
||||
*
|
||||
* When this is called, the control loop will be executed every time and the
|
||||
* application PLL will be adjusted to minimise the error seen on the input error value.
|
||||
*
|
||||
* \param sw_pll Pointer to the sw_pll state struct.
|
||||
* \param error 16b signed input error value
|
||||
* \returns The lock status of the PLL. Locked or unlocked high/low. Note that
|
||||
* this value is only updated when the control loop is running.
|
||||
* The type is sw_pll_lock_status_t.
|
||||
*/
|
||||
sw_pll_lock_status_t sw_pll_lut_do_control_from_error(sw_pll_state_t * const sw_pll, int16_t error);
|
||||
|
||||
|
||||
/**
|
||||
* Helper to do a partial init of the PI controller at runtime without setting the physical PLL and LUT settings.
|
||||
*
|
||||
* Sets Kp, Ki and the windup limits. Note this resets the PFD accumulators too and so PI controller state is reset.
|
||||
*
|
||||
* \param sw_pll Pointer to the state struct to be reset.
|
||||
* \param Kp New Kp in sw_pll_15q16_t format.
|
||||
* \param Ki New Ki in sw_pll_15q16_t format.
|
||||
* \param Kii New Kii in sw_pll_15q16_t format.
|
||||
* \param num_lut_entries The number of elements in the sw_pll LUT.
|
||||
*/
|
||||
static inline void sw_pll_lut_reset(sw_pll_state_t *sw_pll, sw_pll_15q16_t Kp, sw_pll_15q16_t Ki, sw_pll_15q16_t Kii, size_t num_lut_entries)
|
||||
{
|
||||
sw_pll->pi_state.Kp = Kp;
|
||||
sw_pll->pi_state.Ki = Ki;
|
||||
sw_pll->pi_state.Kii = Kii;
|
||||
|
||||
sw_pll->pi_state.error_accum = 0;
|
||||
sw_pll->pi_state.error_accum_accum = 0;
|
||||
if(Ki){
|
||||
sw_pll->pi_state.i_windup_limit = (num_lut_entries << SW_PLL_NUM_FRAC_BITS) / Ki; // Set to twice the max total error input to LUT
|
||||
}else{
|
||||
sw_pll->pi_state.i_windup_limit = 0;
|
||||
}
|
||||
if(Kii){
|
||||
sw_pll->pi_state.ii_windup_limit = (num_lut_entries << SW_PLL_NUM_FRAC_BITS) / Kii; // Set to twice the max total error input to LUT
|
||||
}else{
|
||||
sw_pll->pi_state.ii_windup_limit = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**@}*/ // END: addtogroup sw_pll_lut
|
||||
|
||||
|
||||
/**
|
||||
* \addtogroup sw_pll_sdm sw_pll_sdm
|
||||
*
|
||||
* The public API for using the Software PLL.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* sw_pll_sdm initialisation function.
|
||||
*
|
||||
* This must be called before use of sw_pll_sdm_do_control or sw_pll_sdm_do_control_from_error.
|
||||
* Call this passing a pointer to the sw_pll_state_t stuct declared locally.
|
||||
*
|
||||
* \param sw_pll Pointer to the struct to be initialised.
|
||||
* \param Kp Proportional PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param Ki Integral PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param Kii Double integral PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param loop_rate_count How many counts of the call to sw_pll_sdm_do_control before control is done.
|
||||
* Note this is only used by sw_pll_sdm_do_control. sw_pll_sdm_do_control_from_error
|
||||
* calls the control loop every time so this is ignored.
|
||||
* \param pll_ratio Integer ratio between input reference clock and the PLL output.
|
||||
* Only used by sw_pll_sdm_do_control in the PFD. Don't care otherwise.
|
||||
* Used to calculate the expected port timer increment when control is called.
|
||||
* \param ref_clk_expected_inc Expected ref clock increment each time sw_pll_sdm_do_control is called.
|
||||
* Pass in zero if you are sure the mclk sampling timing is precise. This
|
||||
* will disable the scaling of the mclk count inside sw_pll_sdm_do_control.
|
||||
* Only used by sw_pll_sdm_do_control. Don't care otherwise.
|
||||
* \param app_pll_ctl_reg_val The setting of the app pll control register.
|
||||
* \param app_pll_div_reg_val The setting of the app pll divider register.
|
||||
* \param app_pll_frac_reg_val The initial setting of the app pll fractional register.
|
||||
* \param ctrl_mid_point The nominal control value for the Sigma Delta Modulator output. Normally
|
||||
* close to halfway to allow symmetrical range.
|
||||
* \param ppm_range The pre-calculated PPM range. Used to determine the maximum deviation
|
||||
* of counted mclk before the PLL resets its state. Note this is only used
|
||||
* by sw_pll_sdm_do_control. sw_pll_sdm_do_control_from_error
|
||||
* calls the control loop every time so this is ignored.
|
||||
*
|
||||
*/
|
||||
void sw_pll_sdm_init(sw_pll_state_t * const sw_pll,
|
||||
const sw_pll_15q16_t Kp,
|
||||
const sw_pll_15q16_t Ki,
|
||||
const sw_pll_15q16_t Kii,
|
||||
const size_t loop_rate_count,
|
||||
const size_t pll_ratio,
|
||||
const uint32_t ref_clk_expected_inc,
|
||||
const uint32_t app_pll_ctl_reg_val,
|
||||
const uint32_t app_pll_div_reg_val,
|
||||
const uint32_t app_pll_frac_reg_val,
|
||||
const int32_t ctrl_mid_point,
|
||||
const unsigned ppm_range);
|
||||
|
||||
/**
|
||||
* sw_pll_sdm_do_control control function.
|
||||
*
|
||||
* It implements the PFD and controller and generates a DCO control value for the SDM.
|
||||
*
|
||||
* This must be called periodically for every reference clock transition.
|
||||
* Typically, in an audio system, this would be at the I2S or reference clock input rate.
|
||||
* Eg. 16kHz, 48kHz ...
|
||||
*
|
||||
* When this is called, the control loop will be executed every n times (set by init) and the
|
||||
* Sigma Delta Modulator control value will be set according the error seen on the mclk count value.
|
||||
*
|
||||
* If control is executed, TRUE is returned from the function and the value can be sent to the SDM.
|
||||
* The most recent calculated control output value can be found written to sw_pll->sdm_state.current_ctrl_val.
|
||||
*
|
||||
* If the precise sampling point of mclk is not easily controlled (for example in an I2S callback)
|
||||
* then an additional timer count may be passed in which will scale the mclk count. See i2s_slave
|
||||
* example to show how this is done. This will help reduce input jitter which, in turn, relates
|
||||
* to reduced output jitter.
|
||||
*
|
||||
* \param sw_pll Pointer to the sw_pll state struct.
|
||||
* \param mclk_pt The 16b port timer count of mclk at the time of calling sw_pll_sdm_do_control.
|
||||
* \param ref_pt The 16b port timer ref ount at the time of calling sw_pll_sdm_do_control. This value
|
||||
* is ignored when the pll is initialised with a zero ref_clk_expected_inc and the
|
||||
* control loop will assume that mclk_pt sample timing is precise.
|
||||
*
|
||||
* \returns Whether or not control was executed (controoled by loop_rate_count)
|
||||
*/
|
||||
bool sw_pll_sdm_do_control(sw_pll_state_t * const sw_pll, const uint16_t mclk_pt, const uint16_t ref_pt);
|
||||
|
||||
/**
|
||||
* low level sw_pll_sdm control function for use as pure PLL control loop.
|
||||
*
|
||||
* This must be called periodically.
|
||||
*
|
||||
* Takes the raw error input and applies the PI controller algorithm.
|
||||
* The most recent calculated control output value can be found written to sw_pll->sdm_state.current_ctrl_val.
|
||||
*
|
||||
* \param sw_pll Pointer to the sw_pll state struct.
|
||||
* \param error 16b signed input error value
|
||||
* \returns The controller lock status
|
||||
*/
|
||||
sw_pll_lock_status_t sw_pll_sdm_do_control_from_error(sw_pll_state_t * const sw_pll, int16_t error);
|
||||
|
||||
|
||||
/**
|
||||
* Use to initialise the core sigma delta modulator. Broken out as seperate API as the SDM
|
||||
* is usually run in a dedicated thread which could be on a remote tile.
|
||||
*
|
||||
* \param sw_pll Pointer to the SDM state struct.
|
||||
*/
|
||||
void sw_pll_init_sigma_delta(sw_pll_sdm_state_t *sdm_state);
|
||||
|
||||
|
||||
#ifdef __DOXYGEN__
|
||||
/**
|
||||
* Performs the Sigma Delta Modulation from a control input.
|
||||
* It performs the SDM algorithm, converts the output to a fractional register setting
|
||||
* and then writes the value to the PLL fractional register.
|
||||
* Is typically called in a constant period fast loop and run from a dedicated thread which could be on a remote tile.
|
||||
*
|
||||
* NOTE: Attempting to write the PLL fractional register from more than
|
||||
* one logical core at the same time may result in channel lock-up.
|
||||
* Please ensure the that PLL initiaisation has completed before
|
||||
* the SDM task writes to the register. The provided `simple_sdm` example
|
||||
* implements a method for doing this.
|
||||
*
|
||||
* \param sw_pll Pointer to the SDM state struct.
|
||||
* \param this_tile The ID of the xcore tile that is doing the write.
|
||||
* Use get_local_tile_id() to obtain this.
|
||||
* \param sdm_control_in Current control value.
|
||||
*/
|
||||
static inline void sw_pll_do_sigma_delta(sw_pll_sdm_state_t *sdm_state, tileref_t this_tile, int32_t sdm_control_in);
|
||||
#endif
|
||||
|
||||
/**@}*/ // END: addtogroup sw_pll_sdm
|
||||
|
||||
/**
|
||||
* \addtogroup sw_pll_common sw_pll_common
|
||||
*
|
||||
* The public API for using the Software PLL.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resets PI controller state
|
||||
*
|
||||
* \param sw_pll Pointer to the Software PLL state.
|
||||
*/
|
||||
__attribute__((always_inline))
|
||||
inline void sw_pll_reset_pi_state(sw_pll_state_t * const sw_pll)
|
||||
{
|
||||
sw_pll->pi_state.error_accum = 0;
|
||||
sw_pll->pi_state.error_accum_accum = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a fixed (not phase locked) clock between 11.2896 MHz and 49.152 MHz.
|
||||
* Assumes a 24 MHz XTAL.
|
||||
*
|
||||
* \param frequency Frequency in Hz. An incorrect value will assert.
|
||||
*/
|
||||
void sw_pll_fixed_clock(const unsigned frequency);
|
||||
|
||||
/**@}*/ // END: addtogroup sw_pll_common
|
||||
7
lib_sw_pll/lib_sw_pll/lib_build_info.cmake
Normal file
7
lib_sw_pll/lib_sw_pll/lib_build_info.cmake
Normal file
@@ -0,0 +1,7 @@
|
||||
set(LIB_NAME lib_sw_pll)
|
||||
set(LIB_VERSION 2.2.0)
|
||||
set(LIB_INCLUDES api src)
|
||||
set(LIB_COMPILER_FLAGS -Os -g)
|
||||
set(LIB_DEPENDENT_MODULES "")
|
||||
|
||||
XMOS_REGISTER_MODULE()
|
||||
14
lib_sw_pll/lib_sw_pll/module_build_info
Normal file
14
lib_sw_pll/lib_sw_pll/module_build_info
Normal file
@@ -0,0 +1,14 @@
|
||||
VERSION = 2.2.0
|
||||
|
||||
DEPENDENT_MODULES =
|
||||
|
||||
MODULE_XCC_FLAGS = $(XCC_FLAGS)
|
||||
|
||||
OPTIONAL_HEADERS +=
|
||||
|
||||
EXPORT_INCLUDE_DIRS = api \
|
||||
src
|
||||
|
||||
INCLUDE_DIRS = $(EXPORT_INCLUDE_DIRS)
|
||||
|
||||
SOURCE_DIRS = src
|
||||
138
lib_sw_pll/lib_sw_pll/src/sw_pll_common.c
Normal file
138
lib_sw_pll/lib_sw_pll/src/sw_pll_common.c
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright 2024 XMOS LIMITED.
|
||||
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
#ifdef __XS3A__
|
||||
|
||||
#include "sw_pll.h"
|
||||
|
||||
// Implement a delay in 100MHz timer ticks without using a timer resource
|
||||
static void blocking_delay(const uint32_t delay_ticks)
|
||||
{
|
||||
uint32_t time_delay = get_reference_time() + delay_ticks;
|
||||
while(TIMER_TIMEAFTER(time_delay, get_reference_time()));
|
||||
}
|
||||
|
||||
|
||||
// Set secondary (App) PLL control register safely to work around chip bug.
|
||||
void sw_pll_app_pll_init(const unsigned tileid,
|
||||
const uint32_t app_pll_ctl_reg_val,
|
||||
const uint32_t app_pll_div_reg_val,
|
||||
const uint16_t frac_val_nominal)
|
||||
{
|
||||
// Disable the PLL
|
||||
write_sswitch_reg(tileid, XS1_SSWITCH_SS_APP_PLL_CTL_NUM, (app_pll_ctl_reg_val & 0xF7FFFFFF));
|
||||
// Enable the PLL to invoke a reset on the appPLL.
|
||||
write_sswitch_reg(tileid, XS1_SSWITCH_SS_APP_PLL_CTL_NUM, app_pll_ctl_reg_val);
|
||||
// Must write the CTL register twice so that the F and R divider values are captured using a running clock.
|
||||
write_sswitch_reg(tileid, XS1_SSWITCH_SS_APP_PLL_CTL_NUM, app_pll_ctl_reg_val);
|
||||
// Now disable and re-enable the PLL so we get the full 5us reset time with the correct F and R values.
|
||||
write_sswitch_reg(tileid, XS1_SSWITCH_SS_APP_PLL_CTL_NUM, (app_pll_ctl_reg_val & 0xF7FFFFFF));
|
||||
write_sswitch_reg(tileid, XS1_SSWITCH_SS_APP_PLL_CTL_NUM, app_pll_ctl_reg_val);
|
||||
|
||||
// Wait for PLL to settle.
|
||||
blocking_delay(500 * XS1_TIMER_MHZ);
|
||||
|
||||
// Write the fractional-n register and set to nominal
|
||||
// We set the top bit to enable the frac-n block.
|
||||
write_sswitch_reg(tileid, XS1_SSWITCH_SS_APP_PLL_FRAC_N_DIVIDER_NUM, (0x80000000 | frac_val_nominal));
|
||||
// And then write the clock divider register to enable the output
|
||||
write_sswitch_reg(tileid, XS1_SSWITCH_SS_APP_CLK_DIVIDER_NUM, app_pll_div_reg_val);
|
||||
}
|
||||
|
||||
|
||||
//Found solution: IN 24.000MHz, OUT 49.151786MHz, VCO 3145.71MHz, RD 1, FD 131.071 (m = 1, n = 14), OD 8, FOD 2, ERR -4.36ppm
|
||||
// Measure: 100Hz-40kHz: ~7ps
|
||||
// 100Hz-1MHz: 70ps.
|
||||
// 100Hz high pass: 118ps.
|
||||
#define APP_PLL_CTL_49M 0x0B808200
|
||||
#define APP_PLL_DIV_49M 0x80000001
|
||||
#define APP_PLL_FRAC_49M 0x8000000D
|
||||
|
||||
//Found solution: IN 24.000MHz, OUT 45.157895MHz, VCO 2709.47MHz, RD 1, FD 112.895 (m = 17, n = 19), OD 5, FOD 3, ERR -11.19ppm
|
||||
// Measure: 100Hz-40kHz: 6.5ps
|
||||
// 100Hz-1MHz: 67ps.
|
||||
// 100Hz high pass: 215ps.
|
||||
#define APP_PLL_CTL_45M 0x0A006F00
|
||||
#define APP_PLL_DIV_45M 0x80000002
|
||||
#define APP_PLL_FRAC_45M 0x80001012
|
||||
|
||||
// Found solution: IN 24.000MHz, OUT 24.576000MHz, VCO 2457.60MHz, RD 1, FD 102.400 (m = 2, n = 5), OD 5, FOD 5, ERR 0.0ppm
|
||||
// Measure: 100Hz-40kHz: ~8ps
|
||||
// 100Hz-1MHz: 63ps.
|
||||
// 100Hz high pass: 127ps.
|
||||
#define APP_PLL_CTL_24M 0x0A006500
|
||||
#define APP_PLL_DIV_24M 0x80000004
|
||||
#define APP_PLL_FRAC_24M 0x80000104
|
||||
|
||||
// Found solution: IN 24.000MHz, OUT 22.579186MHz, VCO 3522.35MHz, RD 1, FD 146.765 (m = 13, n = 17), OD 3, FOD 13, ERR -0.641ppm
|
||||
// Measure: 100Hz-40kHz: 7ps
|
||||
// 100Hz-1MHz: 67ps.
|
||||
// 100Hz high pass: 260ps.
|
||||
#define APP_PLL_CTL_22M 0x09009100
|
||||
#define APP_PLL_DIV_22M 0x8000000C
|
||||
#define APP_PLL_FRAC_22M 0x80000C10
|
||||
|
||||
#define APP_PLL_CTL_12M 0x0A006500
|
||||
#define APP_PLL_DIV_12M 0x80000009
|
||||
#define APP_PLL_FRAC_12M 0x80000104
|
||||
|
||||
#define APP_PLL_CTL_11M 0x09009100
|
||||
#define APP_PLL_DIV_11M 0x80000019
|
||||
#define APP_PLL_FRAC_11M 0x80000C10
|
||||
|
||||
// Setup a fixed clock (not phase locked)
|
||||
void sw_pll_fixed_clock(const unsigned frequency)
|
||||
{
|
||||
unsigned ctrl = 0;
|
||||
unsigned div = 0;
|
||||
unsigned frac = 0;
|
||||
|
||||
|
||||
switch(frequency)
|
||||
{
|
||||
case 44100*256:
|
||||
ctrl = APP_PLL_CTL_11M;
|
||||
div = APP_PLL_DIV_11M;
|
||||
frac = APP_PLL_FRAC_11M;
|
||||
break;
|
||||
|
||||
case 48000*256:
|
||||
ctrl = APP_PLL_CTL_12M;
|
||||
div = APP_PLL_DIV_12M;
|
||||
frac = APP_PLL_FRAC_12M;
|
||||
break;
|
||||
|
||||
case 44100*512:
|
||||
ctrl = APP_PLL_CTL_22M;
|
||||
div = APP_PLL_DIV_22M;
|
||||
frac = APP_PLL_FRAC_22M;
|
||||
break;
|
||||
|
||||
case 48000*512:
|
||||
ctrl = APP_PLL_CTL_24M;
|
||||
div = APP_PLL_DIV_24M;
|
||||
frac = APP_PLL_FRAC_24M;
|
||||
break;
|
||||
|
||||
case 44100*1024:
|
||||
ctrl = APP_PLL_CTL_45M;
|
||||
div = APP_PLL_DIV_45M;
|
||||
frac = APP_PLL_FRAC_45M;
|
||||
break;
|
||||
|
||||
case 48000*1024:
|
||||
ctrl = APP_PLL_CTL_49M;
|
||||
div = APP_PLL_DIV_49M;
|
||||
frac = APP_PLL_FRAC_49M;
|
||||
break;
|
||||
|
||||
default:
|
||||
xassert(0);
|
||||
break;
|
||||
}
|
||||
|
||||
sw_pll_app_pll_init(get_local_tile_id(), ctrl, div, frac);
|
||||
}
|
||||
|
||||
|
||||
#endif // __XS3A__
|
||||
123
lib_sw_pll/lib_sw_pll/src/sw_pll_common.h
Normal file
123
lib_sw_pll/lib_sw_pll/src/sw_pll_common.h
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2023-2024 XMOS LIMITED.
|
||||
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The number of consecutive lock positive reports of the control loop before declaring we are finally locked
|
||||
#define SW_PLL_LOCK_COUNT 10
|
||||
|
||||
// Helpers used in this module
|
||||
#define TIMER_TIMEAFTER(A, B) ((int)((B) - (A)) < 0) // Returns non-zero if A is after B, accounting for wrap
|
||||
#define PORT_TIMEAFTER(NOW, EVENT_TIME) ((int16_t)((EVENT_TIME) - (NOW)) < 0) // Returns non-zero if A is after B, accounting for wrap
|
||||
#define MAGNITUDE(A) (A < 0 ? -A : A) // Removes the sign of a value
|
||||
|
||||
typedef int32_t sw_pll_15q16_t; // Type for 15.16 signed fixed point
|
||||
|
||||
#define SW_PLL_NUM_FRAC_BITS 16
|
||||
#define SW_PLL_15Q16(val) ((sw_pll_15q16_t)((float)val * (1 << SW_PLL_NUM_FRAC_BITS)))
|
||||
#define SW_PLL_NUM_LUT_ENTRIES(lut_array) (sizeof(lut_array) / sizeof(lut_array[0]))
|
||||
|
||||
// This is just here to catch an error and provide useful info if you happen to forget to include from XC properly
|
||||
typedef struct xc_check{
|
||||
int *xc_check; // If you see this error, then you need to extern "C"{} the sw_pll include in your XC file.
|
||||
} xc_check;
|
||||
|
||||
typedef enum sw_pll_lock_status_t{
|
||||
SW_PLL_UNLOCKED_LOW = -1,
|
||||
SW_PLL_LOCKED = 0,
|
||||
SW_PLL_UNLOCKED_HIGH = 1
|
||||
} sw_pll_lock_status_t;
|
||||
|
||||
typedef struct sw_pll_pfd_state_t{
|
||||
int16_t mclk_diff; // Raw difference between mclk count and expected mclk count
|
||||
uint16_t ref_clk_pt_last; // Last ref clock value
|
||||
uint32_t ref_clk_expected_inc; // Expected ref clock increment
|
||||
uint64_t ref_clk_scaling_numerator; // Used for a cheap pre-computed divide rather than runtime divide
|
||||
uint16_t mclk_pt_last; // The last mclk port timer count
|
||||
uint32_t mclk_expected_pt_inc; // Expected increment of port timer count
|
||||
uint16_t mclk_max_diff; // Maximum mclk_diff before control loop decides to skip that iteration
|
||||
} sw_pll_pfd_state_t;
|
||||
|
||||
typedef struct sw_pll_pi_state_t{
|
||||
sw_pll_15q16_t Kp; // Proportional constant
|
||||
sw_pll_15q16_t Ki; // Integral constant
|
||||
sw_pll_15q16_t Kii; // Double integral constant
|
||||
int32_t i_windup_limit; // Integral term windup limit
|
||||
int32_t ii_windup_limit; // Double integral term windup limit
|
||||
int32_t error_accum; // Accumulation of the raw mclk_diff term (for I)
|
||||
int32_t error_accum_accum; // Accumulation of the raw mclk_diff term (for II)
|
||||
int32_t iir_y; // Optional IIR low pass filter state
|
||||
} sw_pll_pi_state_t;
|
||||
|
||||
typedef struct sw_pll_lut_state_t{
|
||||
const int16_t * lut_table_base; // Pointer to the base of the fractional look up table
|
||||
size_t num_lut_entries; // Number of LUT entries
|
||||
unsigned nominal_lut_idx; // Initial (mid point normally) LUT index
|
||||
uint16_t current_reg_val; // Last value sent to the register, used by tests
|
||||
} sw_pll_lut_state_t;
|
||||
|
||||
|
||||
typedef struct sw_pll_sdm_state_t{
|
||||
int32_t current_ctrl_val; // The last control value calculated
|
||||
int32_t ctrl_mid_point; // The mid point for the DCO input
|
||||
int32_t ds_x1; // Sigma delta modulator state
|
||||
int32_t ds_x2; // Sigma delta modulator state
|
||||
int32_t ds_x3; // Sigma delta modulator state
|
||||
} sw_pll_sdm_state_t;
|
||||
|
||||
|
||||
typedef struct sw_pll_state_t{
|
||||
|
||||
sw_pll_lock_status_t lock_status; // State showing whether the PLL has locked or is under/over
|
||||
uint8_t lock_counter; // Counter used to determine lock status
|
||||
uint8_t first_loop; // Flag which indicates if the sw_pll is initialising or not
|
||||
unsigned loop_rate_count; // How often the control loop logic runs compared to control call rate
|
||||
unsigned loop_counter; // Intenal loop counter to determine when to do control
|
||||
|
||||
sw_pll_pfd_state_t pfd_state; // Phase Frequency Detector
|
||||
sw_pll_pi_state_t pi_state; // PI(II) controller
|
||||
sw_pll_lut_state_t lut_state; // Look Up Table based DCO
|
||||
sw_pll_sdm_state_t sdm_state; // Sigma Delta Modulator base DCO
|
||||
|
||||
}sw_pll_state_t;
|
||||
|
||||
/**
|
||||
* This is the core PI controller code used by both SDM and LUT SW PLLs.
|
||||
*
|
||||
* \param sw_pll Pointer to the Software PLL state.
|
||||
* \param error The error input to the PI controller.
|
||||
*/
|
||||
__attribute__((always_inline))
|
||||
inline int32_t sw_pll_do_pi_ctrl(sw_pll_state_t * const sw_pll, int16_t error)
|
||||
{
|
||||
sw_pll->pi_state.error_accum += error; // Integral error.
|
||||
sw_pll->pi_state.error_accum = sw_pll->pi_state.error_accum > sw_pll->pi_state.i_windup_limit ? sw_pll->pi_state.i_windup_limit : sw_pll->pi_state.error_accum;
|
||||
sw_pll->pi_state.error_accum = sw_pll->pi_state.error_accum < -sw_pll->pi_state.i_windup_limit ? -sw_pll->pi_state.i_windup_limit : sw_pll->pi_state.error_accum;
|
||||
|
||||
sw_pll->pi_state.error_accum_accum += sw_pll->pi_state.error_accum; // Double integral error.
|
||||
sw_pll->pi_state.error_accum_accum = sw_pll->pi_state.error_accum_accum > sw_pll->pi_state.ii_windup_limit ? sw_pll->pi_state.ii_windup_limit : sw_pll->pi_state.error_accum_accum;
|
||||
sw_pll->pi_state.error_accum_accum = sw_pll->pi_state.error_accum_accum < -sw_pll->pi_state.ii_windup_limit ? -sw_pll->pi_state.ii_windup_limit : sw_pll->pi_state.error_accum_accum;
|
||||
|
||||
// Use long long maths to avoid overflow if ever we had a large error accum term
|
||||
int64_t error_p = ((int64_t)sw_pll->pi_state.Kp * (int64_t)error);
|
||||
int64_t error_i = ((int64_t)sw_pll->pi_state.Ki * (int64_t)sw_pll->pi_state.error_accum);
|
||||
int64_t error_ii = ((int64_t)sw_pll->pi_state.Kii * (int64_t)sw_pll->pi_state.error_accum_accum);
|
||||
|
||||
// Convert back to 32b since we are handling LUTs of around a hundred entries
|
||||
int32_t total_error = (int32_t)((error_p + error_i + error_ii) >> SW_PLL_NUM_FRAC_BITS);
|
||||
|
||||
return total_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the application (secondary) PLL.
|
||||
*
|
||||
* \param tileid The resource ID of the tile that calls this function.
|
||||
* \param app_pll_ctl_reg_val The App PLL control register setting.
|
||||
* \param app_pll_div_reg_val The App PLL divider register setting.
|
||||
* \param frac_val_nominal The App PLL initial fractional register setting.
|
||||
*/ void sw_pll_app_pll_init( const unsigned tileid,
|
||||
const uint32_t app_pll_ctl_reg_val,
|
||||
const uint32_t app_pll_div_reg_val,
|
||||
const uint16_t frac_val_nominal);
|
||||
|
||||
129
lib_sw_pll/lib_sw_pll/src/sw_pll_lut.c
Normal file
129
lib_sw_pll/lib_sw_pll/src/sw_pll_lut.c
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright 2022-2024 XMOS LIMITED.
|
||||
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
#ifdef __XS3A__
|
||||
|
||||
#include "sw_pll.h"
|
||||
|
||||
|
||||
__attribute__((always_inline))
|
||||
static inline uint16_t lookup_pll_frac(sw_pll_state_t * const sw_pll, const int32_t total_error)
|
||||
{
|
||||
const int set = (sw_pll->lut_state.nominal_lut_idx - total_error); //Notice negative term for error
|
||||
unsigned int frac_index = 0;
|
||||
|
||||
if (set < 0)
|
||||
{
|
||||
frac_index = 0;
|
||||
sw_pll->lock_counter = SW_PLL_LOCK_COUNT;
|
||||
sw_pll->lock_status = SW_PLL_UNLOCKED_LOW;
|
||||
}
|
||||
else if (set >= sw_pll->lut_state.num_lut_entries)
|
||||
{
|
||||
frac_index = sw_pll->lut_state.num_lut_entries - 1;
|
||||
sw_pll->lock_counter = SW_PLL_LOCK_COUNT;
|
||||
sw_pll->lock_status = SW_PLL_UNLOCKED_HIGH;
|
||||
}
|
||||
else
|
||||
{
|
||||
frac_index = set;
|
||||
if(sw_pll->lock_counter){
|
||||
sw_pll->lock_counter--;
|
||||
// Keep last unlocked status
|
||||
}
|
||||
else
|
||||
{
|
||||
sw_pll->lock_status = SW_PLL_LOCKED;
|
||||
}
|
||||
}
|
||||
|
||||
return sw_pll->lut_state.lut_table_base[frac_index];
|
||||
}
|
||||
|
||||
void sw_pll_lut_init( sw_pll_state_t * const sw_pll,
|
||||
const sw_pll_15q16_t Kp,
|
||||
const sw_pll_15q16_t Ki,
|
||||
const sw_pll_15q16_t Kii,
|
||||
const size_t loop_rate_count,
|
||||
const size_t pll_ratio,
|
||||
const uint32_t ref_clk_expected_inc,
|
||||
const int16_t * const lut_table_base,
|
||||
const size_t num_lut_entries,
|
||||
const uint32_t app_pll_ctl_reg_val,
|
||||
const uint32_t app_pll_div_reg_val,
|
||||
const unsigned nominal_lut_idx,
|
||||
const unsigned ppm_range)
|
||||
{
|
||||
// Get PLL started and running at nominal
|
||||
sw_pll_app_pll_init(get_local_tile_id(),
|
||||
app_pll_ctl_reg_val,
|
||||
app_pll_div_reg_val,
|
||||
lut_table_base[nominal_lut_idx]);
|
||||
|
||||
// Setup sw_pll with supplied user paramaters
|
||||
sw_pll_lut_reset(sw_pll, Kp, Ki, Kii, num_lut_entries);
|
||||
|
||||
// Setup general controller state
|
||||
sw_pll->lock_status = SW_PLL_UNLOCKED_LOW;
|
||||
sw_pll->lock_counter = SW_PLL_LOCK_COUNT;
|
||||
|
||||
sw_pll->loop_rate_count = loop_rate_count;
|
||||
sw_pll->loop_counter = 0;
|
||||
sw_pll->first_loop = 1;
|
||||
|
||||
sw_pll_reset_pi_state(sw_pll);
|
||||
|
||||
// Setup LUT params
|
||||
sw_pll->lut_state.current_reg_val = app_pll_div_reg_val;
|
||||
sw_pll->lut_state.lut_table_base = lut_table_base;
|
||||
sw_pll->lut_state.num_lut_entries = num_lut_entries;
|
||||
sw_pll->lut_state.nominal_lut_idx = nominal_lut_idx;
|
||||
|
||||
// Setup PFD state
|
||||
sw_pll_pfd_init(&(sw_pll->pfd_state), loop_rate_count, pll_ratio, ref_clk_expected_inc, ppm_range);
|
||||
}
|
||||
|
||||
|
||||
__attribute__((always_inline))
|
||||
inline sw_pll_lock_status_t sw_pll_lut_do_control_from_error(sw_pll_state_t * const sw_pll, int16_t error)
|
||||
{
|
||||
int32_t total_error = sw_pll_do_pi_ctrl(sw_pll, error);
|
||||
sw_pll->lut_state.current_reg_val = lookup_pll_frac(sw_pll, total_error);
|
||||
|
||||
write_sswitch_reg_no_ack(get_local_tile_id(), XS1_SSWITCH_SS_APP_PLL_FRAC_N_DIVIDER_NUM, (0x80000000 | sw_pll->lut_state.current_reg_val));
|
||||
|
||||
return sw_pll->lock_status;
|
||||
}
|
||||
|
||||
sw_pll_lock_status_t sw_pll_lut_do_control(sw_pll_state_t * const sw_pll, const uint16_t mclk_pt, const uint16_t ref_clk_pt)
|
||||
{
|
||||
if (++sw_pll->loop_counter == sw_pll->loop_rate_count)
|
||||
{
|
||||
sw_pll->loop_counter = 0;
|
||||
|
||||
if (sw_pll->first_loop) // First loop around so ensure state is clear
|
||||
{
|
||||
sw_pll->pfd_state.mclk_pt_last = mclk_pt; // load last mclk measurement with sensible data
|
||||
sw_pll_reset_pi_state(sw_pll);
|
||||
sw_pll->lock_counter = SW_PLL_LOCK_COUNT;
|
||||
sw_pll->lock_status = SW_PLL_UNLOCKED_LOW;
|
||||
|
||||
sw_pll->first_loop = 0;
|
||||
|
||||
// Do not set PLL frac as last setting probably the best. At power on we set to nominal (midway in table)
|
||||
}
|
||||
else
|
||||
{
|
||||
sw_pll_calc_error_from_port_timers(&sw_pll->pfd_state, &sw_pll->first_loop, mclk_pt, ref_clk_pt);
|
||||
sw_pll_lut_do_control_from_error(sw_pll, sw_pll->pfd_state.mclk_diff);
|
||||
|
||||
// Save for next iteration to calc diff
|
||||
sw_pll->pfd_state.mclk_pt_last = mclk_pt;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return sw_pll->lock_status;
|
||||
}
|
||||
|
||||
#endif // __XS3A__
|
||||
35
lib_sw_pll/lib_sw_pll/src/sw_pll_pfd.c
Normal file
35
lib_sw_pll/lib_sw_pll/src/sw_pll_pfd.c
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2023-2024 XMOS LIMITED.
|
||||
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
#ifdef __XS3A__
|
||||
|
||||
#include <xcore/assert.h>
|
||||
#include "sw_pll_pfd.h"
|
||||
|
||||
void sw_pll_pfd_init(sw_pll_pfd_state_t *pfd_state,
|
||||
const size_t loop_rate_count,
|
||||
const size_t pll_ratio,
|
||||
const uint32_t ref_clk_expected_inc,
|
||||
const unsigned ppm_range)
|
||||
{
|
||||
pfd_state->mclk_diff = 0;
|
||||
pfd_state->ref_clk_pt_last = 0;
|
||||
pfd_state->ref_clk_expected_inc = ref_clk_expected_inc * loop_rate_count;
|
||||
if(pfd_state->ref_clk_expected_inc) // Avoid div 0 error if ref_clk compensation not used
|
||||
{
|
||||
pfd_state->ref_clk_scaling_numerator = (1ULL << SW_PLL_PFD_PRE_DIV_BITS) / pfd_state->ref_clk_expected_inc + 1; //+1 helps with rounding accuracy
|
||||
}
|
||||
pfd_state->mclk_pt_last = 0;
|
||||
pfd_state->mclk_expected_pt_inc = loop_rate_count * pll_ratio;
|
||||
// Set max PPM deviation before we chose to reset the PLL state. Nominally twice the normal range.
|
||||
pfd_state->mclk_max_diff = (uint64_t)(((uint64_t)ppm_range * 2ULL * (uint64_t)pll_ratio * (uint64_t)loop_rate_count) / 1000000);
|
||||
// Check we can actually support the numbers used in the maths we use
|
||||
const float calc_max = (float)0xffffffffffffffffULL / 1.1; // Add 10% headroom from ULL MAX
|
||||
const float max = (float)pfd_state->ref_clk_expected_inc
|
||||
* (float)pfd_state->ref_clk_scaling_numerator
|
||||
* (float)pfd_state->mclk_expected_pt_inc;
|
||||
// If you have hit this assert then you need to reduce loop_rate_count or possibly the PLL ratio and or MCLK frequency
|
||||
xassert(max < calc_max);
|
||||
}
|
||||
|
||||
#endif // __XS3A__
|
||||
85
lib_sw_pll/lib_sw_pll/src/sw_pll_pfd.h
Normal file
85
lib_sw_pll/lib_sw_pll/src/sw_pll_pfd.h
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2023-2024 XMOS LIMITED.
|
||||
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sw_pll_common.h"
|
||||
|
||||
#pragma once
|
||||
|
||||
#define SW_PLL_PFD_PRE_DIV_BITS 37 // Used pre-computing a divide to save on runtime div usage. Tradeoff between precision and max
|
||||
|
||||
/**
|
||||
* Initialises the Phase Frequency Detector.
|
||||
*
|
||||
* Sets how often the PFD and control loop runs and error limits for resetting
|
||||
*
|
||||
* \param pfd_state Pointer to the struct to be initialised.
|
||||
* \param loop_rate_count How many counts of the call to sw_pll_lut_do_control before control is done.
|
||||
* Note this is only used by sw_pll_lut_do_control. sw_pll_lut_do_control_from_error
|
||||
* calls the control loop every time so this is ignored.
|
||||
* \param pll_ratio Integer ratio between input reference clock and the PLL output.
|
||||
* Only used by sw_pll_lut_do_control for the PFD. Don't care otherwise.
|
||||
* Used to calculate the expected port timer increment when control is called.
|
||||
* \param ref_clk_expected_inc Expected ref clock increment each time sw_pll_lut_do_control is called.
|
||||
* Pass in zero if you are sure the mclk sampling timing is precise. This
|
||||
* will disable the scaling of the mclk count inside sw_pll_lut_do_control.
|
||||
* Only used by sw_pll_lut_do_control. Don't care otherwise.
|
||||
* \param ppm_range The pre-calculated PPM range. Used to determine the maximum deviation
|
||||
* of counted mclk before the PLL resets its state.
|
||||
* Note this is only used by sw_pll_lut_do_control. sw_pll_lut_do_control_from_error
|
||||
* calls the control loop every time so this is ignored.
|
||||
*/
|
||||
void sw_pll_pfd_init(sw_pll_pfd_state_t *pfd_state,
|
||||
const size_t loop_rate_count,
|
||||
const size_t pll_ratio,
|
||||
const uint32_t ref_clk_expected_inc,
|
||||
const unsigned ppm_range);
|
||||
|
||||
/**
|
||||
* Helper to perform the maths needed on the port count, accounting for wrapping, to determine the frequency difference.
|
||||
*
|
||||
* \param pfd_state Pointer to the PFD state.
|
||||
* \param first_loop Pointer to the first_loop var used for resetting when out of range.
|
||||
* \param mclk_pt The clock count from the port counter.
|
||||
* \param ref_clk_pt The clock count from the ref clock counter (optional).
|
||||
*/
|
||||
__attribute__((always_inline))
|
||||
static inline void sw_pll_calc_error_from_port_timers( sw_pll_pfd_state_t * const pfd,
|
||||
uint8_t *first_loop,
|
||||
const uint16_t mclk_pt,
|
||||
const uint16_t ref_clk_pt)
|
||||
{
|
||||
uint16_t mclk_expected_pt = 0;
|
||||
// See if we are using variable loop period sampling, if so, compensate for it by scaling the expected mclk count
|
||||
if(pfd->ref_clk_expected_inc)
|
||||
{
|
||||
uint16_t ref_clk_expected_pt = pfd->ref_clk_pt_last + pfd->ref_clk_expected_inc;
|
||||
// This uses casting trickery to work out the difference between the timer values accounting for wrap at 65536
|
||||
int16_t ref_clk_diff = PORT_TIMEAFTER(ref_clk_pt, ref_clk_expected_pt) ? -(int16_t)(ref_clk_expected_pt - ref_clk_pt) : (int16_t)(ref_clk_pt - ref_clk_expected_pt);
|
||||
pfd->ref_clk_pt_last = ref_clk_pt;
|
||||
|
||||
// This allows for wrapping of the timer when CONTROL_LOOP_COUNT is high
|
||||
// Note we use a pre-computed divide followed by a shift to replace a constant divide with a constant multiply + shift
|
||||
uint32_t mclk_expected_pt_inc = ((uint64_t)pfd->mclk_expected_pt_inc
|
||||
* ((uint64_t)pfd->ref_clk_expected_inc + ref_clk_diff)
|
||||
* pfd->ref_clk_scaling_numerator) >> SW_PLL_PFD_PRE_DIV_BITS;
|
||||
// Below is the line we would use if we do not pre-compute the divide. This can take a long time if we spill over 32b
|
||||
// uint32_t mclk_expected_pt_inc = pfd->mclk_expected_pt_inc * (pfd->ref_clk_expected_inc + ref_clk_diff) / pfd->ref_clk_expected_inc;
|
||||
mclk_expected_pt = pfd->mclk_pt_last + mclk_expected_pt_inc;
|
||||
}
|
||||
else // we are assuming mclk_pt is sampled precisely and needs no compoensation
|
||||
{
|
||||
mclk_expected_pt = pfd->mclk_pt_last + pfd->mclk_expected_pt_inc;
|
||||
}
|
||||
|
||||
// This uses casting trickery to work out the difference between the timer values accounting for wrap at 65536
|
||||
pfd->mclk_diff = PORT_TIMEAFTER(mclk_pt, mclk_expected_pt) ? -(int16_t)(mclk_expected_pt - mclk_pt) : (int16_t)(mclk_pt - mclk_expected_pt);
|
||||
|
||||
// Check to see if something has gone very wrong, for example ref clock stop/start. If so, reset state and keep trying
|
||||
if(MAGNITUDE(pfd->mclk_diff) > pfd->mclk_max_diff)
|
||||
{
|
||||
*first_loop = 1;
|
||||
}
|
||||
}
|
||||
150
lib_sw_pll/lib_sw_pll/src/sw_pll_sdm.c
Normal file
150
lib_sw_pll/lib_sw_pll/src/sw_pll_sdm.c
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright 2022-2024 XMOS LIMITED.
|
||||
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
#ifdef __XS3A__
|
||||
|
||||
#include "sw_pll.h"
|
||||
|
||||
void sw_pll_sdm_controller_init(sw_pll_state_t * const sw_pll,
|
||||
const sw_pll_15q16_t Kp,
|
||||
const sw_pll_15q16_t Ki,
|
||||
const sw_pll_15q16_t Kii,
|
||||
const size_t loop_rate_count,
|
||||
const int32_t ctrl_mid_point)
|
||||
{
|
||||
// Setup sw_pll with supplied user paramaters
|
||||
sw_pll_lut_reset(sw_pll, Kp, Ki, Kii, 0);
|
||||
// override windup limits
|
||||
sw_pll->pi_state.i_windup_limit = SW_PLL_SDM_UPPER_LIMIT - SW_PLL_SDM_LOWER_LIMIT;
|
||||
sw_pll->pi_state.ii_windup_limit = SW_PLL_SDM_UPPER_LIMIT - SW_PLL_SDM_LOWER_LIMIT;
|
||||
sw_pll->sdm_state.ctrl_mid_point = ctrl_mid_point;
|
||||
sw_pll->pi_state.iir_y = 0;
|
||||
sw_pll->sdm_state.current_ctrl_val = ctrl_mid_point;
|
||||
|
||||
sw_pll_reset_pi_state(sw_pll);
|
||||
|
||||
// Setup general controller state
|
||||
sw_pll->lock_status = SW_PLL_UNLOCKED_LOW;
|
||||
sw_pll->lock_counter = SW_PLL_LOCK_COUNT;
|
||||
|
||||
sw_pll->loop_rate_count = loop_rate_count;
|
||||
sw_pll->loop_counter = 0;
|
||||
sw_pll->first_loop = 1;
|
||||
}
|
||||
|
||||
void sw_pll_sdm_init( sw_pll_state_t * const sw_pll,
|
||||
const sw_pll_15q16_t Kp,
|
||||
const sw_pll_15q16_t Ki,
|
||||
const sw_pll_15q16_t Kii,
|
||||
const size_t loop_rate_count,
|
||||
const size_t pll_ratio,
|
||||
const uint32_t ref_clk_expected_inc,
|
||||
const uint32_t app_pll_ctl_reg_val,
|
||||
const uint32_t app_pll_div_reg_val,
|
||||
const uint32_t app_pll_frac_reg_val,
|
||||
const int32_t ctrl_mid_point,
|
||||
const unsigned ppm_range)
|
||||
{
|
||||
// Get PLL started and running at nominal
|
||||
sw_pll_app_pll_init(get_local_tile_id(),
|
||||
app_pll_ctl_reg_val,
|
||||
app_pll_div_reg_val,
|
||||
(uint16_t)(app_pll_frac_reg_val & 0xffff));
|
||||
|
||||
// Setup SDM controller state
|
||||
sw_pll_sdm_controller_init( sw_pll,
|
||||
Kp,
|
||||
Ki,
|
||||
Kii,
|
||||
loop_rate_count,
|
||||
ctrl_mid_point);
|
||||
|
||||
// Setup PFD state
|
||||
sw_pll_pfd_init(&(sw_pll->pfd_state), loop_rate_count, pll_ratio, ref_clk_expected_inc, ppm_range);
|
||||
}
|
||||
|
||||
|
||||
void sw_pll_init_sigma_delta(sw_pll_sdm_state_t *sdm_state){
|
||||
sdm_state->ds_x1 = 0;
|
||||
sdm_state->ds_x2 = 0;
|
||||
sdm_state->ds_x3 = 0;
|
||||
}
|
||||
|
||||
|
||||
__attribute__((always_inline))
|
||||
int32_t sw_pll_sdm_post_control_proc(sw_pll_state_t * const sw_pll, int32_t error)
|
||||
{
|
||||
// Filter some noise into DCO to reduce jitter
|
||||
// First order IIR, make A=0.125
|
||||
// y = y + A(x-y)
|
||||
sw_pll->pi_state.iir_y += ((error - sw_pll->pi_state.iir_y)>>3);
|
||||
|
||||
int32_t dco_ctl = sw_pll->sdm_state.ctrl_mid_point + sw_pll->pi_state.iir_y;
|
||||
|
||||
if(dco_ctl > SW_PLL_SDM_UPPER_LIMIT){
|
||||
dco_ctl = SW_PLL_SDM_UPPER_LIMIT;
|
||||
sw_pll->lock_status = SW_PLL_UNLOCKED_HIGH;
|
||||
sw_pll->lock_counter = SW_PLL_LOCK_COUNT;
|
||||
} else if (dco_ctl < SW_PLL_SDM_LOWER_LIMIT){
|
||||
dco_ctl = SW_PLL_SDM_LOWER_LIMIT;
|
||||
sw_pll->lock_status = SW_PLL_UNLOCKED_LOW;
|
||||
sw_pll->lock_counter = SW_PLL_LOCK_COUNT;
|
||||
} else {
|
||||
if(sw_pll->lock_counter){
|
||||
sw_pll->lock_counter--;
|
||||
} else {
|
||||
sw_pll->lock_status = SW_PLL_LOCKED;
|
||||
}
|
||||
}
|
||||
|
||||
return dco_ctl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
__attribute__((always_inline))
|
||||
inline sw_pll_lock_status_t sw_pll_sdm_do_control_from_error(sw_pll_state_t * const sw_pll, int16_t error)
|
||||
{
|
||||
int32_t ctrl_error = sw_pll_do_pi_ctrl(sw_pll, error);
|
||||
sw_pll->sdm_state.current_ctrl_val = sw_pll_sdm_post_control_proc(sw_pll, ctrl_error);
|
||||
|
||||
return sw_pll->lock_status;
|
||||
}
|
||||
|
||||
|
||||
bool sw_pll_sdm_do_control(sw_pll_state_t * const sw_pll, const uint16_t mclk_pt, const uint16_t ref_clk_pt)
|
||||
{
|
||||
bool control_done = true;
|
||||
|
||||
if (++sw_pll->loop_counter == sw_pll->loop_rate_count)
|
||||
{
|
||||
sw_pll->loop_counter = 0;
|
||||
|
||||
if (sw_pll->first_loop) // First loop around so ensure state is clear
|
||||
{
|
||||
sw_pll->pfd_state.mclk_pt_last = mclk_pt; // load last mclk measurement with sensible data
|
||||
sw_pll->pi_state.iir_y = 0;
|
||||
sw_pll_reset_pi_state(sw_pll);
|
||||
sw_pll->lock_counter = SW_PLL_LOCK_COUNT;
|
||||
sw_pll->lock_status = SW_PLL_UNLOCKED_LOW;
|
||||
|
||||
sw_pll->first_loop = 0;
|
||||
// Do not set current_ctrl_val as last setting probably the best. At power on we set to nominal (midway in settings)
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
sw_pll_calc_error_from_port_timers(&(sw_pll->pfd_state), &(sw_pll->first_loop), mclk_pt, ref_clk_pt);
|
||||
sw_pll_sdm_do_control_from_error(sw_pll, -sw_pll->pfd_state.mclk_diff);
|
||||
|
||||
// Save for next iteration to calc diff
|
||||
sw_pll->pfd_state.mclk_pt_last = mclk_pt;
|
||||
}
|
||||
} else {
|
||||
control_done = false;
|
||||
}
|
||||
|
||||
return control_done;
|
||||
}
|
||||
|
||||
#endif // __XS3A__
|
||||
133
lib_sw_pll/lib_sw_pll/src/sw_pll_sdm.h
Normal file
133
lib_sw_pll/lib_sw_pll/src/sw_pll_sdm.h
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2023-2024 XMOS LIMITED.
|
||||
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
#include "sw_pll.h"
|
||||
|
||||
#pragma once
|
||||
|
||||
#define SW_PLL_SDM_UPPER_LIMIT 980000
|
||||
#define SW_PLL_SDM_LOWER_LIMIT 60000
|
||||
|
||||
typedef int tileref_t;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* sw_pll_sdm_controller_init initialisation function.
|
||||
*
|
||||
* This sets up the PI controller and post processing for the SDM sw_pll. It is provided to allow
|
||||
* cases where the PI controller may be separated from the SDM on a different tile and so we want
|
||||
* to init the SDM without the sigma delta modulator code and physical writes to the app PLL.
|
||||
*
|
||||
* \param sw_pll Pointer to the struct to be initialised.
|
||||
* \param Kp Proportional PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param Ki Integral PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param Kii Double integral PI constant. Use SW_PLL_15Q16() to convert from a float.
|
||||
* \param loop_rate_count How many counts of the call to sw_pll_sdm_do_control before control is done.
|
||||
* \param ctrl_mid_point The nominal control value for the Sigma Delta Modulator output. Normally
|
||||
* close to halfway to allow symmetrical range.
|
||||
*
|
||||
*/
|
||||
void sw_pll_sdm_controller_init(sw_pll_state_t * const sw_pll,
|
||||
const sw_pll_15q16_t Kp,
|
||||
const sw_pll_15q16_t Ki,
|
||||
const sw_pll_15q16_t Kii,
|
||||
const size_t loop_rate_count,
|
||||
const int32_t ctrl_mid_point);
|
||||
|
||||
|
||||
/**
|
||||
* low level sw_pll_calc_sigma_delta function that turns a control signal
|
||||
* into a Sigma Delta Modulated output signal.
|
||||
*
|
||||
*
|
||||
* \param sdm_state Pointer to the SDM state.
|
||||
* \param sdm_in 32b signed input error value. Note limited range.
|
||||
* See SW_PLL_SDM_UPPER_LIMIT and SW_PLL_SDM_LOWER_LIMIT.
|
||||
* \returns Sigma Delta modulated signal.
|
||||
*/
|
||||
__attribute__((always_inline))
|
||||
static inline int32_t sw_pll_calc_sigma_delta(sw_pll_sdm_state_t *sdm_state, int32_t sdm_in){
|
||||
// Third order, 9 level output delta sigma. 20 bit unsigned input.
|
||||
int32_t sdm_out = ((sdm_state->ds_x3<<4) + (sdm_state->ds_x3<<1)) >> 13;
|
||||
if (sdm_out > 8){
|
||||
sdm_out = 8;
|
||||
}
|
||||
if (sdm_out < 0){
|
||||
sdm_out = 0;
|
||||
}
|
||||
sdm_state->ds_x3 += (sdm_state->ds_x2>>5) - (sdm_out<<9) - (sdm_out<<8);
|
||||
sdm_state->ds_x2 += (sdm_state->ds_x1>>5) - (sdm_out<<14);
|
||||
sdm_state->ds_x1 += sdm_in - (sdm_out<<17);
|
||||
|
||||
return sdm_out;
|
||||
}
|
||||
|
||||
/**
|
||||
* low level sw_pll_sdm sw_pll_sdm_out_to_frac_reg function that turns
|
||||
* a sigma delta output signal into a PLL fractional register setting.
|
||||
*
|
||||
* \param sdm_out 32b signed input value.
|
||||
* \returns Fractional register value.
|
||||
*/
|
||||
__attribute__((always_inline))
|
||||
static inline uint32_t sw_pll_sdm_out_to_frac_reg(int32_t sdm_out){
|
||||
// bit 31 is frac enable
|
||||
// bits 15..8 are the f value
|
||||
// bits 7..0 are the p value
|
||||
// Freq - F + (f + 1)/(p + 1)
|
||||
uint32_t frac_val = 0;
|
||||
|
||||
if (sdm_out == 0){
|
||||
frac_val = 0x00000007; // step 0/8
|
||||
}
|
||||
else{
|
||||
frac_val = ((sdm_out - 1) << 8) | 0x80000007; // steps 1/8 to 8/8
|
||||
}
|
||||
|
||||
return frac_val;
|
||||
}
|
||||
|
||||
/**
|
||||
* low level sw_pll_write_frac_reg function that writes the PLL fractional
|
||||
* register.
|
||||
*
|
||||
* NOTE: Attempting to write the PLL fractional register from more than
|
||||
* one logical core at the same time may result in channel lock-up.
|
||||
* Please ensure the that PLL initiaisation has completed before
|
||||
* the SDM task writes to the register. The provided example
|
||||
* implements a method for doing this.
|
||||
*
|
||||
* \param this_tile The ID of the xcore tile that is doing the write.
|
||||
* \param frac_val 32b register value
|
||||
*/
|
||||
__attribute__((always_inline))
|
||||
static inline void sw_pll_write_frac_reg(tileref_t this_tile, uint32_t frac_val){
|
||||
write_sswitch_reg_no_ack(this_tile, XS1_SSWITCH_SS_APP_PLL_FRAC_N_DIVIDER_NUM, frac_val);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs the Sigma Delta Modulation from a control input.
|
||||
* It performs the SDM algorithm, converts the output to a fractional register setting
|
||||
* and then writes the value to the PLL fractional register.
|
||||
* Is typically called in a constant period fast loop and run from a dedicated thread which could be on a remote tile.
|
||||
*
|
||||
* NOTE: Attempting to write the PLL fractional register from more than
|
||||
* one logical core at the same time may result in channel lock-up.
|
||||
* Please ensure the that PLL initiaisation has completed before
|
||||
* the SDM task writes to the register. The provided `simple_sdm` example
|
||||
* implements a method for doing this.
|
||||
*
|
||||
* \param sw_pll Pointer to the SDM state struct.
|
||||
* \param this_tile The ID of the xcore tile that is doing the write.
|
||||
* Use get_local_tile_id() to obtain this.
|
||||
* \param sdm_control_in Current control value.
|
||||
*/
|
||||
__attribute__((always_inline))
|
||||
static inline void sw_pll_do_sigma_delta(sw_pll_sdm_state_t *sdm_state, tileref_t this_tile, int32_t sdm_control_in){
|
||||
|
||||
int32_t sdm_out = sw_pll_calc_sigma_delta(sdm_state, sdm_control_in);
|
||||
uint32_t frac_val = sw_pll_sdm_out_to_frac_reg(sdm_out);
|
||||
sw_pll_write_frac_reg(this_tile, frac_val);
|
||||
}
|
||||
Reference in New Issue
Block a user