Si5351 VFO

By | January 3, 2019

Here is an older project of mine – another variant of a VFO. This time with the popular Si5351 clock generator, allowing to create a complete solution for a superhet, including both the VFO and BFO oscillators.

This project is an evolution of the older AD9850 based VFO (here and here). However, this time I wanted to build a superhet receiver and AD9850 has only a single output, so the BFO would need to be separate. Si5351 has up to 8 outputs (depending on version), covering all the common radio needs easily.


The VFO has the following features:

  • Independent VFO and BFO oscillators
  • RIT/XIT offsets
  • Tx signal input – used to let the microcontroller know when to apply the XIT offset
  • Same display layout as the AD9850 VFO
  • Revised controls – gone are the complicated button combinations, there is now a menu for configuring things such as the IF and BFO frequencies, operating mode, IF mode, etc.
  • Output limited to 1MHz-30MHz (but easily changeable in the code)
  • Supports both 3.3V and 5V LCD modules
  • Easy to adapt to different IF filter frequencies and setups
  • Arduino compatible
  • 2019/11/9 – EEPROM persistence for the settings and frequencies


The VFO uses a standard ATMega328P-AU (in TQFP-32 package), running from the internal RC oscillator at 8MHz. The frequency generation is provided by Si5351a, running from a 25MHz crystal, with all 3 outputs routed to the edge of the board (should fit an SMA connector). In my case the loading capacitors C6, C7 weren’t necessary because my crystal needs only few pF of capacitance – the parasitic capacitance of the PCB is sufficient there.

The user interface is provided by 4 tactile switches (SW1, SW2 & SW3 + RESET) and a clickable rotary encoder (SW4). The output uses a standard 1602 LCD module1. Both 3.3V and 5V (most common) LCDs are supported – select the desired voltage by shorting the corresponding pads of the J4 jumper.

The device is powered from from two LD1117S50TR and LD1117S33TR linear regulators – everything except the LCD module is running directly from 3.3V rail, avoiding the need for voltage translation.

Firmware programming is possible either through the standard 6pin AVR ISP connector (J7) or using a serial bootloader on a 3 pin UART header (J8).

In case of use in a transciever it is possible to connect the external Rx/Tx switching circuitry to the J5 header – if the line /RIG_TX is pulled low, the microcontroller will switch to transmit mode.


Figure 1: VFO Schematic (click to enlarge)


The board was designed in KiCAD 5.0.2, the complete project is included below. The board is double sided, meant to accomodate everything including connectors, the LCD module and all controls.

Most of the passives are 0805 sized, except of C9, C10 which are 1206 sized. Everything should be relatively easy to hand-solder, the special larger “HandSoldering” footprints were used where applicable.

The most tricky component Si5351a comes in a tiny (3x3mm!) MSOP-10 package with 0.5mm pin pitch but it is still doable with a bit of care and careful inspection (use a magnifier!).

Don’t forget to short the correct side of the J4 voltage selection jumper for the LCD or the display will not work!


Figure 2: Top side (click to enlarge)


Figure 3: Bottom side (click to enlarge)


Figure 4: Populated board driving a simple 40m receiver


Figure 5: Populated board driving a simple 40m receiver

I still have a few spare unpopulated boards, if someone is interested in having one (or more), contact me.


The firmware is written in C++ using the Arduino framework for simplicity.

The UI part contains a small template library using both the CRTP pattern to avoid the overhead of virtual functions (saves code memory and a bit of runtime overhead) and compile-time evaluation with variadic templates to permit strong compiler optimizitation. In fact, the version 2.0 of this code used regular inheritance, virtual functions and no templates – and was about 2kB larger. Quite a difference in a micro controller with 32kB of code memory. The drawback is that the code is not exactly easiest to follow and read without some experience in template metaprogramming.

If someone is interested in learning how this works, some good resources are here:

The code does not depend on the standard library because that is not commonly available for AVR. That is also why I had to include minimal implementations of an array and tuple containers. Those don’t claim to be complete or bug-free by any measure, they are meant to be only used for the purposes of this UI library.

Otherwise the code depends on the Etherkit Si5351a library (not the Adafruit one, that one didn’t work for me), Brian Low’s Rotary encoder library and Bounce2 button debouncing library. Install those either using the Arduino library manager or manually, as described on their Github pages.

The functionality is very similar to the AD9850 VFOs, however I didn’t implement the automatic saving and restoring of the last state to/from the EEPROM. I have rarely used that feature and it only complicated the code and was wearing the microcontroller out (EEPROM has a limited number of writes). The code is prepared for it, if someone needs it, it shouldn’t be too difficult to add this back.

