Arduino VFO Project with a Large LCD Display
Cheap and easy-to-build VFO device that is almost indispensable in radio engineering, especially in DIY radio receivers.
Components and supplies
1
rotary encoder
1
Generic 128x64 OLED I2C
1
Electric Switches
1
Push Button
1
SI5351 CLOCK GEN MODULE
1
Arduino Nano
Tools and machines
1
Soldering kit
Apps and platforms
1
Arduino IDE
Project description
Code
Code
cpp
...
1#include <Wire.h> 2#include <Rotary.h> 3#include <si5351.h> 4#include <U8g2lib.h> 5 6// Pin definitions 7#define PIN_TUNESTEP A0 8#define PIN_BAND A1 9#define PIN_RX_TX A2 10#define PIN_ADC A3 11#define PIN_ROT_1 2 12#define PIN_ROT_2 3 13#define PIN_RST 8 14#define PIN_CS 10 15#define PIN_MOSI 11 16#define PIN_SCK 13 17 18// Constants 19#define IF_FREQ 455 20#define BAND_INIT 7 21#define XT_CAL_F 33000 22#define S_GAIN 303 23 24// Frequency range limits 25const uint32_t MIN_FREQ = 10000UL; // 10 kHz 26const uint32_t MAX_FREQ = 225000000UL; // 225 MHz 27 28// Band names stored in program memory 29const char BAND_0[] PROGMEM = " GEN"; 30const char BAND_1[] PROGMEM = " MW"; 31const char BAND_2[] PROGMEM = " 160m"; 32const char BAND_3[] PROGMEM = " 80m"; 33const char BAND_4[] PROGMEM = " 60m"; 34const char BAND_5[] PROGMEM = " 49m"; 35const char BAND_6[] PROGMEM = " 40m"; 36const char BAND_7[] PROGMEM = " 31m"; 37const char BAND_8[] PROGMEM = " 25m"; 38const char BAND_9[] PROGMEM = " 22m"; 39const char BAND_10[] PROGMEM = " 20m"; 40const char BAND_11[] PROGMEM = " 19m"; 41const char BAND_12[] PROGMEM = " 16m"; 42const char BAND_13[] PROGMEM = " 13m"; 43const char BAND_14[] PROGMEM = " 11m"; 44const char BAND_15[] PROGMEM = " 10m"; 45const char BAND_16[] PROGMEM = " 6m"; 46const char BAND_17[] PROGMEM = " WFM"; 47const char BAND_18[] PROGMEM = " AIR"; 48const char BAND_19[] PROGMEM = " 2m"; 49const char BAND_20[] PROGMEM = " 1m"; 50 51const char* const BAND_NAMES[] PROGMEM = { 52 BAND_0, BAND_1, BAND_2, BAND_3, BAND_4, BAND_5, BAND_6, BAND_7, BAND_8, BAND_9, 53 BAND_10, BAND_11, BAND_12, BAND_13, BAND_14, BAND_15, BAND_16, BAND_17, 54 BAND_18, BAND_19, BAND_20 55}; 56 57// Frequency presets stored in program memory 58const uint32_t FREQ_PRESETS[] PROGMEM = { 59 100000UL, // GEN 60 800000UL, // MW 61 1800000UL, // 160m 62 3650000UL, // 80m 63 4985000UL, // 60m 64 6180000UL, // 49m 65 7200000UL, // 40m 66 10000000UL, // 31m 67 11780000UL, // 25m 68 13630000UL, // 22m 69 14100000UL, // 20m 70 15000000UL, // 19m 71 17655000UL, // 16m 72 21525000UL, // 13m 73 27015000UL, // 11m 74 28400000UL, // 10m 75 50000000UL, // 6m 76 100000000UL, // WFM 77 130000000UL, // AIR 78 144000000UL, // 2m 79 220000000UL // 1m 80}; 81 82// Frequency steps 83const uint32_t FREQ_STEPS[] PROGMEM = { 84 1000000UL, // 1 MHz 85 1UL, // 1 Hz 86 10UL, // 10 Hz 87 1000UL, // 1 kHz 88 5000UL, // 5 kHz 89 10000UL // 10 kHz 90}; 91 92// Object initialization 93U8G2_ST7920_128X64_1_SW_SPI u8g2(U8G2_R0, PIN_SCK, PIN_MOSI, PIN_CS, PIN_RST); 94Rotary r = Rotary(PIN_ROT_1, PIN_ROT_2); 95Si5351 si5351; 96 97// Global variables 98uint32_t freq = 7200000UL; // Start at 7.2MHz 99uint32_t freqold; 100uint32_t fstep = 1000; // Default step 1kHz 101int16_t interfreq = IF_FREQ; 102int16_t cal = XT_CAL_F; 103uint8_t smval; 104uint8_t encoder = 1; 105uint8_t stp = 4; 106uint8_t n = 1; 107uint8_t count = BAND_INIT; 108uint8_t prevCount = BAND_INIT; 109uint8_t x, xo; 110bool sts = 0; 111bool displayOK = false; 112 113// Function prototypes 114bool setSi5351Frequency(Si5351& si5351, uint32_t freq, int16_t interfreq); 115void check_inputs(); 116void update_display_paged(); 117void initializeSi5351(); 118 119// Encoder interrupt service routine 120ISR(PCINT2_vect) { 121 char result = r.process(); 122 if (result == DIR_CW) { 123 if (encoder == 1) { 124 uint32_t new_freq = freq + fstep; 125 if (new_freq <= MAX_FREQ) { 126 freq = new_freq; 127 n = (n >= 42) ? 1 : n + 1; 128 } 129 } 130 } 131 else if (result == DIR_CCW) { 132 if (encoder == 1) { 133 uint32_t new_freq = freq; 134 if (freq >= fstep) { 135 new_freq = freq - fstep; 136 if (new_freq >= MIN_FREQ) { 137 freq = new_freq; 138 n = (n <= 1) ? 42 : n - 1; 139 } 140 } 141 } 142 } 143} 144 145void setup() { 146 Serial.begin(9600); 147 Serial.println(F("VFO Starting...")); 148 149 Wire.begin(); 150 151 if (!u8g2.begin()) { 152 Serial.println(F("Display init failed!")); 153 while (1) { delay(1000); } 154 } 155 156 // Display initialization test 157 u8g2.setFont(u8g2_font_6x12_tr); 158 u8g2.firstPage(); 159 do { 160 u8g2.drawFrame(0, 0, 128, 64); 161 u8g2.drawStr(20, 32, "Initializing..."); 162 } while (u8g2.nextPage()); 163 delay(1000); 164 165 Serial.println(F("Display initialized")); 166 displayOK = true; 167 168 // Initialize pins 169 pinMode(PIN_ROT_1, INPUT_PULLUP); 170 pinMode(PIN_ROT_2, INPUT_PULLUP); 171 pinMode(PIN_TUNESTEP, INPUT_PULLUP); 172 pinMode(PIN_BAND, INPUT_PULLUP); 173 pinMode(PIN_RX_TX, INPUT_PULLUP); 174 175 // Initialize Si5351 176 initializeSi5351(); 177 178 // Setup rotary encoder interrupts 179 PCICR |= (1 << PCIE2); 180 PCMSK2 |= (1 << PCINT18) | (1 << PCINT19); 181 sei(); 182 183 // Set initial frequency 184 freq = pgm_read_dword(&FREQ_PRESETS[count - 1]); 185 186 Serial.println(F("Setup complete")); 187} 188 189void initializeSi5351() { 190 Serial.println(F("Initializing Si5351...")); 191 if (!si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0)) { 192 Serial.println(F("Si5351 init failed!")); 193 } 194 si5351.reset(); 195 delay(10); 196 si5351.set_correction(cal, SI5351_PLL_INPUT_XO); 197 si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); 198 si5351.output_enable(SI5351_CLK0, 1); 199} 200 201bool setSi5351Frequency(Si5351& si5351, uint32_t freq, int16_t interfreq) { 202 // Check if frequency is within valid range 203 if (freq < MIN_FREQ || freq > MAX_FREQ) { 204 return false; 205 } 206 207 uint64_t output_freq = (freq + (interfreq * 1000ULL)) * 100ULL; 208 209 // Handle GEN mode specially 210 if (count == 1) { 211 si5351.reset(); 212 delay(10); 213 si5351.set_correction(cal, SI5351_PLL_INPUT_XO); 214 si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); 215 } 216 217 // Set the frequency 218 si5351.set_freq(output_freq, SI5351_CLK0); 219 si5351.output_enable(SI5351_CLK0, 1); 220 221 return true; 222} 223 224void loop() { 225 if (!displayOK) return; 226 227 // Process frequency changes with error handling 228 if (freqold != freq) { 229 if (!setSi5351Frequency(si5351, freq, interfreq)) { 230 // If frequency setting fails, try to recover 231 si5351.reset(); 232 delay(10); 233 si5351.set_correction(cal, SI5351_PLL_INPUT_XO); 234 si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); 235 setSi5351Frequency(si5351, freq, interfreq); 236 } 237 freqold = freq; 238 } 239 240 // Check inputs 241 check_inputs(); 242 243 // Update display 244 update_display_paged(); 245 246 // Read signal meter 247 smval = analogRead(PIN_ADC); 248 x = constrain(map(smval, 0, S_GAIN, 1, 14), 1, 14); 249} 250 251void check_inputs() { 252 if (digitalRead(PIN_TUNESTEP) == LOW) { 253 stp = (stp % 6) + 1; 254 fstep = pgm_read_dword(&FREQ_STEPS[stp - 1]); 255 delay(300); 256 } 257 258 if (digitalRead(PIN_BAND) == LOW) { 259 uint8_t newCount = (count % 21) + 1; 260 261 // Reset Si5351 when entering or leaving GEN mode 262 if (newCount == 1 || count == 1) { 263 si5351.reset(); 264 delay(10); 265 si5351.set_correction(cal, SI5351_PLL_INPUT_XO); 266 si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); 267 si5351.output_enable(SI5351_CLK0, 1); 268 } 269 270 count = newCount; 271 freq = pgm_read_dword(&FREQ_PRESETS[count - 1]); 272 prevCount = count; 273 delay(300); 274 } 275 276 sts = (digitalRead(PIN_RX_TX) == LOW); 277 interfreq = (sts || count == 1) ? 0 : IF_FREQ; 278} 279 280void update_display_paged() { 281 u8g2.firstPage(); 282 do { 283 // Display frequency 284 char buffer[16]; 285 uint32_t m = freq / 1000000UL; 286 uint32_t k = (freq % 1000000UL) / 1000UL; 287 uint32_t h = (freq % 1000UL); 288 289 u8g2.setFont(u8g2_font_10x20_tr); 290 291 if (m < 1) { 292 sprintf(buffer, "%03lu.%03lu", k, h); 293 u8g2.drawStr(41, 17, buffer); 294 } else if (m < 100) { 295 sprintf(buffer, "%lu.%03lu.%03lu", m, k, h); 296 u8g2.drawStr(15, 17, buffer); 297 } else { 298 sprintf(buffer, "%lu.%03lu.%03lu", m, k, h); 299 u8g2.drawStr(15, 17, buffer); 300 } 301 302 // Draw interface elements 303 u8g2.setFont(u8g2_font_6x12_tr); 304 u8g2.drawHLine(0, 22, 128); 305 u8g2.drawHLine(0, 45, 128); 306 u8g2.drawHLine(15, 54, 67); 307 u8g2.drawVLine(105, 26, 15); 308 u8g2.drawVLine(87, 26, 15); 309 u8g2.drawVLine(87, 50, 15); 310 311 // Display RX/TX status 312 u8g2.drawStr(91, 37, sts ? "TX" : "RX"); 313 314 // Display IF frequency 315 sprintf(buffer, "IF:%d", interfreq); 316 u8g2.drawStr(90, 59, buffer); 317 318 // Display LO value 319 sprintf(buffer, "LO:%d", interfreq); 320 u8g2.drawStr(110, 38, buffer); 321 322 // Display step 323 u8g2.drawStr(54, 32, "STEP"); 324 switch(stp) { 325 case 1: u8g2.drawStr(54, 42, "1MHz"); break; 326 case 2: u8g2.drawStr(54, 42, "1Hz"); break; 327 case 3: u8g2.drawStr(54, 42, "10Hz"); break; 328 case 4: u8g2.drawStr(54, 42, "1kHz"); break; 329 case 5: u8g2.drawStr(54, 42, "5kHz"); break; 330 case 6: u8g2.drawStr(54, 42, "10kHz"); break; 331 } 332 333 // Display band name 334 u8g2.setFont(u8g2_font_10x20_tr); 335 strcpy_P(buffer, (char*)pgm_read_word(&(BAND_NAMES[count - 1]))); 336 u8g2.drawStr(0, 40, buffer); 337 338 // Draw meters 339 u8g2.setFont(u8g2_font_6x12_tr); 340 byte y = map(n, 1, 42, 1, 14); 341 342 u8g2.drawStr(0, 54, "TU"); 343 u8g2.drawBox(15 + (y-1)*5, 47, 2, 6); 344 345 u8g2.drawStr(0, 63, "SM"); 346 for (byte i = 1; i <= x; i++) { 347 u8g2.drawBox(15 + (i-1)*5, 57, 2, 6); 348 } 349 350 } while (u8g2.nextPage()); 351}
Documentation
Schematic
...
SchematicJPG.jpg

Comments
Only logged in users can leave comments