Téléverser les fichiers vers "lib/Si5351mcu/src"

This commit is contained in:
Xavier 2026-10-04 19:32:46 +02:00
parent 880c22ff50
commit 7cc911f734
2 changed files with 560 additions and 0 deletions

View File

@ -0,0 +1,394 @@
/*
* si5351mcu - Si5351 library for Arduino MCU tuned for size and click-less
*
* Copyright (C) 2017 Pavel Milanes <pavelmc@gmail.com>
*
* Many chunk of codes are derived-from/copied from other libs
* all GNU GPL licenced:
* - Linux Kernel (www.kernel.org)
* - Hans Summers libs and demo code (qrp-labs.com)
* - Etherkit (NT7S) Si5351 libs on github
* - DK7IH example.
* - Jerry Gaffke integer routines for the bitx20 group
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Arduino.h"
#include "si5351mcu.h"
// wire library loading, if not defined
#ifndef WIRE_H
#include "Wire.h"
#endif
/*****************************************************************************
* This is the default init procedure, it set the Si5351 with this params:
* XTAL 27.000 Mhz
*****************************************************************************/
void Si5351mcu::init(void) {
// init with the default freq
init(int_xtal);
}
/*****************************************************************************
* This is the custom init procedure, it's used to pass a custom xtal
* and has the duty of init the I2C protocol handshake
*****************************************************************************/
void Si5351mcu::init(uint32_t nxtal) {
// set the new base xtal freq
base_xtal = int_xtal = nxtal;
// start I2C (wire) procedures
Wire.begin();
// shut off the spread spectrum by default, DWaite contibuted code
uint8_t regval;
regval = i2cRead(149);
regval &= ~0x80; // set bit 7 LOW to turn OFF spread spectrum mode
i2cWrite(149, regval);
// power off all the outputs
off();
}
/*****************************************************************************
* This function set the freq of the corresponding clock.
*
* In my tests my Si5351 can work between 7,8 Khz and ~225 Mhz [~250 MHz with
* overclocking] as usual YMMV
*
* Click noise:
* - The lib has a reset programmed [aka: click noise] every time it needs to
* change the output divider of a particular MSynth, if you move in big steps
* this can lead to an increased rate of click noise per tunning step.
* - If you move at a pace of a few Hz each time the output divider will
* change at a low rate, hence less click noise per tunning step.
* - The output divider moves [change] faster at high frequencies, so at HF the
* clikc noise is at the real minimum possible.
*
* [See the README.md file for other details]
****************************************************************************/
void Si5351mcu::setFreq(uint8_t clk, uint32_t freq) {
uint8_t a, R = 1, pll_stride = 0, msyn_stride = 0;
uint32_t b, c, f, fvco, outdivider;
uint32_t MSx_P1, MSNx_P1, MSNx_P2, MSNx_P3;
// Overclock option
#ifdef SI_OVERCLOCK
// user a overclock setting for the VCO, max value in my hardware
// was 1.05 to 1.1 GHz, as usual YMMV [See README.md for details]
outdivider = SI_OVERCLOCK / freq;
#else
// normal VCO from the datasheet and AN
// With 900 MHz beeing the maximum internal PLL-Frequency
outdivider = 900000000 / freq;
#endif
// use additional Output divider ("R")
while (outdivider > 900) {
R = R * 2;
outdivider = outdivider / 2;
}
// finds the even divider which delivers the intended Frequency
if (outdivider % 2) outdivider--;
// Calculate the PLL-Frequency (given the even divider)
fvco = outdivider * R * freq;
// Convert the Output Divider to the bit-setting required in register 44
switch (R) {
case 1: R = 0; break;
case 2: R = 16; break;
case 4: R = 32; break;
case 8: R = 48; break;
case 16: R = 64; break;
case 32: R = 80; break;
case 64: R = 96; break;
case 128: R = 112; break;
}
// we have now the integer part of the output msynth
// the b & c is fixed below
MSx_P1 = 128 * outdivider - 512;
// calc the a/b/c for the PLL Msynth
/***************************************************************************
* We will use integer only on the b/c relation, and will >> 5 (/32) both
* to fit it on the 1048 k limit of C and keep the relation
* the most accurate possible, this works fine with xtals from
* 24 to 28 Mhz.
*
* This will give errors of about +/- 2 Hz maximum
* as per my test and simulations in the worst case, well below the
* XTAl ppm error...
*
* This will free more than 1K of the final eeprom
*
****************************************************************************/
a = fvco / int_xtal;
b = (fvco % int_xtal) >> 5; // Integer part of the fraction
// scaled to match "c" limits
c = int_xtal >> 5; // "c" scaled to match it's limits
// in the register
// f is (128*b)/c to mimic the Floor(128*(b/c)) from the datasheet
f = (128 * b) / c;
// build the registers to write
MSNx_P1 = 128 * a + f - 512;
MSNx_P2 = 128 * b - f * c;
MSNx_P3 = c;
// PLLs and CLK# registers are allocated with a stride, we handle that with
// the stride var to make code smaller
if (clk > 0 ) pll_stride = 8;
// HEX makes it easier to human read on bit shifts
uint8_t reg_bank_26[] = {
(MSNx_P3 & 0xFF00) >> 8, // Bits [15:8] of MSNx_P3 in register 26
MSNx_P3 & 0xFF,
(MSNx_P1 & 0x030000L) >> 16,
(MSNx_P1 & 0xFF00) >> 8, // Bits [15:8] of MSNx_P1 in register 29
MSNx_P1 & 0xFF, // Bits [7:0] of MSNx_P1 in register 30
((MSNx_P3 & 0x0F0000L) >> 12) | ((MSNx_P2 & 0x0F0000) >> 16), // Parts of MSNx_P3 and MSNx_P1
(MSNx_P2 & 0xFF00) >> 8, // Bits [15:8] of MSNx_P2 in register 32
MSNx_P2 & 0xFF // Bits [7:0] of MSNx_P2 in register 33
};
// We could do this here - but move it next to the reg_bank_42 write
// i2cWriteBurst(26 + pll_stride, reg_bank_26, sizeof(reg_bank_26));
// Write the output divider msynth only if we need to, in this way we can
// speed up the frequency changes almost by half the time most of the time
// and the main goal is to avoid the nasty click noise on freq change
if (omsynth[clk] != outdivider || o_Rdiv[clk] != R ) {
// CLK# registers are exactly 8 * clk# bytes stride from a base register.
msyn_stride = clk * 8;
// keep track of the change
omsynth[clk] = (uint16_t) outdivider;
o_Rdiv[clk] = R; // cache it now, before we OR mask up R for special divide by 4
// See datasheet, special trick when MSx == 4
// MSx_P1 is always 0 if outdivider == 4, from the above equations, so there is
// no need to set it to 0. ... MSx_P1 = 128 * outdivider - 512;
//
// See para 4.1.3 on the datasheet.
//
if ( outdivider == 4 ) {
R |= 0x0C; // bit set OR mask for MSYNTH divide by 4, for reg 44 {3:2]
}
// HEX makes it easier to human read on bit shifts
uint8_t reg_bank_42[] = {
0, // Bits [15:8] of MS0_P3 (always 0) in register 42
1, // Bits [7:0] of MS0_P3 (always 1) in register 43
((MSx_P1 & 0x030000L ) >> 16) | R, // Bits [17:16] of MSx_P1 in bits [1:0] and R in [7:4] | [3:2]
(MSx_P1 & 0xFF00) >> 8, // Bits [15:8] of MSx_P1 in register 45
MSx_P1 & 0xFF, // Bits [7:0] of MSx_P1 in register 46
0, // Bits [19:16] of MS0_P2 and MS0_P3 are always 0
0, // Bits [15:8] of MS0_P2 are always 0
0 // Bits [7:0] of MS0_P2 are always 0
};
// Get the two write bursts as close together as possible,
// to attempt to reduce any more click glitches. This is
// at the expense of only 24 increased bytes compilation size in AVR 328.
// Everything is already precalculated above, reducing any delay,
// by not doing calculations between the burst writes.
i2cWriteBurst(26 + pll_stride, reg_bank_26, sizeof(reg_bank_26));
i2cWriteBurst(42 + msyn_stride, reg_bank_42, sizeof(reg_bank_42));
//
// https://www.silabs.com/documents/public/application-notes/Si5350-Si5351%20FAQ.pdf
//
// 11.1 "The Int, R and N register values inside the Interpolative Dividers are updated
// when the LSB of R is written via I2C." - Q. does this mean reg 44 or 49 (misprint ?) ???
//
// 10.1 "All outputs are within +/- 500ps of one another at power up (if pre-programmed)
// or if PLLA and PLLB are reset simultaneously via register 177."
//
// 17.1 "The PLL can track any abrupt input frequency changes of 3–4% without losing
// lock to it. Any input frequency changes greater than this amount will not
// necessarily track from the input to the output
// must reset the so called "PLL", in fact the output msynth
reset();
}
else {
i2cWriteBurst(26 + pll_stride, reg_bank_26, sizeof(reg_bank_26));
}
}
/*****************************************************************************
* Reset of the PLLs and multisynths output enable
*
* This must be called to soft reset the PLLs and cycle the output of the
* multisynths: this is the "click" noise source in the RF spectrum.
*
* So it must be avoided at all costs, so this lib just call it at the
* initialization of the PLLs and when a correction is applied
*
* If you are concerned with accuracy you can implement a reset every
* other Mhz to be sure it get exactly on spot.
****************************************************************************/
void Si5351mcu::reset(void) {
// This soft-resets PLL A & B (32 + 128) in just one step
i2cWrite(177, 0xA0);
}
/*****************************************************************************
* Function to disable all outputs
*
* The PLL are kept running, just the m-synths are powered off.
*
* This allows to keep the chip warm and exactly on freq the next time you
* enable an output.
****************************************************************************/
void Si5351mcu::off(void) {
// This disable all the CLK outputs
for (byte i=0; i < SICHANNELS; i++) {
disable(i);
}
}
/*****************************************************************************
* Function to set the correction in Hz over the Si5351 XTAL.
*
* This will call a reset of the PLLs and multi-synths so it will produce a
* click every time it's called
****************************************************************************/
void Si5351mcu::correction(int32_t diff) {
// apply some corrections to the xtal
int_xtal = base_xtal + diff;
// reset the PLLs to apply the correction
reset();
}
/*****************************************************************************
* This function enables the selected output
*
* Beware: ZERO is clock output enabled, in register 16+CLK
*****************************************************************************/
void Si5351mcu::enable(uint8_t clk) {
// var to handle the mask of the registers value
uint8_t m = SICLK0_R;
if (clk > 0) {
m = SICLK12_R;
}
// write the register value
i2cWrite(16 + clk, m + clkpower[clk]);
// 1 & 2 are mutually exclusive
if (clk == 1) disable(2);
if (clk == 2) disable(1);
// update the status of the clk
clkOn[clk] = 1;
}
/*****************************************************************************
* This function disables the selected output
*
* Beware: ONE is clock output disabled, in register 16+CLK
* *****************************************************************************/
void Si5351mcu::disable(uint8_t clk) {
// send
i2cWrite(16 + clk, 0x80);
// update the status of the clk
clkOn[clk] = 0;
}
/****************************************************************************
* Set the power output for each output independently
***************************************************************************/
void Si5351mcu::setPower(uint8_t clk, uint8_t power) {
// set the power to the correct var
clkpower[clk] = power;
// now enable the output to get it applied
enable(clk);
}
/****************************************************************************
* method to send multi-byte burst register data.
***************************************************************************/
uint8_t Si5351mcu::i2cWriteBurst( const uint8_t start_register,
const uint8_t *data,
const uint8_t numbytes) {
// This method saves the massive overhead of having to keep opening
// and closing the I2C bus for consecutive register writes. It
// also saves numbytes - 1 writes for register address selection.
Wire.beginTransmission(SIADDR);
Wire.write(start_register);
Wire.write(data, numbytes);
// All of the bytes queued up in the above write() calls are buffered
// up and will be sent to the slave in one "burst", on the call to
// endTransmission(). This also sends the I2C STOP to the Slave.
return Wire.endTransmission();
// returns non zero on error
}
/****************************************************************************
* function to send the register data to the Si5351, arduino way.
***************************************************************************/
void Si5351mcu::i2cWrite( const uint8_t regist, const uint8_t value) {
// Using the "burst" method instead of
// doing it longhand saves a few bytes
i2cWriteBurst( regist, &value, 1);
}
/****************************************************************************
* Read i2C register, returns -1 on error or timeout
***************************************************************************/
int16_t Si5351mcu::i2cRead( const uint8_t regist ) {
int value;
Wire.beginTransmission(SIADDR);
Wire.write(regist);
Wire.endTransmission();
Wire.requestFrom(SIADDR, 1);
if ( Wire.available() ) {
value = Wire.read();
}
else {
value = -1; // "EOF" in C
}
return value;
}

