The Si5351 spec sheets are complicated and confusing, but in the Arduino universe there are many libraries available for the it. I started out with the Adafruit one, but have modified it to better suit my needs. Several modifications were required to allow register updates at my 2.4 KHz interrupt rate. Others were required, or desired, to clean up some other issues.

Speed-ups:

- Increase the nominal Arduino I2C rate from 100 KHz to 400 KHz (the Si5351 supports a 400 KHz I2C rate).
- The Adafruit library uses individual I2C “write single byte” operations, requiring that the I2C bus start, send an address byte, a data byte, and stop per each byte written. Instead, where possible I use the I2C “burst write” mode which greatly reduces the number of I2C cycles when updating a register bank.
- When only the PLL “B” numerator is changed, we don’t have to update all the PLL divider registers. This speeds up the update.

Not particularly speed-related, but the Adafruit library performs a PLL reset when the dividers are updated. This is not necessary, and creates frequency glitches. I removed the reset operation when updating divisors. This also does speed things up a bit.

Not speed-related, but the Adafruit library uses floating-point when calculating the fractional divider register values. No doubt this is because the Si5351 app note shows the use of the floating-point “floor()” function in the calculation, but this is not required. When the calculations are performed in the proper order, simple integer math is completely accurate.

Here is the gist of the code I use to update the PLL divider numerator. Note the use of floor() in the original Adafruit comments (sorry about the line-wrapping):

void setupPLLnumerator(si5351PLL_t pll, uint8_t a, uint32_t b, uint32_t c) { uint32_t P1; /* PLL config register P1 */ uint32_t P2; /* PLL config register P2 */ uint32_t P3; /* PLL config register P3 */ /* Feedback Multisynth Divider Equation * * where: a = mult, b = num and c = denom * * 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 */ uint32_t f; f = (128 * b) / c; // build the registers to write P1 = 128 * a + f - 512; P2 = 128 * b - f * c; P3 = c; // since c (denom) hasn't changed, there's no need to write first two bytes// bytes to be written = 6 uint8_t reg_bank[] = { //(P3 & 0xFF00) >> 8, // Bits [15:8] of MSNx_P3 in register 26 //P3 & 0xFF, (P1 & 0x030000L) >> 16, (P1 & 0xFF00) >> 8, // Bits [15:8] of MSNx_P1 in register 29 P1 & 0xFF, // Bits [7:0] of MSNx_P1 in register 30 ((P3 & 0x0F0000L) >> 12) | ((P2 & 0x0F0000) >> 16), // Parts of MSNx_P3 and MSNx_P1 (P2 & 0xFF00) >> 8, // Bits [15:8] of MSNx_P2 in register 32 P2 & 0xFF // Bits [7:0] of MSNx_P2 in register 33 }; /* Get the appropriate starting point for the PLL registers */ uint8_t baseaddr = (26); // PLLA i2cWriteBurst(baseaddr + 2, reg_bank, sizeof(reg_bank)); return(); }

The timer-tick ISR runs at 2.4 KHz (416.66us). With these changes to the Si5351 library code, updating the PLL numerator takes 241us. The long Gaussian filter takes 50us per cycle, and the other operations in the ISR take less than 5us, so that leaves about 120us (per 2.4 KHz cycle) free for other program activities. This is comfortably adequate, but trying to run the ISR at a faster rate might prove difficult.

For more information on using the Si5351 quadrature mode in radio applications, see this excellent posting by Hans Summers (QRP Labs): https://qrp-labs.com/images/news/dayton2018/fdim2018.pdf

Here is an alternate Si5351 programming library. I was inspired to improve my I2C functions after looking at this one: https://github.com/etherkit/Si5351Arduino