CTRL library / Real-time
External ADC
This sketch demonstrates how to use storeRaw() with a timer ISR or DMA for non-blocking ADC reads in real-time applications.
A real-time application is one where correctness depends on when the system delivers a result as much as on what it computes. Missing a deadline, even by a few microseconds, can mean a dropped MIDI note, a motor control glitch, a skipped sensor sample, or a timing fault in a safety-critical system. The loop() must complete each iteration within a bounded, predictable time window.
Calling analogRead() blocks the CPU for 10-100 microseconds per read. With 8 knobs that's up to 800us of dead time per loop: enough to blow past a real-time deadline and cause exactly the kind of jitter described above.
The solution is to let a DMA controller or timer ISR fill a buffer with ADC readings in the background, call storeRaw() on each CtrlPot from the ISR, and then call process() as normal from the main loop. The library still handles:
- Q16 fixed-point smoothing (filters jitter from noisy ADC readings)
- Value mapping (raw 0-1023 to your output range, e.g. 0-127 for MIDI)
- Change detection (only fires the callback when the value actually changes)
This means you get zero-copy, non-blocking knob processing with no boilerplate smoothing, deduplication, or ready-flag management.
The code
#include <CtrlPot.h> // Classic ESP32 boards (e.g. the ESP32 Dev Module) have no A1 & A2,// so A4 & A5 are used there instead.#if defined(CONFIG_IDF_TARGET_ESP32)const uint8_t ANALOG_PIN_1 = A4;const uint8_t ANALOG_PIN_2 = A5;#elseconst uint8_t ANALOG_PIN_1 = A1;const uint8_t ANALOG_PIN_2 = A2;#endif void onVolumeChange(int value) { Serial.print("Volume: "); Serial.println(value);} void onFilterChange(int value) { Serial.print("Filter cutoff: "); Serial.println(value);} void onResonanceChange(int value) { Serial.print("Resonance: "); Serial.println(value);} void onDriveChange(int value) { Serial.print("Drive: "); Serial.println(value);} CtrlPot volumeKnob(A0, 127, 0.05, onVolumeChange);CtrlPot filterKnob(ANALOG_PIN_1, 100, 0.05, onFilterChange);CtrlPot resonanceKnob(ANALOG_PIN_2, 100, 0.05, onResonanceChange);CtrlPot driveKnob(A3, 255, 0.05, onDriveChange); // --- DMA / ISR side (runs in background) ---// On Teensy/STM32/ESP32 you would configure a hardware timer// or DMA descriptor to trigger this automatically.// storeRaw() is ISR-safe: it only writes to a volatile buffer and sets a flag.void timerISR() { volumeKnob.storeRaw(analogRead(A0)); filterKnob.storeRaw(analogRead(ANALOG_PIN_1)); resonanceKnob.storeRaw(analogRead(ANALOG_PIN_2)); driveKnob.storeRaw(analogRead(A3));} void setup() { Serial.begin(9600); // In a real project you would configure your timer/DMA here, e.g.: // IntervalTimer myTimer; // myTimer.begin(timerISR, 1000); // read ADC every 1ms} void loop() { // Simulate the ISR firing (in a real project the timer does this). timerISR(); // process() detects the pending ISR values and consumes them. // No analogRead() calls happen here: zero blocking time. volumeKnob.process(); filterKnob.process(); resonanceKnob.process(); driveKnob.process(); // Your audio/DSP processing goes here, uninterrupted by ADC reads.}