Friday, March 10, 2023

Minimal Si5351 VFO for Bush 40 DSB Transceiver

Recently I've been going a bit "old school" and built the Soldersmoke Direct Conversion receiver with its PTO VFO and another VK3YE Beach 40 DSB transceiver with a ceramic resonator based VFO (it can be slightly pulled).

I was thinking about a minimum VFO configuration using just an Arduino Nano, a rotary encoder and an Si5351. If you count a Nano as a single component you could argue that this is a three component VFO.

My implementation boots up on 7.1Mhz and can be tuned up and down with the rotary encoder. There's no display (although that can be easily added) but a frequency counter could be added. 

The wiring is like this circuit on the Arduino project hub but I haven't added the display.

Power enters through the VIN pin on the Nano which can take voltages up to 16V (I'm running 12V) and regulates down to 5V and 3.3V which I feed to the Si5351.

I prototyped this on a breadboard first:

Next I built the same circuit on matrix board with simple point to point wiring. Here it is driving the mixer on the Beach 40.

It's a very compact and usable VFO. I have a few ideas about some extra features such as stopping at band edges and maybe lighting an LED when you hit the band edge.

Observant readers will spot my LEDs on the boards and power wiring - I've been bitten by being buzzed about why things weren't working when the fault of in the power line. Adding a few LED power indicators makes it clear.

Here's my simple source code for this VFO (blogger messes code up so use the link to the Gist):

Simple VFO for a direct conversion receiver.

Si5351 controlled by a rotary encoder.
Based on code from Paul, VK3HN

const unsigned long int FREQ_DEFAULT = 7100000ULL;

#define ENCODER_A 3 // DT
#define ENCODER_B 2 // CLK

#include <RotaryEncoder.h> // by Maattias Hertel
#include <si5351.h> // Etherkit Si3531 library Jason Mildrum, V2.1.4
#include <Wire.h> // built in
Si5351 si5351; // I2C address defaults to x60 in the NT7S lib
RotaryEncoder gEncoder = RotaryEncoder(ENCODER_A, ENCODER_B, RotaryEncoder::LatchMode::FOUR3);
long gEncoderPosition = 0;

unsigned long int gFrequency = FREQ_DEFAULT;
unsigned long int gStep = 100;

void setup() {
gFrequency = FREQ_DEFAULT;
si5351.set_freq(FREQ_DEFAULT * SI5351_FREQ_MULT, SI5351_CLK0);
si5351.output_enable(SI5351_CLK0, 1);

void loop() {
// check for change in the rotary encoder
long newEncoderPosition = gEncoder.getPosition();
if(newEncoderPosition != gEncoderPosition) {
long encoderDifference = newEncoderPosition - gEncoderPosition;
gEncoderPosition = newEncoderPosition;

void setupRotaryEncoder() {
attachInterrupt(digitalPinToInterrupt(ENCODER_A), checkPosition, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_B), checkPosition, CHANGE);

// This interrupt routine will be called on any change of one of the input signals
void checkPosition() {
gEncoder.tick(); // just call tick() to check the state.

void frequencyAdjust(int delta) {
Serial.print("Adjust: ");
gFrequency += (delta * gStep);

void setVfoFrequency(unsigned long int frequency) {
si5351.set_freq(frequency * SI5351_FREQ_MULT, SI5351_CLK0); //
Serial.print("set frequency: ");

void setupOscillator() {
bool i2c_found = si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0);
Serial.print("si5351: ");
Serial.println(i2c_found ? "Found" : "Missing");
si5351.set_correction(135000, SI5351_PLL_INPUT_XO); // Library update 26/4/2020: requires destination register address ... si5351.set_correction(19100, SI5351_PLL_INPUT_XO);
si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLA);
si5351.set_freq(500000000ULL, SI5351_CLK0);
si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_4MA);
si5351.output_enable(SI5351_CLK0, 1); // turn VFO on

No comments: