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

View File

@@ -0,0 +1,3 @@
python/sw_pll/pll_calc.py
register_setup.h
fractions.h

62
lib_sw_pll/CHANGELOG.rst Normal file
View File

@@ -0,0 +1,62 @@
lib_sw_pll change log
=====================
2.2.0
-----
* FIXED: Enable PLL output after delay to allow it to settle
* FIXED: Fixed frequency settings for 11,289,600Hz
2.1.0
-----
* ADDED: Support for XCommon CMake build system
* ADDED: Reset PI controller state API
* ADDED: Fixed frequency (non phase-locked) clock PLL API
* FIXED: Init resets PI controller state
* FIXED: Now compiles from XC using XCommon
* ADDED: Guard source code with __XS3A__ to allow library inclusion in non-
xcore-ai projects
* CHANGED: Reduce PLL initialisation stabilisation delay from 10 ms to 500 us
* ADDED: Split SDM init function to allow separation across tiles
* FIXED: Use non-ACK write to PLL in Sigma Delta Modulator
2.0.0
-----
* ADDED: Double integral term to controller
* ADDED: Sigma Delta Modulator option for PLL
* CHANGED: Refactored Python model into analogous objects
1.1.0
-----
* ADDED: Function to reset the constants and PI controller state at runtime
* CHANGED: Framework repositories used by the examples
1.0.0
-----
* ADDED: Low-level error input API
* FIXED: Divide by zero exception when not using ref clk compensation
0.3.0
-----
* ADDED: Documentation
* ADDED: Simulator can now generate a modulated test tone to measure jitter
* CHANGED: Updated tools version to 15.2.1
0.2.0
-----
* REMOVED: support for Kii term (speed optimisation)
* CHANGED: used pre-calculated divide to improve cycle usage
* CHANGED: use of const in API
* FIXED: possible overflow where mclk_inc * refclk_inc is > 32b
0.1.0
-----
* ADDED: initial version

27
lib_sw_pll/CMakeLists.txt Normal file
View File

@@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 3.21)
## Disable in-source build.
if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
message(FATAL_ERROR "In-source build is not allowed! Please specify a build folder.\n\tex:cmake -B build")
endif()
## Project declaration
project(lib_sw_pll)
## Enable languages for project
enable_language(CXX C ASM)
## Add library subdirectories
add_subdirectory(lib_sw_pll)
## Add top level project targets
if(PROJECT_IS_TOP_LEVEL)
include(examples/examples.cmake)
add_subdirectory(modules/fwk_core)
add_subdirectory(modules/fwk_io)
add_subdirectory(tests/test_app)
add_subdirectory(tests/test_app_low_level_api)
add_subdirectory(tests/test_app_sdm_dco)
add_subdirectory(tests/test_app_sdm_ctrl)
endif()

84
lib_sw_pll/LICENSE.rst Normal file
View File

@@ -0,0 +1,84 @@
*******************************
XMOS PUBLIC LICENCE: Version 1
*******************************
Subject to the conditions and limitations below, permission is hereby granted by XMOS LIMITED (“XMOS”), free of charge, to any person or entity obtaining a copy of the XMOS Software.
**1. Definitions**
**“Applicable Patent Rights”** means: (a) where XMOS is the grantor of the rights, (i) claims of patents that are now or in future owned by or assigned to XMOS and (ii) that cover subject matter contained in the Software, but only to the extent it is necessary to use, reproduce or distribute the Software without infringement; and (b) where you are the grantor of the rights, (i) claims of patents that are now or in future owned by or assigned to you and (ii) that cover the subject matter contained in your Derivatives, taken alone or in combination with the Software.
**“Compiled Code”** means any compiled, binary, machine readable or executable version of the Source Code.
**“Contributor”** means any person or entity that creates or contributes to the creation of Derivatives.
**“Derivatives”** means any addition to, deletion from and/or change to the substance, structure of the Software, any previous Derivatives, the combination of the Derivatives and the Software and/or any respective portions thereof.
**“Source Code”** means the human readable code that is suitable for making modifications but excluding any Compiled Code.
**“Software”** means the software and associated documentation files which XMOS makes available and which contain a notice identifying the software as original XMOS software and referring to the software being subject to the terms of this XMOS Public Licence.
This Licence refers to XMOS Software and does not relate to any XMOS hardware or devices which are protected by intellectual property rights (including patent and trade marks) which may be sold to you under a separate agreement.
**2. Licence**
**Permitted Uses, Conditions and Restrictions.** Subject to the conditions below, XMOS grants you a worldwide, royalty free, non-exclusive licence, to the extent of any Patent Rights to do the following:
2.1 **Unmodified Software.** You may use, copy, display, publish, distribute and make available unmodified copies of the Software:
2.1.1 for personal or academic, non-commercial purposes; or
2.1.2 for commercial purposes provided the Software is at all times used on a device designed, licensed or developed by XMOS and, provided that in each instance (2.1.1 and 2.1.2):
(a) you must retain and reproduce in all copies of the Software the copyright and proprietary notices and disclaimers of XMOS as they appear in the Software, and keep intact all notices and disclaimers in the Software files that refer to this Licence; and
(b) you must include a copy of this Licence with every copy of the Software and documentation you publish, distribute and make available and you may not offer or impose any terms on such Software that alter or restrict this Licence or the intent of such Licence, except as permitted below (Additional Terms).
The licence above does not include any Compiled Code which XMOS may make available under a separate support and licence agreement.
2.2 **Derivatives.** You may create and modify Derivatives and use, copy, display, publish, distribute and make available Derivatives:
2.2.1 for personal or academic, non-commercial purposes; or
2.2.2 for commercial purposes, provided the Derivatives are at all times used on a device designed, licensed or developed by XMOS and, provided that in each instance (2.2.1 and 2.2.2):
(a) you must comply with the terms of clause 2.1 with respect to the Derivatives;
(b) you must copy (to the extent it doesnt already exist) the notice below in each file of the Derivatives, and ensure all the modified files carry prominent notices stating that you have changed the files and the date of any change; and
(c) if you sublicence, distribute or otherwise make the Software and/or the Derivatives available for commercial purposes, you must provide that the Software and Derivatives are at all times used on a device designed, licensed or developed by XMOS.
Without limitation to these terms and clause 3 below, the Source Code and Compiled Code to your Derivatives may at your discretion (but without obligation) be released, copied, displayed, published, distributed and made available; and if you elect to do so, it must be under the terms of this Licence including the terms of the licence at clauses 2.2.1, 2.2.2 and clause 3 below.
2.3 **Distribution of Executable Versions.** If you distribute or make available Derivatives, you must include a prominent notice in the code itself as well as in all related documentation, stating that the Source Code of the Software from which the Derivatives are based is available under the terms of this Licence, with information on how and where to obtain such Source Code.
**3. Your Grant of Rights.** In consideration and as a condition to this Licence, you grant to any person or entity receiving or distributing any Derivatives, a non-exclusive, royalty free, perpetual, irrevocable license under your Applicable Patent Rights and all other intellectual property rights owned or controlled by you, to use, copy, display, publish, distribute and make available your Derivatives of the same scope and extent as XMOSs licence under clause 2.2 above.
**4. Combined Products.** You may create a combined product by combining Software, Derivatives and other code not covered by this Licence as a single application or product. In such instance, you must comply with the requirements of this Licence for any portion of the Software and/or Derivatives.
**5. Additional Terms.** You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations and/or other rights consistent with the term of this Licence (“Additional Terms”) to any legitimate recipients of the Software and/or Derivatives. The terms on which you provide such Additional Terms are on your sole responsibility and you shall indemnify, defend and hold XMOS harmless against any claims asserted against XMOS.
**6. New Versions.** XMOS may publish revised and/or new versions of this Licence from time to time to accommodate changes to the Licence terms, new versions, updates and bug fixes of the Software. Each version will be given a distinguishing version number. Once Software has been published under a particular version of this Licence, you may continue to use it under the terms of that version. You may also choose to use the latest version of the Software under any subsequent version published by XMOS. Only XMOS shall have the right to modify these terms.
**7. IPR and Ownership**
Any rights, including all intellectual property rights and all trademarks not expressly granted herein are reserved in full by the authors or copyright holders. Any requests for additional permissions by XMOS including any rights to use XMOS trademarks, should be made (without obligation) to XMOS at **support@xmos.com**
Nothing herein shall limit any rights that XMOS is otherwise entitled to under the doctrines of patent exhaustion, implied license, or legal estoppel. Neither the name of the authors, the copyright holders or any contributors may be used to endorse or promote any Derivatives from this Software without specific written permission. Any attempt to deal with the Software which does not comply with this Licence shall be void and shall automatically terminate any rights granted under this licence (including any licence of any intellectual property rights granted herein).
Subject to the licences granted under this Licence any Contributor retains all rights, title and interest in and to any Derivatives made by Contributor subject to the underlying rights of XMOS in the Software. XMOS shall retain all rights, title and interest in the Software and any Derivatives made by XMOS (“XMOS Derivatives”). XMOS Derivatives will not automatically be subject to this Licence and XMOS shall be entitled to licence such rights on any terms (without obligation) as it sees fit.
**8. Termination**
8.1 This Licence will automatically terminate immediately, without notice to you, if:
(a) you fail to comply with the terms of this Licence; and/or
(b) you directly or indirectly commence any action for patent or intellectual property right infringement against XMOS, or any parent, group, affiliate or subsidiary of XMOS; provided XMOS did not first commence an action or patent infringement against you in that instance; and/or
(c) the terms of this Licence are held by any court of competent jurisdiction to be unenforceable in whole or in part.
**9. Critical Applications.** Unless XMOS has agreed in writing with you an agreement specifically governing use of the Goods in military, aerospace, automotive or medically related functions (collectively and individually hereinafter referred to as "Special Use"), any permitted use of the Software excludes Special Use. Notwithstanding any agreement between XMOS and you for Special Use, Special Use shall be at your own risk, and you shall fully indemnify XMOS against any damages, losses, costs and claims (direct and indirect) arising out of any Special Use.
**10. NO WARRANTY OR SUPPORT.** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL XMOS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, WARRANTY, CIVIL TORT (INCLUDING NEGLIGENCE), PRODUCTS LIABILITY OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE INCLUDING GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES EVEN IF SUCH PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND NOT WITHSTANDING THE FAILURE OF ESSENTIAL PURPOSE. IN SOME JURISDICTIONS PARTIES ARE UNABLE TO LIMIT LIABILTY IN THIS WAY, IF THIS APPLIES TO YOUR JURISDICTION THIS LIABILITY CLAUSE ABOVE MAY NOT APPLY. NOTWITHSTANDING THE ABOVE, IN NO EVENT SHALL XMOSs TOTAL LIABILITY TO YOU FOR ALL DAMAGES, LOSS OR OTHERWISE EXCEED $50.
**11. Governing Law and Jurisdiction.** This Licence constitutes the entire agreement between the parties with respect to the subject matter hereof. The Licence shall be governed by the laws of England and the conflict of laws and UN Convention on Contracts for the International Sale of Goods, shall not apply.

92
lib_sw_pll/README.rst Normal file
View File

@@ -0,0 +1,92 @@
lib_sw_pll
==========
:Version: 2.2.0
:Vendor: XMOS
This library contains software that, together with the on-chip application PLL, provides a PLL that will generate a clock that is phase-locked to an input clock.
It supports both Look Up Table (LUT) and Sigma Delta Modulated (SDM) Digitally Controlled Oscillators (DCO), a Phase Frequency Detector (PFD) and
configurable Proportional Integral (PI) controllers which together form a hybrid Software/Hardware Phase Locked Loop (PLL).
Examples are provided showing a master clock locking to a low frequency input reference clock and also to an I2S slave interface.
In addition, an API providing a range of fixed clocks supporting common master clock frequencies between 11.2896 MHz and 49.152 MHz is available
in cases where phase locking is not required.
*********************************
Building and running the examples
*********************************
Ensure a correctly configured installation of the XMOS tools and open an XTC command shell. Please check that the XMOS tools are correctly
sourced by running the following command::
$ xcc
xcc: no input files
.. note::
Instructions for installing and configuring the XMOS tools appear on `the XMOS web site <https://www.xmos.ai/software-tools/>`_.
Clone the lib_sw_pll repository::
git clone git@github.com:xmos/lib_sw_pll.git
cd lib_sw_pll
Place the fwk_core and fwk_io repositories in the modules directory of lib_sw_pll. These are required dependencies for the example apps.
To do so, from the root of lib_sw_pll (where this read me file exists) type::
mkdir modules
cd modules
git clone --recurse-submodules git@github.com:xmos/fwk_core.git
git clone --recurse-submodules git@github.com:xmos/fwk_io.git
cd ..
.. note::
The fwk_core and fwk_io repositories have not been sub-moduled into this Git repository because only the examples depend upon them.
Run the following commands in the lib_sw_pll root folder to build the firmware.
On linux::
cmake -B build -DCMAKE_TOOLCHAIN_FILE=modules/fwk_io/xmos_cmake_toolchain/xs3a.cmake
cd build
make simple_lut simple_sdm i2s_slave_lut
On Windows::
cmake -G "Ninja" -B build -DCMAKE_TOOLCHAIN_FILE=modules/fwk_io/xmos_cmake_toolchain/xs3a.cmake
cd build
ninja simple_lut simple_sdm i2s_slave_lut
To run the firmware, first connect LRCLK and BCLK (connects the test clock output to the PLL reference input)
and run the following command where <my_example> can be *simple_lut* or *simple_sdm* which use the XCORE-AI-EXPLORER board
or *i2s_slave_lut* which uses the XK-VOICE-SQ66 board::
xrun --xscope <my_example>.xe
For simple_xxx.xe, to see the PLL lock, put one scope probe on either LRCLK/BCLK (reference input) and the other on PORT_I2S_DAC_DATA to see the
recovered clock which has been hardware divided back down to the same rate as the input reference clock.
For i2s_slave_lut.xe you will need to connect a 48kHz I2S master to the LRCLK, BCLK pins. You may then observe the I2S input data being
looped back to the output and the MCLK being generated. A divided version of MCLK is output on PORT_I2S_DATA2 which allows
direct comparison of the input reference (LRCLK) with the recovered clock at the same, and locked, frequency.
*********************************
Generating new PLL configurations
*********************************
Please see `doc/rst/sw_pll.rst` for further details on how to design and build new sw_pll configurations. This covers the tradeoff between lock range,
oscillator noise and resource usage.
Required Software (dependencies)
================================
* None
Documentation
=============
You can find the documentation for this software in the /doc directory of the package.
Support
=======
This package is supported by XMOS Ltd. Issues can be raised against the software at: http://www.xmos.com/support

