CircuitPython Code

It's easy to use the Si5351 clock generator with CircuitPython and the Adafruit CircuitPython SI5351 module.  This module allows you to easily write Python code that controls the clock output of the board.

First wire up a Si5351 to your board exactly as shown on the previous pages for Arduino using an I2C connection.  Here's an example of wiring a Feather M0 to the sensor with I2C:

  • Board 3V to sensor VIN
  • Board GND to sensor GND
  • Board SCL to sensor SCL
  • Board SDA to sensor SDA

Next you'll need to install the Adafruit CircuitPython SI5351 library on your CircuitPython board.

First make sure you are running the latest version of Adafruit CircuitPython for your board.

Next you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle.  Our introduction guide has a great page on how to install the library bundle for both express and non-express boards.

Remember for non-express boards like the, you'll need to manually install the necessary libraries from the bundle:

  • adafruit_si5351.mpy
  • adafruit_bus_device

You can also download the adafruit_si5351.mpy from its releases page on Github.

Before continuing make sure your board's lib folder or root filesystem has the adafruit_si5351.mpy, and adafruit_bus_device files and folders copied over.

Next connect to the board's serial REPL so you are at the CircuitPython >>> prompt.

Usage

To demonstrate the usage of the sensor we'll initialize it and control the clocks from the board's Python REPL.  Run the following code to import the necessary modules and initialize the I2C connection with the sensor:

import board
import busio
import adafruit_si5351
i2c = busio.I2C(board.SCL, board.SDA)
si5351 = adafruit_si5351.SI5351(i2c)

Remember if you're using a board that doesn't support hardware I2C (like the ESP8266) you need to use the bitbangio module instead:

import board
import bitbangio
import adafruit_si5351
i2c = bitbangio.I2C(board.SCL, board.SDA)
si5351 = adafruit_si5351.SI5351(i2c)

Now you're ready to configure the PLLs and clock outputs on the chip.  The SI5351 is somewhat complex and allows quite many configurations to balance the accuracy and range of clock output values.  You'll want to review the datasheet to understand the capabilities and features of the chip.

Configure Phase Lock Loops

First you'll need to configure one of the two PLLs (phase-locked loops, special hardware that can 'multiply' the 25 MHz crystal to much faster speeds) which will be used as the source of any of the clock outputs.  The Si5351 object has two PLL objects you can manipulate:

  • pll_a This is the PLL A hardware on the chip.
  • pll_b This is the PLL B hardware on the chip.

You can configure each PLL's multiplier by calling the configure_integer or configure_fractional function.  The integer configuration lets you set an integer multiplier for the 25 MHz base crystal frequency.  This is the most accurate way to control frequency but you can only set integer multiples.  For example to set PLL A to 500 MHz you'd set an integer multiplier of 20 (25 MHz * 20 = 500 MHz):

si5351.pll_a.configure_integer(20)

Don't forget, if you read the datasheet you'll see the Si5351 only supports a PLL multiple of 15-90!

If you're curious you can read the frequency property of the PLL to read back what frequency it is configured to use:

print('PLL A: {0} MHz'.format(si5351.pll_a.frequency/1000000))

The other way to configure a PLL is with a fractional multiplier.  This allows you to set both the integer multiple and a fractional multiple (specified by a numerator and denominator).  Fractional multipliers can let you specify an even greater range of frequencies, but they might not be accurate and could be susceptible to jitter over time.

For example to configure PLL B to 512.5 MHz you can specify a multiplier of 20.5 by calling:

si5351.pll_b.configure_fractional(20, 1, 2)

Notice the configure_fractional function takes in again the integer multiple (20) but also adds a numerator (1) and denominator (2) value.  The actual multiplier will be the combination of the integer multiplier plus the numerator/denominator.  In this case 20.5 or 20 1/2 is the multiplier.

Again you can read the frequency property to confirm the value that was set.

print('PLL B: {0} MHz'.format(si5351.pll_b.frequency/1000000))

Configure Clock Outputs

Once a PLL is configured you can then configure any of the three clock outputs of the Si5351.  Each clock output is fed in a PLL as input and will divide it down to a final frequency value.  Any PLL can be plugged in to any clock output, but remember you only have two PLLs to configure and use. 

Each of the clocks is exposed on the SI5351 object:

  • clock_0 The clock 0 output.
  • clock_1 The clock 1 output.
  • clock_2 The clock 2 output.

Just like configuring the PLLs you can configure the clock divider with a configure_integer and configure_fractional function.  Again the same trade-off of precision vs. range applies for integer vs. fractional dividers applies.  For example to set clock 0 to use PLL A as a source (running at 500 MHz as configured above) and a divider of 4 you could call:

si5351.clock_0.configure_integer(si5351.pll_a, 4)

Notice you tell the configure_integer function both the PLL to use and the integer divider value.  You can point at either of the pll_a or pll_b objects on the Si5351 to use a specific PLL as the source.

The final frequency set by the clock will then be configured as the PLL source frequency (500 MHz here) divided by the divider value (4), or 125 MHz.  You can check by reading the frequency property of the clock:

print('Clock 0: {0:0.3f} MHz'.format(si5351.clock_0.frequency/1000000))

