Overview
This is the next step after the AD9850 lab DDS. The SI5351A is a fractional-N clock generator with Multisynth outputs - not a classic DDS DAC, but for a bench VFO / clock source it covers HF and into VHF with 1 Hz programming resolution. The firmware lives in Pico_SI5351_47l16 on GitHub.
Product goal: 2 kHz–250 MHz in 1 Hz steps. Current firmware clamps in vfo.h are 5 kHz–160 MHz (easy to widen once the board and cal are happy at the band edges).
Why this stack
I wanted a SI5351A frequency synthesizer that remembered where you left it - not an Arduino Uno with a scratchpad VFO that forgets on unplug. Two I2C buses keep the noisy / busy OLED traffic off the SI5351 bus, and the Microchip 47L16 gives true non-volatile SRAM-backed settings with CRC so a half-written record does not brick the boot frequency.
Hardware
| Item | Detail |
|---|---|
| MCU | Raspberry Pi Pico (RP2040) |
| Synthesizer | SI5351A module, 25 MHz crystal, CLK0 used |
| Main display | SH1106 128×64 OLED (I2C0) |
| Mini display | SSD1306 64×32 OLED (I2C1) |
| Memory | 47L16 EERAM (I2C1) |
| Tuning | EC11 rotary encoder + switch; Back and Confirm buttons (polled, no interrupts) |
Pin map
| Signal | GPIO |
|---|---|
| I2C0 SDA / SCL (SI5351, SH1106) | GP0 / GP1 |
| I2C1 SDA / SCL (47L16, SSD1306) | GP26 / GP27 |
| Back | GP11 |
| Confirm | GP12 |
| Encoder A / B / SW | GP13 / GP14 / GP15 |
Both Wire buses run at 800 kHz. Encoder and buttons are active-low with internal pull-ups.
How the SI5351 path works
This project does not use any third-party SI5351A Arduino library. The popular ones proved unreliable on the bench and often fail with Chinese knock-off modules, so si5351.cpp programs the chip directly over I2C using Multisynth / PLL math from Silicon Labs AN619.
- Init - outputs disabled, CLK1/CLK2 powered down, CLK0 enabled after a soft PLL reset, 10 pF crystal load.
- Low frequencies - an R-divider (1…128) keeps the Multisynth input above ~500 kHz so the chip stays in a happy Multisynth range while CLK0 still delivers audio / LF.
- Mid band - even Multisynth divider aimed at ~750 MHz VCO; fractional PLLA feedback from a calibrated XTAL constant.
- 100 MHz and up - Multisynth DIVBY4 in integer mode (per Silicon Labs AN619) for VHF.
- Calibration tale - measure CLK0 at 150 MHz, put the Hz error into
OFFSET_AT_150MHZinvfo.h. The firmware scales that 6:1 (150 MHz / 25 MHz) intoCALIBRATED_XTALso every later tune uses the corrected reference.
Tuning and auto-step
Step sizes: 1 Hz, 10 Hz, 100 Hz, 1 kHz, 10 kHz, 100 kHz, 1 MHz, 10 MHz. Short encoder press cycles the step. Long press toggles auto-step: when you roll past a decade digit, the step size bumps up so you can sweep MHz without holding the button. Near the software min/max, the firmware tries smaller steps before flashing LOWER LIMIT / UPPER LIMIT.
EC11 encoder (no interrupts)
The EC11 is read entirely from the main loop in encoder.cpp - no pin-change or external interrupts. Each loop() pass samples encoder A/B/SW (GP13/GP14/GP15) with digitalRead.
Rotation (quadrature)
- Pack A and B into a 2-bit Gray code and compare to the last sample.
- No change → no motion.
- Both bits flipping at once is treated as bounce/glitch: store the new state but do not count a step.
- Valid single-bit transitions walk the Gray-code ring to decide +1 or −1 edge direction.
- Detents: one mechanical click produces four quadrature edges. Edges accumulate until
|accum| >= ENCODER_DETENT(4), then one frequency step is emitted - one detent = one tune step. - Edge timing: edges closer than 1 ms are ignored; a direction reverse within 6 ms of the last accepted edge is ignored (contact bounce often looks like a quick reverse).
Push switch and button debounce
- Encoder switch: a level change is accepted only after it has disagreed with the latched state for more than
DEBOUNCE_MS(40 ms). On release, hold ≥longPressDelay(350 ms) is a long press (auto-step); shorter is a short press (next step size). - Back / Confirm use the same 40 ms latch debounce in
buttons.cpp.
All of these pins are INPUT_PULLUP; contacts are active-low to ground.
47L16 memory tale
Settings are a packed 32-byte record: signature, version, size, sequence counter, frequency, step index, auto-step flag, reserved, tail magic, CRC32. Writes go to the 47L16 SRAM window over I2C1. On boot, load + validate; if CRC or magic fails, default to 10 MHz / 1 Hz / auto-step on and seed SRAM. Encoder motion auto-saves when dirty; Confirm forces a save flash message; Back reloads SRAM and re-programs the SI5351.
Automatic save at power-down: you do not have to press Confirm before unplugging. The 47L16 holds the live settings in SRAM while you tune, then AutoStores that SRAM into EEPROM when power drops, so frequency, step size, and auto-step come back on the next boot.
STORE/RECALL opcodes are also wired in the driver for a manual EEPROM mirror if needed - the day-to-day path is fast SRAM plus the chip’s power-down AutoStore so you are not burning endurance on every click.
Controls
- Rotate encoder - change frequency by the current step; displays update; dirty settings auto-save.
- Short press encoder - next step size.
- Long press encoder (~350 ms) - toggle auto-step on/off.
- Confirm - save settings now (flash:
SETTINGS SAVED). Optional - power-down still AutoStores automatically. - Back - recall settings (flash:
SETTINGS RECALLED).
Both OLEDs show frequency (MHz with fractional digits), step size, and Auto ON/OFF. Activity tracking is in place for a future dimmer (stub currently).
Software / libraries
Display and board support only - no SI5351 library:
- Arduino-Pico (RP2040 board package)
- Adafruit SH110X
- Adafruit SSD1306
- Adafruit GFX
Build and upload
- Clone Pico_SI5351_47l16.
- Install Arduino-Pico and the Adafruit libraries above.
- Open
Pico_SI5351_47l16.ino(folder name must match). - Board: Raspberry Pi Pico — compile and upload.
- Calibrate
OFFSET_AT_150MHZafter a counter check at 150 MHz if you care about absolute accuracy.
GitHub Actions compiles on push (compile check only; no board upload).