View File

@@ -0,0 +1,3 @@
include(${CMAKE_CURRENT_LIST_DIR}/simple_lut/simple_lut.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/simple_sdm/simple_sdm.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/i2s_slave_lut/i2s_slave_lut.cmake)

View File

@@ -0,0 +1,41 @@
#**********************
# Gather Sources
#**********************
file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.c ${CMAKE_CURRENT_LIST_DIR}/src/*.xc)
set(APP_INCLUDES
${CMAKE_CURRENT_LIST_DIR}/src
)
#**********************
# Flags
#**********************
set(APP_COMPILER_FLAGS
-Os
-g
-report
-fxscope
-mcmodel=large
-Wno-xcore-fptrgroup
${CMAKE_CURRENT_LIST_DIR}/src/config.xscope
${CMAKE_CURRENT_LIST_DIR}/src/xvf3800_qf60.xn
)
set(APP_COMPILE_DEFINITIONS
)
set(APP_LINK_OPTIONS
-report
${CMAKE_CURRENT_LIST_DIR}/src/config.xscope
${CMAKE_CURRENT_LIST_DIR}/src/xvf3800_qf60.xn
)
#**********************
# Tile Targets
#**********************
add_executable(i2s_slave_lut)
target_sources(i2s_slave_lut PUBLIC ${APP_SOURCES})
target_include_directories(i2s_slave_lut PUBLIC ${APP_INCLUDES})
target_compile_definitions(i2s_slave_lut PRIVATE ${APP_COMPILE_DEFINITIONS})
target_compile_options(i2s_slave_lut PRIVATE ${APP_COMPILER_FLAGS})
target_link_options(i2s_slave_lut PRIVATE ${APP_LINK_OPTIONS})
target_link_libraries(i2s_slave_lut PUBLIC lib_sw_pll lib_i2s)

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ======================================================= -->
<!-- The 'ioMode' attribute on the xSCOPEconfig -->
<!-- element can take the following values: -->
<!-- "none", "basic", "timed" -->
<!-- -->
<!-- The 'type' attribute on Probe -->
<!-- elements can take the following values: -->
<!-- "STARTSTOP", "CONTINUOUS", "DISCRETE", "STATEMACHINE" -->
<!-- -->
<!-- The 'datatype' attribute on Probe -->
<!-- elements can take the following values: -->
<!-- "NONE", "UINT", "INT", "FLOAT" -->
<!-- ======================================================= -->
<xSCOPEconfig ioMode="basic" enabled="true">
<!-- For example: -->
<!-- <Probe name="I2S_RX_0" type="CONTINUOUS" datatype="INT" units="Value" enabled="true"/> -->
<!-- From the target code, call: xscope_int(PROBE_NAME, value); -->
</xSCOPEconfig>

View File

@@ -0,0 +1,417 @@
// Header file listing fraction options searched
// These values to go in the bottom 16 bits of the secondary PLL fractional-n divider register.
short frac_values_80[413] = {
0x0F16, // Index: 0 Fraction: 16/23 = 0.6957
0x364E, // Index: 1 Fraction: 55/79 = 0.6962
0x2637, // Index: 2 Fraction: 39/56 = 0.6964
0x1620, // Index: 3 Fraction: 23/33 = 0.6970
0x344B, // Index: 4 Fraction: 53/76 = 0.6974
0x1D2A, // Index: 5 Fraction: 30/43 = 0.6977
0x2434, // Index: 6 Fraction: 37/53 = 0.6981
0x2B3E, // Index: 7 Fraction: 44/63 = 0.6984
0x3248, // Index: 8 Fraction: 51/73 = 0.6986
0x0609, // Index: 9 Fraction: 7/10 = 0.7000
0x354C, // Index: 10 Fraction: 54/77 = 0.7013
0x2E42, // Index: 11 Fraction: 47/67 = 0.7015
0x2738, // Index: 12 Fraction: 40/57 = 0.7018
0x202E, // Index: 13 Fraction: 33/47 = 0.7021
0x1924, // Index: 14 Fraction: 26/37 = 0.7027
0x2C3F, // Index: 15 Fraction: 45/64 = 0.7031
0x121A, // Index: 16 Fraction: 19/27 = 0.7037
0x3146, // Index: 17 Fraction: 50/71 = 0.7042
0x1E2B, // Index: 18 Fraction: 31/44 = 0.7045
0x2A3C, // Index: 19 Fraction: 43/61 = 0.7049
0x364D, // Index: 20 Fraction: 55/78 = 0.7051
0x0B10, // Index: 21 Fraction: 12/17 = 0.7059
0x344A, // Index: 22 Fraction: 53/75 = 0.7067
0x2839, // Index: 23 Fraction: 41/58 = 0.7069
0x1C28, // Index: 24 Fraction: 29/41 = 0.7073
0x2D40, // Index: 25 Fraction: 46/65 = 0.7077
0x1017, // Index: 26 Fraction: 17/24 = 0.7083
0x374E, // Index: 27 Fraction: 56/79 = 0.7089
0x2636, // Index: 28 Fraction: 39/55 = 0.7091
0x151E, // Index: 29 Fraction: 22/31 = 0.7097
0x3044, // Index: 30 Fraction: 49/69 = 0.7101
0x1A25, // Index: 31 Fraction: 27/38 = 0.7105
0x1F2C, // Index: 32 Fraction: 32/45 = 0.7111
0x2433, // Index: 33 Fraction: 37/52 = 0.7115
0x293A, // Index: 34 Fraction: 42/59 = 0.7119
0x2E41, // Index: 35 Fraction: 47/66 = 0.7121
0x3348, // Index: 36 Fraction: 52/73 = 0.7123
0x384F, // Index: 37 Fraction: 57/80 = 0.7125
0x0406, // Index: 38 Fraction: 5/7 = 0.7143
0x3449, // Index: 39 Fraction: 53/74 = 0.7162
0x2F42, // Index: 40 Fraction: 48/67 = 0.7164
0x2A3B, // Index: 41 Fraction: 43/60 = 0.7167
0x2534, // Index: 42 Fraction: 38/53 = 0.7170
0x202D, // Index: 43 Fraction: 33/46 = 0.7174
0x1B26, // Index: 44 Fraction: 28/39 = 0.7179
0x3246, // Index: 45 Fraction: 51/71 = 0.7183
0x161F, // Index: 46 Fraction: 23/32 = 0.7188
0x2838, // Index: 47 Fraction: 41/57 = 0.7193
0x1118, // Index: 48 Fraction: 18/25 = 0.7200
0x3043, // Index: 49 Fraction: 49/68 = 0.7206
0x1E2A, // Index: 50 Fraction: 31/43 = 0.7209
0x2B3C, // Index: 51 Fraction: 44/61 = 0.7213
0x384E, // Index: 52 Fraction: 57/79 = 0.7215
0x0C11, // Index: 53 Fraction: 13/18 = 0.7222
0x2E40, // Index: 54 Fraction: 47/65 = 0.7231
0x212E, // Index: 55 Fraction: 34/47 = 0.7234
0x364B, // Index: 56 Fraction: 55/76 = 0.7237
0x141C, // Index: 57 Fraction: 21/29 = 0.7241
0x3144, // Index: 58 Fraction: 50/69 = 0.7246
0x1C27, // Index: 59 Fraction: 29/40 = 0.7250
0x2432, // Index: 60 Fraction: 37/51 = 0.7255
0x2C3D, // Index: 61 Fraction: 45/62 = 0.7258
0x3448, // Index: 62 Fraction: 53/73 = 0.7260
0x070A, // Index: 63 Fraction: 8/11 = 0.7273
0x3245, // Index: 64 Fraction: 51/70 = 0.7286
0x2A3A, // Index: 65 Fraction: 43/59 = 0.7288
0x222F, // Index: 66 Fraction: 35/48 = 0.7292
0x1A24, // Index: 67 Fraction: 27/37 = 0.7297
0x2D3E, // Index: 68 Fraction: 46/63 = 0.7302
0x1219, // Index: 69 Fraction: 19/26 = 0.7308
0x3042, // Index: 70 Fraction: 49/67 = 0.7313
0x1D28, // Index: 71 Fraction: 30/41 = 0.7317
0x2837, // Index: 72 Fraction: 41/56 = 0.7321
0x3346, // Index: 73 Fraction: 52/71 = 0.7324
0x0A0E, // Index: 74 Fraction: 11/15 = 0.7333
0x394E, // Index: 75 Fraction: 58/79 = 0.7342
0x2E3F, // Index: 76 Fraction: 47/64 = 0.7344
0x2330, // Index: 77 Fraction: 36/49 = 0.7347
0x1821, // Index: 78 Fraction: 25/34 = 0.7353
0x2634, // Index: 79 Fraction: 39/53 = 0.7358
0x3447, // Index: 80 Fraction: 53/72 = 0.7361
0x0D12, // Index: 81 Fraction: 14/19 = 0.7368
0x3A4F, // Index: 82 Fraction: 59/80 = 0.7375
0x2C3C, // Index: 83 Fraction: 45/61 = 0.7377
0x1E29, // Index: 84 Fraction: 31/42 = 0.7381
0x2F40, // Index: 85 Fraction: 48/65 = 0.7385
0x1016, // Index: 86 Fraction: 17/23 = 0.7391
0x3548, // Index: 87 Fraction: 54/73 = 0.7397
0x2431, // Index: 88 Fraction: 37/50 = 0.7400
0x384C, // Index: 89 Fraction: 57/77 = 0.7403
0x131A, // Index: 90 Fraction: 20/27 = 0.7407
0x2A39, // Index: 91 Fraction: 43/58 = 0.7414
0x161E, // Index: 92 Fraction: 23/31 = 0.7419
0x3041, // Index: 93 Fraction: 49/66 = 0.7424
0x1922, // Index: 94 Fraction: 26/35 = 0.7429
0x3649, // Index: 95 Fraction: 55/74 = 0.7432
0x1C26, // Index: 96 Fraction: 29/39 = 0.7436
0x1F2A, // Index: 97 Fraction: 32/43 = 0.7442
0x222E, // Index: 98 Fraction: 35/47 = 0.7447
0x2532, // Index: 99 Fraction: 38/51 = 0.7451
0x2836, // Index: 100 Fraction: 41/55 = 0.7455
0x2B3A, // Index: 101 Fraction: 44/59 = 0.7458
0x2E3E, // Index: 102 Fraction: 47/63 = 0.7460
0x3142, // Index: 103 Fraction: 50/67 = 0.7463
0x3446, // Index: 104 Fraction: 53/71 = 0.7465
0x374A, // Index: 105 Fraction: 56/75 = 0.7467
0x3A4E, // Index: 106 Fraction: 59/79 = 0.7468
0x0203, // Index: 107 Fraction: 3/4 = 0.7500
0x394C, // Index: 108 Fraction: 58/77 = 0.7532
0x3648, // Index: 109 Fraction: 55/73 = 0.7534
0x3344, // Index: 110 Fraction: 52/69 = 0.7536
0x3040, // Index: 111 Fraction: 49/65 = 0.7538
0x2D3C, // Index: 112 Fraction: 46/61 = 0.7541
0x2A38, // Index: 113 Fraction: 43/57 = 0.7544
0x2734, // Index: 114 Fraction: 40/53 = 0.7547
0x2430, // Index: 115 Fraction: 37/49 = 0.7551
0x212C, // Index: 116 Fraction: 34/45 = 0.7556
0x1E28, // Index: 117 Fraction: 31/41 = 0.7561
0x3A4D, // Index: 118 Fraction: 59/78 = 0.7564
0x1B24, // Index: 119 Fraction: 28/37 = 0.7568
0x3445, // Index: 120 Fraction: 53/70 = 0.7571
0x1820, // Index: 121 Fraction: 25/33 = 0.7576
0x2E3D, // Index: 122 Fraction: 47/62 = 0.7581
0x151C, // Index: 123 Fraction: 22/29 = 0.7586
0x2835, // Index: 124 Fraction: 41/54 = 0.7593
0x3B4E, // Index: 125 Fraction: 60/79 = 0.7595
0x1218, // Index: 126 Fraction: 19/25 = 0.7600
0x3546, // Index: 127 Fraction: 54/71 = 0.7606
0x222D, // Index: 128 Fraction: 35/46 = 0.7609
0x3242, // Index: 129 Fraction: 51/67 = 0.7612
0x0F14, // Index: 130 Fraction: 16/21 = 0.7619
0x3C4F, // Index: 131 Fraction: 61/80 = 0.7625
0x2C3A, // Index: 132 Fraction: 45/59 = 0.7627
0x1C25, // Index: 133 Fraction: 29/38 = 0.7632
0x2936, // Index: 134 Fraction: 42/55 = 0.7636
0x3647, // Index: 135 Fraction: 55/72 = 0.7639
0x0C10, // Index: 136 Fraction: 13/17 = 0.7647
0x303F, // Index: 137 Fraction: 49/64 = 0.7656
0x232E, // Index: 138 Fraction: 36/47 = 0.7660
0x3A4C, // Index: 139 Fraction: 59/77 = 0.7662
0x161D, // Index: 140 Fraction: 23/30 = 0.7667
0x3748, // Index: 141 Fraction: 56/73 = 0.7671
0x202A, // Index: 142 Fraction: 33/43 = 0.7674
0x2A37, // Index: 143 Fraction: 43/56 = 0.7679
0x3444, // Index: 144 Fraction: 53/69 = 0.7681
0x090C, // Index: 145 Fraction: 10/13 = 0.7692
0x3849, // Index: 146 Fraction: 57/74 = 0.7703
0x2E3C, // Index: 147 Fraction: 47/61 = 0.7705
0x242F, // Index: 148 Fraction: 37/48 = 0.7708
0x1A22, // Index: 149 Fraction: 27/35 = 0.7714
0x2B38, // Index: 150 Fraction: 44/57 = 0.7719
0x3C4E, // Index: 151 Fraction: 61/79 = 0.7722
0x1015, // Index: 152 Fraction: 17/22 = 0.7727
0x394A, // Index: 153 Fraction: 58/75 = 0.7733
0x2834, // Index: 154 Fraction: 41/53 = 0.7736
0x171E, // Index: 155 Fraction: 24/31 = 0.7742
0x3646, // Index: 156 Fraction: 55/71 = 0.7746
0x1E27, // Index: 157 Fraction: 31/40 = 0.7750
0x2530, // Index: 158 Fraction: 38/49 = 0.7755
0x2C39, // Index: 159 Fraction: 45/58 = 0.7759
0x3342, // Index: 160 Fraction: 52/67 = 0.7761
0x3A4B, // Index: 161 Fraction: 59/76 = 0.7763
0x0608, // Index: 162 Fraction: 7/9 = 0.7778
0x3B4C, // Index: 163 Fraction: 60/77 = 0.7792
0x3443, // Index: 164 Fraction: 53/68 = 0.7794
0x2D3A, // Index: 165 Fraction: 46/59 = 0.7797
0x2631, // Index: 166 Fraction: 39/50 = 0.7800
0x1F28, // Index: 167 Fraction: 32/41 = 0.7805
0x3848, // Index: 168 Fraction: 57/73 = 0.7808
0x181F, // Index: 169 Fraction: 25/32 = 0.7812
0x2A36, // Index: 170 Fraction: 43/55 = 0.7818
0x3C4D, // Index: 171 Fraction: 61/78 = 0.7821
0x1116, // Index: 172 Fraction: 18/23 = 0.7826
0x2E3B, // Index: 173 Fraction: 47/60 = 0.7833
0x1C24, // Index: 174 Fraction: 29/37 = 0.7838
0x2732, // Index: 175 Fraction: 40/51 = 0.7843
0x3240, // Index: 176 Fraction: 51/65 = 0.7846
0x3D4E, // Index: 177 Fraction: 62/79 = 0.7848
0x0A0D, // Index: 178 Fraction: 11/14 = 0.7857
0x3A4A, // Index: 179 Fraction: 59/75 = 0.7867
0x2F3C, // Index: 180 Fraction: 48/61 = 0.7869
0x242E, // Index: 181 Fraction: 37/47 = 0.7872
0x3E4F, // Index: 182 Fraction: 63/80 = 0.7875
0x1920, // Index: 183 Fraction: 26/33 = 0.7879
0x2833, // Index: 184 Fraction: 41/52 = 0.7885
0x3746, // Index: 185 Fraction: 56/71 = 0.7887
0x0E12, // Index: 186 Fraction: 15/19 = 0.7895
0x303D, // Index: 187 Fraction: 49/62 = 0.7903
0x212A, // Index: 188 Fraction: 34/43 = 0.7907
0x3442, // Index: 189 Fraction: 53/67 = 0.7910
0x1217, // Index: 190 Fraction: 19/24 = 0.7917
0x3C4C, // Index: 191 Fraction: 61/77 = 0.7922
0x2934, // Index: 192 Fraction: 42/53 = 0.7925
0x161C, // Index: 193 Fraction: 23/29 = 0.7931
0x313E, // Index: 194 Fraction: 50/63 = 0.7937
0x1A21, // Index: 195 Fraction: 27/34 = 0.7941
0x3948, // Index: 196 Fraction: 58/73 = 0.7945
0x1E26, // Index: 197 Fraction: 31/39 = 0.7949
0x222B, // Index: 198 Fraction: 35/44 = 0.7955
0x2630, // Index: 199 Fraction: 39/49 = 0.7959
0x2A35, // Index: 200 Fraction: 43/54 = 0.7963
0x2E3A, // Index: 201 Fraction: 47/59 = 0.7966
0x323F, // Index: 202 Fraction: 51/64 = 0.7969
0x3644, // Index: 203 Fraction: 55/69 = 0.7971
0x3A49, // Index: 204 Fraction: 59/74 = 0.7973
0x3E4E, // Index: 205 Fraction: 63/79 = 0.7975
0x0304, // Index: 206 Fraction: 4/5 = 0.8000
0x3C4B, // Index: 207 Fraction: 61/76 = 0.8026
0x3846, // Index: 208 Fraction: 57/71 = 0.8028
0x3441, // Index: 209 Fraction: 53/66 = 0.8030
0x303C, // Index: 210 Fraction: 49/61 = 0.8033
0x2C37, // Index: 211 Fraction: 45/56 = 0.8036
0x2832, // Index: 212 Fraction: 41/51 = 0.8039
0x242D, // Index: 213 Fraction: 37/46 = 0.8043
0x2028, // Index: 214 Fraction: 33/41 = 0.8049
0x3D4C, // Index: 215 Fraction: 62/77 = 0.8052
0x1C23, // Index: 216 Fraction: 29/36 = 0.8056
0x3542, // Index: 217 Fraction: 54/67 = 0.8060
0x181E, // Index: 218 Fraction: 25/31 = 0.8065
0x2D38, // Index: 219 Fraction: 46/57 = 0.8070
0x1419, // Index: 220 Fraction: 21/26 = 0.8077
0x3A48, // Index: 221 Fraction: 59/73 = 0.8082
0x252E, // Index: 222 Fraction: 38/47 = 0.8085
0x3643, // Index: 223 Fraction: 55/68 = 0.8088
0x1014, // Index: 224 Fraction: 17/21 = 0.8095
0x3F4E, // Index: 225 Fraction: 64/79 = 0.8101
0x2E39, // Index: 226 Fraction: 47/58 = 0.8103
0x1D24, // Index: 227 Fraction: 30/37 = 0.8108
0x2A34, // Index: 228 Fraction: 43/53 = 0.8113
0x3744, // Index: 229 Fraction: 56/69 = 0.8116
0x0C0F, // Index: 230 Fraction: 13/16 = 0.8125
0x3C4A, // Index: 231 Fraction: 61/75 = 0.8133
0x2F3A, // Index: 232 Fraction: 48/59 = 0.8136
0x222A, // Index: 233 Fraction: 35/43 = 0.8140
0x3845, // Index: 234 Fraction: 57/70 = 0.8143
0x151A, // Index: 235 Fraction: 22/27 = 0.8148
0x3440, // Index: 236 Fraction: 53/65 = 0.8154
0x1E25, // Index: 237 Fraction: 31/38 = 0.8158
0x2730, // Index: 238 Fraction: 40/49 = 0.8163
0x303B, // Index: 239 Fraction: 49/60 = 0.8167
0x3946, // Index: 240 Fraction: 58/71 = 0.8169
0x080A, // Index: 241 Fraction: 9/11 = 0.8182
0x3A47, // Index: 242 Fraction: 59/72 = 0.8194
0x313C, // Index: 243 Fraction: 50/61 = 0.8197
0x2831, // Index: 244 Fraction: 41/50 = 0.8200
0x1F26, // Index: 245 Fraction: 32/39 = 0.8205
0x3642, // Index: 246 Fraction: 55/67 = 0.8209
0x161B, // Index: 247 Fraction: 23/28 = 0.8214
0x3B48, // Index: 248 Fraction: 60/73 = 0.8219
0x242C, // Index: 249 Fraction: 37/45 = 0.8222
0x323D, // Index: 250 Fraction: 51/62 = 0.8226
0x404E, // Index: 251 Fraction: 65/79 = 0.8228
0x0D10, // Index: 252 Fraction: 14/17 = 0.8235
0x3C49, // Index: 253 Fraction: 61/74 = 0.8243
0x2E38, // Index: 254 Fraction: 47/57 = 0.8246
0x2027, // Index: 255 Fraction: 33/40 = 0.8250
0x333E, // Index: 256 Fraction: 52/63 = 0.8254
0x1216, // Index: 257 Fraction: 19/23 = 0.8261
0x3D4A, // Index: 258 Fraction: 62/75 = 0.8267
0x2A33, // Index: 259 Fraction: 43/52 = 0.8269
0x171C, // Index: 260 Fraction: 24/29 = 0.8276
0x343F, // Index: 261 Fraction: 53/64 = 0.8281
0x1C22, // Index: 262 Fraction: 29/35 = 0.8286
0x3E4B, // Index: 263 Fraction: 63/76 = 0.8289
0x2128, // Index: 264 Fraction: 34/41 = 0.8293
0x262E, // Index: 265 Fraction: 39/47 = 0.8298
0x2B34, // Index: 266 Fraction: 44/53 = 0.8302
0x303A, // Index: 267 Fraction: 49/59 = 0.8305
0x3540, // Index: 268 Fraction: 54/65 = 0.8308
0x3A46, // Index: 269 Fraction: 59/71 = 0.8310
0x3F4C, // Index: 270 Fraction: 64/77 = 0.8312
0x0405, // Index: 271 Fraction: 5/6 = 0.8333
0x414E, // Index: 272 Fraction: 66/79 = 0.8354
0x3C48, // Index: 273 Fraction: 61/73 = 0.8356
0x3742, // Index: 274 Fraction: 56/67 = 0.8358
0x323C, // Index: 275 Fraction: 51/61 = 0.8361
0x2D36, // Index: 276 Fraction: 46/55 = 0.8364
0x2830, // Index: 277 Fraction: 41/49 = 0.8367
0x232A, // Index: 278 Fraction: 36/43 = 0.8372
0x424F, // Index: 279 Fraction: 67/80 = 0.8375
0x1E24, // Index: 280 Fraction: 31/37 = 0.8378
0x3843, // Index: 281 Fraction: 57/68 = 0.8382
0x191E, // Index: 282 Fraction: 26/31 = 0.8387
0x2E37, // Index: 283 Fraction: 47/56 = 0.8393
0x1418, // Index: 284 Fraction: 21/25 = 0.8400
0x3944, // Index: 285 Fraction: 58/69 = 0.8406
0x242B, // Index: 286 Fraction: 37/44 = 0.8409
0x343E, // Index: 287 Fraction: 53/63 = 0.8413
0x0F12, // Index: 288 Fraction: 16/19 = 0.8421
0x3A45, // Index: 289 Fraction: 59/70 = 0.8429
0x2A32, // Index: 290 Fraction: 43/51 = 0.8431
0x1A1F, // Index: 291 Fraction: 27/32 = 0.8438
0x404C, // Index: 292 Fraction: 65/77 = 0.8442
0x252C, // Index: 293 Fraction: 38/45 = 0.8444
0x3039, // Index: 294 Fraction: 49/58 = 0.8448
0x3B46, // Index: 295 Fraction: 60/71 = 0.8451
0x0A0C, // Index: 296 Fraction: 11/13 = 0.8462
0x3C47, // Index: 297 Fraction: 61/72 = 0.8472
0x313A, // Index: 298 Fraction: 50/59 = 0.8475
0x262D, // Index: 299 Fraction: 39/46 = 0.8478
0x424E, // Index: 300 Fraction: 67/79 = 0.8481
0x1B20, // Index: 301 Fraction: 28/33 = 0.8485
0x2C34, // Index: 302 Fraction: 45/53 = 0.8491
0x3D48, // Index: 303 Fraction: 62/73 = 0.8493
0x1013, // Index: 304 Fraction: 17/20 = 0.8500
0x3842, // Index: 305 Fraction: 57/67 = 0.8507
0x272E, // Index: 306 Fraction: 40/47 = 0.8511
0x3E49, // Index: 307 Fraction: 63/74 = 0.8514
0x161A, // Index: 308 Fraction: 23/27 = 0.8519
0x333C, // Index: 309 Fraction: 52/61 = 0.8525
0x1C21, // Index: 310 Fraction: 29/34 = 0.8529
0x3F4A, // Index: 311 Fraction: 64/75 = 0.8533
0x2228, // Index: 312 Fraction: 35/41 = 0.8537
0x282F, // Index: 313 Fraction: 41/48 = 0.8542
0x2E36, // Index: 314 Fraction: 47/55 = 0.8545
0x343D, // Index: 315 Fraction: 53/62 = 0.8548
0x3A44, // Index: 316 Fraction: 59/69 = 0.8551
0x404B, // Index: 317 Fraction: 65/76 = 0.8553
0x0506, // Index: 318 Fraction: 6/7 = 0.8571
0x424D, // Index: 319 Fraction: 67/78 = 0.8590
0x3C46, // Index: 320 Fraction: 61/71 = 0.8592
0x363F, // Index: 321 Fraction: 55/64 = 0.8594
0x3038, // Index: 322 Fraction: 49/57 = 0.8596
0x2A31, // Index: 323 Fraction: 43/50 = 0.8600
0x242A, // Index: 324 Fraction: 37/43 = 0.8605
0x434E, // Index: 325 Fraction: 68/79 = 0.8608
0x1E23, // Index: 326 Fraction: 31/36 = 0.8611
0x3740, // Index: 327 Fraction: 56/65 = 0.8615
0x181C, // Index: 328 Fraction: 25/29 = 0.8621
0x444F, // Index: 329 Fraction: 69/80 = 0.8625
0x2B32, // Index: 330 Fraction: 44/51 = 0.8627
0x3E48, // Index: 331 Fraction: 63/73 = 0.8630
0x1215, // Index: 332 Fraction: 19/22 = 0.8636
0x323A, // Index: 333 Fraction: 51/59 = 0.8644
0x1F24, // Index: 334 Fraction: 32/37 = 0.8649
0x2C33, // Index: 335 Fraction: 45/52 = 0.8654
0x3942, // Index: 336 Fraction: 58/67 = 0.8657
0x0C0E, // Index: 337 Fraction: 13/15 = 0.8667
0x3A43, // Index: 338 Fraction: 59/68 = 0.8676
0x2D34, // Index: 339 Fraction: 46/53 = 0.8679
0x2025, // Index: 340 Fraction: 33/38 = 0.8684
0x343C, // Index: 341 Fraction: 53/61 = 0.8689
0x1316, // Index: 342 Fraction: 20/23 = 0.8696
0x424C, // Index: 343 Fraction: 67/77 = 0.8701
0x2E35, // Index: 344 Fraction: 47/54 = 0.8704
0x1A1E, // Index: 345 Fraction: 27/31 = 0.8710
0x3C45, // Index: 346 Fraction: 61/70 = 0.8714
0x2126, // Index: 347 Fraction: 34/39 = 0.8718
0x282E, // Index: 348 Fraction: 41/47 = 0.8723
0x2F36, // Index: 349 Fraction: 48/55 = 0.8727
0x363E, // Index: 350 Fraction: 55/63 = 0.8730
0x3D46, // Index: 351 Fraction: 62/71 = 0.8732
0x444E, // Index: 352 Fraction: 69/79 = 0.8734
0x0607, // Index: 353 Fraction: 7/8 = 0.8750
0x3F48, // Index: 354 Fraction: 64/73 = 0.8767
0x3840, // Index: 355 Fraction: 57/65 = 0.8769
0x3138, // Index: 356 Fraction: 50/57 = 0.8772
0x2A30, // Index: 357 Fraction: 43/49 = 0.8776
0x2328, // Index: 358 Fraction: 36/41 = 0.8780
0x4049, // Index: 359 Fraction: 65/74 = 0.8784
0x1C20, // Index: 360 Fraction: 29/33 = 0.8788
0x3239, // Index: 361 Fraction: 51/58 = 0.8793
0x1518, // Index: 362 Fraction: 22/25 = 0.8800
0x3A42, // Index: 363 Fraction: 59/67 = 0.8806
0x2429, // Index: 364 Fraction: 37/42 = 0.8810
0x333A, // Index: 365 Fraction: 52/59 = 0.8814
0x424B, // Index: 366 Fraction: 67/76 = 0.8816
0x0E10, // Index: 367 Fraction: 15/17 = 0.8824
0x434C, // Index: 368 Fraction: 68/77 = 0.8831
0x343B, // Index: 369 Fraction: 53/60 = 0.8833
0x252A, // Index: 370 Fraction: 38/43 = 0.8837
0x3C44, // Index: 371 Fraction: 61/69 = 0.8841
0x1619, // Index: 372 Fraction: 23/26 = 0.8846
0x353C, // Index: 373 Fraction: 54/61 = 0.8852
0x1E22, // Index: 374 Fraction: 31/35 = 0.8857
0x454E, // Index: 375 Fraction: 70/79 = 0.8861
0x262B, // Index: 376 Fraction: 39/44 = 0.8864
0x2E34, // Index: 377 Fraction: 47/53 = 0.8868
0x363D, // Index: 378 Fraction: 55/62 = 0.8871
0x3E46, // Index: 379 Fraction: 63/71 = 0.8873
0x464F, // Index: 380 Fraction: 71/80 = 0.8875
0x0708, // Index: 381 Fraction: 8/9 = 0.8889
0x4048, // Index: 382 Fraction: 65/73 = 0.8904
0x383F, // Index: 383 Fraction: 57/64 = 0.8906
0x3036, // Index: 384 Fraction: 49/55 = 0.8909
0x282D, // Index: 385 Fraction: 41/46 = 0.8913
0x2024, // Index: 386 Fraction: 33/37 = 0.8919
0x3940, // Index: 387 Fraction: 58/65 = 0.8923
0x181B, // Index: 388 Fraction: 25/28 = 0.8929
0x424A, // Index: 389 Fraction: 67/75 = 0.8933
0x292E, // Index: 390 Fraction: 42/47 = 0.8936
0x3A41, // Index: 391 Fraction: 59/66 = 0.8939
0x1012, // Index: 392 Fraction: 17/19 = 0.8947
0x3B42, // Index: 393 Fraction: 60/67 = 0.8955
0x2A2F, // Index: 394 Fraction: 43/48 = 0.8958
0x444C, // Index: 395 Fraction: 69/77 = 0.8961
0x191C, // Index: 396 Fraction: 26/29 = 0.8966
0x3C43, // Index: 397 Fraction: 61/68 = 0.8971
0x2226, // Index: 398 Fraction: 35/39 = 0.8974
0x2B30, // Index: 399 Fraction: 44/49 = 0.8980
0x343A, // Index: 400 Fraction: 53/59 = 0.8983
0x3D44, // Index: 401 Fraction: 62/69 = 0.8986
0x464E, // Index: 402 Fraction: 71/79 = 0.8987
0x0809, // Index: 403 Fraction: 9/10 = 0.9000
0x3F46, // Index: 404 Fraction: 64/71 = 0.9014
0x363C, // Index: 405 Fraction: 55/61 = 0.9016
0x2D32, // Index: 406 Fraction: 46/51 = 0.9020
0x2428, // Index: 407 Fraction: 37/41 = 0.9024
0x4047, // Index: 408 Fraction: 65/72 = 0.9028
0x1B1E, // Index: 409 Fraction: 28/31 = 0.9032
0x2E33, // Index: 410 Fraction: 47/52 = 0.9038
0x4148, // Index: 411 Fraction: 66/73 = 0.9041
0x1214, // Index: 412 Fraction: 19/21 = 0.9048
};

View File

@@ -0,0 +1,229 @@
// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <xscope.h>
#include <xs1.h>
#include <xcore/assert.h>
#include "sw_pll.h"
#include "i2s.h"
#define MCLK_FREQUENCY 12288000
#define I2S_FREQUENCY 48000
#define PLL_RATIO (MCLK_FREQUENCY / I2S_FREQUENCY)
#define BCLKS_PER_LRCLK 64
#define CONTROL_LOOP_COUNT 512
#define PPM_RANGE 150
#define NUM_I2S_CHANNELS 2
#define NUM_I2S_LINES ((NUM_I2S_CHANNELS + 1) / 2)
// These are generated from sw_pll_sim.py
#include "fractions.h"
#include "register_setup.h"
void setup_recovered_ref_clock_output(port_t p_recovered_ref_clk, xclock_t clk_recovered_ref_clk, port_t p_mclk, unsigned divider, port_t p_lrclk)
{
xassert(divider < 512);
// Connect clock block with divide to mclk
clock_enable(clk_recovered_ref_clk);
clock_set_source_port(clk_recovered_ref_clk, p_mclk);
clock_set_divide(clk_recovered_ref_clk, divider / 2);
printf("Divider: %u\n", divider);
// Output the divided mclk on a port
port_enable(p_recovered_ref_clk);
port_set_clock(p_recovered_ref_clk, clk_recovered_ref_clk);
port_set_out_clock(p_recovered_ref_clk);
// Wait for LR_CLK transition so they are synched on scope
port_enable(p_lrclk);
port_set_trigger_in_equal(p_lrclk, 0);
(void) port_in(p_lrclk);
port_set_trigger_in_equal(p_lrclk, 1);
(void) port_in(p_lrclk);
clock_start(clk_recovered_ref_clk);
}
typedef struct i2s_callback_args_t {
bool did_restart; // Set by init
int curr_lock_status;
int32_t loopback_samples[NUM_I2S_CHANNELS];
port_t p_mclk_count; // Used for keeping track of MCLK output for sw_pll
port_t p_bclk_count; // Used for keeping track of BCLK input for sw_pll
xclock_t i2s_ck_bclk;
sw_pll_state_t *sw_pll; // Pointer to sw_pll state (if used)
} i2s_callback_args_t;
uint32_t random = 0x80085; //Initial seed
void pseudo_rand_uint32(uint32_t *r){
#define CRC_POLY (0xEB31D82E)
asm volatile("crc32 %0, %2, %3" : "=r" (*r) : "0" (*r), "r" (-1), "r" (CRC_POLY));
}
I2S_CALLBACK_ATTR
static void i2s_init(void *app_data, i2s_config_t *i2s_config){
printf("I2S init\n");
i2s_callback_args_t *cb_args = app_data;
i2s_config->mode = I2S_MODE_I2S;
i2s_config->mclk_bclk_ratio = (MCLK_FREQUENCY / I2S_FREQUENCY);
cb_args->did_restart = true;
}
I2S_CALLBACK_ATTR
static i2s_restart_t i2s_restart_check(void *app_data){
i2s_callback_args_t *cb_args = app_data;
// Add random jitter
hwtimer_t tmr = hwtimer_alloc();
pseudo_rand_uint32(&random);
hwtimer_delay(tmr, random & 0x3f);
hwtimer_free(tmr);
static uint16_t old_mclk_pt = 0;
static uint16_t old_bclk_pt = 0;
port_clear_buffer(cb_args->p_bclk_count);
port_in(cb_args->p_bclk_count); // Block until BCLK transition to synchronise
uint16_t mclk_pt = port_get_trigger_time(cb_args->p_mclk_count); // Immediately sample mclk_count
uint16_t bclk_pt = port_get_trigger_time(cb_args->p_bclk_count); // Now grab bclk_count (which won't have changed)
old_mclk_pt = mclk_pt;
old_bclk_pt = bclk_pt;
sw_pll_lut_do_control(cb_args->sw_pll, mclk_pt, bclk_pt);
if(cb_args->sw_pll->lock_status != cb_args->curr_lock_status){
cb_args->curr_lock_status = cb_args->sw_pll->lock_status;
const char msg[3][16] = {"UNLOCKED LOW", "LOCKED", "UNLOCKED HIGH"};
printf("%s\n", msg[cb_args->curr_lock_status+1]);
}
// Debug only. Print raw error term to check jitter on mclk sampling
// static int cnt=0; if(++cnt == CONTROL_LOOP_COUNT) {cnt=0;printintln(cb_args->sw_pll->mclk_diff);}
return I2S_NO_RESTART;
}
I2S_CALLBACK_ATTR
static void i2s_send(void *app_data, size_t num_out, int32_t *i2s_sample_buf){
i2s_callback_args_t *cb_args = app_data;
for(int i = 0; i < NUM_I2S_CHANNELS; i++){
i2s_sample_buf[i] = cb_args->loopback_samples[i];
}
}
I2S_CALLBACK_ATTR
static void i2s_receive(void *app_data, size_t num_in, const int32_t *i2s_sample_buf){
i2s_callback_args_t *cb_args = app_data;
for(int i = 0; i < NUM_I2S_CHANNELS; i++){
cb_args->loopback_samples[i] = i2s_sample_buf[i];
}
}
void sw_pll_test(void){
// I2S resources
port_t p_i2s_dout[NUM_I2S_LINES] = {PORT_I2S_DATA1};
port_t p_i2s_din[NUM_I2S_LINES] = {PORT_I2S_DATA0};
port_t p_bclk = PORT_I2S_BCLK;
port_t p_lrclk = PORT_I2S_LRCLK;
xclock_t i2s_ck_bclk = XS1_CLKBLK_1;
port_enable(p_bclk);
// NOTE: p_lrclk does not need to be enabled by the caller
// sw-pll resources
port_t p_mclk = PORT_MCLK;
port_t p_mclk_count = PORT_MCLK_COUNT;
xclock_t clk_mclk = XS1_CLKBLK_2;
port_t p_bclk_count = XS1_PORT_16A; // Any unused port
// Create clock from mclk port and use it to clock clk_mclk.
port_enable(p_mclk);
clock_enable(clk_mclk);
clock_set_source_port(clk_mclk, p_mclk);
// Clock p_mclk_count from clk_mclk
port_enable(p_mclk_count);
port_set_clock(p_mclk_count, clk_mclk);
clock_start(clk_mclk);
// Enable p_bclk_count to count bclks cycles
port_enable(p_bclk_count);
port_set_clock(p_bclk_count, i2s_ck_bclk);
printf("Initialising SW PLL\n");
sw_pll_state_t sw_pll;
sw_pll_lut_init(&sw_pll,
SW_PLL_15Q16(0.0),
SW_PLL_15Q16(1.0),
SW_PLL_15Q16(0.0),
CONTROL_LOOP_COUNT,
PLL_RATIO,
BCLKS_PER_LRCLK,
frac_values_80,
SW_PLL_NUM_LUT_ENTRIES(frac_values_80),
APP_PLL_CTL_REG,
APP_PLL_DIV_REG,
SW_PLL_NUM_LUT_ENTRIES(frac_values_80) / 2,
PPM_RANGE);
printf("i_windup_limit: %ld\n", sw_pll.pi_state.i_windup_limit);
// Initialise app_data
i2s_callback_args_t app_data = {
.did_restart = false,
.curr_lock_status = SW_PLL_UNLOCKED_LOW,
.loopback_samples = {0},
.p_mclk_count = p_mclk_count,
.p_bclk_count = p_bclk_count,
.i2s_ck_bclk = i2s_ck_bclk,
.sw_pll = &sw_pll
};
// Initialise callback function pointers
i2s_callback_group_t i2s_cb_group;
i2s_cb_group.init = i2s_init;
i2s_cb_group.restart_check = i2s_restart_check;
i2s_cb_group.receive = i2s_receive;
i2s_cb_group.send = i2s_send;
i2s_cb_group.app_data = &app_data;
printf("Starting I2S slave\n");
// Make a test output to observe the recovered mclk divided down to the refclk frequency
xclock_t clk_recovered_ref_clk = XS1_CLKBLK_3;
port_t p_recovered_ref_clk = PORT_I2S_DATA2;
setup_recovered_ref_clock_output(p_recovered_ref_clk, clk_recovered_ref_clk, p_mclk, PLL_RATIO, p_lrclk);
i2s_slave(
&i2s_cb_group,
p_i2s_dout,
NUM_I2S_LINES,
p_i2s_din,
NUM_I2S_LINES,
p_bclk,
p_lrclk,
i2s_ck_bclk);
}

View File

@@ -0,0 +1,20 @@
// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <platform.h>
#include <xs1.h>
extern void sw_pll_test(void);
int main(void)
{
par
{
on tile[0]: par {
}
on tile[1]: par {
sw_pll_test();
}
}
return 0;
}

View File

@@ -0,0 +1,16 @@
/* Autogenerated by app_pll_model.py using command:
/Users/ed/sandboxes/lib_sw_pll/python/sw_pll/pll_calc.py -i 24.0 -a -m 80 -t 12.288 -p 6.0 -e 5 -r --fracmin 0.695 --fracmax 0.905 --header
Picked output solution #62
Input freq: 24000000
F: 203
R: 1
f: 3
p: 4
OD: 1
ACD: 24
Output freq: 12288000.0
VCO freq: 2457600000.0 */
#define APP_PLL_CTL_REG 0x0880CB01
#define APP_PLL_DIV_REG 0x80000018
#define APP_PLL_FRAC_REG 0x80000304

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Network xmlns="http://www.xmos.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.xmos.com http://www.xmos.com" ManuallySpecifiedRouting="true">
<Type>Board</Type>
<Name>XS3 MC Audio</Name>
<Declarations>
<Declaration>tileref tile[2]</Declaration>
<Declaration>tileref usb_tile</Declaration>
</Declarations>
<Packages>
<Package id="0" Type="XS3-UnA-1024-QF60A">
<Nodes>
<Node Id="0" InPackageId="0" Type="XS3-L16A-1024" Oscillator="24MHz" SystemFrequency="800MHz" referencefrequency="100MHz">
<Boot>
<Source Location="SPI:bootFlash"/>
</Boot>
<Tile Number="0" Reference="tile[0]">
<!-- USB Ports -->
<Port Location="XS1_PORT_1H" Name="PORT_USB_TX_READYIN"/>
<Port Location="XS1_PORT_1J" Name="PORT_USB_CLK"/>
<Port Location="XS1_PORT_1K" Name="PORT_USB_TX_READYOUT"/>
<Port Location="XS1_PORT_1I" Name="PORT_USB_RX_READY"/>
<Port Location="XS1_PORT_1E" Name="PORT_USB_FLAG0"/>
<Port Location="XS1_PORT_1F" Name="PORT_USB_FLAG1"/>
<Port Location="XS1_PORT_8A" Name="PORT_USB_TXD"/>
<Port Location="XS1_PORT_8B" Name="PORT_USB_RXD"/>
<!-- QSPI Ports -->
<Port Location="XS1_PORT_1B" Name="PORT_SQI_CS"/>
<Port Location="XS1_PORT_1C" Name="PORT_SQI_SCLK"/>
<Port Location="XS1_PORT_4B" Name="PORT_SQI_SIO"/>
<!-- Mic related ports -->
<Port Location="XS1_PORT_1M" Name="PORT_PDM_CLK"/>
<Port Location="XS1_PORT_8D" Name="PORT_PDM_DATA"/> <!-- only bits 4,5,6 & 7 are used due to overlap with 1b ports -->
<Port Location="XS1_PORT_1L" Name="PORT_PDM_MCLK"/>
<!-- I2C Master Ports -->
<Port Location="XS1_PORT_1N" Name="PORT_I2C_SCL"/>
<Port Location="XS1_PORT_1O" Name="PORT_I2C_SDA"/>
<!-- SPI Slave Ports -->
<Port Location="XS1_PORT_1A" Name="PORT_SPI_SLAVE_CS"/>
<Port Location="XS1_PORT_1C" Name="PORT_SPI_SLAVE_SCLK"/>
<Port Location="XS1_PORT_1D" Name="PORT_SPI_SLAVE_MOSI"/>
<Port Location="XS1_PORT_1P" Name="PORT_SPI_SLAVE_MISO"/>
<!-- GPIO Ports -->
<Port Location="XS1_PORT_8C" Name="GPO_TILE_0_PORT_8C"/> <!-- only bits 3,4,5,6 & 7 brought out on pckg -->
</Tile>
<Tile Number="1" Reference="tile[1]">
<!-- Audio Ports -->
<Port Location="XS1_PORT_1D" Name="PORT_MCLK"/>
<Port Location="XS1_PORT_1B" Name="PORT_I2S_LRCLK"/>
<Port Location="XS1_PORT_1C" Name="PORT_I2S_BCLK"/>
<Port Location="XS1_PORT_1A" Name="PORT_I2S_DATA0"/>
<Port Location="XS1_PORT_1G" Name="PORT_I2S_DATA1"/>
<Port Location="XS1_PORT_1K" Name="PORT_I2S_DATA2"/> <!-- Optional in some configs. NOTE shared with GPIO_TILE_1_PORT_1K -->
<!-- MCLK recovery port. This is used internally to count MCLKs and isn't pinned out -->
<Port Location="XS1_PORT_32A" Name="PORT_MCLK_COUNT"/>
<!-- GPIO Ports -->
<Port Location="XS1_PORT_4A" Name="GPI_TILE_1_PORT_4A"/> <!-- only bit 3 is pinned out on package -->
<Port Location="XS1_PORT_1F" Name="GPIO_TILE_1_PORT_1F"/>
<Port Location="XS1_PORT_1K" Name="GPIO_TILE_1_PORT_1K"/> <!-- NOTE shared with PORT_I2S_DATA2 -->
</Tile>
</Node>
</Nodes>
</Package>
</Packages>
<Nodes>
<Node Id="2" Type="device:" RoutingId="0x8000">
<Service Id="0" Proto="xscope_host_data(chanend c);">
<Chanend Identifier="c" end="3"/>
</Service>
</Node>
</Nodes>
<Links>
<Link Encoding="2wire" Delays="4,4" Flags="XSCOPE">
<LinkEndpoint NodeId="0" Link="XL0"/>
<LinkEndpoint NodeId="2" Chanend="1"/>
</Link>
</Links>
<ExternalDevices>
<Device NodeId="0" Tile="0" Class="SQIFlash" Name="bootFlash">
<Attribute Name="PORT_SQI_CS" Value="PORT_SQI_CS"/>
<Attribute Name="PORT_SQI_SCLK" Value="PORT_SQI_SCLK"/>
<Attribute Name="PORT_SQI_SIO" Value="PORT_SQI_SIO"/>
</Device>
</ExternalDevices>
<JTAGChain>
<JTAGDevice NodeId="0"/>
</JTAGChain>
</Network>

