This project shows how to maintain a true circular history of high‑rate SAR‑ADC data on an ESP32‑S2 Feather V2 while staying completely inside Espressif’s official ADC‑continuous driver. We let the driver do what it does best—DMA blocks into its internal 4 KB “pool”—and immediately copy each finished frame into a larger power‑of‑two RAM ring that overwrites the oldest samples. That design costs a single memcpy() per frame but avoids patching the driver, giving you oscilloscope‑style pre‑trigger capture at up to ≈1 MSPS with plenty of CPU head‑room. (docs.espressif.com, reddit.com)
- Allocated with
max_store_buf_size = 4096,flush_pool = falseso overflow is detectable (driver raiseson_pool_ovf). (docs.espressif.com, docs.espressif.com) - DMA writes through a linked list of 4 KB
lldesc_tdescriptors. (reddit.com)
circ_buf[]– DMA‑capable internal RAM sized to 2ⁿ samples (e.g. 32768 × 2 bytes ≈ 64 kB).- Critical section (
portENTER_CRITICAL(&s_data_lock)) protects the write pointer because ISR and task share one core. (docs.espressif.com) - Copy cost < 1 µs per 256‑byte frame on a 240 MHz S2, leaving > 90 % CPU idle at 1 MSPS. (github.com)
ADC -> DMA --> IDF Pool ----> circ_buf[] -----> Trigger analysis
(flush reset) memcpy (task)
- Driver Init – see
continuous_adc_init(). - DMA task (in
app_main) copies each frame intocirc_bufand accumulates voltage stats. - UI task (
processing_task) prints once per second and can later export pre/post‑trigger data. flush_poolcleared so you detect backlog viaESP_ERR_INVALID_STATEinstead of silent data loss. (docs.espressif.com)
idf.py set-target esp32s2 # Feather V2
idf.py menuconfig # Optional: tweak stack & speed
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor| Macro | Description | Typical |
|---|---|---|
SAMPLE_FREQ_HZ |
ADC clock (≤ 2.2 MSPS on S2) | 1 000 000 |
EXAMPLE_READ_LEN |
Driver frame size | 256–2048 bytes |
CIRC_BUF_SAMPLES |
Size of user ring (power‑of‑two) | 32 768 |
RAM usage ≈ READ_LEN + CIRC_BUF_SAMPLES*2 + stack → 64 kB by default.
- External GPIO ISR sets
trigger_seen = 1. - After post‑trigger delay, call
adc_continuous_stop(). - Linearise ring for upload:
size_t start = (wr - PRE_SAMPLES) & BUF_MASK;
for (size_t i=0;i<TOTAL;i++)
out[i] = circ_buf[(start+i) & BUF_MASK];Driver enables line‑fitting calibration to trim reference‑voltage error (< 3 mV typical). (docs.espressif.com) Disable if only relative accuracy matters.
- Copy overhead ≈ 12 % CPU at 1 MSPS, 256‑byte frames.
- UART printing is the bottleneck—keep logs sparse.
- Max proven throughput (no Wi‑Fi) ≈ 8 MSPS before ring overruns. (github.com)
- Swap
memcpyfor GDMA memcpy once exposed on S2. - Move sample export to USB CDC for higher bandwidth.
- Fork driver (Option B) to remove the copy entirely.
- IDF ADC continuous driver – pool & overflow. (docs.espressif.com)
- ESP32‑S2 driver variant. (docs.espressif.com)
- Reddit
lldesc_tDMA explanation. (reddit.com) - FreeRTOS spinlock note. (docs.espressif.com)
- Calibration driver doc. (docs.espressif.com)
- Continuous‑read sample. (docs.espressif.com)
- DroneBot I2S primer (for Option C background). (dronebotworkshop.com)
- Forum circular‑SPI DMA example. (esp32.com)
- ADC accuracy issue motivating calibration. (github.com)
- Reddit note on 13‑bit width (ESP32‑S2). (reddit.com)