Project · Instrumentation · ADC
A pocket scope from one ADC pin
You don't always need a $300 instrument. For “is this signal alive, how big is it, what frequency?” an ESP32's own 12-bit ADC answers all three: burst-sample the pin, do three loops of maths, and read Vpp, Vrms and frequency off a live dashboard. Not a Rigol — but a real measurement tool that fits in a matchbox.
First: don't cook the pin
The ADC tolerates 0–3.3 V, nothing else. Real-world signals swing negative and spike — so the front-end matters more than the code: a coupling capacitor into a two-resistor mid-rail bias (so AC swings around 1.65 V), and a pair of clamping diodes to 3V3 and GND for everything unexpected. Also: use an ADC1 pin (GPIO32–39) — ADC2 stops working the moment Wi-Fi is on.
The maths — three loops, no FFT needed
For “what is this signal”, time-domain maths beats reaching for an FFT: min/max gives Vpp, the mean-removed RMS gives Vrms, and counting rising zero-crossings of the mean-removed signal gives frequency — robust even on a noisy trace:
// --- the scope maths: mean, Vpp, Vrms, zero-cross frequency ---
float mean = 0, mn = 99, mx = -99;
for (int i = 0; i < N; i++) { mean += buf[i]; if (buf[i] < mn) mn = buf[i]; if (buf[i] > mx) mx = buf[i]; }
mean /= N;
float sq = 0; int crossings = 0;
for (int i = 1; i < N; i++) {
float a = buf[i - 1] - mean, b = buf[i] - mean;
sq += b * b;
if (a < 0 && b >= 0) crossings++; // rising zero-crossings
}
vpp = mx - mn;
vrms = sqrtf(sq / (N - 1));
freq = crossings * SPS / N; // crossings per second Sample-rate honesty: plain analogRead() reaches roughly 10 kSps —
by Nyquist that's clean up to a few kHz, perfect for mains hum, audio and sensor waveforms. Need
more? The ESP32's I2S/DMA ADC path streams 50 kSps+ without the CPU touching each sample — same
maths, bigger buffer.
The dashboard
dash.layout("Scope", RICON_SIGNAL);
dash.stat("Frequency", &freq, "Hz");
dash.stat("Vpp", &vpp, "V");
dash.stat("Vrms", &vrms, "V");
dash.chart("Signal level", &level, "V"); // the live "screen"
dash.beginAP("RisalDash-Scope", "12345678");
// acquire a burst — real board:
// for (int i = 0; i < N; i++) buf[i] = analogRead(PIN) * 3.3f / 4095.0f; The shipped example (examples/04_Templates/Oscilloscope) runs on a
synthetic 45–55 Hz sine + noise, so you can watch the frequency counter track the
drift before any wire is connected — then swap the demo loop for analogRead(). Add the
battery-monitor divider from the battery guide and the same
pin measures DC rails too.
What it's honestly good for
| Task | Verdict |
|---|---|
| Is this sensor/oscillator alive? | ✅ perfect |
| Mains hum, audio-range signals | ✅ clean measurements |
| PWM duty & frequency up to ~5 kHz | ✅ works |
| MHz digital buses, µs edges | ❌ get a logic analyser / real scope |
Watch the maths track a drifting signal before you wire anything.