Files
drivers/si5351/si5351.go
T
deadprogram 50778af656 si5351: add many missing functions needed for convenient use.
This adds many missing functions needed for convenient use that are
reinterpreted from the C code in https://github.com/dmalnati/picoinf

Thank you!

Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-11 17:59:07 +00:00

630 lines
17 KiB
Go

package si5351
import (
"encoding/binary"
"errors"
"fmt"
"math"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/regmap"
)
// Device wraps an I2C connection to a SI5351 device.
type Device struct {
bus drivers.I2C
Address uint8
rw regmap.Device8I2C
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 {
rw := regmap.Device8I2C{}
rw.SetBus(bus, AddressDefault, binary.BigEndian)
return Device{
bus: bus,
rw: rw,
Address: AddressDefault,
crystalFreq: CRYSTAL_FREQ_25MHZ,
crystalLoad: CRYSTAL_LOAD_10PF,
}
}
// Configure sets up the device for communication
// TODO error handling
func (d *Device) Configure() error {
// // Disable all outputs setting CLKx_DIS high
d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0xFF)
// Set the load capacitance for the XTAL
d.rw.Write8(CRYSTAL_INTERNAL_LOAD_CAPACITANCE, d.crystalLoad)
// Power down all output drivers
buf := []byte{CLK0_CONTROL, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}
d.bus.Tx(uint16(d.Address), buf, nil)
// Disable spread spectrum output.
if err := d.DisableSpreadSpectrum(); err != nil {
return err
}
d.initialised = true
return nil
}
// Connected returns whether a device at SI5351 address has been found.
func (d *Device) Connected() (bool, error) {
if err := d.bus.Tx(uint16(d.Address), []byte{}, []byte{0}); err != nil {
return false, err
}
return true, nil
}
// EnableSpreadSpectrum enables spread spectrum modulation to reduce EMI.
func (d *Device) EnableSpreadSpectrum() error {
data, err := d.rw.Read8(SPREAD_SPECTRUM_PARAMETERS)
if err != nil {
return err
}
data |= 0x80
return d.rw.Write8(SPREAD_SPECTRUM_PARAMETERS, data)
}
func (d *Device) DisableSpreadSpectrum() error {
data, err := d.rw.Read8(SPREAD_SPECTRUM_PARAMETERS)
if err != nil {
return err
}
data &^= 0x80
return d.rw.Write8(SPREAD_SPECTRUM_PARAMETERS, data)
}
func (d *Device) OutputEnable(output uint8, enable bool) error {
if !d.initialised {
return ErrNotInitialised
}
// Read the current value of the OUTPUT_ENABLE_CONTROL register
regVal, err := d.rw.Read8(OUTPUT_ENABLE_CONTROL)
if err != nil {
return err
}
// Modify regVal based on clk and enable
if enable {
regVal &= ^(1 << output)
} else {
regVal |= (1 << output)
}
// Write the modified value back to the OUTPUT_ENABLE_CONTROL register
return d.rw.Write8(OUTPUT_ENABLE_CONTROL, regVal)
}
func (d *Device) EnableOutputs() error {
if !d.initialised {
return ErrNotInitialised
}
return d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0x00)
}
func (d *Device) DisableOutputs() error {
if !d.initialised {
return ErrNotInitialised
}
return d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0xFF)
}
// packRegSet packs P1, P2, P3 values into the 8-byte register format
// used by both PLL and Multisynth configuration.
// For multisynth, rDivBits should contain the R divider value shifted left by 4.
// For PLL, rDivBits should be 0.
func packRegSet(p1, p2, p3 uint32, rDivBits uint8) [8]byte {
var data [8]byte
data[0] = uint8((p3 & 0xFF00) >> 8)
data[1] = uint8(p3 & 0xFF)
data[2] = uint8((p1&0x30000)>>16) | rDivBits
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)
return data
}
// 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) error {
// Basic validation
switch {
case !d.initialised:
return ErrNotInitialised
// mult = 15..90
case !((mult > 14) && (mult < 91)):
return ErrInvalidParameter
// Avoid divide by zero
case !(denom > 0):
return ErrInvalidParameter
// 20-bit limit
case !(num <= 0xFFFFF):
return ErrInvalidParameter
// 20-bit limit
case !(denom <= 0xFFFFF):
return ErrInvalidParameter
}
// Calculate PLL register values
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
}
// Pack and write registers
data := packRegSet(p1, p2, p3, 0)
if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil {
return err
}
// Reset both PLLs
if err := d.rw.Write8(PLL_RESET, (1<<7)|(1<<5)); err != nil {
return err
}
// 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 nil
}
// 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) error {
// Basic validation
switch {
case !d.initialised:
return ErrNotInitialised
// Channel range
case !(output < 3):
return fmt.Errorf("output channel must be between 0 and 2")
// Divider integer value
case !((div > 3) && (div < 2049)):
return ErrInvalidParameter
// Avoid divide by zero
case !(denom > 0):
return ErrInvalidParameter
// 20-bit limit
case !(num <= 0xFFFFF):
return ErrInvalidParameter
// 20-bit limit
case !(denom <= 0xFFFFF):
return ErrInvalidParameter
// Make sure the requested PLL has been initialised
case pll == PLL_A && !d.pllaConfigured:
return ErrInvalidParameter
case pll == PLL_B && !d.pllbConfigured:
return ErrInvalidParameter
}
// Calculate register values
var reg si5351RegSet
switch {
case num == 0:
// Integer mode
reg.p1 = 128*div - 512
reg.p2 = 0
reg.p3 = denom
case denom == 1:
// Fractional mode, simplified calculations
reg.p1 = 128*div + 128*num - 512
reg.p2 = 128*num - 128
reg.p3 = 1
default:
// Fractional mode
reg.p1 = uint32(128*float64(div) + math.Floor(128*(float64(num)/float64(denom))) - 512)
reg.p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom))))
reg.p3 = denom
}
// Determine if we should use integer mode
intMode := num == 0
// Use existing R divider value (0 if not previously set)
rDiv := d.lastRdivValue[output] >> 4
return d.setMS(output, reg, intMode, rDiv, pll)
}
func (d *Device) ConfigureRdiv(output uint8, div uint8) 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, err := d.rw.Read8(register)
if err != nil {
return err
}
d.lastRdivValue[output] = (div & 0x07) << 4
data = (data & 0x0F) | d.lastRdivValue[output]
return d.rw.Write8(register, data)
}
// si5351RegSet holds the register values for multisynth configuration
type si5351RegSet struct {
p1 uint32
p2 uint32
p3 uint32
}
var ErrFrequencyOutOfRange = errors.New("Si5351 frequency out of range")
var ErrClockConflict = errors.New("Si5351 clock conflict with existing configuration")
// SetFrequency sets the clock frequency of the specified CLK output.
// Frequency range is 8 kHz to 150 MHz.
//
// freq - Output frequency in Hz
// output - Clock output (0, 1, or 2 for this driver)
// pll - The PLL to use (PLL_A or PLL_B)
func (d *Device) SetFrequency(freq uint64, output uint8, pll uint8) error {
switch {
case !d.initialised:
return ErrNotInitialised
case output > 2:
return ErrInvalidParameter
}
switch {
// Lower bounds check
case freq < CLKOUT_MIN_FREQ:
freq = CLKOUT_MIN_FREQ
// Upper bounds check
case freq > MULTISYNTH_MAX_FREQ:
freq = MULTISYNTH_MAX_FREQ
}
// Select the proper R divider value for low frequencies
rDiv := d.selectRDiv(&freq)
// Calculate PLL and multisynth parameters
var pllFreq uint64
switch {
case pll == PLL_A && d.pllaConfigured:
pllFreq = uint64(d.pllaFreq)
case pll == PLL_B && d.pllbConfigured:
pllFreq = uint64(d.pllbFreq)
default:
// PLL not configured, calculate optimal PLL frequency
pllFreq = d.calculatePLLFreq(freq)
}
// Calculate multisynth divider parameters
msReg := d.multisynthCalc(freq, pllFreq)
// Determine if we should use integer mode
intMode := msReg.p2 == 0
// Configure PLL if not already configured or if we need a new frequency
if (pll == PLL_A && !d.pllaConfigured) || (pll == PLL_B && !d.pllbConfigured) {
if err := d.setPLL(pllFreq, pll); err != nil {
return err
}
}
// Set multisynth registers
if err := d.setMS(output, msReg, intMode, rDiv, pll); err != nil {
return err
}
// Enable output
return d.OutputEnable(output, true)
}
// selectRDiv selects the appropriate R divider for low frequencies
// and modifies the frequency accordingly
func (d *Device) selectRDiv(freq *uint64) uint8 {
var rDiv uint8 = 0
if *freq >= CLKOUT_MIN_FREQ && *freq < CLKOUT_MIN_FREQ*2 {
rDiv = R_DIV_128
*freq *= 128
} else if *freq >= CLKOUT_MIN_FREQ*2 && *freq < CLKOUT_MIN_FREQ*4 {
rDiv = R_DIV_64
*freq *= 64
} else if *freq >= CLKOUT_MIN_FREQ*4 && *freq < CLKOUT_MIN_FREQ*8 {
rDiv = R_DIV_32
*freq *= 32
} else if *freq >= CLKOUT_MIN_FREQ*8 && *freq < CLKOUT_MIN_FREQ*16 {
rDiv = R_DIV_16
*freq *= 16
} else if *freq >= CLKOUT_MIN_FREQ*16 && *freq < CLKOUT_MIN_FREQ*32 {
rDiv = R_DIV_8
*freq *= 8
} else if *freq >= CLKOUT_MIN_FREQ*32 && *freq < CLKOUT_MIN_FREQ*64 {
rDiv = R_DIV_4
*freq *= 4
} else if *freq >= CLKOUT_MIN_FREQ*64 && *freq < CLKOUT_MIN_FREQ*128 {
rDiv = R_DIV_2
*freq *= 2
}
return rDiv
}
// calculatePLLFreq calculates an optimal PLL frequency for the given output frequency
func (d *Device) calculatePLLFreq(freq uint64) uint64 {
// Try to find an integer divider that puts PLL in valid range (600-900 MHz)
// Start with a divider that gives us a PLL freq near 750 MHz (middle of range)
targetPLL := uint64(750000000)
divider := targetPLL / freq
// Ensure divider is in valid range (8-900 for fractional, 4/6/8 for integer)
switch {
case divider < 8:
divider = 8
case divider > 900:
divider = 900
}
pllFreq := freq * divider
// Ensure PLL frequency is in valid range
switch {
case pllFreq < PLL_VCO_MIN:
pllFreq = PLL_VCO_MIN
case pllFreq > PLL_VCO_MAX:
pllFreq = PLL_VCO_MAX
}
return pllFreq
}
// multisynthCalc calculates the multisynth register values
func (d *Device) multisynthCalc(freq, pllFreq uint64) si5351RegSet {
var reg si5351RegSet
// Calculate the division ratio
// divider = pllFreq / freq
a := uint32(pllFreq / freq)
remainder := pllFreq % freq
// Calculate b and c for fractional part
// We use c = SI5351_PLL_C_MAX (max 20-bit value) for best resolution
c := uint32(SI5351_PLL_C_MAX)
b := uint32((uint64(remainder) * uint64(c)) / freq)
// Calculate P1, P2, P3
// P1 = 128 * a + floor(128 * b / c) - 512
// P2 = 128 * b - c * floor(128 * b / c)
// P3 = c
floor128bc := uint32((128 * uint64(b)) / uint64(c))
reg.p1 = 128*a + floor128bc - 512
reg.p2 = 128*b - c*floor128bc
reg.p3 = c
return reg
}
// setPLL configures the PLL with the specified frequency
func (d *Device) setPLL(pllFreq uint64, pll uint8) error {
// Calculate PLL multiplier from crystal frequency
// pllFreq = crystalFreq * (a + b/c)
xtalFreq := uint64(d.crystalFreq)
a := uint32(pllFreq / xtalFreq)
remainder := pllFreq % xtalFreq
// Use max denominator for best resolution
c := uint32(SI5351_PLL_C_MAX)
b := uint32((remainder * uint64(c)) / xtalFreq)
return d.ConfigurePLL(pll, uint8(a), b, c)
}
// setMS sets the multisynth registers for the specified output
func (d *Device) setMS(output uint8, reg si5351RegSet, intMode bool, rDiv uint8, pll uint8) error {
// Get the appropriate starting point for the registers
var baseaddr uint8
switch output {
case 0:
baseaddr = MULTISYNTH0_PARAMETERS_1
case 1:
baseaddr = MULTISYNTH1_PARAMETERS_1
case 2:
baseaddr = MULTISYNTH2_PARAMETERS_1
default:
return ErrInvalidParameter
}
// Store R divider value
d.lastRdivValue[output] = (rDiv & 0x07) << 4
// Pack and write registers
data := packRegSet(reg.p1, reg.p2, reg.p3, d.lastRdivValue[output])
if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil {
return err
}
// Configure the clk control register
clkControlReg := uint8(0x0F) // 8mA drive strength, powered up
if pll == PLL_B {
clkControlReg |= (1 << 5) // Use PLLB
}
if intMode {
clkControlReg |= (1 << 6) // Integer mode
}
var clkReg uint8
switch output {
case 0:
clkReg = CLK0_CONTROL
case 1:
clkReg = CLK1_CONTROL
case 2:
clkReg = CLK2_CONTROL
}
return d.rw.Write8(clkReg, clkControlReg)
}
// GetFreqStep returns the frequency step size of the radio in Hz.
// This is the smallest frequency increment that can be achieved,
// determined by the PLL frequency and denominator resolution.
// If pll is PLL_A, uses PLLA settings; if PLL_B, uses PLLB settings.
// Returns 0 if the specified PLL is not configured.
func (d *Device) GetFreqStep(pll uint8) uint64 {
// The frequency step at the output is:
// step = pllFreq / (SI5351_PLL_C_MAX * multisynth_divider)
//
// However, since multisynth divider varies per output, we return
// the base step from the PLL, which is:
// step = pllFreq / SI5351_PLL_C_MAX
var pllFreq uint64
switch pll {
case PLL_A:
if !d.pllaConfigured {
return 0
}
pllFreq = uint64(d.pllaFreq)
case PLL_B:
if !d.pllbConfigured {
return 0
}
pllFreq = uint64(d.pllbFreq)
default:
return 0
}
return pllFreq / SI5351_PLL_C_MAX
}