QC

Arduino VFO project with a large LCD screen

Cheap and easy to build VFO device, almost indispensable in radio engineering, especially in DIY radio receivers.

Devices and components

Arduino-Nano

rotary encoder

Generic 128x64 OLED I2C

Electrical switches

Push button

SI5351 CLOCK GENERATION MODULE

Materials and tools

Soldering kit

Software and tools

Arduino IDE

Project description

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

DiagramJPG.jpg




Note: Content and images are from: https://projecthub.arduino.cc/, with some modifications.
If you want it removed due to copyright reasons, please leave a comment. Thank you.
I want to share this article more widely so that everyone knows about Arduino and your project.

Easy remote control

Remotely control an LED with ease. Devices and components Arduino Nano 33 BLE with headers Box 525 Resistors precision 1% - 17 values Breadb...