Like the PLL configuration you can also set a fractional divider with the configure_fractional function on the clock.  Let's set clock 1 to have PLL B as the source and a divider of 4.5:

si5351.clock_1.configure_fractional(si5351.pll_b, 4, 1, 2)

If you do the math you should expect a source PLL of 512.5 MHz divided by 4.5 would yield a clock output of 113.8889 MHz.  Check the frequency property to be sure:

print('Clock 1: {0:0.3f} MHz'.format(si5351.clock_1.frequency/1000000))

Enabling Outputs

There's one last thing to do before the clock outputs will actually work, you need to enable them.  You can enable all the clock outputs by setting the outputs_enabled property to True:

si5351.outputs_enabled = True

Now the clock 0, 1, 2 outputs should output a square wave at the configured frequency based on their PLL and multipliers & dividers.  You might need to use an oscilloscope that's very fast to measure some of these outputs as the Si5351 can generate quite high clockspeeds well above 100 MHz!

If you want to disable the outputs set outputs_enabled to False.

That's all there is to using the Si5351 clock generator with CircuitPython!  Here's a complete example that will configure all 3 clocks in various ways.  Save this as main.py on your board and read the comments to see how it configures each clock:

# Simple demo of the SI5351 clock generator.
# This is like the Arduino library example:
#   https://github.com/adafruit/Adafruit_Si5351_Library/blob/master/examples/si5351/si5351.ino
# Which will configure the chip with:
#  - PLL A at 900mhz
#  - PLL B at 616.66667mhz
#  - Clock 0 at 112.5mhz, using PLL A as a source divided by 8
#  - Clock 1 at 13.553115mhz, using PLL B as a source divided by 45.5
#  - Clock 2 at 10.76khz, using PLL B as a source divided by 900 and further
#    divided with an R divider of 64.
import board
import busio

import adafruit_si5351


# Initialize I2C bus.
i2c = busio.I2C(board.SCL, board.SDA)

# Initialize SI5351.
si5351 = adafruit_si5351.SI5351(i2c)
# Alternatively you can specify the I2C address if it has been changed:
#si5351 = adafruit_si5351.SI5351(i2c, address=0x61)

# Now configue the PLLs and clock outputs.
# The PLLs can be configured with a multiplier and division of the on-board
# 25mhz reference crystal.  For example configure PLL A to 900mhz by multiplying
# by 36.  This uses an integer multiplier which is more accurate over time
# but allows less of a range of frequencies compared to a fractional
# multiplier shown next.
si5351.pll_a.configure_integer(36)  # Multiply 25mhz by 36
print('PLL A frequency: {0}mhz'.format(si5351.pll_a.frequency/1000000))

# And next configure PLL B to 616.6667mhz by multiplying 25mhz by 24.667 using
# the fractional multiplier configuration.  Notice you specify the integer
# multiplier and then a numerator and denominator as separate values, i.e.
# numerator 2 and denominator 3 means 2/3 or 0.667.  This fractional
# configuration is susceptible to some jitter over time but can set a larger
# range of frequencies.
si5351.pll_b.configure_fractional(24, 2, 3)  # Multiply 25mhz by 24.667 (24 2/3)
print('PLL B frequency: {0}mhz'.format(si5351.pll_b.frequency/1000000))

# Now configure the clock outputs.  Each is driven by a PLL frequency as input
# and then further divides that down to a specific frequency.
# Configure clock 0 output to be driven by PLL A divided by 8, so an output
# of 112.5mhz (900mhz / 8).  Again this uses the most precise integer division
# but can't set as wide a range of values.
si5351.clock_0.configure_integer(si5351.pll_a, 8)
print('Clock 0: {0}mhz'.format(si5351.clock_0.frequency/1000000))

# Next configure clock 1 to be driven by PLL B divided by 45.5 to get
# 13.5531mhz (616.6667mhz / 45.5).  This uses fractional division and again
# notice the numerator and denominator are explicitly specified.  This is less
# precise but allows a large range of frequencies.
si5351.clock_1.configure_fractional(si5351.pll_b, 45, 1, 2) # Divide by 45.5 (45 1/2)
print('Clock 1: {0}mhz'.format(si5351.clock_1.frequency/1000000))

# Finally configure clock 2 to be driven by PLL B divided once by 900 to get
# down to 685.15 khz and then further divided by a special R divider that
# divides 685.15 khz by 64 to get a final output of 10.706khz.
si5351.clock_2.configure_integer(si5351.pll_b, 900)
# Set the R divider, this can be a value of:
#  - R_DIV_1: divider of 1
#  - R_DIV_2: divider of 2
#  - R_DIV_4: divider of 4
#  - R_DIV_8: divider of 8
#  - R_DIV_16: divider of 16
#  - R_DIV_32: divider of 32
#  - R_DIV_64: divider of 64
#  - R_DIV_128: divider of 128
si5351.clock_2.r_divider = adafruit_si5351.R_DIV_64
print('Clock 2: {0}khz'.format(si5351.clock_2.frequency/1000))

# After configuring PLLs and clocks, enable the outputs.
si5351.outputs_enabled = True
# You can disable them by setting false.
Last updated on 2018-02-04 at 09.32.23 PM Published on 2014-08-12 at 05.53.05 PM