The serial port is also unused in the current version (apart from debugging). It could be used either for remote control or e.g. for band switching in the future if someone wants to implement it. The connector is there.


When uploading the firmware, do make sure to select a board variant that supports 8MHz internal oscillator (normal Arduino Uno uses 16MHz crystal/resonator).

The instructions how to add a custom board configured for the internal 8MHz RC oscillator are, for example, here.

Basic controls

Encoder left/right Tuning, menu navigation, changing values
Encoder button Change tuning step, confirm selection
Button A Hold down to enter “cursor” mode where you can rapidly dial in the desired frequency decade by decade
Button B Set RIT offset (XIT when /RIG_TX is active as well)
Button C Enter/exit the settings menu

The numeric settings (such as the IF and BFO frequencies in the settings) are changed by clicking the encoder, scrolling the cursor to the decade you want to adjust, clicking again to confirm and then rotating the encoder to change the value. Click to confirm again. The mode is exited by moving the cursor completely to the left of the display and clicking the encoder button again.


Operating mode – for the offsets see below

AM BFO off
LSB BFO freq = BFO center + BFO SSB offset
USB BFO freq = BFO center – BFO SSB offset
CW BFO freq = BFO center – BFO CW offset

IF mode

Off Frequency on display is the frequency output
F+IF VFO outputs the displayed frequency + the configured IF (additive IF)
F-IF VFO outputs the displayed frequency – the configured IF (subtractive IF)

IF & BFO frequency

IF and BFO center frequency, useful for peaking filters, etc. The default in the code is 8987.5kHz because that is the center frequency of a surplus Yaesu filter my receiver is using.

Invert sidebands

A small hack that reverses the meaning the LSB/USB modes when active. Rarely useful, mostly for debugging and testing.


Everything that is configurable is collected in the file definitions.h. The following values are likely the ones that will need to be customized depending on your radio (filters, etc.):

Variable Initial value Meaning
VFOFreq 7100000 Initial frequency of the VFO in Hz
IFFreq 8987500 Initial IF frequency in Hz
BFOFreq 8987500 Initial BFO frequency in Hz. BFO center frequency in Hz. Frequency from which the offsets are subtracted/added.
BFOSSBOffset 1250 How far to offset the BFO from the center when receiving SSB. Normally it should be 1/2 of the bandwidth of your IF filter.
BFOCWOffset 600 How far to offset the BFO from the center when receiving CW.
TuningIncrements 100000, 10, 50, 100, 500, 1000, 2500, 5000, 10000 Available tuning steps in Hz
RitIncrements 10, 100, 1000, 1 Available RIT/XIT tuning steps in Hz
TuningIncrementIdx 4 Initial tuning step (500 Hz in this case)
RitIncrementIdx 0 Initial RIT tuning step (10 Hz in this case)
Rit 0 Initial RIT in Hz
SidebandsSwap false Initial value of whether to swap the meaning of LSB/USB mode setting
FreqLimitUp 30e6 Upper frequency limit for the VFO (30 MHz)
FreqLimitDown 1e6 Lower frequency limit for the VFO (1 MHz)
SI5351Addr 0x62 I2C address of the Si5351a – according to the datasheet it should be 0x60 but chips with 0x62, 0x6f are common too. Try to change if the VFO is not tuning.
SI5351Correction 2004 Frequency correction for the Si5351 crystal – if you have accurate counter you could try to tweak this to calibrate the frequency of the VFO


Schematic + PCB:

  • KiCAD 5.0.2 project, including the Gerber files (in the “gerbers” subdirectory), so no need to install KiCAD if you only want to have the PCBs manufactured. Just extract the gerbers and upload them to the board fabricator of your choice.
  • Bill of material

Firmware (GPL v3 licensed):



Some other designs use the cheap 0.96″ OLED displays – I didn’t want to use one of these because they are both very electrically noisy (not something desirable around a sensitive receiver) and I find them too tiny to be comfortably readable, despite the excellent contrast. Also the squarish aspect ratio looks odd on a front panel.

