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.