diff --git a/examples/si5351/main.go b/examples/si5351/main.go new file mode 100644 index 000000000..5ecef7d7f --- /dev/null +++ b/examples/si5351/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/si5351" +) + +// 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.5531mhz, 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. + +func main() { + + time.Sleep(5 * time.Second) + + println("Si5351 Clockgen Test") + println() + + // Configure I2C bus + machine.I2C0.Configure(machine.I2CConfig{}) + + // Create driver instance + clockgen := si5351.New(machine.I2C0) + + // Verify device wired properly + if !clockgen.Connected() { + for { + println("Ooops, no Si5351 detected ... Check your wiring!") + time.Sleep(time.Second) + } + } + + // Initialise device + clockgen.Configure() + + // 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. + clockgen.ConfigurePLL(si5351.PLL_A, 36, 0, 1) // Multiply 25mhz by 36 + println("PLL A frequency: 900mhz") + + // 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. + clockgen.ConfigurePLL(si5351.PLL_B, 24, 2, 3) // Multiply 25mhz by 24.667 (24 2/3) + println("PLL B frequency: 616.6667mhz") + + // 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. + clockgen.ConfigureMultisynth(0, si5351.PLL_A, 8, 0, 1) // Divide by 8 (8 0/1) + println("Clock 0: 112.5mhz") + + // 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. + clockgen.ConfigureMultisynth(1, si5351.PLL_B, 45, 1, 2) // Divide by 45.5 (45 1/2) + println("Clock 1: 13.5531mhz") + + // 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. + clockgen.ConfigureMultisynth(2, si5351.PLL_B, 900, 0, 1) // Divide by 900 (900 0/1) + // 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 + clockgen.ConfigureRdiv(2, si5351.R_DIV_64) + println("Clock 2: 10.706khz") + + // After configuring PLLs and clocks, enable the outputs. + clockgen.EnableOutputs() + + for { + time.Sleep(5 * time.Second) + println() + println("Clock 0: 112.5mhz") + println("Clock 1: 13.5531mhz") + println("Clock 2: 10.706khz") + } + +} diff --git a/si5351/registers.go b/si5351/registers.go new file mode 100644 index 000000000..79300adfb --- /dev/null +++ b/si5351/registers.go @@ -0,0 +1,64 @@ +package si5351 + +// The I2C address which this device listens to. +const AddressDefault = 0x60 // Assumes ADDR pin is low +const AddressAlternative = 0x61 // Assumes ADDR pin is high + +const ( + OUTPUT_ENABLE_CONTROL = 3 + + CLK0_CONTROL = 16 + CLK1_CONTROL = 17 + CLK2_CONTROL = 18 + CLK3_CONTROL = 19 + CLK4_CONTROL = 20 + CLK5_CONTROL = 21 + CLK6_CONTROL = 22 + CLK7_CONTROL = 23 + + MULTISYNTH0_PARAMETERS_1 = 42 + MULTISYNTH0_PARAMETERS_3 = 44 + MULTISYNTH1_PARAMETERS_1 = 50 + MULTISYNTH1_PARAMETERS_3 = 52 + MULTISYNTH2_PARAMETERS_1 = 58 + MULTISYNTH2_PARAMETERS_3 = 60 + + SPREAD_SPECTRUM_PARAMETERS = 149 + + PLL_RESET = 177 + + CRYSTAL_INTERNAL_LOAD_CAPACITANCE = 183 +) + +const ( + CRYSTAL_LOAD_6PF = (1 << 6) + CRYSTAL_LOAD_8PF = (2 << 6) + CRYSTAL_LOAD_10PF = (3 << 6) +) + +const ( + CRYSTAL_FREQ_25MHZ = 25000000 + CRYSTAL_FREQ_27MHZ = 27000000 +) + +const ( + PLL_A = iota + PLL_B +) + +const ( + R_DIV_1 = iota + R_DIV_2 + R_DIV_4 + R_DIV_8 + R_DIV_16 + R_DIV_32 + R_DIV_64 + R_DIV_128 +) + +const ( + MULTISYNTH_DIV_4 = 4 + MULTISYNTH_DIV_6 = 6 + MULTISYNTH_DIV_8 = 8 +) diff --git a/si5351/si5351.go b/si5351/si5351.go new file mode 100644 index 000000000..65f564728 --- /dev/null +++ b/si5351/si5351.go @@ -0,0 +1,443 @@ +package si5351 + +import ( + "errors" + "math" + + "tinygo.org/x/drivers" +) + +// Device wraps an I2C connection to a SI5351 device. +type Device struct { + bus drivers.I2C + Address uint8 + + buf [8]byte + initialised bool + crystalFreq uint32 + crystalLoad uint8 + pllaConfigured bool + pllaFreq uint32 + pllbConfigured bool + pllbFreq uint32 + lastRdivValue [3]uint8 +} + +var errNotInitialised = errors.New("Si5351 not initialised") +var errInvalidParameter = errors.New("Si5351 invalid parameter") + +// New creates a new SI5351 connection. The I2C bus must already be configured. +// +// This function only creates the Device object, it does not touch the device. +func New(bus drivers.I2C) Device { + return Device{ + bus: bus, + Address: AddressDefault, + crystalFreq: CRYSTAL_FREQ_25MHZ, + crystalLoad: CRYSTAL_LOAD_10PF, + } +} + +// Configure sets up the device for communication +// TODO error handling +func (d *Device) Configure() { + + data := d.buf[:1] + + // Disable all outputs setting CLKx_DIS high + data[0] = 0xFF + d.bus.WriteRegister(d.Address, OUTPUT_ENABLE_CONTROL, data) + + // Set the load capacitance for the XTAL + data[0] = d.crystalLoad + d.bus.WriteRegister(d.Address, CRYSTAL_INTERNAL_LOAD_CAPACITANCE, data) + + data = d.buf[:8] + + // Power down all output drivers + for i := range data { + data[i] = 0x80 + } + d.bus.WriteRegister(d.Address, CLK0_CONTROL, data) + + // Disable spread spectrum output. + d.DisableSpreadSpectrum() + + d.initialised = true + +} + +// Connected returns whether a device at SI5351 address has been found. +func (d *Device) Connected() bool { + err := d.bus.Tx(uint16(d.Address), []byte{}, []byte{0}) + return err == nil +} + +func (d *Device) EnableSpreadSpectrum() (err error) { + data := d.buf[:1] + err = d.bus.ReadRegister(d.Address, SPREAD_SPECTRUM_PARAMETERS, data) + if err != nil { + return + } + data[0] |= 0x80 + err = d.bus.WriteRegister(d.Address, SPREAD_SPECTRUM_PARAMETERS, data) + return +} + +func (d *Device) DisableSpreadSpectrum() (err error) { + data := d.buf[:1] + err = d.bus.ReadRegister(d.Address, SPREAD_SPECTRUM_PARAMETERS, data) + if err != nil { + return + } + data[0] &^= 0x80 + err = d.bus.WriteRegister(d.Address, SPREAD_SPECTRUM_PARAMETERS, data) + return +} + +func (d *Device) EnableOutputs() (err error) { + if !d.initialised { + return errNotInitialised + } + data := d.buf[:1] + data[0] = 0x00 + err = d.bus.WriteRegister(d.Address, OUTPUT_ENABLE_CONTROL, data) + return +} + +func (d *Device) DisableOutputs() (err error) { + if !d.initialised { + return errNotInitialised + } + data := d.buf[:1] + data[0] = 0xFF + err = d.bus.WriteRegister(d.Address, OUTPUT_ENABLE_CONTROL, data) + return +} + +// ConfigurePLL sets the multiplier for the specified PLL +// pll The PLL to configure, which must be one of the following: +// - PLL_A +// - PLL_B +// +// mult The PLL integer multiplier (must be between 15 and 90) +// +// num The 20-bit numerator for fractional output (0..1,048,575). +// Set this to '0' for integer output. +// +// denom The 20-bit denominator for fractional output (1..1,048,575). +// Set this to '1' or higher to avoid divider by zero errors. +// +// PLL Configuration +// fVCO is the PLL output, and must be between 600..900MHz, where: +// +// fVCO = fXTAL * (a+(b/c)) +// +// fXTAL = the crystal input frequency +// a = an integer between 15 and 90 +// b = the fractional numerator (0..1,048,575) +// c = the fractional denominator (1..1,048,575) +// +// NOTE: Try to use integers whenever possible to avoid clock jitter +// (only use the a part, setting b to '0' and c to '1'). +// +// See: http://www.silabs.com/Support%20Documents/TechnicalDocs/AN619.pdf +func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) (err error) { + + // Basic validation + if !d.initialised { + return errNotInitialised + } + // mult = 15..90 + if !((mult > 14) && (mult < 91)) { + return errInvalidParameter + } + // Avoid divide by zero + if !(denom > 0) { + return errInvalidParameter + } + // 20-bit limit + if !(num <= 0xFFFFF) { + return errInvalidParameter + } + // 20-bit limit + if !(denom <= 0xFFFFF) { + return errInvalidParameter + } + + // PLL Multiplier Equations + // + // P1 register is an 18-bit value using following formula: + // + // P1[17:0] = 128 * mult + floor(128*(num/denom)) - 512 + // + // P2 register is a 20-bit value using the following formula: + // + // P2[19:0] = 128 * num - denom * floor(128*(num/denom)) + // + // P3 register is a 20-bit value using the following formula: + // + // P3[19:0] = denom + // + + // Set PLL config registers + var p1, p2, p3 uint32 + if num == 0 { + // Integer mode + p1 = 128*uint32(mult) - 512 + p2 = num + p3 = denom + } else { + // Fractional mode + p1 = uint32(128*float64(mult) + math.Floor(128*(float64(num)/float64(denom))) - 512) + p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom)))) + p3 = denom + } + + // Get the appropriate starting point for the PLL registers + baseaddr := uint8(26) + if pll == PLL_B { + baseaddr = 34 + } + + // The datasheet is a nightmare of typos and inconsistencies here! + data := d.buf[:8] + data[0] = uint8((p3 & 0x0000FF00) >> 8) + data[1] = uint8(p3 & 0x000000FF) + data[2] = uint8((p1 & 0x00030000) >> 16) + data[3] = uint8((p1 & 0x0000FF00) >> 8) + data[4] = uint8(p1 & 0x000000FF) + data[5] = uint8(((p3 & 0x000F0000) >> 12) | ((p2 & 0x000F0000) >> 16)) + data[6] = uint8((p2 & 0x0000FF00) >> 8) + data[7] = uint8(p2 & 0x000000FF) + d.bus.WriteRegister(d.Address, baseaddr, data) + + // Reset both PLLs + data = d.buf[:1] + data[0] = (1 << 7) | (1 << 5) + d.bus.WriteRegister(d.Address, PLL_RESET, data) + + // Store the frequency settings for use with the Multisynth helper + fvco := float64(d.crystalFreq) * (float64(mult) + (float64(num) / float64(denom))) + if pll == PLL_A { + d.pllaConfigured = true + d.pllaFreq = uint32(math.Floor(fvco)) + } else { + d.pllbConfigured = true + d.pllbFreq = uint32(math.Floor(fvco)) + } + return +} + +// ConfigureMultisynth divider, which determines the +// output clock frequency based on the specified PLL input. +// +// output The output channel to use (0..2) +// +// pll The PLL input source to use, which must be one of: +// - PLL_A +// - PLL_B +// +// div The integer divider for the Multisynth output. +// +// If pure integer values are used, this value must be one of: +// - MULTISYNTH_DIV_4 +// - MULTISYNTH_DIV_6 +// - MULTISYNTH_DIV_8 +// If fractional output is used, this value must be between 8 and 900. +// +// num The 20-bit numerator for fractional output (0..1,048,575). +// +// Set this to '0' for integer output. +// +// denom The 20-bit denominator for fractional output (1..1,048,575). +// +// Set this to '1' or higher to avoid divide by zero errors. +// +// # Output Clock Configuration +// +// The multisynth dividers are applied to the specified PLL output, +// and are used to reduce the PLL output to a valid range (500kHz +// to 160MHz). The relationship can be seen in this formula, where +// fVCO is the PLL output frequency and MSx is the multisynth divider: +// +// fOUT = fVCO / MSx +// +// Valid multisynth dividers are 4, 6, or 8 when using integers, +// or any fractional values between 8 + 1/1,048,575 and 900 + 0/1 +// The following formula is used for the fractional mode divider: +// +// a + b / c +// +// a = The integer value, which must be 4, 6 or 8 in integer mode (MSx_INT=1) or 8..900 in fractional mode (MSx_INT=0). +// b = The fractional numerator (0..1,048,575) +// c = The fractional denominator (1..1,048,575) +// +// NOTE: Try to use integers whenever possible to avoid clock jitter +// NOTE: For output frequencies > 150MHz, you must set the divider +// +// to 4 and adjust to PLL to generate the frequency (for example +// a PLL of 640 to generate a 160MHz output clock). This is not +// yet supported in the driver, which limits frequencies to 500kHz .. 150MHz. +// +// NOTE: For frequencies below 500kHz (down to 8kHz) Rx_DIV must be +// +// used, but this isn't currently implemented in the driver. +func (d *Device) ConfigureMultisynth(output uint8, pll uint8, div uint32, num uint32, denom uint32) (err error) { + + // Basic validation + if !d.initialised { + return errNotInitialised + } + // Channel range + if !(output < 3) { + return errInvalidParameter + } + // Divider integer value + if !((div > 3) && (div < 2049)) { + return errInvalidParameter + } + // Avoid divide by zero + if !(denom > 0) { + return errInvalidParameter + } + // 20-bit limit + if !(num <= 0xFFFFF) { + return errInvalidParameter + } + // 20-bit limit + if !(denom <= 0xFFFFF) { + return errInvalidParameter + } + + // Make sure the requested PLL has been initialised + if pll == PLL_A && !d.pllaConfigured { + return errInvalidParameter + } + if pll == PLL_B && !d.pllbConfigured { + return errInvalidParameter + } + + // Output Multisynth Divider Equations + // + // where: a = div, b = num and c = denom + // + // P1 register is an 18-bit value using following formula: + // + // P1[17:0] = 128 * a + floor(128*(b/c)) - 512 + // + // P2 register is a 20-bit value using the following formula: + // + // P2[19:0] = 128 * b - c * floor(128*(b/c)) + // + // P3 register is a 20-bit value using the following formula: + // + // P3[19:0] = c + // + + // Set PLL config registers + var p1, p2, p3 uint32 + if num == 0 { + // Integer mode + p1 = 128*div - 512 + p2 = 0 + p3 = denom + } else if denom == 1 { + // Fractional mode, simplified calculations + p1 = 128*div + 128*num - 512 + p2 = 128*num - 128 + p3 = 1 + } else { + // Fractional mode + p1 = uint32(128*float64(div) + math.Floor(128*(float64(num)/float64(denom))) - 512) + p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom)))) + p3 = denom + } + + // Get the appropriate starting point for the PLL registers + baseaddr := uint8(0) + switch output { + case 0: + baseaddr = MULTISYNTH0_PARAMETERS_1 + break + case 1: + baseaddr = MULTISYNTH1_PARAMETERS_1 + break + case 2: + baseaddr = MULTISYNTH2_PARAMETERS_1 + break + } + + // Set the MSx config registers + data := d.buf[:8] + data[0] = uint8((p3 & 0xFF00) >> 8) + data[1] = uint8(p3 & 0xFF) + data[2] = uint8(((p1 & 0x30000) >> 16)) | d.lastRdivValue[output] + data[3] = uint8((p1 & 0xFF00) >> 8) + data[4] = uint8(p1 & 0xFF) + data[5] = uint8(((p3 & 0xF0000) >> 12) | ((p2 & 0xF0000) >> 16)) + data[6] = uint8((p2 & 0xFF00) >> 8) + data[7] = uint8(p2 & 0xFF) + err = d.bus.WriteRegister(d.Address, baseaddr, data) + if err != nil { + return + } + + // Configure the clk control and enable the output + // TODO: Check if the clk control byte needs to be updated. + clkControlReg := uint8(0x0F) // 8mA drive strength, MS0 as CLK0 source, Clock not inverted, powered up + if pll == PLL_B { + clkControlReg |= (1 << 5) // Uses PLLB + } + if num == 0 { + clkControlReg |= (1 << 6) // Integer mode + } + + var register uint8 + switch output { + case 0: + register = CLK0_CONTROL + break + case 1: + register = CLK1_CONTROL + break + case 2: + register = CLK2_CONTROL + break + } + + data = d.buf[:1] + data[0] = clkControlReg + err = d.bus.WriteRegister(d.Address, register, data) + + return +} + +func (d *Device) ConfigureRdiv(output uint8, div uint8) (err error) { + // Channel range + if !(output < 3) { + return errInvalidParameter + } + + var register uint8 + switch output { + case 0: + register = MULTISYNTH0_PARAMETERS_3 + case 1: + register = MULTISYNTH1_PARAMETERS_3 + case 2: + register = MULTISYNTH2_PARAMETERS_3 + } + + data := d.buf[:1] + err = d.bus.ReadRegister(d.Address, register, data) + if err != nil { + return + } + + d.lastRdivValue[output] = (div & 0x07) << 4 + data[0] = (data[0] & 0x0F) | d.lastRdivValue[output] + err = d.bus.WriteRegister(d.Address, register, data) + + return +}