• Single SDR receiver using 2x NE612 - Double conversion, superheterodyne (0.1-30 MHz)

QC

Single SDR receiver using 2x NE612 - Double conversion, superheterodyne (0.1-30 MHz)

This project presents a simple but highly efficient SDR (Software Defined Radio) receiver based on a double-conversion superheterodyne architecture using two NE612 mixer integrated circuits.

Devices and components

capacitors

455 kHz filter

465KHz resonator

Materials and tools

Soldering iron kit

Software and tools

Arduino IDE

Project description

Code VFO

With offset

1// By mircemk, June 2026
2
3#include <WiFi.h>
4#include <WebServer.h>
5#include <si5351.h>
6#include <Wire.h>
7
8Si5351 si5351;
9unsigned long frequency = 7000000;
10bool offsetActive = false;
11const unsigned long IF_OFFSET = 455000; // 455 kHz Offset за суперхетеродин
12
13const char* ssid = "Si5351_VFO_Final_Complete";
14const char* password = "vfo12345678";
15
16WebServer server(80);
17
18const char VFO_HTML[] PROGMEM = R"rawliteral(
19<!DOCTYPE html><html><head>
20<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
21<style>
22 :root {
23 --panel-bg: #E0AB07;
24 --inner-bezel: #E0AB07;
25 --lcd-bg: #0077c2;
26 --btn-band: #7f0000;
27 --btn-step: #27ae60;
28 --btn-mode: #2980b9;
29 --btn-mem: #8e44ad;
30 --gold-border: #f1c40f;
31 --text-color: #ecf0f1;
32 }
33 * { -webkit-tap-highlight-color: transparent; box-sizing: border-box; user-select: none; }
34 body { background: #000; margin: 0; display: flex; justify-content: center; font-family: 'Arial Black', sans-serif; color: var(--text-color); overflow: hidden; }
35 .vfo-main-frame { background: var(--panel-bg); width: 100%; max-width: 400px; height: 100vh; display: flex; flex-direction: column; align-items: center; position: relative; }
36
37 .bezel-display { background: var(--inner-bezel); width: 92%; margin-top: 15px; padding: 10px; border-radius: 8px; box-shadow: inset 4px 4px 10px #000; position: relative; }
38 .fs-zone { position: absolute; left: 0; top: 0; width: 30%; height: 100%; z-index: 10; cursor: pointer; }
39 .mem-zone { position: absolute; left: 30%; top: 0; width: 40%; height: 100%; z-index: 10; cursor: pointer; }
40 .reset-zone { position: absolute; right: 0; top: 0; width: 30%; height: 100%; z-index: 10; cursor: pointer; }
41
42 .display { background: var(--lcd-bg); border: 4px solid #111; padding: 10px; box-shadow: inset 0 0 25px #000; height: 125px; display: flex; flex-direction: column; justify-content: space-between; position: relative; transition: background 0.2s; }
43 .display.mem-active { background: #e67e22; }
44 .display.reset-flash { background: #e74c3c; }
45
46 #f-display { font-size: 55px; font-weight: 900; margin: 0; text-align: right; text-shadow: 2px 2px 4px #000; letter-spacing: -1px; line-height: 1; }
47 .display-info { display: flex; justify-content: space-between; font-size: 14px; color: rgba(255,255,255,0.9); font-family: Arial, sans-serif; }
48 .display-footer { display: flex; align-items: center; border-top: 1px solid rgba(255,255,255,0.2); padding-top: 5px; margin-bottom: 4px; }
49 #mode-label, .sig-text { font-size: 15px; font-weight: bold; color: #fff; }
50
51 .s-meter-container { display: flex; align-items: center; gap: 6px; flex-grow: 1; justify-content: flex-end; margin-left: 25px; }
52 .s-grid { display: flex; gap: 1px; height: 10px; width: 115px; background: rgba(0,0,0,0.3); border: 1px solid #111; }
53 .s-seg { flex: 1; background: #222; }
54 .s-on { background: #ffffff; box-shadow: 0 0 6px #ffffff; }
55
56 /* OFFSET DUGME */
57 .offset-btn {
58 position: absolute; right: 17px; top: 175px;
59 width: 60px; height: 60px; border-radius: 50%;
60 background: #3d0000; border: 3px solid #222;
61 color: #fff; font-size: 9px; font-weight: bold;
62 display: flex; align-items: center; justify-content: center;
63 cursor: pointer; box-shadow: 3px 3px 8px #000; z-index: 50;
64 }
65 .offset-btn.active { background: #ff0000; box-shadow: 0 0 15px #ff0000; border-color: #f1c40f; }
66
67 .bezel-knob { background: var(--inner-bezel); width: 270px; height: 270px; margin: 25px 0; border-radius: 50%; box-shadow: inset 3px 3px 10px #000; display: flex; justify-content: center; align-items: center; }
68 #knob { width: 240px; height: 240px; background: conic-gradient(#333, #777, #333); border-radius: 50%; border: 12px solid #1a1a1a; position: relative; will-change: transform; cursor: pointer; }
69
70 .controls-container { width: 94%; display: flex; flex-direction: column; }
71 .grid { display: grid; gap: 6px; width: 100%; grid-template-columns: repeat(4, 1fr); }
72 .btn { border: 3px solid var(--gold-border); border-radius: 8px; color: #fff; font-weight: 900; font-size: 15px; padding: 11px 0; text-align: center; cursor: pointer; box-shadow: 3px 5px 8px #000; text-transform: uppercase; }
73 .b-band { background: var(--btn-band); }
74 .b-step { background: var(--btn-step); font-size: 20px; padding: 12px; grid-column: span 4; margin: 12px 0; }
75 .b-mode { background: var(--btn-mode); }
76 .b-mem { background: var(--btn-mem); font-size: 13px; margin-top: 6px; }
77</style>
78</head><body>
79 <div class="vfo-main-frame">
80 <div class="bezel-display">
81 <div class="fs-zone" onclick="toggleFS()"></div>
82 <div class="mem-zone" onclick="startMem()"></div>
83 <div class="reset-zone" onclick="clearAllMem()"></div>
84 <div class="display" id="main-display">
85 <div class="display-info"><span id="band-label">40M HAM</span><span id="step-label">100Hz</span></div>
86 <h1 id="f-display">07.000.000</h1>
87 <div class="display-footer">
88 <span id="mode-label">USB</span>
89 <div class="s-meter-container">
90 <span class="sig-text">Sig:</span>
91 <div class="s-grid" id="s-grid"></div>
92 </div>
93 </div>
94 </div>
95 </div>
96 <div id="offset-led" class="offset-btn" onclick="toggleOffset()">OFFSET</div>
97 <div class="bezel-knob"><div id="knob"></div></div>
98 <div class="controls-container">
99 <div class="grid">
100 <div class="btn b-band" onclick="setBand(531000)">MW</div>
101 <div class="btn b-band" onclick="setBand(1810000)">160</div>
102 <div class="btn b-band" onclick="setBand(3500000)">80</div>
103 <div class="btn b-band" onclick="setBand(7000000)">40</div>
104 <div class="btn b-band" onclick="setBand(14000000)">20</div>
105 <div class="btn b-band" onclick="setBand(18068000)">17</div>
106 <div class="btn b-band" onclick="setBand(21000000)">15</div>
107 <div class="btn b-band" onclick="setBand(24890000)">12</div>
108 </div>
109 <div class="btn b-step" id="step-btn" onclick="nextStep()">STEP: 100Hz</div>
110 <div class="grid">
111 <div class="btn b-mode" onclick="setMode('AM')">AM</div>
112 <div class="btn b-mode" onclick="setMode('USB')">USB</div>
113 <div class="btn b-mode" onclick="setMode('LSB')">LSB</div>
114 <div class="btn b-mode" onclick="setMode('FM')">FM</div>
115 <div class="btn b-mem" id="m1" onclick="handleMem(1)">M1</div>
116 <div class="btn b-mem" id="m2" onclick="handleMem(2)">M2</div>
117 <div class="btn b-mem" id="m3" onclick="handleMem(3)">M3</div>
118 <div class="btn b-mem" id="m4" onclick="handleMem(4)">M4</div>
119 </div>
120 </div>
121 </div>
122 <script>
123 var freq = 7000000; var lastAngle = 0; var curRot = 0; var isDrag = false; var lastSent = 0;
124 var steps = [10, 100, 1000, 5000, 10000, 100000]; var stepIdx = 1;
125 var isMemMode = false; var isOffset = false;
126
127 const sGrid = document.getElementById('s-grid');
128 for(let i=0; i<20; i++) { let d=document.createElement('div'); d.className='s-seg'; sGrid.appendChild(d); }
129
130 function toggleOffset() {
131 isOffset = !isOffset;
132 document.getElementById('offset-led').classList.toggle('active');
133 fetch('/setOffset?state=' + (isOffset ? 1 : 0));
134 }
135 function sendFreq() {
136 let now = Date.now();
137 if (now - lastSent > 50) { fetch('/set?f=' + freq); lastSent = now; }
138 }
139 function updateUI() {
140 document.getElementById('f-display').innerText = Number(freq).toLocaleString('de-DE').replace(/,/g, '.');
141 updateBandLabel();
142 }
143 function move(e) {
144 if (!isDrag) return;
145 let ev = e.touches ? e.touches[0] : e;
146 let r = document.getElementById('knob').getBoundingClientRect();
147 let ang = Math.atan2(ev.clientY - (r.top + r.height/2), ev.clientX - (r.left + r.width/2)) * 180 / Math.PI;
148 let d = ang - lastAngle;
149 if (d > 180) d -= 360; if (d < -180) d += 360;
150 curRot += d;
151 freq += Math.round(d) * (steps[stepIdx] / 10);
152 if (freq < 100000) freq = 100000;
153 updateUI();
154 document.getElementById('knob').style.transform = 'rotate(' + curRot + 'deg)';
155 sendFreq();
156 lastAngle = ang;
157 }
158 let knob = document.getElementById('knob');
159 knob.addEventListener('mousedown', function(e) { isDrag = true; let r = knob.getBoundingClientRect(); lastAngle = Math.atan2(e.clientY - (r.top + r.height/2), e.clientX - (r.left + r.width/2)) * 180 / Math.PI; });
160 knob.addEventListener('touchstart', function(e) { isDrag = true; let r = knob.getBoundingClientRect(); lastAngle = Math.atan2(e.touches[0].clientY - (r.top + r.height/2), e.touches[0].clientX - (r.left + r.width/2)) * 180 / Math.PI; e.preventDefault(); }, {passive: false});
161 window.addEventListener('mouseup', () => isDrag = false);
162 window.addEventListener('touchend', () => isDrag = false);
163 window.addEventListener('mousemove', move);
164 window.addEventListener('touchmove', move, {passive: false});
165
166 function toggleFS() { if(!document.fullscreenElement) document.documentElement.requestFullscreen().catch(e=>{}); else document.exitFullscreen(); }
167 function setBand(f) { freq = f; updateUI(); sendFreq(); }
168 function setMode(m) { document.getElementById('mode-label').innerText = m; }
169 function nextStep() { stepIdx = (stepIdx + 1) % steps.length; let labels = ["10Hz", "100Hz", "1KHz", "5KHz", "10KHz", "100KHz"]; document.getElementById('step-btn').innerText = "STEP: " + labels[stepIdx]; }
170 function startMem() { isMemMode = true; document.getElementById('main-display').classList.add('mem-active'); }
171 function clearAllMem() { localStorage.clear(); document.getElementById('main-display').classList.add('reset-flash'); setTimeout(() => location.reload(), 300); }
172 function handleMem(id) {
173 if(isMemMode) { localStorage.setItem('m'+id, freq); location.reload(); }
174 else { let s = localStorage.getItem('m'+id); if(s) { freq = parseInt(s); updateUI(); sendFreq(); } }
175 }
176 function loadSavedMem() { for(let i=1; i<=4; i++){ let s = localStorage.getItem('m'+i); if(s) document.getElementById('m'+i).innerText = (parseInt(s)/1000000).toFixed(3); } }
177 function updateBandLabel() {
178 let b = document.getElementById('band-label');
179 if (freq >= 7000000 && freq <= 7200000) b.innerText = "40M HAM";
180 else if (freq >= 531000 && freq <= 1602000) b.innerText = "MW BROADCAST";
181 else b.innerText = "GEN";
182 }
183 setInterval(() => {
184 fetch('/getS').then(r => r.text()).then(v => {
185 let segs = document.querySelectorAll('.s-seg');
186 let act = Math.floor((v/100)*20);
187 segs.forEach((s,i) => { if(i<act) s.classList.add('s-on'); else s.classList.remove('s-on'); });
188 });
189 }, 250);
190 loadSavedMem(); updateUI();
191 </script>
192</body></html>
193)rawliteral";
194
195void updateSi5351() {
196 unsigned long outFreq = frequency;
197 if (offsetActive) outFreq += IF_OFFSET;
198 si5351.set_freq(outFreq * 100ULL, SI5351_CLK0);
199}
200
201void setup() {
202 Serial.begin(115200);
203 Wire.begin(21, 22);
204
205 pinMode(32, INPUT);
206 analogReadResolution(12);
207 analogSetAttenuation(ADC_6db);
208
209 si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0);
210 updateSi5351();
211
212 WiFi.mode(WIFI_AP);
213 WiFi.softAP(ssid, password);
214 WiFi.setTxPower(WIFI_POWER_19_5dBm); // Максимално засилување на Wi-Fi
215
216 server.on("/", []() { server.send(200, "text/html", VFO_HTML); });
217 server.on("/set", []() {
218 if (server.hasArg("f")) {
219 frequency = server.arg("f").toInt();
220 updateSi5351();
221 server.send(200, "text/plain", "OK");
222 }
223 });
224 server.on("/setOffset", []() {
225 if (server.hasArg("state")) {
226 offsetActive = server.arg("state").toInt() == 1;
227 updateSi5351();
228 server.send(200, "text/plain", "OK");
229 }
230 });
231 server.on("/getS", []() {
232 int val = analogRead(32);
233 int percent = map(val, 0, 1200, 0, 100);
234 if(percent > 100) percent = 100;
235 server.send(200, "text/plain", String(percent));
236 });
237
238 server.begin();
239}
240
241void loop() { server.handleClient(); }

Documentation

drmreceiversuper

drmreceiversuper.png




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.

Arduino Tutorial: Mini Piano

In this video I show you how to make a mini piano with Arduino. Devices and components Arduino Uno Rev3 Jumper wires (generic) Buzzer Breadb...