22 thoughts on “Si5351 VFO

  1. Ben

    Just wanted to say thank you for this. I just built this and it is working wonderfully. (I build your AD9850 version a few years ago.)

    1. Klaus


      I translate my English with google da …..

      Thanks for publishing it here on the net.
      For me the whole thing runs with an Arduino Nano and an external 1602 display.
      I will take it as a quartz replacement.

      73 de Klaus, DL2HAD

      1. Jan Post author


        Thank you and I am glad that it is useful for you!

        73 de Jan, OM2ATC

  2. Tony

    Hi Jan,
    I am rebuilding a hf man-pack (tr28) with a range of 1-8Mhz.
    I have tried many si5351 vfo’s which were just not right.
    Then I found your vfo and after lots of attempts I got it right…… learning Arduino etc.
    It is everything in one practical package .. brilliant!!
    As the man-pack has side-band filters I changed the mode to AM SSB CW. 1976 vintage
    Would it be possible to change the tuning-steps to a cursor under the relevant number ( same line)?
    Now I am pushing my luck———What about resetting the previous numbers to zero when changing the
    tuning-step eg 7,069,400 to 7,070,000 ?
    Once again great design!
    Tony ZR6ALG

    1. Jan Post author


      I am glad that the thing works for you.

      Re display of the tuning steps – you mean to display it as a list of numbers with the selected step underlined? I am afraid that wouldn’t fit on the display (there is only 16 characters per line!). But feel free to hack, the display is defined in vfo-screen.h, in the render_impl() function. It is commented and should be fairly obvious what needs to be modified if you prefer a different style of display.

      If you want to reset the frequency to the default one when changing the step (not sure why you would want to do that, though), you can easily change that yourself. In vfo-screen.h, search for the function handle_input_impl() (line 195) and you will see two if statements that change the step up or down depending on the sign of the ev.ay variable (the encoder rotation). If you want to reset the frequency, just add there:

      m_state.vfo_freq = Defs::VFOFreq;
      m_state.vfo_dirty = true;

      And recompile. That should do the job. You can replace Defs::VFOFreq with your own frequency if you don’t want to use the default starting frequency from definitions.h.

      1. Tony

        Hi Jan,
        Thanks for the prompt reply.
        I am sorry I did not explain my self properly re tuning-steps
        Instead of the freq being displayed on the bottom line in Hz is it possible
        to place the cursor under the appropriate digit on the main freq, eg if the
        tuning-step is 100Hz place the cursor under the 3rd digit, so you will see
        the step position at a glance, and not have the freq displayed below.
        The other query is if you are on 7.022500 Mhz and want to go to 7.070000 Mhz
        and select the 10000Khz step you will have (after tuning) 7.722500 Mhz.
        If the last 5 digits are reset to zero when the 10000 Khz step is selected you would have 7.070000 Mhz.
        This would save you from having to step back for each digit.
        My programming skills are still in the baby steps mode!! hi
        Thanks again
        Tony ZR6ALG

        1. Jan Post author

          Ah I see.

          Re cursor – feel free to add that yourself for your radio but I am certainly not going to add this. I am afraid it would just confuse people because they would think the cursor tuning mode is enabled. Furthermore, the tuning steps are not only powers of 10 (whole decades). How will you indicate e.g. a very common 500Hz or 2500Hz tuning step then?

          The question about resetting the frequency – I think what you want is to actually round the frequency to the nearest multiple of the tuning step (again, zeroing the digits is not enough – the tuning steps are not only whole decades!). Feel free to add this in the place I have indicated above but I am not going to add this into my code. Again, I think that would be confusing and annoying if the receiver jumps around/changes frequency merely because you have changed the tuning step.

          >My programming skills are still in the baby steps mode!!

          Well, good opportunity to learn something new! These modifications you are asking for are a good starting project, because they are relatively simple and you don’t need to go deep into the C++ “plumbing” of the code (especially the CRTP templates are tricky).

          1. Tony

            Hi Jan,
            Points taken, I will soldier on and keep you posted.
            An odd thing occurred when I powered up the the vfo, there was no freq displayed,and if I turned the encoder a freq of 1Mhz appeared and it would not save.
            I reloaded a clean copy same problem. I then reloaded a fresh bootloader and loaded the vfo s/w … problem solved. It appeared that the bootloader had got corrupted.
            Thanks for your help and patience!
            Tony ZR6ALG

  3. Jan Post author

    Feel free to get in touch if you have any questions.
    Re the weird problem you had – that could be also EEPROM corruption. If the data in the EEPROM are not what the program expects, all bets are off. If the EEPROM is erased/empty, it will detect that and initialize it properly but if there are some old data in there, anything could happen.

  4. misael

    me interesa su sketch como lo puedo correr en arduino nano, si5351a, lcd 16×2

    1. Jan Post author


      Sorry, my Spanish is non-existent but I assume you want to run this on an Arduino Nano. In that case the code should work as-is, but you will need to check that the pins I have used exist on a Nano and if not (or if conflict) change the code to use different ones.

      Otherwise it should work.

  5. misael

    hello jan my English excuses
    friend already made the project everything it was successful alone I should change the pines asinacion for the nano and the address of si5351 0x60 that it is the one that I use; thank you and alone a comment because double VFO doesn’t add him with a fifth ideal botton to be the project of radios made at home. 73s cordial

    1. Jan Post author

      Cool, congrats! The thing with the si5351 address is documented in the article, I have encountered chips with different addresses. It is strange, frankly – maybe there are fake chips around?

      If you want to add another button for dual VFO, well – if you have a free pin, adding a button is easy. What is less easy is extending the code to handle the state of two VFOs, especially if you want to keep track of more than just 2 frequencies (RIT/XIT, sideband, etc.) I am not planning on doing it because I don’t have need for it but if you do this, feel free to send me the patch or link to your version. I will add it to the post for others to find.

  6. John Greusel

    Hi Jan,

    I haven’t built the board yet but trying to test compile I’m getting these errors. The only one that seems problematic is the Rotary one. Also based on other comments it sounds like I’ll need to install the bootloader. I just did an AVR project that didn’t use that but rather just had me upload a hex file.

    John Greusel

    Warning: Board breadboard:avr:atmega328bb doesn’t define a ‘build.board’ preference. Auto-set to: AVR_ATMEGA328BB
    C:\vfo-si5351.2\vfo-si5351.2.ino: In function ‘void setup()’:
    vfo-si5351.2:54:11: error: ‘class Rotary’ has no member named ‘begin’
    encoder.begin(true); // enable encoder pull-ups
    Multiple libraries were found for “si5351.h”
    Used: C:\Users\John\Documents\Arduino\libraries\Si5351Arduino-master
    Not used: C:\Users\John\Documents\Arduino\libraries\Etherkit_Si5351
    Multiple libraries were found for “LiquidCrystal.h”
    Used: C:\Users\John\Documents\Arduino\libraries\LiquidCrystal
    Not used: C:\Users\John\Documents\Arduino\libraries\OLD_LiquidCrystal
    Not used: C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.42.0_x86__mdqgnx93n4wtt\libraries\LiquidCrystal
    exit status 1
    ‘class Rotary’ has no member named ‘begin’

    1. Jan Post author

      You have most likely an incorrect version of the Rotary library.
      Do make sure you use this one, linked from the article:

      You have also multiple version of the si5351 library installed, that’s not good because it will likely cause problems. Same as for the LiquidCrystal lib.

      1. John Greusel

        Thanks Jan,

        I did succeed in compiling and I generated a binary file.
        Interestingly it created two- one with bootloader and one without. I believe I may be able to use AVRDUDESS (or something similar and upload the binary file I’ve created? Alternatively I can us my USBtiny with the Arduino software and load it that way.


        1. Jan Post author

          That’s a bit weird that you have two binaries and one with a bootloader? Are you sure about that? I wonder how did you compile that because that’s not how the Arduino IDE works. Bootloader is flashed separately, it makes little sense for it to be bundled with the binary.

          If you have an Arduino with a bootloader (i.e. normal Arduino board), then you can upload it over USB, as a normal “sketch” using the built-in bootloader.

          If you don’t have an Arduino with a bootloader (i.e. a blank chip and not an Arduino board), then you can use USBTiny (programmer) and the ISP connection to flash it using the avrdude command line utility (AVRDUDESS is a graphic frontend for avrdude). But that is the same thing as clicking “Upload using Programmer” in the Arduino IDE.

  7. John Greusel

    I was surprised too. Making the binaries was more of “let”s see what happens.” Here are the file names that were created:


    I didn’t tel it to do that it just defaulted to making these two hex files.


  8. John Greusel

    Hello Jan,

    Works great. My SI5351 address was 0x60 so had to edit that. My encoder runs backwards for up/down frequency. Is there a setting for that in the sketch or should I reverse the pins?

    Thanks for another great project!


    1. Jan Post author

      Hello John,

      I am glad that it works for you.

      Re encoder – at the top of the main.cpp/main.ino file you will find the line:

      Rotary encoder = Rotary(2, 3); // Sets the pins the rotary encoder uses. Must be interrupt pins.

      Just change this to:

      Rotary encoder = Rotary(3, 2); // Sets the pins the rotary encoder uses. Must be interrupt pins.

      That should reverse the direction of the encoder. Of course, swapping the physical pins works too.


Leave a Reply

Your email address will not be published. Required fields are marked *