View File

@@ -0,0 +1,60 @@
// Copyright 2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <xs1.h>
#include <stdio.h>
#include <xcore/hwtimer.h>
#include <xcore/port.h>
#include "sw_pll_common.h"
#define DO_CLOCKS \
printf("Ref Hz: %d\n", clock_rate >> 1); \
\
unsigned cycle_ticks_int = XS1_TIMER_HZ / clock_rate; \
unsigned cycle_ticks_remaidner = XS1_TIMER_HZ % clock_rate; \
unsigned carry = 0; \
\
period_trig += XS1_TIMER_HZ * 1; \
unsigned time_now = hwtimer_get_time(period_tmr); \
while(TIMER_TIMEAFTER(period_trig, time_now)) \
{ \
port_out(p_clock_gen, port_val); \
hwtimer_wait_until(clock_tmr, time_trig); \
time_trig += cycle_ticks_int; \
carry += cycle_ticks_remaidner; \
if(carry >= clock_rate){ \
time_trig++; \
carry -= clock_rate; \
} \
port_val ^= 1; \
time_now = hwtimer_get_time(period_tmr); \
}
void clock_gen(unsigned ref_frequency, unsigned ppm_range) // Step from - to + this
{
unsigned clock_rate = ref_frequency * 2; // Note double because we generate edges at this rate
unsigned clock_rate_low = (unsigned)(clock_rate * (1.0 - (float)ppm_range / 1000000.0));
unsigned clock_rate_high = (unsigned)(clock_rate * (1.0 + (float)ppm_range / 1000000.0));
unsigned step_size = (clock_rate_high - clock_rate_low) / 20;
printf("Sweep range: %d %d %d, step size: %d\n", clock_rate_low / 2, clock_rate / 2, clock_rate_high / 2, step_size);
hwtimer_t period_tmr = hwtimer_alloc();
unsigned period_trig = hwtimer_get_time(period_tmr);
hwtimer_t clock_tmr = hwtimer_alloc();
unsigned time_trig = hwtimer_get_time(clock_tmr);
port_t p_clock_gen = PORT_I2S_BCLK;
port_enable(p_clock_gen);
unsigned port_val = 1;
for(unsigned clock_rate = clock_rate_low; clock_rate <= clock_rate_high; clock_rate += 2 * step_size){
DO_CLOCKS
}
for(unsigned clock_rate = clock_rate_high; clock_rate > clock_rate_low; clock_rate -= 2 * step_size){
DO_CLOCKS
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
// Runs a task in a thread that produces a clock that sweeps a reference clock
// between + and - the ppm value specified.
//
// param ref_frequency Nominal frequency in Hz
// param ppm_range The range to sweep
void clock_gen(unsigned ref_frequency, unsigned ppm_range);

View File

@@ -0,0 +1,43 @@
// Copyright 2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdio.h>
#include "resource_setup.h"
void setup_ref_and_mclk_ports_and_clocks(port_t p_mclk, xclock_t clk_mclk, port_t p_clock_counter, xclock_t clk_ref_clk, port_t p_ref_clk_timing)
{
// Create clock from mclk port and use it to clock the p_clock_counter port.
clock_enable(clk_mclk);
port_enable(p_mclk);
clock_set_source_port(clk_mclk, p_mclk);
// Clock p_clock_counter from MCLK
port_enable(p_clock_counter);
port_set_clock(p_clock_counter, clk_mclk);
clock_start(clk_mclk);
// Create clock from ref_clock_port and use it to clock the p_clock_counter port.
clock_enable(clk_ref_clk);
clock_set_source_port(clk_ref_clk, p_clock_counter);
port_enable(p_ref_clk_timing);
port_set_clock(p_ref_clk_timing, clk_ref_clk);
clock_start(clk_ref_clk);
}
void setup_recovered_ref_clock_output(port_t p_recovered_ref_clk, xclock_t clk_recovered_ref_clk, port_t p_mclk, unsigned divider)
{
// Connect clock block with divide to mclk
clock_enable(clk_recovered_ref_clk);
clock_set_source_port(clk_recovered_ref_clk, p_mclk);
clock_set_divide(clk_recovered_ref_clk, divider / 2);
printf("Divider: %u\n", divider);
// Output the divided mclk on a port
port_enable(p_recovered_ref_clk);
port_set_clock(p_recovered_ref_clk, clk_recovered_ref_clk);
port_set_out_clock(p_recovered_ref_clk);
clock_start(clk_recovered_ref_clk);
}

View File

@@ -0,0 +1,28 @@
// Copyright 2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <xcore/port.h>
#pragma once
// Sets up the provided resources so that we can count PLL output clocks.
// We do this by clocking the input reference clock port with the output from the PLL
// and its internal counter is used to count the PLL clock cycles(normal timers cannot count custom clocks)
// It also sets up a dummy port clocked by the input reference to act as a timing barrier so that
// the output clock count can be precisely sampled.
//
// param p_mclk The mclk output port (Always P1D on tile[1])
// param clk_mclk The clockblock for mclk out
// param p_clock_counter The port used for counting mclks - in this case the ref input clock
// param clk_ref_clk The clockblock for the timing barrier
// param p_ref_clk_timing The port used for t he timing barrier
void setup_ref_and_mclk_ports_and_clocks(port_t p_mclk, xclock_t clk_mclk, port_t p_clock_counter, xclock_t clk_ref_clk, port_t p_ref_clk_timing);
// Sets up a divided version of the PLL output so it can visually be compared (eg. on a DSO)
// with the input reference clock to the PLL
//
// param p_recovered_ref_clk The port used to drive the recovered and divided clock
// param clk_recovered_ref_clk The clockblock used to drive the recovered and divided clock
// param p_mclk The mclk output port (Always P1D on tile[1])
// param divider The divide value from mclk to the divided output
void setup_recovered_ref_clock_output(port_t p_recovered_ref_clk, xclock_t clk_recovered_ref_clk, port_t p_mclk, unsigned divider);

View File

@@ -0,0 +1,50 @@
#**********************
# Gather Sources
#**********************
file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.c
${CMAKE_CURRENT_LIST_DIR}/src/*.xc
${CMAKE_CURRENT_LIST_DIR}/../shared/src/*.c )
set(APP_INCLUDES ${CMAKE_CURRENT_LIST_DIR}/src
${CMAKE_CURRENT_LIST_DIR}/../shared/src
)
#**********************
# Flags
#**********************
set(APP_COMPILER_FLAGS
-Os
-g
-report
-fxscope
-mcmodel=large
-Wno-xcore-fptrgroup
${CMAKE_CURRENT_LIST_DIR}/src/config.xscope
-target=XCORE-AI-EXPLORER
)
set(APP_COMPILE_DEFINITIONS
DEBUG_PRINT_ENABLE=1
PLATFORM_SUPPORTS_TILE_0=1
PLATFORM_SUPPORTS_TILE_1=1
PLATFORM_SUPPORTS_TILE_2=0
PLATFORM_SUPPORTS_TILE_3=0
PLATFORM_USES_TILE_0=1
PLATFORM_USES_TILE_1=1
)
set(APP_LINK_OPTIONS
-report
-target=XCORE-AI-EXPLORER
${CMAKE_CURRENT_LIST_DIR}/src/config.xscope
)
#**********************
# Tile Targets
#**********************
add_executable(simple_lut)
target_sources(simple_lut PUBLIC ${APP_SOURCES})
target_include_directories(simple_lut PUBLIC ${APP_INCLUDES})
target_compile_definitions(simple_lut PRIVATE ${APP_COMPILE_DEFINITIONS})
target_compile_options(simple_lut PRIVATE ${APP_COMPILER_FLAGS})
target_link_options(simple_lut PRIVATE ${APP_LINK_OPTIONS})
target_link_libraries(simple_lut PUBLIC lib_sw_pll)

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ======================================================= -->
<!-- The 'ioMode' attribute on the xSCOPEconfig -->
<!-- element can take the following values: -->
<!-- "none", "basic", "timed" -->
<!-- -->
<!-- The 'type' attribute on Probe -->
<!-- elements can take the following values: -->
<!-- "STARTSTOP", "CONTINUOUS", "DISCRETE", "STATEMACHINE" -->
<!-- -->
<!-- The 'datatype' attribute on Probe -->
<!-- elements can take the following values: -->
<!-- "NONE", "UINT", "INT", "FLOAT" -->
<!-- ======================================================= -->
<xSCOPEconfig ioMode="basic" enabled="true">
<!-- For example: -->
<!-- <Probe name="I2S_RX_0" type="CONTINUOUS" datatype="INT" units="Value" enabled="true"/> -->
<!-- From the target code, call: xscope_int(PROBE_NAME, value); -->
</xSCOPEconfig>

View File

@@ -0,0 +1,417 @@
// Header file listing fraction options searched
// These values to go in the bottom 16 bits of the secondary PLL fractional-n divider register.
short frac_values_80[413] = {
0x0F16, // Index: 0 Fraction: 16/23 = 0.6957
0x364E, // Index: 1 Fraction: 55/79 = 0.6962
0x2637, // Index: 2 Fraction: 39/56 = 0.6964
0x1620, // Index: 3 Fraction: 23/33 = 0.6970
0x344B, // Index: 4 Fraction: 53/76 = 0.6974
0x1D2A, // Index: 5 Fraction: 30/43 = 0.6977
0x2434, // Index: 6 Fraction: 37/53 = 0.6981
0x2B3E, // Index: 7 Fraction: 44/63 = 0.6984
0x3248, // Index: 8 Fraction: 51/73 = 0.6986
0x0609, // Index: 9 Fraction: 7/10 = 0.7000
0x354C, // Index: 10 Fraction: 54/77 = 0.7013
0x2E42, // Index: 11 Fraction: 47/67 = 0.7015
0x2738, // Index: 12 Fraction: 40/57 = 0.7018
0x202E, // Index: 13 Fraction: 33/47 = 0.7021
0x1924, // Index: 14 Fraction: 26/37 = 0.7027
0x2C3F, // Index: 15 Fraction: 45/64 = 0.7031
0x121A, // Index: 16 Fraction: 19/27 = 0.7037
0x3146, // Index: 17 Fraction: 50/71 = 0.7042
0x1E2B, // Index: 18 Fraction: 31/44 = 0.7045
0x2A3C, // Index: 19 Fraction: 43/61 = 0.7049
0x364D, // Index: 20 Fraction: 55/78 = 0.7051
0x0B10, // Index: 21 Fraction: 12/17 = 0.7059
0x344A, // Index: 22 Fraction: 53/75 = 0.7067
0x2839, // Index: 23 Fraction: 41/58 = 0.7069
0x1C28, // Index: 24 Fraction: 29/41 = 0.7073
0x2D40, // Index: 25 Fraction: 46/65 = 0.7077
0x1017, // Index: 26 Fraction: 17/24 = 0.7083
0x374E, // Index: 27 Fraction: 56/79 = 0.7089
0x2636, // Index: 28 Fraction: 39/55 = 0.7091
0x151E, // Index: 29 Fraction: 22/31 = 0.7097
0x3044, // Index: 30 Fraction: 49/69 = 0.7101
0x1A25, // Index: 31 Fraction: 27/38 = 0.7105
0x1F2C, // Index: 32 Fraction: 32/45 = 0.7111
0x2433, // Index: 33 Fraction: 37/52 = 0.7115
0x293A, // Index: 34 Fraction: 42/59 = 0.7119
0x2E41, // Index: 35 Fraction: 47/66 = 0.7121
0x3348, // Index: 36 Fraction: 52/73 = 0.7123
0x384F, // Index: 37 Fraction: 57/80 = 0.7125
0x0406, // Index: 38 Fraction: 5/7 = 0.7143
0x3449, // Index: 39 Fraction: 53/74 = 0.7162
0x2F42, // Index: 40 Fraction: 48/67 = 0.7164
0x2A3B, // Index: 41 Fraction: 43/60 = 0.7167
0x2534, // Index: 42 Fraction: 38/53 = 0.7170
0x202D, // Index: 43 Fraction: 33/46 = 0.7174
0x1B26, // Index: 44 Fraction: 28/39 = 0.7179
0x3246, // Index: 45 Fraction: 51/71 = 0.7183
0x161F, // Index: 46 Fraction: 23/32 = 0.7188
0x2838, // Index: 47 Fraction: 41/57 = 0.7193
0x1118, // Index: 48 Fraction: 18/25 = 0.7200
0x3043, // Index: 49 Fraction: 49/68 = 0.7206
0x1E2A, // Index: 50 Fraction: 31/43 = 0.7209
0x2B3C, // Index: 51 Fraction: 44/61 = 0.7213
0x384E, // Index: 52 Fraction: 57/79 = 0.7215
0x0C11, // Index: 53 Fraction: 13/18 = 0.7222
0x2E40, // Index: 54 Fraction: 47/65 = 0.7231
0x212E, // Index: 55 Fraction: 34/47 = 0.7234
0x364B, // Index: 56 Fraction: 55/76 = 0.7237
0x141C, // Index: 57 Fraction: 21/29 = 0.7241
0x3144, // Index: 58 Fraction: 50/69 = 0.7246
0x1C27, // Index: 59 Fraction: 29/40 = 0.7250
0x2432, // Index: 60 Fraction: 37/51 = 0.7255
0x2C3D, // Index: 61 Fraction: 45/62 = 0.7258
0x3448, // Index: 62 Fraction: 53/73 = 0.7260
0x070A, // Index: 63 Fraction: 8/11 = 0.7273
0x3245, // Index: 64 Fraction: 51/70 = 0.7286
0x2A3A, // Index: 65 Fraction: 43/59 = 0.7288
0x222F, // Index: 66 Fraction: 35/48 = 0.7292
0x1A24, // Index: 67 Fraction: 27/37 = 0.7297
0x2D3E, // Index: 68 Fraction: 46/63 = 0.7302
0x1219, // Index: 69 Fraction: 19/26 = 0.7308
0x3042, // Index: 70 Fraction: 49/67 = 0.7313
0x1D28, // Index: 71 Fraction: 30/41 = 0.7317
0x2837, // Index: 72 Fraction: 41/56 = 0.7321
0x3346, // Index: 73 Fraction: 52/71 = 0.7324
0x0A0E, // Index: 74 Fraction: 11/15 = 0.7333
0x394E, // Index: 75 Fraction: 58/79 = 0.7342
0x2E3F, // Index: 76 Fraction: 47/64 = 0.7344
0x2330, // Index: 77 Fraction: 36/49 = 0.7347
0x1821, // Index: 78 Fraction: 25/34 = 0.7353
0x2634, // Index: 79 Fraction: 39/53 = 0.7358
0x3447, // Index: 80 Fraction: 53/72 = 0.7361
0x0D12, // Index: 81 Fraction: 14/19 = 0.7368
0x3A4F, // Index: 82 Fraction: 59/80 = 0.7375
0x2C3C, // Index: 83 Fraction: 45/61 = 0.7377
0x1E29, // Index: 84 Fraction: 31/42 = 0.7381
0x2F40, // Index: 85 Fraction: 48/65 = 0.7385
0x1016, // Index: 86 Fraction: 17/23 = 0.7391
0x3548, // Index: 87 Fraction: 54/73 = 0.7397
0x2431, // Index: 88 Fraction: 37/50 = 0.7400
0x384C, // Index: 89 Fraction: 57/77 = 0.7403
0x131A, // Index: 90 Fraction: 20/27 = 0.7407
0x2A39, // Index: 91 Fraction: 43/58 = 0.7414
0x161E, // Index: 92 Fraction: 23/31 = 0.7419
0x3041, // Index: 93 Fraction: 49/66 = 0.7424
0x1922, // Index: 94 Fraction: 26/35 = 0.7429
0x3649, // Index: 95 Fraction: 55/74 = 0.7432
0x1C26, // Index: 96 Fraction: 29/39 = 0.7436
0x1F2A, // Index: 97 Fraction: 32/43 = 0.7442
0x222E, // Index: 98 Fraction: 35/47 = 0.7447
0x2532, // Index: 99 Fraction: 38/51 = 0.7451
0x2836, // Index: 100 Fraction: 41/55 = 0.7455
0x2B3A, // Index: 101 Fraction: 44/59 = 0.7458
0x2E3E, // Index: 102 Fraction: 47/63 = 0.7460
0x3142, // Index: 103 Fraction: 50/67 = 0.7463
0x3446, // Index: 104 Fraction: 53/71 = 0.7465
0x374A, // Index: 105 Fraction: 56/75 = 0.7467
0x3A4E, // Index: 106 Fraction: 59/79 = 0.7468
0x0203, // Index: 107 Fraction: 3/4 = 0.7500
0x394C, // Index: 108 Fraction: 58/77 = 0.7532
0x3648, // Index: 109 Fraction: 55/73 = 0.7534
0x3344, // Index: 110 Fraction: 52/69 = 0.7536
0x3040, // Index: 111 Fraction: 49/65 = 0.7538
0x2D3C, // Index: 112 Fraction: 46/61 = 0.7541
0x2A38, // Index: 113 Fraction: 43/57 = 0.7544
0x2734, // Index: 114 Fraction: 40/53 = 0.7547
0x2430, // Index: 115 Fraction: 37/49 = 0.7551
0x212C, // Index: 116 Fraction: 34/45 = 0.7556
0x1E28, // Index: 117 Fraction: 31/41 = 0.7561
0x3A4D, // Index: 118 Fraction: 59/78 = 0.7564
0x1B24, // Index: 119 Fraction: 28/37 = 0.7568
0x3445, // Index: 120 Fraction: 53/70 = 0.7571
0x1820, // Index: 121 Fraction: 25/33 = 0.7576
0x2E3D, // Index: 122 Fraction: 47/62 = 0.7581
0x151C, // Index: 123 Fraction: 22/29 = 0.7586
0x2835, // Index: 124 Fraction: 41/54 = 0.7593
0x3B4E, // Index: 125 Fraction: 60/79 = 0.7595
0x1218, // Index: 126 Fraction: 19/25 = 0.7600
0x3546, // Index: 127 Fraction: 54/71 = 0.7606
0x222D, // Index: 128 Fraction: 35/46 = 0.7609
0x3242, // Index: 129 Fraction: 51/67 = 0.7612
0x0F14, // Index: 130 Fraction: 16/21 = 0.7619
0x3C4F, // Index: 131 Fraction: 61/80 = 0.7625
0x2C3A, // Index: 132 Fraction: 45/59 = 0.7627
0x1C25, // Index: 133 Fraction: 29/38 = 0.7632
0x2936, // Index: 134 Fraction: 42/55 = 0.7636
0x3647, // Index: 135 Fraction: 55/72 = 0.7639
0x0C10, // Index: 136 Fraction: 13/17 = 0.7647
0x303F, // Index: 137 Fraction: 49/64 = 0.7656
0x232E, // Index: 138 Fraction: 36/47 = 0.7660
0x3A4C, // Index: 139 Fraction: 59/77 = 0.7662
0x161D, // Index: 140 Fraction: 23/30 = 0.7667
0x3748, // Index: 141 Fraction: 56/73 = 0.7671
0x202A, // Index: 142 Fraction: 33/43 = 0.7674
0x2A37, // Index: 143 Fraction: 43/56 = 0.7679
0x3444, // Index: 144 Fraction: 53/69 = 0.7681
0x090C, // Index: 145 Fraction: 10/13 = 0.7692
0x3849, // Index: 146 Fraction: 57/74 = 0.7703
0x2E3C, // Index: 147 Fraction: 47/61 = 0.7705
0x242F, // Index: 148 Fraction: 37/48 = 0.7708
0x1A22, // Index: 149 Fraction: 27/35 = 0.7714
0x2B38, // Index: 150 Fraction: 44/57 = 0.7719
0x3C4E, // Index: 151 Fraction: 61/79 = 0.7722
0x1015, // Index: 152 Fraction: 17/22 = 0.7727
0x394A, // Index: 153 Fraction: 58/75 = 0.7733
0x2834, // Index: 154 Fraction: 41/53 = 0.7736
0x171E, // Index: 155 Fraction: 24/31 = 0.7742
0x3646, // Index: 156 Fraction: 55/71 = 0.7746
0x1E27, // Index: 157 Fraction: 31/40 = 0.7750
0x2530, // Index: 158 Fraction: 38/49 = 0.7755
0x2C39, // Index: 159 Fraction: 45/58 = 0.7759
0x3342, // Index: 160 Fraction: 52/67 = 0.7761
0x3A4B, // Index: 161 Fraction: 59/76 = 0.7763
0x0608, // Index: 162 Fraction: 7/9 = 0.7778
0x3B4C, // Index: 163 Fraction: 60/77 = 0.7792
0x3443, // Index: 164 Fraction: 53/68 = 0.7794
0x2D3A, // Index: 165 Fraction: 46/59 = 0.7797
0x2631, // Index: 166 Fraction: 39/50 = 0.7800
0x1F28, // Index: 167 Fraction: 32/41 = 0.7805
0x3848, // Index: 168 Fraction: 57/73 = 0.7808
0x181F, // Index: 169 Fraction: 25/32 = 0.7812
0x2A36, // Index: 170 Fraction: 43/55 = 0.7818
0x3C4D, // Index: 171 Fraction: 61/78 = 0.7821
0x1116, // Index: 172 Fraction: 18/23 = 0.7826
0x2E3B, // Index: 173 Fraction: 47/60 = 0.7833
0x1C24, // Index: 174 Fraction: 29/37 = 0.7838
0x2732, // Index: 175 Fraction: 40/51 = 0.7843
0x3240, // Index: 176 Fraction: 51/65 = 0.7846
0x3D4E, // Index: 177 Fraction: 62/79 = 0.7848
0x0A0D, // Index: 178 Fraction: 11/14 = 0.7857
0x3A4A, // Index: 179 Fraction: 59/75 = 0.7867
0x2F3C, // Index: 180 Fraction: 48/61 = 0.7869
0x242E, // Index: 181 Fraction: 37/47 = 0.7872
0x3E4F, // Index: 182 Fraction: 63/80 = 0.7875
0x1920, // Index: 183 Fraction: 26/33 = 0.7879
0x2833, // Index: 184 Fraction: 41/52 = 0.7885
0x3746, // Index: 185 Fraction: 56/71 = 0.7887
0x0E12, // Index: 186 Fraction: 15/19 = 0.7895
0x303D, // Index: 187 Fraction: 49/62 = 0.7903
0x212A, // Index: 188 Fraction: 34/43 = 0.7907
0x3442, // Index: 189 Fraction: 53/67 = 0.7910
0x1217, // Index: 190 Fraction: 19/24 = 0.7917
0x3C4C, // Index: 191 Fraction: 61/77 = 0.7922
0x2934, // Index: 192 Fraction: 42/53 = 0.7925
0x161C, // Index: 193 Fraction: 23/29 = 0.7931
0x313E, // Index: 194 Fraction: 50/63 = 0.7937
0x1A21, // Index: 195 Fraction: 27/34 = 0.7941
0x3948, // Index: 196 Fraction: 58/73 = 0.7945
0x1E26, // Index: 197 Fraction: 31/39 = 0.7949
0x222B, // Index: 198 Fraction: 35/44 = 0.7955
0x2630, // Index: 199 Fraction: 39/49 = 0.7959
0x2A35, // Index: 200 Fraction: 43/54 = 0.7963
0x2E3A, // Index: 201 Fraction: 47/59 = 0.7966
0x323F, // Index: 202 Fraction: 51/64 = 0.7969
0x3644, // Index: 203 Fraction: 55/69 = 0.7971
0x3A49, // Index: 204 Fraction: 59/74 = 0.7973
0x3E4E, // Index: 205 Fraction: 63/79 = 0.7975
0x0304, // Index: 206 Fraction: 4/5 = 0.8000
0x3C4B, // Index: 207 Fraction: 61/76 = 0.8026
0x3846, // Index: 208 Fraction: 57/71 = 0.8028
0x3441, // Index: 209 Fraction: 53/66 = 0.8030
0x303C, // Index: 210 Fraction: 49/61 = 0.8033
0x2C37, // Index: 211 Fraction: 45/56 = 0.8036
0x2832, // Index: 212 Fraction: 41/51 = 0.8039
0x242D, // Index: 213 Fraction: 37/46 = 0.8043
0x2028, // Index: 214 Fraction: 33/41 = 0.8049
0x3D4C, // Index: 215 Fraction: 62/77 = 0.8052
0x1C23, // Index: 216 Fraction: 29/36 = 0.8056
0x3542, // Index: 217 Fraction: 54/67 = 0.8060
0x181E, // Index: 218 Fraction: 25/31 = 0.8065
0x2D38, // Index: 219 Fraction: 46/57 = 0.8070
0x1419, // Index: 220 Fraction: 21/26 = 0.8077
0x3A48, // Index: 221 Fraction: 59/73 = 0.8082
0x252E, // Index: 222 Fraction: 38/47 = 0.8085
0x3643, // Index: 223 Fraction: 55/68 = 0.8088
0x1014, // Index: 224 Fraction: 17/21 = 0.8095
0x3F4E, // Index: 225 Fraction: 64/79 = 0.8101
0x2E39, // Index: 226 Fraction: 47/58 = 0.8103
0x1D24, // Index: 227 Fraction: 30/37 = 0.8108
0x2A34, // Index: 228 Fraction: 43/53 = 0.8113
0x3744, // Index: 229 Fraction: 56/69 = 0.8116
0x0C0F, // Index: 230 Fraction: 13/16 = 0.8125
0x3C4A, // Index: 231 Fraction: 61/75 = 0.8133
0x2F3A, // Index: 232 Fraction: 48/59 = 0.8136
0x222A, // Index: 233 Fraction: 35/43 = 0.8140
0x3845, // Index: 234 Fraction: 57/70 = 0.8143
0x151A, // Index: 235 Fraction: 22/27 = 0.8148
0x3440, // Index: 236 Fraction: 53/65 = 0.8154
0x1E25, // Index: 237 Fraction: 31/38 = 0.8158
0x2730, // Index: 238 Fraction: 40/49 = 0.8163
0x303B, // Index: 239 Fraction: 49/60 = 0.8167
0x3946, // Index: 240 Fraction: 58/71 = 0.8169
0x080A, // Index: 241 Fraction: 9/11 = 0.8182
0x3A47, // Index: 242 Fraction: 59/72 = 0.8194
0x313C, // Index: 243 Fraction: 50/61 = 0.8197
0x2831, // Index: 244 Fraction: 41/50 = 0.8200
0x1F26, // Index: 245 Fraction: 32/39 = 0.8205
0x3642, // Index: 246 Fraction: 55/67 = 0.8209
0x161B, // Index: 247 Fraction: 23/28 = 0.8214
0x3B48, // Index: 248 Fraction: 60/73 = 0.8219
0x242C, // Index: 249 Fraction: 37/45 = 0.8222
0x323D, // Index: 250 Fraction: 51/62 = 0.8226
0x404E, // Index: 251 Fraction: 65/79 = 0.8228
0x0D10, // Index: 252 Fraction: 14/17 = 0.8235
0x3C49, // Index: 253 Fraction: 61/74 = 0.8243
0x2E38, // Index: 254 Fraction: 47/57 = 0.8246
0x2027, // Index: 255 Fraction: 33/40 = 0.8250
0x333E, // Index: 256 Fraction: 52/63 = 0.8254
0x1216, // Index: 257 Fraction: 19/23 = 0.8261
0x3D4A, // Index: 258 Fraction: 62/75 = 0.8267
0x2A33, // Index: 259 Fraction: 43/52 = 0.8269
0x171C, // Index: 260 Fraction: 24/29 = 0.8276
0x343F, // Index: 261 Fraction: 53/64 = 0.8281
0x1C22, // Index: 262 Fraction: 29/35 = 0.8286
0x3E4B, // Index: 263 Fraction: 63/76 = 0.8289
0x2128, // Index: 264 Fraction: 34/41 = 0.8293
0x262E, // Index: 265 Fraction: 39/47 = 0.8298
0x2B34, // Index: 266 Fraction: 44/53 = 0.8302
0x303A, // Index: 267 Fraction: 49/59 = 0.8305
0x3540, // Index: 268 Fraction: 54/65 = 0.8308
0x3A46, // Index: 269 Fraction: 59/71 = 0.8310
0x3F4C, // Index: 270 Fraction: 64/77 = 0.8312
0x0405, // Index: 271 Fraction: 5/6 = 0.8333
0x414E, // Index: 272 Fraction: 66/79 = 0.8354
0x3C48, // Index: 273 Fraction: 61/73 = 0.8356
0x3742, // Index: 274 Fraction: 56/67 = 0.8358
0x323C, // Index: 275 Fraction: 51/61 = 0.8361
0x2D36, // Index: 276 Fraction: 46/55 = 0.8364
0x2830, // Index: 277 Fraction: 41/49 = 0.8367
0x232A, // Index: 278 Fraction: 36/43 = 0.8372
0x424F, // Index: 279 Fraction: 67/80 = 0.8375
0x1E24, // Index: 280 Fraction: 31/37 = 0.8378
0x3843, // Index: 281 Fraction: 57/68 = 0.8382
0x191E, // Index: 282 Fraction: 26/31 = 0.8387
0x2E37, // Index: 283 Fraction: 47/56 = 0.8393
0x1418, // Index: 284 Fraction: 21/25 = 0.8400
0x3944, // Index: 285 Fraction: 58/69 = 0.8406
0x242B, // Index: 286 Fraction: 37/44 = 0.8409
0x343E, // Index: 287 Fraction: 53/63 = 0.8413
0x0F12, // Index: 288 Fraction: 16/19 = 0.8421
0x3A45, // Index: 289 Fraction: 59/70 = 0.8429
0x2A32, // Index: 290 Fraction: 43/51 = 0.8431
0x1A1F, // Index: 291 Fraction: 27/32 = 0.8438
0x404C, // Index: 292 Fraction: 65/77 = 0.8442
0x252C, // Index: 293 Fraction: 38/45 = 0.8444
0x3039, // Index: 294 Fraction: 49/58 = 0.8448
0x3B46, // Index: 295 Fraction: 60/71 = 0.8451
0x0A0C, // Index: 296 Fraction: 11/13 = 0.8462
0x3C47, // Index: 297 Fraction: 61/72 = 0.8472
0x313A, // Index: 298 Fraction: 50/59 = 0.8475
0x262D, // Index: 299 Fraction: 39/46 = 0.8478
0x424E, // Index: 300 Fraction: 67/79 = 0.8481
0x1B20, // Index: 301 Fraction: 28/33 = 0.8485
0x2C34, // Index: 302 Fraction: 45/53 = 0.8491
0x3D48, // Index: 303 Fraction: 62/73 = 0.8493
0x1013, // Index: 304 Fraction: 17/20 = 0.8500
0x3842, // Index: 305 Fraction: 57/67 = 0.8507
0x272E, // Index: 306 Fraction: 40/47 = 0.8511
0x3E49, // Index: 307 Fraction: 63/74 = 0.8514
0x161A, // Index: 308 Fraction: 23/27 = 0.8519
0x333C, // Index: 309 Fraction: 52/61 = 0.8525
0x1C21, // Index: 310 Fraction: 29/34 = 0.8529
0x3F4A, // Index: 311 Fraction: 64/75 = 0.8533
0x2228, // Index: 312 Fraction: 35/41 = 0.8537
0x282F, // Index: 313 Fraction: 41/48 = 0.8542
0x2E36, // Index: 314 Fraction: 47/55 = 0.8545
0x343D, // Index: 315 Fraction: 53/62 = 0.8548
0x3A44, // Index: 316 Fraction: 59/69 = 0.8551
0x404B, // Index: 317 Fraction: 65/76 = 0.8553
0x0506, // Index: 318 Fraction: 6/7 = 0.8571
0x424D, // Index: 319 Fraction: 67/78 = 0.8590
0x3C46, // Index: 320 Fraction: 61/71 = 0.8592
0x363F, // Index: 321 Fraction: 55/64 = 0.8594
0x3038, // Index: 322 Fraction: 49/57 = 0.8596
0x2A31, // Index: 323 Fraction: 43/50 = 0.8600
0x242A, // Index: 324 Fraction: 37/43 = 0.8605
0x434E, // Index: 325 Fraction: 68/79 = 0.8608
0x1E23, // Index: 326 Fraction: 31/36 = 0.8611
0x3740, // Index: 327 Fraction: 56/65 = 0.8615
0x181C, // Index: 328 Fraction: 25/29 = 0.8621
0x444F, // Index: 329 Fraction: 69/80 = 0.8625
0x2B32, // Index: 330 Fraction: 44/51 = 0.8627
0x3E48, // Index: 331 Fraction: 63/73 = 0.8630
0x1215, // Index: 332 Fraction: 19/22 = 0.8636
0x323A, // Index: 333 Fraction: 51/59 = 0.8644
0x1F24, // Index: 334 Fraction: 32/37 = 0.8649
0x2C33, // Index: 335 Fraction: 45/52 = 0.8654
0x3942, // Index: 336 Fraction: 58/67 = 0.8657
0x0C0E, // Index: 337 Fraction: 13/15 = 0.8667
0x3A43, // Index: 338 Fraction: 59/68 = 0.8676
0x2D34, // Index: 339 Fraction: 46/53 = 0.8679
0x2025, // Index: 340 Fraction: 33/38 = 0.8684
0x343C, // Index: 341 Fraction: 53/61 = 0.8689
0x1316, // Index: 342 Fraction: 20/23 = 0.8696
0x424C, // Index: 343 Fraction: 67/77 = 0.8701
0x2E35, // Index: 344 Fraction: 47/54 = 0.8704
0x1A1E, // Index: 345 Fraction: 27/31 = 0.8710
0x3C45, // Index: 346 Fraction: 61/70 = 0.8714
0x2126, // Index: 347 Fraction: 34/39 = 0.8718
0x282E, // Index: 348 Fraction: 41/47 = 0.8723
0x2F36, // Index: 349 Fraction: 48/55 = 0.8727
0x363E, // Index: 350 Fraction: 55/63 = 0.8730
0x3D46, // Index: 351 Fraction: 62/71 = 0.8732
0x444E, // Index: 352 Fraction: 69/79 = 0.8734
0x0607, // Index: 353 Fraction: 7/8 = 0.8750
0x3F48, // Index: 354 Fraction: 64/73 = 0.8767
0x3840, // Index: 355 Fraction: 57/65 = 0.8769
0x3138, // Index: 356 Fraction: 50/57 = 0.8772
0x2A30, // Index: 357 Fraction: 43/49 = 0.8776
0x2328, // Index: 358 Fraction: 36/41 = 0.8780
0x4049, // Index: 359 Fraction: 65/74 = 0.8784
0x1C20, // Index: 360 Fraction: 29/33 = 0.8788
0x3239, // Index: 361 Fraction: 51/58 = 0.8793
0x1518, // Index: 362 Fraction: 22/25 = 0.8800
0x3A42, // Index: 363 Fraction: 59/67 = 0.8806
0x2429, // Index: 364 Fraction: 37/42 = 0.8810
0x333A, // Index: 365 Fraction: 52/59 = 0.8814
0x424B, // Index: 366 Fraction: 67/76 = 0.8816
0x0E10, // Index: 367 Fraction: 15/17 = 0.8824
0x434C, // Index: 368 Fraction: 68/77 = 0.8831
0x343B, // Index: 369 Fraction: 53/60 = 0.8833
0x252A, // Index: 370 Fraction: 38/43 = 0.8837
0x3C44, // Index: 371 Fraction: 61/69 = 0.8841
0x1619, // Index: 372 Fraction: 23/26 = 0.8846
0x353C, // Index: 373 Fraction: 54/61 = 0.8852
0x1E22, // Index: 374 Fraction: 31/35 = 0.8857
0x454E, // Index: 375 Fraction: 70/79 = 0.8861
0x262B, // Index: 376 Fraction: 39/44 = 0.8864
0x2E34, // Index: 377 Fraction: 47/53 = 0.8868
0x363D, // Index: 378 Fraction: 55/62 = 0.8871
0x3E46, // Index: 379 Fraction: 63/71 = 0.8873
0x464F, // Index: 380 Fraction: 71/80 = 0.8875
0x0708, // Index: 381 Fraction: 8/9 = 0.8889
0x4048, // Index: 382 Fraction: 65/73 = 0.8904
0x383F, // Index: 383 Fraction: 57/64 = 0.8906
0x3036, // Index: 384 Fraction: 49/55 = 0.8909
0x282D, // Index: 385 Fraction: 41/46 = 0.8913
0x2024, // Index: 386 Fraction: 33/37 = 0.8919
0x3940, // Index: 387 Fraction: 58/65 = 0.8923
0x181B, // Index: 388 Fraction: 25/28 = 0.8929
0x424A, // Index: 389 Fraction: 67/75 = 0.8933
0x292E, // Index: 390 Fraction: 42/47 = 0.8936
0x3A41, // Index: 391 Fraction: 59/66 = 0.8939
0x1012, // Index: 392 Fraction: 17/19 = 0.8947
0x3B42, // Index: 393 Fraction: 60/67 = 0.8955
0x2A2F, // Index: 394 Fraction: 43/48 = 0.8958
0x444C, // Index: 395 Fraction: 69/77 = 0.8961
0x191C, // Index: 396 Fraction: 26/29 = 0.8966
0x3C43, // Index: 397 Fraction: 61/68 = 0.8971
0x2226, // Index: 398 Fraction: 35/39 = 0.8974
0x2B30, // Index: 399 Fraction: 44/49 = 0.8980
0x343A, // Index: 400 Fraction: 53/59 = 0.8983
0x3D44, // Index: 401 Fraction: 62/69 = 0.8986
0x464E, // Index: 402 Fraction: 71/79 = 0.8987
0x0809, // Index: 403 Fraction: 9/10 = 0.9000
0x3F46, // Index: 404 Fraction: 64/71 = 0.9014
0x363C, // Index: 405 Fraction: 55/61 = 0.9016
0x2D32, // Index: 406 Fraction: 46/51 = 0.9020
0x2428, // Index: 407 Fraction: 37/41 = 0.9024
0x4047, // Index: 408 Fraction: 65/72 = 0.9028
0x1B1E, // Index: 409 Fraction: 28/31 = 0.9032
0x2E33, // Index: 410 Fraction: 47/52 = 0.9038
0x4148, // Index: 411 Fraction: 66/73 = 0.9041
0x1214, // Index: 412 Fraction: 19/21 = 0.9048
};

View File

@@ -0,0 +1,29 @@
// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <platform.h>
#include <xs1.h>
#include <stdlib.h>
extern void sw_pll_test(void);
extern "C" {
#include "clock_gen.h"
}
int main(void)
{
par
{
on tile[0]: par {
}
on tile[1]: par {
sw_pll_test();
{
clock_gen(48000, 500);
exit(0);
}
}
}
return 0;
}

View File

@@ -0,0 +1,16 @@
/* Autogenerated by app_pll_model.py using command:
/Users/ed/sandboxes/lib_sw_pll/python/sw_pll/pll_calc.py -i 24.0 -a -m 80 -t 12.288 -p 6.0 -e 5 -r --fracmin 0.695 --fracmax 0.905 --header
Picked output solution #62
Input freq: 24000000
F: 203
R: 1
f: 3
p: 4
OD: 1
ACD: 24
Output freq: 12288000.0
VCO freq: 2457600000.0 */
#define APP_PLL_CTL_REG 0x0880CB01
#define APP_PLL_DIV_REG 0x80000018
#define APP_PLL_FRAC_REG 0x80000304

View File

@@ -0,0 +1,73 @@
// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdio.h>
#include <stdlib.h>
#include <xscope.h>
#include <xs1.h>
#include "sw_pll.h"
#include "resource_setup.h"
#define MCLK_FREQUENCY 12288000
#define REF_FREQUENCY 48000
#define PLL_RATIO (MCLK_FREQUENCY / REF_FREQUENCY)
#define CONTROL_LOOP_COUNT 512
#define PPM_RANGE 500
// These are generated from sw_pll_sim.py
#include "fractions.h"
#include "register_setup.h"
void sw_pll_test(void){
// Declare mclk and refclk resources and connect up
port_t p_mclk = PORT_MCLK_IN;
xclock_t clk_mclk = XS1_CLKBLK_1;
port_t p_clock_counter = PORT_I2S_LRCLK;
xclock_t clk_ref_clk = XS1_CLKBLK_2;
port_t p_ref_clk_timing = XS1_PORT_32A;
setup_ref_and_mclk_ports_and_clocks(p_mclk, clk_mclk, p_clock_counter, clk_ref_clk, p_ref_clk_timing);
// Make a test output to observe the recovered mclk divided down to the refclk frequency
xclock_t clk_recovered_ref_clk = XS1_CLKBLK_3;
port_t p_recovered_ref_clk = PORT_I2S_DAC_DATA;
setup_recovered_ref_clock_output(p_recovered_ref_clk, clk_recovered_ref_clk, p_mclk, PLL_RATIO);
sw_pll_state_t sw_pll;
sw_pll_lut_init(&sw_pll,
SW_PLL_15Q16(0.0),
SW_PLL_15Q16(1.0),
SW_PLL_15Q16(0.0),
CONTROL_LOOP_COUNT,
PLL_RATIO,
0, /* No jitter compensation needed */
frac_values_80,
SW_PLL_NUM_LUT_ENTRIES(frac_values_80),
APP_PLL_CTL_REG,
APP_PLL_DIV_REG,
SW_PLL_NUM_LUT_ENTRIES(frac_values_80) / 2,
PPM_RANGE);
sw_pll_lock_status_t lock_status = SW_PLL_LOCKED;
uint32_t max_time = 0;
while(1)
{
port_in(p_ref_clk_timing); // This blocks each time round the loop until it can sample input (rising edges of word clock). So we know the count will be +1 each time.
uint16_t mclk_pt = port_get_trigger_time(p_clock_counter);// Get the port timer val from p_clock_counter (which is clocked running from the PLL output).
uint32_t t0 = get_reference_time();
sw_pll_lut_do_control(&sw_pll, mclk_pt, 0);
uint32_t t1 = get_reference_time();
if(t1 - t0 > max_time){
max_time = t1 - t0;
printf("Max ticks taken: %lu\n", max_time);
}
if(sw_pll.lock_status != lock_status){
lock_status = sw_pll.lock_status;
const char msg[3][16] = {"UNLOCKED LOW\0", "LOCKED\0", "UNLOCKED HIGH\0"};
printf("%s\n", msg[lock_status+1]);
}
}
}

View File

@@ -0,0 +1,50 @@
#**********************
# Gather Sources
#**********************
file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.c
${CMAKE_CURRENT_LIST_DIR}/src/*.xc
${CMAKE_CURRENT_LIST_DIR}/../shared/src/*.c )
set(APP_INCLUDES ${CMAKE_CURRENT_LIST_DIR}/src
${CMAKE_CURRENT_LIST_DIR}/../shared/src
)
#**********************
# Flags
#**********************
set(APP_COMPILER_FLAGS
-Os
-g
-report
-fxscope
-mcmodel=large
-Wno-xcore-fptrgroup
${CMAKE_CURRENT_LIST_DIR}/src/config.xscope
-target=XCORE-AI-EXPLORER
)
set(APP_COMPILE_DEFINITIONS
DEBUG_PRINT_ENABLE=1
PLATFORM_SUPPORTS_TILE_0=1
PLATFORM_SUPPORTS_TILE_1=1
PLATFORM_SUPPORTS_TILE_2=0
PLATFORM_SUPPORTS_TILE_3=0
PLATFORM_USES_TILE_0=1
PLATFORM_USES_TILE_1=1
)
set(APP_LINK_OPTIONS
-report
-target=XCORE-AI-EXPLORER
${CMAKE_CURRENT_LIST_DIR}/src/config.xscope
)
#**********************
# Tile Targets
#**********************
add_executable(simple_sdm)
target_sources(simple_sdm PUBLIC ${APP_SOURCES})
target_include_directories(simple_sdm PUBLIC ${APP_INCLUDES})
target_compile_definitions(simple_sdm PRIVATE ${APP_COMPILE_DEFINITIONS})
target_compile_options(simple_sdm PRIVATE ${APP_COMPILER_FLAGS})
target_link_options(simple_sdm PRIVATE ${APP_LINK_OPTIONS})
target_link_libraries(simple_sdm PUBLIC lib_sw_pll)

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ======================================================= -->
<!-- The 'ioMode' attribute on the xSCOPEconfig -->
<!-- element can take the following values: -->
<!-- "none", "basic", "timed" -->
<!-- -->
<!-- The 'type' attribute on Probe -->
<!-- elements can take the following values: -->
<!-- "STARTSTOP", "CONTINUOUS", "DISCRETE", "STATEMACHINE" -->
<!-- -->
<!-- The 'datatype' attribute on Probe -->
<!-- elements can take the following values: -->
<!-- "NONE", "UINT", "INT", "FLOAT" -->
<!-- ======================================================= -->
<xSCOPEconfig ioMode="basic" enabled="true">
<!-- For example: -->
<!-- <Probe name="I2S_RX_0" type="CONTINUOUS" datatype="INT" units="Value" enabled="true"/> -->
<!-- From the target code, call: xscope_int(PROBE_NAME, value); -->
</xSCOPEconfig>

View File

@@ -0,0 +1,33 @@
// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <platform.h>
#include <xs1.h>
#include <stdlib.h>
extern void sw_pll_sdm_test(chanend c_sdm_control);
extern void sdm_task(chanend c_sdm_control);
extern "C" {
#include "clock_gen.h"
}
int main(void)
{
chan c_sdm_control;
par
{
on tile[0]: par {
}
on tile[1]: par {
sw_pll_sdm_test(c_sdm_control);
sdm_task(c_sdm_control);
{
clock_gen(96000, 300);
exit(0);
}
}
}
return 0;
}

View File

@@ -0,0 +1,15 @@
/* Autogenerated SDM App PLL setup by dco_model.py using 24.576_1M profile */
/* Input freq: 24000000
F: 146
R: 0
f: 4
p: 10
OD: 5
ACD: 5
*/
#define APP_PLL_CTL_REG 0x0A809200
#define APP_PLL_DIV_REG 0x80000005
#define APP_PLL_FRAC_REG 0x8000040A
#define SW_PLL_SDM_CTRL_MID 478151
#define SW_PLL_SDM_RATE 1000000

View File

@@ -0,0 +1,137 @@
// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <xscope.h>
#include <xs1.h>
#include <platform.h>
#include <xcore/select.h>
#include "sw_pll.h"
#include "resource_setup.h"
#define MCLK_FREQUENCY 24576000
#define REF_FREQUENCY 96000
#define PLL_RATIO (MCLK_FREQUENCY / REF_FREQUENCY)
#define CONTROL_LOOP_COUNT 512
#include "register_setup.h"
void sdm_task(chanend_t c_sdm_control){
printf("sdm_task\n");
const uint32_t sdm_interval = XS1_TIMER_HZ / SW_PLL_SDM_RATE; // in 10ns ticks = 1MHz
sw_pll_sdm_state_t sdm_state;
sw_pll_init_sigma_delta(&sdm_state);
tileref_t this_tile = get_local_tile_id();
hwtimer_t tmr = hwtimer_alloc();
int32_t trigger_time = hwtimer_get_time(tmr) + sdm_interval;
bool running = true;
int32_t sdm_in = 0; // Zero is an invalid number and the SDM will not write the frac reg until
// the first control value has been received. This avoids issues with
// channel lockup if two tasks (eg. init and SDM) try to write at the same
// time.
while(running){
// Poll for new SDM control value
SELECT_RES(
CASE_THEN(c_sdm_control, ctrl_update),
DEFAULT_THEN(default_handler)
)
{
ctrl_update:
{
sdm_in = chan_in_word(c_sdm_control);
}
break;
default_handler:
{
// Do nothing & fall-through
}
break;
}
// Wait until the timer value has been reached
// This implements a timing barrier and keeps
// the loop rate constant.
hwtimer_wait_until(tmr, trigger_time);
trigger_time += sdm_interval;
// Do not write to the frac reg until we get out first
// control value. This will avoid the writing of the
// frac reg from two different threads which may cause
// a channel deadlock.
if(sdm_in){
sw_pll_do_sigma_delta(&sdm_state, this_tile, sdm_in);
}
}
}
void sw_pll_send_ctrl_to_sdm_task(chanend_t c_sdm_control, int32_t dco_ctl){
chan_out_word(c_sdm_control, dco_ctl);
}
void sw_pll_sdm_test(chanend_t c_sdm_control){
// Declare mclk and refclk resources and connect up
port_t p_mclk = PORT_MCLK_IN;
xclock_t clk_mclk = XS1_CLKBLK_1;
port_t p_clock_counter = PORT_I2S_LRCLK;
xclock_t clk_ref_clk = XS1_CLKBLK_2;
port_t p_ref_clk_timing = XS1_PORT_32A;
setup_ref_and_mclk_ports_and_clocks(p_mclk, clk_mclk, p_clock_counter, clk_ref_clk, p_ref_clk_timing);
// Make a test output to observe the recovered mclk divided down to the refclk frequency
xclock_t clk_recovered_ref_clk = XS1_CLKBLK_3;
port_t p_recovered_ref_clk = PORT_I2S_DAC_DATA;
setup_recovered_ref_clock_output(p_recovered_ref_clk, clk_recovered_ref_clk, p_mclk, PLL_RATIO);
sw_pll_state_t sw_pll;
sw_pll_sdm_init(&sw_pll,
SW_PLL_15Q16(0.0),
SW_PLL_15Q16(32.0),
SW_PLL_15Q16(0.25),
CONTROL_LOOP_COUNT,
PLL_RATIO,
0, /* No jitter compensation needed */
APP_PLL_CTL_REG,
APP_PLL_DIV_REG,
APP_PLL_FRAC_REG,
SW_PLL_SDM_CTRL_MID,
3000 /*PPM_RANGE FOR PFD*/);
sw_pll_lock_status_t lock_status = SW_PLL_LOCKED;
uint32_t max_time = 0;
while(1)
{
port_in(p_ref_clk_timing); // This blocks each time round the loop until it can sample input (rising edges of word clock). So we know the count will be +1 each time.
uint16_t mclk_pt = port_get_trigger_time(p_clock_counter);// Get the port timer val from p_clock_counter (which is running from MCLK). So this is basically a 16 bit free running counter running from MCLK.
uint32_t t0 = get_reference_time();
bool ctrl_done = sw_pll_sdm_do_control(&sw_pll, mclk_pt, 0);
uint32_t t1 = get_reference_time();
if(ctrl_done){
sw_pll_send_ctrl_to_sdm_task(c_sdm_control, sw_pll.sdm_state.current_ctrl_val);
}
if(t1 - t0 > max_time){
max_time = t1 - t0;
printf("Max ticks taken: %lu\n", max_time);
}
if(sw_pll.lock_status != lock_status){
lock_status = sw_pll.lock_status;
const char msg[3][16] = {"UNLOCKED LOW\0", "LOCKED\0", "UNLOCKED HIGH\0"};
printf("%s\n", msg[lock_status+1]);
}
}
}

View 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}
)

View 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

View 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()

View 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

View 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__

View 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);

View 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__

View 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__

View 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;
}
}

View 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__

View 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

19
lib_sw_pll/settings.yml Normal file
View File

@@ -0,0 +1,19 @@
---
################################
# Settings file for docs build #
################################
project: SW_PLL
version: 2.2.0
documentation:
title: Software PLL
root_doc: doc/index.rst
exclude_patterns_path: doc/exclude-patterns.inc
substitutions_path: doc/substitutions.inc
doxygen_projects:
SW_PLL:
doxyfile_path: doc/Doxyfile.inc
pdfs:
doc/index:
pdf_title: '{{project}} Programming Guide \newline'
pdf_filename: '{{project}}_programming_guide_v{{version}}'

17
lib_sw_pll/setup.py Normal file
View File

@@ -0,0 +1,17 @@
# Copyright 2023 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from setuptools import setup, find_packages
setup(
name="sw_pll",
version="2.0.0",
packages=["sw_pll"],
package_dir={
"": "python"
},
install_requires=[
"numpy",
"matplotlib",
"pyvcd"
]
)

View File

@@ -0,0 +1,32 @@
#! /usr/bin/env bash
#
# clone required repos and install python packages needed by ci
# clone NAME URL HASH
#
# supposedly, this is the quickest way to clone a single
# commit from a remote git repo
clone() {
mkdir $1
pushd $1
git init
git remote add origin $2
git fetch --depth 1 origin $3
git checkout FETCH_HEAD
git submodule update --init --recursive --depth 1 --jobs $(nproc)
popd
}
set -ex
rm -rf modules
mkdir -p modules
pushd modules
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 9a0e6899ffae42d82e6047b49e1186eee42289af
pip install -e infr_apps -e infr_scripts_py
popd

View File

@@ -0,0 +1,8 @@
#! /usr/bin/env bash
#
# 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 test_app_sdm_dco --target test_app_sdm_ctrl --target simple_lut --target simple_sdm --target i2s_slave_lut -j$(nproc)

View File

@@ -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

View File

@@ -0,0 +1,11 @@
#! /usr/bin/env bash
#
# test stuff
set -ex
pushd python/sw_pll
python sw_pll_sim.py LUT
python sw_pll_sim.py SDM
popd