View File

@ -0,0 +1,166 @@
/*
* si5351mcu - Si5351 library for Arduino MCU tuned for size and click-less
*
* Copyright (C) 2017 Pavel Milanes <pavelmc@gmail.com>
*
* Many chunk of codes are derived-from/copied from other libs
* all GNU GPL licenced:
* - Linux Kernel (www.kernel.org)
* - Hans Summers libs and demo code (qrp-labs.com)
* - Etherkit (NT7S) Si5351 libs on github
* - DK7IH example.
* - Jerry Gaffke integer routines for the bitx20 group
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/****************************************************************************
* This lib tricks:
*
* CLK0 will use PLLA
* CLK1 will use PLLB
* CLK2 will use PLLB
*
* Lib defaults
* - XTAL is 27 Mhz.
* - Always put the internal 8pF across the xtal legs to GND
* - lowest power output (2mA)
* - After the init all outputs are off, you need to enable them in your code.
*
* The correction procedure is not for the PPM as other libs, this
* is just +/- Hz to the XTAL freq, you may get a click noise after
* applying a correction
*
* The init procedure is mandatory as it set the Xtal (the default or a custom
* one) and prepare the Wire (I2C) library for operation.
****************************************************************************/
#ifndef SI5351MCU_H
#define SI5351MCU_H
// rigor includes
#include "Arduino.h"
#include "Wire.h"
// default I2C address of the Si5351A - other variants may differ
#define SIADDR 0x60
// The number of output channels - 3 for Si5351A 10 pin
#define SICHANNELS 3
// register's power modifiers
#define SIOUT_2mA 0
#define SIOUT_4mA 1
#define SIOUT_6mA 2
#define SIOUT_8mA 3
// registers base (2mA by default)
#define SICLK0_R 76 // 0b01001100
#define SICLK12_R 108 // 0b01101100
class Si5351mcu {
private:
// base xtal freq, over this we apply the correction factor
// by default 27 MHz
uint32_t base_xtal = 27000000L;
// this is the work value, with the correction applied
// via the correction() procedure
uint32_t int_xtal = base_xtal;
// clk# power holders (2ma by default)
uint8_t clkpower[SICHANNELS] = { 0 };
// local var to keep track of when to reset the "pll"
/*********************************************************
* BAD CONCEPT on the datasheet and AN:
*
* The chip has a soft-reset for PLL A & B but in
* practice the PLL does not need to be reseted.
*
* Test shows that if you fix the Msynth output
* dividers and move any of the VCO from bottom to top
* the frequency moves smooth and clean, no reset needed
*
* The reset is needed when you changes the value of the
* Msynth output divider, even so it's not always needed
* so we use this var to keep track of all three and only
* reset the "PLL" when this value changes to be sure
*
* It's a word (16 bit) because the final max value is 900
*********************************************************/
uint16_t omsynth[SICHANNELS] = { 0 };
uint8_t o_Rdiv[SICHANNELS] = { 0 };
public:
// var to check the clock state
bool clkOn[SICHANNELS] = { 0 }; // This should not really be public - use isEnabled()
public:
// default init procedure
void init(void);
// custom init procedure (XTAL in Hz);
void init(uint32_t);
// reset all PLLs
static void reset(void);
// set CLKx(0..2) to freq (Hz)
void setFreq(uint8_t, uint32_t);
// pass a correction factor
void correction(int32_t);
// enable some CLKx output
void enable(uint8_t);
// disable some CLKx output
void disable(uint8_t);
// disable all outputs
void off(void);
// set power output to a specific clk
void setPower(uint8_t, uint8_t);
// used to talk with the chip, via Arduino Wire lib
//
// declared as static, since they do not reference any this-> class attributes
//
static void i2cWrite( const uint8_t reg, const uint8_t val );
static uint8_t i2cWriteBurst( const uint8_t start_register, const uint8_t *data, const uint8_t numbytes );
static int16_t i2cRead( const uint8_t reg );
inline const bool isEnabled( const uint8_t channel ) {
return channel < SICHANNELS && clkOn[ channel ] != 0;
};
inline const uint8_t getPower( const uint8_t channel ) {
return channel < SICHANNELS ? clkpower[ channel ] : 0;
};
inline const uint32_t getXtalBase( void ) {
return base_xtal;
};
inline const uint32_t getXtalCurrent( void ) {
return int_xtal;
};
};
#endif //SI5351MCU_H