From 69ead0474c039a6deeb3ef3b7d3e4a3733b30f69 Mon Sep 17 00:00:00 2001 From: Jason Milldrum <2057457+NT7S@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:23:00 -0700 Subject: [PATCH 1/5] Add I2C bus selection via TwoWire* constructor (#103, #106) Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = SI5351_BUS_BASE_ADDR) selects the bus; Si5351(uint8_t) still selects Wire. init(), the device probe and all reads/writes go through the selected bus. init() still calls begin() with no arguments so ESP32 setPins()/RP2040 setSDA() choices are kept. --- README.md | 24 ++++++++++++++++++++++++ src/si5351.cpp | 46 +++++++++++++++++++++++++++------------------- src/si5351.h | 6 +++++- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 5f99e3a..7c61dba 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,26 @@ The standard I2C bus address for the Si5351 is 0x60, however there are other ICs Si5351 si5351(0x61); +Selecting an I2C Bus +-------------------- +By default the library uses the standard _Wire_ bus. On boards with more than one I2C peripheral (ESP32, RP2040, SAMD, Teensy, STM32 and others), pass a pointer to the _TwoWire_ instance the Si5351 is connected to, optionally followed by the bus address: + + #include "Wire.h" + #include "si5351.h" + + Si5351 si5351(&Wire1); // second I2C bus, address 0x60 + // Si5351 si5351(&Wire1, 0x61); // second I2C bus, alternate address + + void setup() + { + Wire1.setPins(13, 15); // ESP32: choose SDA/SCL before init() + si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0); + } + +A _TwoWire_ object you create yourself (for example `TwoWire wire1(1);` on ESP32) works the same way: `Si5351 si5351(&wire1);`. + +_init()_ calls _begin()_ on the selected bus with no arguments, as earlier versions did on _Wire_. That keeps pins set beforehand with _setPins()_ (ESP32) or _setSDA()/setSCL()_ (RP2040). If your board needs pins or a clock passed to _begin()_ itself, call _begin(...)_ on the bus yourself before _init()_; on ESP32 a bus that is already started is left as it is. The existing address-only constructor, `Si5351 si5351(0x61);`, still selects _Wire_. + Startup Conditions ------------------ This library initializes the Si5351 parameters to the following values upon startup and on reset: @@ -711,6 +731,10 @@ This library does not currently support the spread spectrum function of the Si53 Changelog --------- +* Unreleased + + * Add I2C bus selection: `Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = 0x60)` so the Si5351 can be on Wire1 or any other TwoWire bus (#103, #106). `Si5351()` and `Si5351(addr)` behave as before + * v2.3.0 * Reduce flash usage by roughly 0.6 to 3 KB on AVR (depending on which features a sketch uses) with no change to the public API diff --git a/src/si5351.cpp b/src/si5351.cpp index a6bfcf7..840a2cd 100644 --- a/src/si5351.cpp +++ b/src/si5351.cpp @@ -68,7 +68,13 @@ static void frac_to_reg(uint32_t a, uint32_t b, uint32_t c, struct Si5351RegSet /********************/ Si5351::Si5351(uint8_t i2c_addr): - i2c_bus_addr(i2c_addr) + Si5351(&Wire, i2c_addr) +{ +} + +Si5351::Si5351(TwoWire *wire, uint8_t i2c_addr): + i2c_bus_addr(i2c_addr), + _wire(wire) { xtal_freq[0] = SI5351_XTAL_FREQ; @@ -96,13 +102,15 @@ Si5351::Si5351(uint8_t i2c_addr): */ bool Si5351::init(uint8_t xtal_load_c, uint32_t xo_freq, int32_t corr) { - // Start I2C comms - Wire.begin(); + // Start I2C comms on the selected bus. Called with no arguments so that + // pins chosen beforehand (e.g. ESP32 setPins()) are kept; on ESP32 a bus + // already started by the sketch with begin(sda, scl, freq) is left as is. + _wire->begin(); // Check for a device on the bus, bail out if it is not there - Wire.beginTransmission(i2c_bus_addr); + _wire->beginTransmission(i2c_bus_addr); uint8_t reg_val; - reg_val = Wire.endTransmission(); + reg_val = _wire->endTransmission(); if(reg_val == 0) { @@ -1200,37 +1208,37 @@ void Si5351::set_ref_freq(uint32_t ref_freq, enum si5351_pll_input ref_osc) uint8_t Si5351::si5351_write_bulk(uint8_t addr, uint8_t bytes, uint8_t *data) { - Wire.beginTransmission(i2c_bus_addr); - Wire.write(addr); + _wire->beginTransmission(i2c_bus_addr); + _wire->write(addr); for(int i = 0; i < bytes; i++) { - Wire.write(data[i]); + _wire->write(data[i]); } - return Wire.endTransmission(); + return _wire->endTransmission(); } uint8_t Si5351::si5351_write(uint8_t addr, uint8_t data) { - Wire.beginTransmission(i2c_bus_addr); - Wire.write(addr); - Wire.write(data); - return Wire.endTransmission(); + _wire->beginTransmission(i2c_bus_addr); + _wire->write(addr); + _wire->write(data); + return _wire->endTransmission(); } uint8_t Si5351::si5351_read(uint8_t addr) { uint8_t reg_val = 0; - Wire.beginTransmission(i2c_bus_addr); - Wire.write(addr); - Wire.endTransmission(); + _wire->beginTransmission(i2c_bus_addr); + _wire->write(addr); + _wire->endTransmission(); - Wire.requestFrom(i2c_bus_addr, (uint8_t)1); + _wire->requestFrom(i2c_bus_addr, (uint8_t)1); - while(Wire.available()) + while(_wire->available()) { - reg_val = Wire.read(); + reg_val = _wire->read(); } return reg_val; diff --git a/src/si5351.h b/src/si5351.h index 132a522..39b545a 100644 --- a/src/si5351.h +++ b/src/si5351.h @@ -279,7 +279,10 @@ struct Si5351IntStatus class Si5351 { public: - Si5351(uint8_t i2c_addr = SI5351_BUS_BASE_ADDR); + // Select the I2C bus (TwoWire instance) and address, e.g. Si5351 si5351(&Wire1); + Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = SI5351_BUS_BASE_ADDR); + // Address only, on the default Wire bus (kept for existing sketches) + Si5351(uint8_t i2c_addr); bool init(uint8_t, uint32_t, int32_t); void reset(void); uint8_t set_freq(uint64_t, enum si5351_clock); @@ -328,6 +331,7 @@ class Si5351 int32_t ref_correction[2]; uint8_t clkin_div; uint8_t i2c_bus_addr; + TwoWire *_wire; bool clk_first_set[8]; }; From ee18a6b22c2c9a3fa59b41017ace3dde062708b7 Mon Sep 17 00:00:00 2001 From: Jason Milldrum <2057457+NT7S@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:42:26 -0700 Subject: [PATCH 2/5] Address review of TwoWire bus selection - Integer-address overloads so Si5351(0), Si5351(NULL) and any integer type select the address path as before (no ambiguity with TwoWire *) - Null TwoWire * falls back to Wire - Shared setup_obj() helper instead of a C++11 delegating constructor - README: begin() re-runs in init() on non-ESP32 cores and may reset setClock(), so call setClock() after init(); mark setPins() ESP32-only; changelog uses SI5351_BUS_BASE_ADDR --- README.md | 9 ++++++--- src/si5351.cpp | 11 ++++++----- src/si5351.h | 15 +++++++++++++-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7c61dba..c65e244 100644 --- a/README.md +++ b/README.md @@ -260,13 +260,16 @@ By default the library uses the standard _Wire_ bus. On boards with more than on void setup() { - Wire1.setPins(13, 15); // ESP32: choose SDA/SCL before init() + #if defined(ARDUINO_ARCH_ESP32) + Wire1.setPins(13, 15); // ESP32 only: choose SDA/SCL before init() + #endif si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0); + // Wire1.setClock(400000); // optional: set the bus speed after init() } A _TwoWire_ object you create yourself (for example `TwoWire wire1(1);` on ESP32) works the same way: `Si5351 si5351(&wire1);`. -_init()_ calls _begin()_ on the selected bus with no arguments, as earlier versions did on _Wire_. That keeps pins set beforehand with _setPins()_ (ESP32) or _setSDA()/setSCL()_ (RP2040). If your board needs pins or a clock passed to _begin()_ itself, call _begin(...)_ on the bus yourself before _init()_; on ESP32 a bus that is already started is left as it is. The existing address-only constructor, `Si5351 si5351(0x61);`, still selects _Wire_. +_init()_ calls _begin()_ on the selected bus with no arguments, as earlier versions did on _Wire_. That keeps pins set beforehand with _setPins()_ (ESP32) or _setSDA()/setSCL()_ (RP2040). If your board needs pins passed to _begin()_ itself, call _begin(...)_ on the bus yourself before _init()_. On ESP32 a bus that is already started is left as it is. On most other cores (AVR, SAMD, RP2040, STM32 and others) _begin()_ runs again inside _init()_ and may reset a bus speed chosen earlier with _setClock()_, so call _setClock()_ after _init()_. A null bus pointer selects _Wire_. The existing address-only constructor, `Si5351 si5351(0x61);`, still selects _Wire_. Startup Conditions ------------------ @@ -733,7 +736,7 @@ Changelog * Unreleased - * Add I2C bus selection: `Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = 0x60)` so the Si5351 can be on Wire1 or any other TwoWire bus (#103, #106). `Si5351()` and `Si5351(addr)` behave as before + * Add I2C bus selection: `Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = SI5351_BUS_BASE_ADDR)` so the Si5351 can be on Wire1 or any other TwoWire bus (#103, #106). `Si5351()` and `Si5351(addr)` (any integer type, including a literal 0 or NULL) behave as before. No C++11 features are required * v2.3.0 diff --git a/src/si5351.cpp b/src/si5351.cpp index 840a2cd..deaa147 100644 --- a/src/si5351.cpp +++ b/src/si5351.cpp @@ -67,15 +67,16 @@ static void frac_to_reg(uint32_t a, uint32_t b, uint32_t c, struct Si5351RegSet /* Public functions */ /********************/ -Si5351::Si5351(uint8_t i2c_addr): - Si5351(&Wire, i2c_addr) +Si5351::Si5351(TwoWire *wire, uint8_t i2c_addr) { + setup_obj(wire, i2c_addr); } -Si5351::Si5351(TwoWire *wire, uint8_t i2c_addr): - i2c_bus_addr(i2c_addr), - _wire(wire) +// Shared constructor body (no C++11 delegating constructor needed) +void Si5351::setup_obj(TwoWire *wire, uint8_t i2c_addr) { + i2c_bus_addr = i2c_addr; + _wire = wire ? wire : &Wire; xtal_freq[0] = SI5351_XTAL_FREQ; // Start by using XO ref osc as default for each PLL diff --git a/src/si5351.h b/src/si5351.h index 39b545a..a04c8c0 100644 --- a/src/si5351.h +++ b/src/si5351.h @@ -280,9 +280,19 @@ class Si5351 { public: // Select the I2C bus (TwoWire instance) and address, e.g. Si5351 si5351(&Wire1); + // A null bus pointer falls back to Wire. Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = SI5351_BUS_BASE_ADDR); - // Address only, on the default Wire bus (kept for existing sketches) - Si5351(uint8_t i2c_addr); + // Address only, on the default Wire bus (kept for existing sketches). + // One overload per integer type so that any integer argument, including a + // literal 0 or NULL, selects the address path exactly as before instead of + // being ambiguous with the TwoWire * constructor. + Si5351(uint8_t i2c_addr) { setup_obj(&Wire, i2c_addr); } + Si5351(int i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(unsigned int i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(long i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(unsigned long i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(long long i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(unsigned long long i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } bool init(uint8_t, uint32_t, int32_t); void reset(void); uint8_t set_freq(uint64_t, enum si5351_clock); @@ -321,6 +331,7 @@ class Si5351 enum si5351_pll_input pllb_ref_osc; uint32_t xtal_freq[2]; private: + void setup_obj(TwoWire *, uint8_t); uint64_t pll_calc(enum si5351_pll, uint64_t, struct Si5351RegSet *, int32_t, uint8_t); uint64_t multisynth_calc(uint64_t, uint64_t, struct Si5351RegSet *); uint64_t multisynth67_calc(uint64_t, uint64_t, struct Si5351RegSet *); From a4cfc9a17f4c9059f0ec0cd8608e5a9352efd984 Mon Sep 17 00:00:00 2001 From: Jason Milldrum <2057457+NT7S@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:00:12 -0700 Subject: [PATCH 3/5] README bus-selection wording; fix si5351_vcxo get_correction() call - nullptr or (TwoWire *)0 selects Wire via the bus constructor; NULL/0 mean address 0 - Note the harmless ESP32 'Bus already started in Master Mode' log - si5351_vcxo: get_correction() takes the PLL input; use SI5351_PLL_INPUT_XO --- README.md | 2 +- examples/si5351_vcxo/si5351_vcxo.ino | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c65e244..827de34 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ By default the library uses the standard _Wire_ bus. On boards with more than on A _TwoWire_ object you create yourself (for example `TwoWire wire1(1);` on ESP32) works the same way: `Si5351 si5351(&wire1);`. -_init()_ calls _begin()_ on the selected bus with no arguments, as earlier versions did on _Wire_. That keeps pins set beforehand with _setPins()_ (ESP32) or _setSDA()/setSCL()_ (RP2040). If your board needs pins passed to _begin()_ itself, call _begin(...)_ on the bus yourself before _init()_. On ESP32 a bus that is already started is left as it is. On most other cores (AVR, SAMD, RP2040, STM32 and others) _begin()_ runs again inside _init()_ and may reset a bus speed chosen earlier with _setClock()_, so call _setClock()_ after _init()_. A null bus pointer selects _Wire_. The existing address-only constructor, `Si5351 si5351(0x61);`, still selects _Wire_. +_init()_ calls _begin()_ on the selected bus with no arguments, as earlier versions did on _Wire_. That keeps pins set beforehand with _setPins()_ (ESP32) or _setSDA()/setSCL()_ (RP2040). If your board needs pins passed to _begin()_ itself, call _begin(...)_ on the bus yourself before _init()_. On ESP32 a bus that is already started is left as it is; the core logs the warning "Bus already started in Master Mode", which is harmless and the same as with earlier versions of this library. On most other cores (AVR, SAMD, RP2040, STM32 and others) _begin()_ runs again inside _init()_ and may reset a bus speed chosen earlier with _setClock()_, so call _setClock()_ after _init()_. To get the default _Wire_ bus through the bus constructor, pass `nullptr` or `(TwoWire *)0`; a plain `NULL` or `0` is treated as I2C address 0, as in earlier versions. The existing address-only constructor, `Si5351 si5351(0x61);`, still selects _Wire_. Startup Conditions ------------------ diff --git a/examples/si5351_vcxo/si5351_vcxo.ino b/examples/si5351_vcxo/si5351_vcxo.ino index afc7ffe..03f3ee7 100644 --- a/examples/si5351_vcxo/si5351_vcxo.ino +++ b/examples/si5351_vcxo/si5351_vcxo.ino @@ -49,7 +49,7 @@ void loop() // Read the Status Register and print it every 10 seconds si5351.update_status(); Serial.print("corr: "); - Serial.print(si5351.get_correction()); + Serial.print(si5351.get_correction(SI5351_PLL_INPUT_XO)); Serial.print(" SYS_INIT: "); Serial.print(si5351.dev_status.SYS_INIT); Serial.print(" LOL_A: "); From abf9b2a8f8743cde95e7fb6f2bda17e0216002e7 Mon Sep 17 00:00:00 2001 From: Jason Milldrum <2057457+NT7S@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:54:43 -0700 Subject: [PATCH 4/5] Version 2.4.0 --- README.md | 5 +++-- library.json | 2 +- library.properties | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 827de34..c6b74bd 100644 --- a/README.md +++ b/README.md @@ -734,9 +734,10 @@ This library does not currently support the spread spectrum function of the Si53 Changelog --------- -* Unreleased +* v2.4.0 - * Add I2C bus selection: `Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = SI5351_BUS_BASE_ADDR)` so the Si5351 can be on Wire1 or any other TwoWire bus (#103, #106). `Si5351()` and `Si5351(addr)` (any integer type, including a literal 0 or NULL) behave as before. No C++11 features are required + * Add I2C bus selection: `Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = SI5351_BUS_BASE_ADDR)` so the Si5351 can be on Wire1 or any other TwoWire bus (closes #103, #106). `Si5351()` and `Si5351(addr)` (any integer type, including a literal 0 or NULL) behave as before. No new C++11 features are required + * Fix compile error in the si5351_vcxo example (`get_correction()` now called with `SI5351_PLL_INPUT_XO`) * v2.3.0 diff --git a/library.json b/library.json index bb2faa2..8bedf55 100644 --- a/library.json +++ b/library.json @@ -16,7 +16,7 @@ "maintainer": true } ], - "version": "2.3.0", + "version": "2.4.0", "frameworks": "arduino", "platforms": "*" } \ No newline at end of file diff --git a/library.properties b/library.properties index b9e8eeb..3b45d97 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=Etherkit Si5351 -version=2.3.0 +version=2.4.0 author=Jason Milldrum maintainer=Jason Milldrum sentence=A full-featured library for the Si5351 series of clock generator ICs from Silicon Labs From 8472d61ba6c4f731e7b6c021f6e82311039641f8 Mon Sep 17 00:00:00 2001 From: Jason Milldrum <2057457+NT7S@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:58:24 -0700 Subject: [PATCH 5/5] README: v2.4.0 changelog notes on nullptr bus fallback and Uno cost --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c6b74bd..3e0fb33 100644 --- a/README.md +++ b/README.md @@ -737,6 +737,8 @@ Changelog * v2.4.0 * Add I2C bus selection: `Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = SI5351_BUS_BASE_ADDR)` so the Si5351 can be on Wire1 or any other TwoWire bus (closes #103, #106). `Si5351()` and `Si5351(addr)` (any integer type, including a literal 0 or NULL) behave as before. No new C++11 features are required + * Passing `nullptr` or `(TwoWire *)0` to the bus constructor selects `Wire`; `NULL` or `0` still mean I2C address 0 + * Cost on the Uno basic example: +138 B flash, +2 B RAM * Fix compile error in the si5351_vcxo example (`get_correction()` now called with `SI5351_PLL_INPUT_XO`) * v2.3.0