From ec7aa9238a58a49cc14e0b8f4593484f0538e222 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 31 Jul 2026 21:26:52 +0200 Subject: [PATCH 01/18] add ADC manager to support continuous reading alongside single shot readings --- lib/wled_ADCmanager/library.json | 4 + lib/wled_ADCmanager/wled_ADCmanager.cpp | 269 ++++++++++++++++++++++++ lib/wled_ADCmanager/wled_ADCmanager.h | 94 +++++++++ wled00/wled.h | 1 + 4 files changed, 368 insertions(+) create mode 100644 lib/wled_ADCmanager/library.json create mode 100644 lib/wled_ADCmanager/wled_ADCmanager.cpp create mode 100644 lib/wled_ADCmanager/wled_ADCmanager.h diff --git a/lib/wled_ADCmanager/library.json b/lib/wled_ADCmanager/library.json new file mode 100644 index 0000000000..093d4028e8 --- /dev/null +++ b/lib/wled_ADCmanager/library.json @@ -0,0 +1,4 @@ +{ + "name": "wled-ADCmanager", + "build": { "libArchive": false } +} diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp new file mode 100644 index 0000000000..d371ecf6c6 --- /dev/null +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -0,0 +1,269 @@ +/* + * ADC manager to handle continous ADC sampling in parallel with single-shot pin reads + * by @dedehai (2026) licensed under EUPL 1.2 license + */ + +#include "wled_adcmanager.h" + +// prevent macro recursion of arduino overrides +#undef analogRead +#if defined(ARDUINO_ARCH_ESP32) +#undef analogReadMilliVolts +#endif + +#ifdef ARDUINO_ARCH_ESP32 + +#include + +static bool _isADC1(uint8_t pin, int8_t ch) { +#if defined(CONFIG_IDF_TARGET_ESP32) + (void)ch; return (pin >= 32 && pin <= 39); +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + return (ch >= 0 && ch <= 9); +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + return (ch >= 0 && ch <= 9); +#elif defined(CONFIG_IDF_TARGET_ESP32C3) + return (ch >= 0 && ch <= 4); +#elif defined(CONFIG_IDF_TARGET_ESP32C6) + (void)ch; return (pin <= 5); +#else + (void)pin; (void)ch; return true; +#endif +} + +bool WLEDAdcManager::_pinToChannel(uint8_t pin, adc_channel_t* ch) { + int8_t c = digitalPinToAnalogChannel(pin); + if (c < 0 || !_isADC1(pin, c)) return false; + *ch = (adc_channel_t)c; + return true; +} + +WLEDAdcManager& WLEDAdcManager::instance() { + static WLEDAdcManager inst; + return inst; +} + +WLEDAdcManager::WLEDAdcManager() + : _pin(0xFF), _channel(ADC_CHANNEL_0), _sampleRate(0), _samplesPerFrame(0), _handle(nullptr), _running(false), _cache(nullptr), _cacheSize(0), _cacheCount(0), _cali(nullptr) { + _mutex = xSemaphoreCreateMutex(); +} + +WLEDAdcManager::~WLEDAdcManager() { + end(); + if (_mutex) vSemaphoreDelete(_mutex); +#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED + if (_cali) adc_cali_delete_scheme_line_fitting(_cali); +#endif +} + +bool WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame) { + xSemaphoreTake(_mutex, portMAX_DELAY); + _destroyHandle(); + _cacheCount = 0; + + if (!_pinToChannel(pin, &_channel)) { + xSemaphoreGive(_mutex); + return false; + } + _pin = pin; + _sampleRate = sampleRateHz; + _samplesPerFrame = samplesPerFrame; + + if (_cache) free(_cache); + _cacheSize = samplesPerFrame; + _cache = (int16_t*)calloc(_cacheSize, sizeof(int16_t)); + + bool ok = _createHandle(); + xSemaphoreGive(_mutex); + return ok; +} + +void WLEDAdcManager::end() { + xSemaphoreTake(_mutex, portMAX_DELAY); + _destroyHandle(); + _cacheCount = 0; + if (_cache) { free(_cache); _cache = nullptr; _cacheSize = 0; } + _pin = 0xFF; + xSemaphoreGive(_mutex); +} + +bool WLEDAdcManager::_createHandle() { + if (_handle) return true; + size_t frameBytes = (size_t)_samplesPerFrame * sizeof(adc_digi_output_data_t); + adc_continuous_handle_cfg_t hcfg = { + .max_store_buf_size = frameBytes * 4, + .conv_frame_size = frameBytes, + .flags = { .flush_pool = true }, + }; + if (adc_continuous_new_handle(&hcfg, &_handle) != ESP_OK) return false; + + adc_digi_pattern_config_t pat = { + .atten = ADC_ATTEN_DB_12, + .channel = (uint8_t)_channel, + .unit = ADC_UNIT_1, + .bit_width = ADC_BITWIDTH_12, + }; + adc_continuous_config_t cfg = { + .pattern_num = 1, + .adc_pattern = &pat, + .sample_freq_hz = _sampleRate, + .conv_mode = ADC_CONV_SINGLE_UNIT_1, + .format = WLED_ADC_DIGI_FORMAT, + }; + if (adc_continuous_config(_handle, &cfg) != ESP_OK || + adc_continuous_start(_handle) != ESP_OK) { + _destroyHandle(); + return false; + } + _running = true; + return true; +} + +void WLEDAdcManager::_destroyHandle() { + if (_handle) { + adc_continuous_stop(_handle); + adc_continuous_deinit(_handle); + _handle = nullptr; + } + _running = false; +} + +void WLEDAdcManager::_drainToCache() { + if (!_handle || !_cache) return; + adc_continuous_stop(_handle); + adc_digi_output_data_t temp[64]; + _cacheCount = 0; + while (_cacheCount < _cacheSize) { + uint32_t n = 0; + if (adc_continuous_read(_handle, (uint8_t*)temp, sizeof(temp), &n, 0) != ESP_OK || n == 0) break; + uint16_t cnt = n / sizeof(adc_digi_output_data_t); + for (uint16_t i = 0; i < cnt && _cacheCount < _cacheSize; i++) { + _cache[_cacheCount++] = (int16_t)((int)(temp[i].WLED_ADC_OUT_TYPE.data) - 2048); + } + } +} + +bool WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs) { + if (!buffer || !numSamples) return false; + xSemaphoreTake(_mutex, portMAX_DELAY); + + uint16_t out = 0; + + if (_cacheCount) { + uint16_t copy = _cacheCount < numSamples ? _cacheCount : numSamples; + memcpy(buffer, _cache, copy * sizeof(int16_t)); + out = copy; + if (copy < _cacheCount) { + memmove(_cache, _cache + copy, (_cacheCount - copy) * sizeof(int16_t)); + } + _cacheCount -= copy; + } + + if (out >= numSamples) { + xSemaphoreGive(_mutex); + return true; + } + if (!_handle) { + xSemaphoreGive(_mutex); + return false; + } + + adc_continuous_start(_handle); + adc_digi_output_data_t temp[64]; + + while (out < numSamples) { + uint32_t n = 0; + uint16_t want = (numSamples - out) < 64 ? (numSamples - out) : 64; + size_t wantBytes = want * sizeof(adc_digi_output_data_t); + + if (adc_continuous_read(_handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(timeoutMs)) != ESP_OK || n == 0) { + adc_continuous_stop(_handle); + xSemaphoreGive(_mutex); + return false; + } + uint16_t got = n / sizeof(adc_digi_output_data_t); + for (uint16_t i = 0; i < got; i++) { + buffer[out++] = (int16_t)((int)(temp[i].WLED_ADC_OUT_TYPE.data) - 2048); + } + } + + adc_continuous_stop(_handle); + xSemaphoreGive(_mutex); + return true; +} + +bool WLEDAdcManager::_oneshotRead(adc_channel_t ch, int* outRaw) { + adc_oneshot_unit_handle_t h; + adc_oneshot_unit_init_cfg_t icfg = { .unit_id = ADC_UNIT_1, .ulp_mode = ADC_ULP_MODE_DISABLE }; + if (adc_oneshot_new_unit(&icfg, &h) != ESP_OK) return false; + + adc_oneshot_chan_cfg_t ccfg = { .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_12 }; + adc_oneshot_config_channel(h, ch, &ccfg); + + bool ok = (adc_oneshot_read(h, ch, outRaw) == ESP_OK); + adc_oneshot_del_unit(h); + return ok; +} + +int WLEDAdcManager::analogRead(uint8_t pin) { + Serial.println("ADCread"); + int raw = 0; + adc_channel_t ch; + if (!_pinToChannel(pin, &ch)) return 0; + + xSemaphoreTake(_mutex, portMAX_DELAY); + if (_running) { + _drainToCache(); + _destroyHandle(); + _oneshotRead(ch, &raw); + _createHandle(); + } else { + _oneshotRead(ch, &raw); + } + xSemaphoreGive(_mutex); + return raw; +} + +bool WLEDAdcManager::_initCali() { + if (_cali) return true; +#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED + adc_cali_line_fitting_config_t cfg = { + .unit_id = ADC_UNIT_1, + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + if (adc_cali_create_scheme_line_fitting(&cfg, &_cali) == ESP_OK) return true; +#endif + return false; +} + +int WLEDAdcManager::analogReadMilliVolts(uint8_t pin) { + int raw = analogRead(pin); + if (!_cali && !_initCali()) return (raw * 3300) / 4095; + + int mv = 0; + return (adc_cali_raw_to_voltage(_cali, raw, &mv) == ESP_OK) ? mv : (raw * 3300) / 4095; +} + +#endif // ARDUINO_ARCH_ESP32 + +// C wrappers +extern "C" { + +int wled_adc_analog_read(uint8_t pin) { +#ifdef ARDUINO_ARCH_ESP32 + return WLEDAdcManager::instance().analogRead(pin); +#else + eturn analogRead(pin); +#endif +} + +int wled_adc_analog_read_mv(uint8_t pin) { +#ifdef ARDUINO_ARCH_ESP32 + return WLEDAdcManager::instance().analogReadMilliVolts(pin); +#else + eturn (int)(analogRead(pin) / 1023.0f); +#endif +} + +} \ No newline at end of file diff --git a/lib/wled_ADCmanager/wled_ADCmanager.h b/lib/wled_ADCmanager/wled_ADCmanager.h new file mode 100644 index 0000000000..6cf5dda881 --- /dev/null +++ b/lib/wled_ADCmanager/wled_ADCmanager.h @@ -0,0 +1,94 @@ +/* + * ADC manager to handle continous ADC sampling in parallel with single-shot pin reads + * by @dedehai (2026) licensed under EUPL 1.2 license + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int wled_adc_analog_read(uint8_t pin); +int wled_adc_analog_read_mv(uint8_t pin); + +#ifdef __cplusplus +} + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) + #define WLED_ADC_DIGI_FORMAT ADC_DIGI_OUTPUT_FORMAT_TYPE1 + #define WLED_ADC_OUT_TYPE type1 +#else + #define WLED_ADC_DIGI_FORMAT ADC_DIGI_OUTPUT_FORMAT_TYPE2 + #define WLED_ADC_OUT_TYPE type2 +#endif + +class WLEDAdcManager { +public: + static WLEDAdcManager& instance(); + + bool begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame); + void end(); + + bool readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs = 100); + + int analogRead(uint8_t pin); + int analogReadMilliVolts(uint8_t pin); + + bool isRunning() const { return _running; } + +private: + WLEDAdcManager(); + ~WLEDAdcManager(); + WLEDAdcManager(const WLEDAdcManager&) = delete; + WLEDAdcManager& operator=(const WLEDAdcManager&) = delete; + + static bool _pinToChannel(uint8_t pin, adc_channel_t* ch); + + bool _createHandle(); + void _destroyHandle(); + void _drainToCache(); + bool _oneshotRead(adc_channel_t ch, int* outRaw); + bool _initCali(); + + SemaphoreHandle_t _mutex; + + uint8_t _pin; + adc_channel_t _channel; + uint32_t _sampleRate; + uint16_t _samplesPerFrame; + adc_continuous_handle_t _handle; + bool _running; + + int16_t* _cache; + uint16_t _cacheSize; + uint16_t _cacheCount; + + adc_cali_handle_t _cali; +}; + +#endif // ARDUINO_ARCH_ESP32 + +// override of native Arduino functions for compatibility with external usermods +#undef analogRead +#define analogRead(pin) wled_adc_analog_read(pin) +#if defined(ARDUINO_ARCH_ESP32) +#undef analogReadMilliVolts +#define analogReadMilliVolts(pin) wled_adc_analog_read_mv(pin) +#else +#define analogReadMilliVolts(pin) wled_adc_analog_read_mv(pin) +#endif + +#endif // __cplusplus \ No newline at end of file diff --git a/wled00/wled.h b/wled00/wled.h index 9bafb49196..89194d1ddb 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -196,6 +196,7 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #ifndef WLED_DISABLE_ESPNOW #include #endif +#include #include "colors.h" #include "fcn_declare.h" #ifndef WLED_DISABLE_OTA From 87ad1f1737fca65105e64427ed79f8f1ae617e06 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 4 Aug 2026 20:34:55 +0200 Subject: [PATCH 02/18] lots of improvements after testing --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 164 +++++++++++++++++------- 1 file changed, 119 insertions(+), 45 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index d371ecf6c6..28b154024e 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -1,10 +1,23 @@ /* * ADC manager to handle continous ADC sampling in parallel with single-shot pin reads * by @dedehai (2026) licensed under EUPL 1.2 license + * + * calling begin() will start sampling an ADC pin in continuous mode + * any subsequent call for analogRead() or analogReadMilliVolts() will stop the continuous sampling immediately, + * drain the already sampled data into a buffer, read a pin in one-shot mode, then continue the sampling. + * replaces analogRead() and analogReadMilliVolts() with managed functions for code compatibility + + TODO: + - could add the option to use hardware IIR filter, although the lowest coefficient setting of 2 already has a 3dB cutoff around 2kHz (to be confirmed) at 20kHz sample rate + - IIR filter are supported on all modern ESP32 but probably lacking on ESP32 classic, there we would need to do it in post-processing i.e. when writing the sample buffer + - need to add a "buffer full" callback? + - add namespace? */ -#include "wled_adcmanager.h" +//#include "wled_adcmanager.h" +#include "wled.h" +//namespace wled { // prevent macro recursion of arduino overrides #undef analogRead #if defined(ARDUINO_ARCH_ESP32) @@ -13,6 +26,8 @@ #ifdef ARDUINO_ARCH_ESP32 +#define DMA_BLOCKSIZE 128 // DMA buffer block size, IDF driver uses 5 blocks under the hood, there is an ISR call each time a block finishes so dont make it too small + #include static bool _isADC1(uint8_t pin, int8_t ch) { @@ -44,7 +59,16 @@ WLEDAdcManager& WLEDAdcManager::instance() { } WLEDAdcManager::WLEDAdcManager() - : _pin(0xFF), _channel(ADC_CHANNEL_0), _sampleRate(0), _samplesPerFrame(0), _handle(nullptr), _running(false), _cache(nullptr), _cacheSize(0), _cacheCount(0), _cali(nullptr) { + : _pin(0xFF) + , _channel(ADC_CHANNEL_0) + , _sampleRate(0) + , _samplesPerFrame(0) + , _handle(nullptr) + , _running(false) + , _cache(nullptr) + , _cacheSize(0) + , _cacheCount(0) + , _cali(nullptr) { _mutex = xSemaphoreCreateMutex(); } @@ -58,7 +82,7 @@ WLEDAdcManager::~WLEDAdcManager() { bool WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame) { xSemaphoreTake(_mutex, portMAX_DELAY); - _destroyHandle(); + _endContinuousADC(); _cacheCount = 0; if (!_pinToChannel(pin, &_channel)) { @@ -73,30 +97,54 @@ bool WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesP _cacheSize = samplesPerFrame; _cache = (int16_t*)calloc(_cacheSize, sizeof(int16_t)); - bool ok = _createHandle(); + bool ok = _initContinuousADC(); xSemaphoreGive(_mutex); return ok; } void WLEDAdcManager::end() { xSemaphoreTake(_mutex, portMAX_DELAY); - _destroyHandle(); + _endContinuousADC(); _cacheCount = 0; if (_cache) { free(_cache); _cache = nullptr; _cacheSize = 0; } _pin = 0xFF; xSemaphoreGive(_mutex); } -bool WLEDAdcManager::_createHandle() { +/* +// buffer overflow callback, we need to watch this as permanent overflow causes stalls in combination with wifi -> it does not it was an IDF bug +volatile bool _overflow = false; +static bool IRAM_ATTR __attribute__((noinline)) _onPoolOvf(adc_continuous_handle_t handle, + const adc_continuous_evt_data_t* edata, + void* user_data) { + //auto* mgr = static_cast(user_data); + _overflow = true; // one word write, ISR-safe, no locks, no copying + return false; // nothing to wake +} + +void WLEDAdcManager::checkADC() { + if (_running && _overflow) { + adc_continuous_stop(_handle); + adc_continuous_flush_pool(_handle); // flush remaining data, we want fresh samples + adc_continuous_start(_handle); + _overflow = false; + } +} +*/ +bool WLEDAdcManager::_initContinuousADC() { if (_handle) return true; size_t frameBytes = (size_t)_samplesPerFrame * sizeof(adc_digi_output_data_t); adc_continuous_handle_cfg_t hcfg = { - .max_store_buf_size = frameBytes * 4, - .conv_frame_size = frameBytes, - .flags = { .flush_pool = true }, + .max_store_buf_size = frameBytes * 1, // hold two frames in buffer, caller needs to drain it fast enough to avoid data loss + .conv_frame_size = DMA_BLOCKSIZE, // use fixed DMA buffer size of 256 bytes (ADC driver creates 5 DMA descriptors with one buffer each, at 20kHz this means an interrupt every 1.4ms + .flags = { .flush_pool = false }, // do not flush the store buffer on overrun but discard new samples (true means discard oldest, is much slower and can cause issues, do not set true) }; if (adc_continuous_new_handle(&hcfg, &_handle) != ESP_OK) return false; + // register buffer overflow callback (sets flag, main loop needs to call checkADC() to clear overflow - this is to prevent wifi stalling due to a now fixed IDF bug causing a lockup) + //adc_continuous_evt_cbs_t cbs = { .on_conv_done = nullptr, .on_pool_ovf = _onPoolOvf }; + //adc_continuous_register_event_callbacks(_handle, &cbs, this); + adc_digi_pattern_config_t pat = { .atten = ADC_ATTEN_DB_12, .channel = (uint8_t)_channel, @@ -112,14 +160,14 @@ bool WLEDAdcManager::_createHandle() { }; if (adc_continuous_config(_handle, &cfg) != ESP_OK || adc_continuous_start(_handle) != ESP_OK) { - _destroyHandle(); + _endContinuousADC(); return false; } _running = true; return true; } -void WLEDAdcManager::_destroyHandle() { +void WLEDAdcManager::_endContinuousADC() { if (_handle) { adc_continuous_stop(_handle); adc_continuous_deinit(_handle); @@ -130,25 +178,27 @@ void WLEDAdcManager::_destroyHandle() { void WLEDAdcManager::_drainToCache() { if (!_handle || !_cache) return; - adc_continuous_stop(_handle); - adc_digi_output_data_t temp[64]; + adc_digi_output_data_t temp[32]; // size of data packets to request, 32 samples at 22kHz is 1.5ms, leftover samples are lost _cacheCount = 0; while (_cacheCount < _cacheSize) { uint32_t n = 0; + // read what is available in the buffer in chunks (no timeout means do not wait for any additional samples) if (adc_continuous_read(_handle, (uint8_t*)temp, sizeof(temp), &n, 0) != ESP_OK || n == 0) break; uint16_t cnt = n / sizeof(adc_digi_output_data_t); for (uint16_t i = 0; i < cnt && _cacheCount < _cacheSize; i++) { - _cache[_cacheCount++] = (int16_t)((int)(temp[i].WLED_ADC_OUT_TYPE.data) - 2048); + _cache[_cacheCount++] = (int16_t)(temp[i].WLED_ADC_OUT_TYPE.data); } } + adc_continuous_stop(_handle); // stop after reading (if stopped before, no samples are being read) } +// read samples acquired in continuous mode. They are written as 12bit unsigned values into the passed buffer bool WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs) { - if (!buffer || !numSamples) return false; + if (!_running || !buffer || !numSamples) return false; xSemaphoreTake(_mutex, portMAX_DELAY); uint16_t out = 0; - + // note: DMA uses SOC_ADC_DIGI_MAX_BITWIDTH which is 12bits on all checked units TODO: should make sure and handle this to future proof it if (_cacheCount) { uint16_t copy = _cacheCount < numSamples ? _cacheCount : numSamples; memcpy(buffer, _cache, copy * sizeof(int16_t)); @@ -167,46 +217,63 @@ bool WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t xSemaphoreGive(_mutex); return false; } - - adc_continuous_start(_handle); - adc_digi_output_data_t temp[64]; + const int tmpBfrSize = 128; // use buffer on stack to read samples from the driver + adc_digi_output_data_t temp[tmpBfrSize]; while (out < numSamples) { uint32_t n = 0; - uint16_t want = (numSamples - out) < 64 ? (numSamples - out) : 64; + uint16_t want = (numSamples - out) < tmpBfrSize ? (numSamples - out) : tmpBfrSize; size_t wantBytes = want * sizeof(adc_digi_output_data_t); - - if (adc_continuous_read(_handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(timeoutMs)) != ESP_OK || n == 0) { - adc_continuous_stop(_handle); + esp_err_t err = adc_continuous_read(_handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(timeoutMs)); + // TODO: add the possibility to "read what is available" and return the number of samples read? + if (err != ESP_OK || n == 0) { + // something went wrong, re-init the continuous transfer + DEBUG_PRINTF_P(PSTR("ADC read error %d, got n=%d restarting"),err,n); + _endContinuousADC(); // tear it down + _initContinuousADC(); // re-init and start sampling xSemaphoreGive(_mutex); return false; } + // copy the data into 16bit buffer uint16_t got = n / sizeof(adc_digi_output_data_t); for (uint16_t i = 0; i < got; i++) { - buffer[out++] = (int16_t)((int)(temp[i].WLED_ADC_OUT_TYPE.data) - 2048); + buffer[out++] = (int16_t)(temp[i].WLED_ADC_OUT_TYPE.data); // TODO: need to check bit width? see not above } } - adc_continuous_stop(_handle); + //adc_continuous_flush_pool(_handle); // flush remaining data -> no need, just let the samples accumulate + xSemaphoreGive(_mutex); return true; } +// a one-shot read takes about 0.8-2.5ms if continuous reading is active, 0.2ms otherwise bool WLEDAdcManager::_oneshotRead(adc_channel_t ch, int* outRaw) { adc_oneshot_unit_handle_t h; - adc_oneshot_unit_init_cfg_t icfg = { .unit_id = ADC_UNIT_1, .ulp_mode = ADC_ULP_MODE_DISABLE }; + adc_oneshot_unit_init_cfg_t icfg = { + .unit_id = ADC_UNIT_1, + .ulp_mode = ADC_ULP_MODE_DISABLE }; if (adc_oneshot_new_unit(&icfg, &h) != ESP_OK) return false; - adc_oneshot_chan_cfg_t ccfg = { .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_12 }; + adc_oneshot_chan_cfg_t ccfg = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12 }; adc_oneshot_config_channel(h, ch, &ccfg); bool ok = (adc_oneshot_read(h, ch, outRaw) == ESP_OK); adc_oneshot_del_unit(h); + if (ok) { + // The raw result is in SOC_ADC_RTC_MAX_BITWIDTH bits, independent of .bitwidth set above, we use 12bit in WLED + #if (SOC_ADC_RTC_MAX_BITWIDTH > 12) + *outRaw >>= (SOC_ADC_RTC_MAX_BITWIDTH - 12); + #elif (SOC_ADC_RTC_MAX_BITWIDTH < 12) + *outRaw <<= (12 - SOC_ADC_RTC_MAX_BITWIDTH); + #endif + } return ok; } int WLEDAdcManager::analogRead(uint8_t pin) { - Serial.println("ADCread"); int raw = 0; adc_channel_t ch; if (!_pinToChannel(pin, &ch)) return 0; @@ -214,9 +281,9 @@ int WLEDAdcManager::analogRead(uint8_t pin) { xSemaphoreTake(_mutex, portMAX_DELAY); if (_running) { _drainToCache(); - _destroyHandle(); + _endContinuousADC(); _oneshotRead(ch, &raw); - _createHandle(); + _initContinuousADC(); // re-init and start sampling again } else { _oneshotRead(ch, &raw); } @@ -237,7 +304,13 @@ bool WLEDAdcManager::_initCali() { return false; } +#if SOC_ADC_DIG_IIR_FILTER_SUPPORTED +#endif + int WLEDAdcManager::analogReadMilliVolts(uint8_t pin) { + int result_mv = 0; + adc_channel_t ch; + if (!_pinToChannel(pin, &ch)) return 0; int raw = analogRead(pin); if (!_cali && !_initCali()) return (raw * 3300) / 4095; @@ -250,20 +323,21 @@ int WLEDAdcManager::analogReadMilliVolts(uint8_t pin) { // C wrappers extern "C" { -int wled_adc_analog_read(uint8_t pin) { -#ifdef ARDUINO_ARCH_ESP32 - return WLEDAdcManager::instance().analogRead(pin); -#else - eturn analogRead(pin); -#endif -} + int wled_adc_analog_read(uint8_t pin) { + #ifdef ARDUINO_ARCH_ESP32 + return WLEDAdcManager::instance().analogRead(pin); + #else + return analogRead(pin); + #endif + } -int wled_adc_analog_read_mv(uint8_t pin) { -#ifdef ARDUINO_ARCH_ESP32 - return WLEDAdcManager::instance().analogReadMilliVolts(pin); -#else - eturn (int)(analogRead(pin) / 1023.0f); -#endif -} + int wled_adc_analog_read_mv(uint8_t pin) { + #ifdef ARDUINO_ARCH_ESP32 + return WLEDAdcManager::instance().analogReadMilliVolts(pin); + #else + return (int)(analogRead(pin) / 1023.0f); + #endif + } -} \ No newline at end of file +} +//} // namespace wled \ No newline at end of file From 1bb9f4713679f3e87e92a1a85603925a672d613b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 4 Aug 2026 20:55:58 +0200 Subject: [PATCH 03/18] remove the "C plusplus" shims, we do not need function pointer access --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 24 -------------------- lib/wled_ADCmanager/wled_ADCmanager.h | 29 ++++++++----------------- 2 files changed, 9 insertions(+), 44 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index 28b154024e..e5c1a6819a 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -17,7 +17,6 @@ //#include "wled_adcmanager.h" #include "wled.h" -//namespace wled { // prevent macro recursion of arduino overrides #undef analogRead #if defined(ARDUINO_ARCH_ESP32) @@ -317,27 +316,4 @@ int WLEDAdcManager::analogReadMilliVolts(uint8_t pin) { int mv = 0; return (adc_cali_raw_to_voltage(_cali, raw, &mv) == ESP_OK) ? mv : (raw * 3300) / 4095; } - #endif // ARDUINO_ARCH_ESP32 - -// C wrappers -extern "C" { - - int wled_adc_analog_read(uint8_t pin) { - #ifdef ARDUINO_ARCH_ESP32 - return WLEDAdcManager::instance().analogRead(pin); - #else - return analogRead(pin); - #endif - } - - int wled_adc_analog_read_mv(uint8_t pin) { - #ifdef ARDUINO_ARCH_ESP32 - return WLEDAdcManager::instance().analogReadMilliVolts(pin); - #else - return (int)(analogRead(pin) / 1023.0f); - #endif - } - -} -//} // namespace wled \ No newline at end of file diff --git a/lib/wled_ADCmanager/wled_ADCmanager.h b/lib/wled_ADCmanager/wled_ADCmanager.h index 6cf5dda881..9e621f28a2 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.h +++ b/lib/wled_ADCmanager/wled_ADCmanager.h @@ -7,16 +7,6 @@ #include -#ifdef __cplusplus -extern "C" { -#endif - -int wled_adc_analog_read(uint8_t pin); -int wled_adc_analog_read_mv(uint8_t pin); - -#ifdef __cplusplus -} - #ifdef ARDUINO_ARCH_ESP32 #include @@ -41,13 +31,13 @@ class WLEDAdcManager { bool begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame); void end(); + bool isRunning() const { return _running; } bool readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs = 100); int analogRead(uint8_t pin); int analogReadMilliVolts(uint8_t pin); - - bool isRunning() const { return _running; } + //void checkADC(); // check ADC status, reset if overflow happened (watchdog function, needs to be called frequently if used, i.e. put this in main loop) private: WLEDAdcManager(); @@ -57,8 +47,8 @@ class WLEDAdcManager { static bool _pinToChannel(uint8_t pin, adc_channel_t* ch); - bool _createHandle(); - void _destroyHandle(); + bool _initContinuousADC(); + void _endContinuousADC(); void _drainToCache(); bool _oneshotRead(adc_channel_t ch, int* outRaw); bool _initCali(); @@ -82,13 +72,12 @@ class WLEDAdcManager { #endif // ARDUINO_ARCH_ESP32 // override of native Arduino functions for compatibility with external usermods -#undef analogRead -#define analogRead(pin) wled_adc_analog_read(pin) #if defined(ARDUINO_ARCH_ESP32) +#undef analogRead +#define analogRead(pin) WLEDAdcManager::instance().analogRead(pin) #undef analogReadMilliVolts -#define analogReadMilliVolts(pin) wled_adc_analog_read_mv(pin) +#define analogReadMilliVolts(pin) WLEDAdcManager::instance().analogReadMilliVolts(pin) #else -#define analogReadMilliVolts(pin) wled_adc_analog_read_mv(pin) +// ESP8266: do not override analogRead +// we could add analogReadMilliVolts() which would just return (int)(analogRead(pin) / 1023.0f); #endif - -#endif // __cplusplus \ No newline at end of file From 7ada9df23ba0b48c7e29e2b5590b3cd7e47b0ee6 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 5 Aug 2026 07:35:42 +0200 Subject: [PATCH 04/18] add possibility to do partial buffer reads, add comments --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 79 ++++++++++++++----------- lib/wled_ADCmanager/wled_ADCmanager.h | 2 +- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index e5c1a6819a..da7f5de210 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -2,16 +2,18 @@ * ADC manager to handle continous ADC sampling in parallel with single-shot pin reads * by @dedehai (2026) licensed under EUPL 1.2 license * - * calling begin() will start sampling an ADC pin in continuous mode - * any subsequent call for analogRead() or analogReadMilliVolts() will stop the continuous sampling immediately, + * supports sampling a single pin in continuous ADC mode + * calling begin() will start sampling an ADC pin at the given sample rate + * if sample rate is higher than the read interval i.e. sampleRateHz/samplesPerFrame, new samples are discarded + * if read interval is faster, the read function waits until samples are available or the given timeout elapses + * any subsequent call for analogRead() or analogReadMilliVolts() will pause the continuous sampling, * drain the already sampled data into a buffer, read a pin in one-shot mode, then continue the sampling. * replaces analogRead() and analogReadMilliVolts() with managed functions for code compatibility TODO: - could add the option to use hardware IIR filter, although the lowest coefficient setting of 2 already has a 3dB cutoff around 2kHz (to be confirmed) at 20kHz sample rate - IIR filter are supported on all modern ESP32 but probably lacking on ESP32 classic, there we would need to do it in post-processing i.e. when writing the sample buffer - - need to add a "buffer full" callback? - - add namespace? + - need to add a "buffer full" callback? -> is added but no longer needed with the bug being fixed in latest tasmota IDF */ //#include "wled_adcmanager.h" @@ -25,7 +27,8 @@ #ifdef ARDUINO_ARCH_ESP32 -#define DMA_BLOCKSIZE 128 // DMA buffer block size, IDF driver uses 5 blocks under the hood, there is an ISR call each time a block finishes so dont make it too small +#define ADCMANAGER_DMA_BLOCKSIZE 128 // DMA buffer block size, IDF driver uses 5 blocks under the hood, there is an ISR call each time a block finishes so dont make it too small +#define ADCMANAGER_READBUFFERSIZE (128 * sizeof(adc_digi_output_data_t)) // size of stack buffer for reading samples from the driver #include @@ -79,6 +82,7 @@ WLEDAdcManager::~WLEDAdcManager() { #endif } +// initilizes the manager and the hardware and starts sampling bool WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame) { xSemaphoreTake(_mutex, portMAX_DELAY); _endContinuousADC(); @@ -101,6 +105,7 @@ bool WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesP return ok; } +// stop sampling and de-initilize the hardware so it can be used by analogRead() or to sample a different pin void WLEDAdcManager::end() { xSemaphoreTake(_mutex, portMAX_DELAY); _endContinuousADC(); @@ -130,12 +135,13 @@ void WLEDAdcManager::checkADC() { } } */ +// initialize the hardware bool WLEDAdcManager::_initContinuousADC() { - if (_handle) return true; + if (_handle) return true; // already initialized size_t frameBytes = (size_t)_samplesPerFrame * sizeof(adc_digi_output_data_t); adc_continuous_handle_cfg_t hcfg = { .max_store_buf_size = frameBytes * 1, // hold two frames in buffer, caller needs to drain it fast enough to avoid data loss - .conv_frame_size = DMA_BLOCKSIZE, // use fixed DMA buffer size of 256 bytes (ADC driver creates 5 DMA descriptors with one buffer each, at 20kHz this means an interrupt every 1.4ms + .conv_frame_size = ADCMANAGER_DMA_BLOCKSIZE, // use fixed DMA buffer size of 256 bytes (ADC driver creates 5 DMA descriptors with one buffer each, at 20kHz this means an interrupt every 1.4ms .flags = { .flush_pool = false }, // do not flush the store buffer on overrun but discard new samples (true means discard oldest, is much slower and can cause issues, do not set true) }; if (adc_continuous_new_handle(&hcfg, &_handle) != ESP_OK) return false; @@ -157,8 +163,8 @@ bool WLEDAdcManager::_initContinuousADC() { .conv_mode = ADC_CONV_SINGLE_UNIT_1, .format = WLED_ADC_DIGI_FORMAT, }; - if (adc_continuous_config(_handle, &cfg) != ESP_OK || - adc_continuous_start(_handle) != ESP_OK) { + // initialize and start sampling + if (adc_continuous_config(_handle, &cfg) != ESP_OK || adc_continuous_start(_handle) != ESP_OK) { _endContinuousADC(); return false; } @@ -176,7 +182,7 @@ void WLEDAdcManager::_endContinuousADC() { } void WLEDAdcManager::_drainToCache() { - if (!_handle || !_cache) return; + if (!_handle || !_cache) return; // note: checking _handle is redundant but also does not hurt (caller checks _running) adc_digi_output_data_t temp[32]; // size of data packets to request, 32 samples at 22kHz is 1.5ms, leftover samples are lost _cacheCount = 0; while (_cacheCount < _cacheSize) { @@ -188,16 +194,19 @@ void WLEDAdcManager::_drainToCache() { _cache[_cacheCount++] = (int16_t)(temp[i].WLED_ADC_OUT_TYPE.data); } } - adc_continuous_stop(_handle); // stop after reading (if stopped before, no samples are being read) } // read samples acquired in continuous mode. They are written as 12bit unsigned values into the passed buffer -bool WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs) { +// tries to read "numSamples" and returns the actual number of samples written into the buffer +// it waits up to timeoutMs per fetch of tmpBfrSize (128) samples, if not enough samples are available, it returns what it got +// to poll the buffer and "just give me what you got" use a timeout of 0. On read error, it restarts the driver so no action needed by caller. +uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs) { if (!_running || !buffer || !numSamples) return false; + // note: DMA uses SOC_ADC_DIGI_MAX_BITWIDTH which is 12bits on all checked units TODO: should make sure and handle this to future proof it xSemaphoreTake(_mutex, portMAX_DELAY); - uint16_t out = 0; - // note: DMA uses SOC_ADC_DIGI_MAX_BITWIDTH which is 12bits on all checked units TODO: should make sure and handle this to future proof it + uint16_t out = 0; // number of samples written to the buffer + // check if any data was cached during an intermediate analogRead() if (_cacheCount) { uint16_t copy = _cacheCount < numSamples ? _cacheCount : numSamples; memcpy(buffer, _cache, copy * sizeof(int16_t)); @@ -210,13 +219,10 @@ bool WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t if (out >= numSamples) { xSemaphoreGive(_mutex); - return true; - } - if (!_handle) { - xSemaphoreGive(_mutex); - return false; + return out; // already get enough samples from cache } - const int tmpBfrSize = 128; // use buffer on stack to read samples from the driver + + const int tmpBfrSize = ADCMANAGER_READBUFFERSIZE; // use fixed size buffer on stack to read samples from the driver in chunks adc_digi_output_data_t temp[tmpBfrSize]; while (out < numSamples) { @@ -224,26 +230,31 @@ bool WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t uint16_t want = (numSamples - out) < tmpBfrSize ? (numSamples - out) : tmpBfrSize; size_t wantBytes = want * sizeof(adc_digi_output_data_t); esp_err_t err = adc_continuous_read(_handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(timeoutMs)); - // TODO: add the possibility to "read what is available" and return the number of samples read? - if (err != ESP_OK || n == 0) { - // something went wrong, re-init the continuous transfer - DEBUG_PRINTF_P(PSTR("ADC read error %d, got n=%d restarting"),err,n); - _endContinuousADC(); // tear it down - _initContinuousADC(); // re-init and start sampling - xSemaphoreGive(_mutex); - return false; - } + // copy the data into 16bit buffer - uint16_t got = n / sizeof(adc_digi_output_data_t); - for (uint16_t i = 0; i < got; i++) { - buffer[out++] = (int16_t)(temp[i].WLED_ADC_OUT_TYPE.data); // TODO: need to check bit width? see not above + if (n > 0) { + uint16_t got = n / sizeof(adc_digi_output_data_t); + for (uint16_t i = 0; i < got; i++) { + buffer[out++] = (int16_t)(temp[i].WLED_ADC_OUT_TYPE.data); + } + } + + if (err == ESP_ERR_TIMEOUT) { + break; // not enough samples within timeout frame, return what we got + } + if (err != ESP_OK) { + DEBUG_PRINTF_P(PSTR("ADC read error %d, got n=%d restarting"), err, n); + _endContinuousADC(); + _initContinuousADC(); + break; } + else if (n == 0) break; // should not happen, just in case (if no samples are read, it should not be ESP_OK) } - //adc_continuous_flush_pool(_handle); // flush remaining data -> no need, just let the samples accumulate + //adc_continuous_flush_pool(_handle); // flush remaining data -> no need, just let the samples accumulate, uncomment if you need freshest samples only xSemaphoreGive(_mutex); - return true; + return out; } // a one-shot read takes about 0.8-2.5ms if continuous reading is active, 0.2ms otherwise diff --git a/lib/wled_ADCmanager/wled_ADCmanager.h b/lib/wled_ADCmanager/wled_ADCmanager.h index 9e621f28a2..689afc377e 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.h +++ b/lib/wled_ADCmanager/wled_ADCmanager.h @@ -33,7 +33,7 @@ class WLEDAdcManager { void end(); bool isRunning() const { return _running; } - bool readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs = 100); + uint16_t readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs = 100); int analogRead(uint8_t pin); int analogReadMilliVolts(uint8_t pin); From 0a4d5ecd22df72fe0bb541fbee68814273b81b04 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 5 Aug 2026 08:17:12 +0200 Subject: [PATCH 05/18] add note, change buffer to bytes not samples --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index da7f5de210..b5d8d91c01 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -10,6 +10,10 @@ * drain the already sampled data into a buffer, read a pin in one-shot mode, then continue the sampling. * replaces analogRead() and analogReadMilliVolts() with managed functions for code compatibility + +Note: chip revision 0 and 1 of the C6 have a hardware bug and the effective ADC resolution is only 8bit, was solved in rev. 2 (around mid 2025) + https://docs.espressif.com/projects/esp-chip-errata/en/latest/esp32c6/03-errata-description/esp32c6/sar-adc-missing-lower-four-bits.html#sar-adc-loss-of-precision-in-lower-four-bits-of-sar-adc + TODO: - could add the option to use hardware IIR filter, although the lowest coefficient setting of 2 already has a 3dB cutoff around 2kHz (to be confirmed) at 20kHz sample rate - IIR filter are supported on all modern ESP32 but probably lacking on ESP32 classic, there we would need to do it in post-processing i.e. when writing the sample buffer @@ -28,7 +32,7 @@ #ifdef ARDUINO_ARCH_ESP32 #define ADCMANAGER_DMA_BLOCKSIZE 128 // DMA buffer block size, IDF driver uses 5 blocks under the hood, there is an ISR call each time a block finishes so dont make it too small -#define ADCMANAGER_READBUFFERSIZE (128 * sizeof(adc_digi_output_data_t)) // size of stack buffer for reading samples from the driver +#define ADCMANAGER_READBUFFERSAMPLES 256 // number of bytes read per chunk from the ADC buffer (stack buffer), samples is bytes/sizeof(adc_digi_output_data_t) i.e. divide by 4 #include @@ -222,7 +226,7 @@ uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint3 return out; // already get enough samples from cache } - const int tmpBfrSize = ADCMANAGER_READBUFFERSIZE; // use fixed size buffer on stack to read samples from the driver in chunks + const int tmpBfrSize = ADCMANAGER_READBUFFERSAMPLES; // use fixed size buffer on stack to read samples from the driver in chunks adc_digi_output_data_t temp[tmpBfrSize]; while (out < numSamples) { From b728126e4e15955c33f7981f36ffe75d1153183b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 7 Aug 2026 00:40:34 +0200 Subject: [PATCH 06/18] pack variables into a struct to save some RAM if continuous mode is unused --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 140 +++++++++++++++--------- lib/wled_ADCmanager/wled_ADCmanager.h | 18 +-- 2 files changed, 90 insertions(+), 68 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index b5d8d91c01..2add837f93 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -11,7 +11,7 @@ * replaces analogRead() and analogReadMilliVolts() with managed functions for code compatibility -Note: chip revision 0 and 1 of the C6 have a hardware bug and the effective ADC resolution is only 8bit, was solved in rev. 2 (around mid 2025) +Note: C6 chip revision 0 and 1 have a hardware bug and the effective ADC resolution is only 8bit, was solved in rev. 2 (around mid 2025) https://docs.espressif.com/projects/esp-chip-errata/en/latest/esp32c6/03-errata-description/esp32c6/sar-adc-missing-lower-four-bits.html#sar-adc-loss-of-precision-in-lower-four-bits-of-sar-adc TODO: @@ -52,6 +52,17 @@ static bool _isADC1(uint8_t pin, int8_t ch) { #endif } +struct WLEDAdcManager::ContinuousCtx { + uint8_t pin; + adc_channel_t channel; + uint32_t sampleRate; + uint16_t samplesPerFrame; + adc_continuous_handle_t handle; + int16_t* cache; + uint16_t cacheSize; + uint16_t cacheCount; +}; + bool WLEDAdcManager::_pinToChannel(uint8_t pin, adc_channel_t* ch) { int8_t c = digitalPinToAnalogChannel(pin); if (c < 0 || !_isADC1(pin, c)) return false; @@ -65,16 +76,9 @@ WLEDAdcManager& WLEDAdcManager::instance() { } WLEDAdcManager::WLEDAdcManager() - : _pin(0xFF) - , _channel(ADC_CHANNEL_0) - , _sampleRate(0) - , _samplesPerFrame(0) - , _handle(nullptr) - , _running(false) - , _cache(nullptr) - , _cacheSize(0) - , _cacheCount(0) - , _cali(nullptr) { + : _mutex(nullptr) + , _cali(nullptr) + , _ctx(nullptr) { _mutex = xSemaphoreCreateMutex(); } @@ -89,33 +93,60 @@ WLEDAdcManager::~WLEDAdcManager() { // initilizes the manager and the hardware and starts sampling bool WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame) { xSemaphoreTake(_mutex, portMAX_DELAY); - _endContinuousADC(); - _cacheCount = 0; - if (!_pinToChannel(pin, &_channel)) { + // tear down any previous session completely + if (_ctx) { + _endContinuousADC(); + if (_ctx->cache) { free(_ctx->cache); _ctx->cache = nullptr; } + free(_ctx); + _ctx = nullptr; + } + + adc_channel_t channel; + if (!_pinToChannel(pin, &channel)) { + xSemaphoreGive(_mutex); + return false; + } + + _ctx = (ContinuousCtx*)calloc(1, sizeof(ContinuousCtx)); + if (!_ctx) { xSemaphoreGive(_mutex); return false; } - _pin = pin; - _sampleRate = sampleRateHz; - _samplesPerFrame = samplesPerFrame; - if (_cache) free(_cache); - _cacheSize = samplesPerFrame; - _cache = (int16_t*)calloc(_cacheSize, sizeof(int16_t)); + _ctx->pin = pin; + _ctx->channel = channel; + _ctx->sampleRate = sampleRateHz; + _ctx->samplesPerFrame = samplesPerFrame; + _ctx->cacheSize = samplesPerFrame; + _ctx->cacheCount = 0; + _ctx->cache = (int16_t*)calloc(_ctx->cacheSize, sizeof(int16_t)); + if (!_ctx->cache) { + free(_ctx); + _ctx = nullptr; + xSemaphoreGive(_mutex); + return false; + } bool ok = _initContinuousADC(); + if (!ok) { + free(_ctx->cache); + free(_ctx); + _ctx = nullptr; + } xSemaphoreGive(_mutex); return ok; } -// stop sampling and de-initilize the hardware so it can be used by analogRead() or to sample a different pin +// stop sampling and deinitialize the AdcManager continuous mode (use this if you want to sample a different pin or do not need to sample anymore) void WLEDAdcManager::end() { xSemaphoreTake(_mutex, portMAX_DELAY); - _endContinuousADC(); - _cacheCount = 0; - if (_cache) { free(_cache); _cache = nullptr; _cacheSize = 0; } - _pin = 0xFF; + if (_ctx) { + _endContinuousADC(); + if (_ctx->cache) { free(_ctx->cache); _ctx->cache = nullptr; } + free(_ctx); + _ctx = nullptr; + } xSemaphoreGive(_mutex); } @@ -141,14 +172,15 @@ void WLEDAdcManager::checkADC() { */ // initialize the hardware bool WLEDAdcManager::_initContinuousADC() { - if (_handle) return true; // already initialized - size_t frameBytes = (size_t)_samplesPerFrame * sizeof(adc_digi_output_data_t); + if (!_ctx) return false; // begin() not called + if (_ctx->handle) return true; // already initialized + size_t frameBytes = (size_t)_ctx->samplesPerFrame * sizeof(adc_digi_output_data_t); adc_continuous_handle_cfg_t hcfg = { .max_store_buf_size = frameBytes * 1, // hold two frames in buffer, caller needs to drain it fast enough to avoid data loss .conv_frame_size = ADCMANAGER_DMA_BLOCKSIZE, // use fixed DMA buffer size of 256 bytes (ADC driver creates 5 DMA descriptors with one buffer each, at 20kHz this means an interrupt every 1.4ms .flags = { .flush_pool = false }, // do not flush the store buffer on overrun but discard new samples (true means discard oldest, is much slower and can cause issues, do not set true) }; - if (adc_continuous_new_handle(&hcfg, &_handle) != ESP_OK) return false; + if (adc_continuous_new_handle(&hcfg, &_ctx->handle) != ESP_OK) return false; // register buffer overflow callback (sets flag, main loop needs to call checkADC() to clear overflow - this is to prevent wifi stalling due to a now fixed IDF bug causing a lockup) //adc_continuous_evt_cbs_t cbs = { .on_conv_done = nullptr, .on_pool_ovf = _onPoolOvf }; @@ -156,46 +188,46 @@ bool WLEDAdcManager::_initContinuousADC() { adc_digi_pattern_config_t pat = { .atten = ADC_ATTEN_DB_12, - .channel = (uint8_t)_channel, + .channel = (uint8_t)_ctx->channel, .unit = ADC_UNIT_1, .bit_width = ADC_BITWIDTH_12, }; adc_continuous_config_t cfg = { .pattern_num = 1, .adc_pattern = &pat, - .sample_freq_hz = _sampleRate, + .sample_freq_hz = _ctx->sampleRate, .conv_mode = ADC_CONV_SINGLE_UNIT_1, .format = WLED_ADC_DIGI_FORMAT, }; // initialize and start sampling - if (adc_continuous_config(_handle, &cfg) != ESP_OK || adc_continuous_start(_handle) != ESP_OK) { + if (adc_continuous_config(_ctx->handle, &cfg) != ESP_OK || adc_continuous_start(_ctx->handle) != ESP_OK) { _endContinuousADC(); return false; } - _running = true; return true; } +// stop sampling and de-initilize the hardware so it can be used by analogRead() but keeps the continuous _ctx configuration void WLEDAdcManager::_endContinuousADC() { - if (_handle) { - adc_continuous_stop(_handle); - adc_continuous_deinit(_handle); - _handle = nullptr; + if (!_ctx) return; + if (_ctx->handle) { + adc_continuous_stop(_ctx->handle); + adc_continuous_deinit(_ctx->handle); + _ctx->handle = nullptr; } - _running = false; } void WLEDAdcManager::_drainToCache() { - if (!_handle || !_cache) return; // note: checking _handle is redundant but also does not hurt (caller checks _running) + if (!_ctx || !_ctx->handle || !_ctx->cache) return; // safety check adc_digi_output_data_t temp[32]; // size of data packets to request, 32 samples at 22kHz is 1.5ms, leftover samples are lost - _cacheCount = 0; - while (_cacheCount < _cacheSize) { + _ctx->cacheCount = 0; + while (_ctx->cacheCount < _ctx->cacheSize) { uint32_t n = 0; // read what is available in the buffer in chunks (no timeout means do not wait for any additional samples) - if (adc_continuous_read(_handle, (uint8_t*)temp, sizeof(temp), &n, 0) != ESP_OK || n == 0) break; + if (adc_continuous_read(_ctx->handle, (uint8_t*)temp, sizeof(temp), &n, 0) != ESP_OK || n == 0) break; uint16_t cnt = n / sizeof(adc_digi_output_data_t); - for (uint16_t i = 0; i < cnt && _cacheCount < _cacheSize; i++) { - _cache[_cacheCount++] = (int16_t)(temp[i].WLED_ADC_OUT_TYPE.data); + for (uint16_t i = 0; i < cnt && _ctx->cacheCount < _ctx->cacheSize; i++) { + _ctx->cache[_ctx->cacheCount++] = (int16_t)(temp[i].WLED_ADC_OUT_TYPE.data); } } } @@ -205,20 +237,20 @@ void WLEDAdcManager::_drainToCache() { // it waits up to timeoutMs per fetch of tmpBfrSize (128) samples, if not enough samples are available, it returns what it got // to poll the buffer and "just give me what you got" use a timeout of 0. On read error, it restarts the driver so no action needed by caller. uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs) { - if (!_running || !buffer || !numSamples) return false; + if (!_ctx || !buffer || !numSamples) return 0; // note: DMA uses SOC_ADC_DIGI_MAX_BITWIDTH which is 12bits on all checked units TODO: should make sure and handle this to future proof it xSemaphoreTake(_mutex, portMAX_DELAY); uint16_t out = 0; // number of samples written to the buffer // check if any data was cached during an intermediate analogRead() - if (_cacheCount) { - uint16_t copy = _cacheCount < numSamples ? _cacheCount : numSamples; - memcpy(buffer, _cache, copy * sizeof(int16_t)); + if (_ctx->cacheCount) { + uint16_t copy = _ctx->cacheCount < numSamples ? _ctx->cacheCount : numSamples; + memcpy(buffer, _ctx->cache, copy * sizeof(int16_t)); out = copy; - if (copy < _cacheCount) { - memmove(_cache, _cache + copy, (_cacheCount - copy) * sizeof(int16_t)); + if (copy < _ctx->cacheCount) { + memmove(_ctx->cache, _ctx->cache + copy, (_ctx->cacheCount - copy) * sizeof(int16_t)); } - _cacheCount -= copy; + _ctx->cacheCount -= copy; } if (out >= numSamples) { @@ -233,7 +265,7 @@ uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint3 uint32_t n = 0; uint16_t want = (numSamples - out) < tmpBfrSize ? (numSamples - out) : tmpBfrSize; size_t wantBytes = want * sizeof(adc_digi_output_data_t); - esp_err_t err = adc_continuous_read(_handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(timeoutMs)); + esp_err_t err = adc_continuous_read(_ctx->handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(timeoutMs)); // copy the data into 16bit buffer if (n > 0) { @@ -261,7 +293,7 @@ uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint3 return out; } -// a one-shot read takes about 0.8-2.5ms if continuous reading is active, 0.2ms otherwise +// a one-shot read takes about 0.7-2.5ms if continuous reading is active, depending on chip type (ESP32 is slower, newer ones are faster), 0.2ms otherwise bool WLEDAdcManager::_oneshotRead(adc_channel_t ch, int* outRaw) { adc_oneshot_unit_handle_t h; adc_oneshot_unit_init_cfg_t icfg = { @@ -293,9 +325,9 @@ int WLEDAdcManager::analogRead(uint8_t pin) { if (!_pinToChannel(pin, &ch)) return 0; xSemaphoreTake(_mutex, portMAX_DELAY); - if (_running) { + if (_ctx) { // continuous sampling is used _drainToCache(); - _endContinuousADC(); + _endContinuousADC(); // stop sampling and free the ADC hardware if in use _oneshotRead(ch, &raw); _initContinuousADC(); // re-init and start sampling again } else { diff --git a/lib/wled_ADCmanager/wled_ADCmanager.h b/lib/wled_ADCmanager/wled_ADCmanager.h index 689afc377e..859bf2383e 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.h +++ b/lib/wled_ADCmanager/wled_ADCmanager.h @@ -31,7 +31,7 @@ class WLEDAdcManager { bool begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame); void end(); - bool isRunning() const { return _running; } + bool isRunning() const { return _ctx != nullptr; } uint16_t readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs = 100); @@ -40,7 +40,7 @@ class WLEDAdcManager { //void checkADC(); // check ADC status, reset if overflow happened (watchdog function, needs to be called frequently if used, i.e. put this in main loop) private: - WLEDAdcManager(); +WLEDAdcManager(); ~WLEDAdcManager(); WLEDAdcManager(const WLEDAdcManager&) = delete; WLEDAdcManager& operator=(const WLEDAdcManager&) = delete; @@ -53,20 +53,10 @@ class WLEDAdcManager { bool _oneshotRead(adc_channel_t ch, int* outRaw); bool _initCali(); + struct ContinuousCtx; // forward declaration SemaphoreHandle_t _mutex; - - uint8_t _pin; - adc_channel_t _channel; - uint32_t _sampleRate; - uint16_t _samplesPerFrame; - adc_continuous_handle_t _handle; - bool _running; - - int16_t* _cache; - uint16_t _cacheSize; - uint16_t _cacheCount; - adc_cali_handle_t _cali; + ContinuousCtx* _ctx; // nullptr when continuous mode is idle }; #endif // ARDUINO_ARCH_ESP32 From 445e77f7ad8f05b2562bc431770ffc4f02abae1c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 6 Aug 2026 18:01:04 +0200 Subject: [PATCH 07/18] update comment --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index 2add837f93..6ff231e2f9 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -18,9 +18,9 @@ Note: C6 chip revision 0 and 1 have a hardware bug and the effective ADC resolut - could add the option to use hardware IIR filter, although the lowest coefficient setting of 2 already has a 3dB cutoff around 2kHz (to be confirmed) at 20kHz sample rate - IIR filter are supported on all modern ESP32 but probably lacking on ESP32 classic, there we would need to do it in post-processing i.e. when writing the sample buffer - need to add a "buffer full" callback? -> is added but no longer needed with the bug being fixed in latest tasmota IDF + - there is an edge-case issue: when continuous sampling is running, several analog pins are configured and the pin-info page is open it can lead to crashes (some issue with semaphore) */ -//#include "wled_adcmanager.h" #include "wled.h" // prevent macro recursion of arduino overrides @@ -32,7 +32,7 @@ Note: C6 chip revision 0 and 1 have a hardware bug and the effective ADC resolut #ifdef ARDUINO_ARCH_ESP32 #define ADCMANAGER_DMA_BLOCKSIZE 128 // DMA buffer block size, IDF driver uses 5 blocks under the hood, there is an ISR call each time a block finishes so dont make it too small -#define ADCMANAGER_READBUFFERSAMPLES 256 // number of bytes read per chunk from the ADC buffer (stack buffer), samples is bytes/sizeof(adc_digi_output_data_t) i.e. divide by 4 +#define ADCMANAGER_READBUFFERSAMPLES 128 // number of samples to read per chunk from the ADC buffer (stack buffer), do not set higher than 128 or stack overflow may occur #include From feab0b180e4eafeff14e627a9e9a61add38495f7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 08:56:33 +0200 Subject: [PATCH 08/18] better ADC channel detection, timeout on mutex, fix race --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 38 +++++++++---------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index 6ff231e2f9..16bbaa9b4c 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -33,25 +33,10 @@ Note: C6 chip revision 0 and 1 have a hardware bug and the effective ADC resolut #define ADCMANAGER_DMA_BLOCKSIZE 128 // DMA buffer block size, IDF driver uses 5 blocks under the hood, there is an ISR call each time a block finishes so dont make it too small #define ADCMANAGER_READBUFFERSAMPLES 128 // number of samples to read per chunk from the ADC buffer (stack buffer), do not set higher than 128 or stack overflow may occur +#define ADCMANAGER_LOCK_TIMEOUT_MS 10 // timeout for mutex lock, this should be as short as possible but still allow for readSamples() to complete before doing analogRead(), may need tweaking #include -static bool _isADC1(uint8_t pin, int8_t ch) { -#if defined(CONFIG_IDF_TARGET_ESP32) - (void)ch; return (pin >= 32 && pin <= 39); -#elif defined(CONFIG_IDF_TARGET_ESP32S2) - return (ch >= 0 && ch <= 9); -#elif defined(CONFIG_IDF_TARGET_ESP32S3) - return (ch >= 0 && ch <= 9); -#elif defined(CONFIG_IDF_TARGET_ESP32C3) - return (ch >= 0 && ch <= 4); -#elif defined(CONFIG_IDF_TARGET_ESP32C6) - (void)ch; return (pin <= 5); -#else - (void)pin; (void)ch; return true; -#endif -} - struct WLEDAdcManager::ContinuousCtx { uint8_t pin; adc_channel_t channel; @@ -65,11 +50,12 @@ struct WLEDAdcManager::ContinuousCtx { bool WLEDAdcManager::_pinToChannel(uint8_t pin, adc_channel_t* ch) { int8_t c = digitalPinToAnalogChannel(pin); - if (c < 0 || !_isADC1(pin, c)) return false; + if (c < 0 || c >= SOC_ADC_CHANNEL_NUM(0)) return false; // check if channel is withing ADC1 range (SOC_ADC_CHANNEL_NUM(0) is the number of channels on ADC1) *ch = (adc_channel_t)c; return true; } + WLEDAdcManager& WLEDAdcManager::instance() { static WLEDAdcManager inst; return inst; @@ -92,7 +78,7 @@ WLEDAdcManager::~WLEDAdcManager() { // initilizes the manager and the hardware and starts sampling bool WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame) { - xSemaphoreTake(_mutex, portMAX_DELAY); + if (!_mutex || xSemaphoreTake(_mutex, pdMS_TO_TICKS(ADCMANAGER_LOCK_TIMEOUT_MS)) != pdTRUE) return false; // tear down any previous session completely if (_ctx) { @@ -140,7 +126,7 @@ bool WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesP // stop sampling and deinitialize the AdcManager continuous mode (use this if you want to sample a different pin or do not need to sample anymore) void WLEDAdcManager::end() { - xSemaphoreTake(_mutex, portMAX_DELAY); + if (!_mutex || xSemaphoreTake(_mutex, pdMS_TO_TICKS(ADCMANAGER_LOCK_TIMEOUT_MS)) != pdTRUE) return; if (_ctx) { _endContinuousADC(); if (_ctx->cache) { free(_ctx->cache); _ctx->cache = nullptr; } @@ -176,7 +162,7 @@ bool WLEDAdcManager::_initContinuousADC() { if (_ctx->handle) return true; // already initialized size_t frameBytes = (size_t)_ctx->samplesPerFrame * sizeof(adc_digi_output_data_t); adc_continuous_handle_cfg_t hcfg = { - .max_store_buf_size = frameBytes * 1, // hold two frames in buffer, caller needs to drain it fast enough to avoid data loss + .max_store_buf_size = frameBytes, // hold single frame in buffer, caller needs to drain it fast enough to avoid data loss (can increase to avoid data loss but still need to drain fast enough at one point) .conv_frame_size = ADCMANAGER_DMA_BLOCKSIZE, // use fixed DMA buffer size of 256 bytes (ADC driver creates 5 DMA descriptors with one buffer each, at 20kHz this means an interrupt every 1.4ms .flags = { .flush_pool = false }, // do not flush the store buffer on overrun but discard new samples (true means discard oldest, is much slower and can cause issues, do not set true) }; @@ -220,7 +206,6 @@ void WLEDAdcManager::_endContinuousADC() { void WLEDAdcManager::_drainToCache() { if (!_ctx || !_ctx->handle || !_ctx->cache) return; // safety check adc_digi_output_data_t temp[32]; // size of data packets to request, 32 samples at 22kHz is 1.5ms, leftover samples are lost - _ctx->cacheCount = 0; while (_ctx->cacheCount < _ctx->cacheSize) { uint32_t n = 0; // read what is available in the buffer in chunks (no timeout means do not wait for any additional samples) @@ -237,9 +222,14 @@ void WLEDAdcManager::_drainToCache() { // it waits up to timeoutMs per fetch of tmpBfrSize (128) samples, if not enough samples are available, it returns what it got // to poll the buffer and "just give me what you got" use a timeout of 0. On read error, it restarts the driver so no action needed by caller. uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs) { - if (!_ctx || !buffer || !numSamples) return 0; + if (!buffer || !numSamples) return 0; // note: DMA uses SOC_ADC_DIGI_MAX_BITWIDTH which is 12bits on all checked units TODO: should make sure and handle this to future proof it - xSemaphoreTake(_mutex, portMAX_DELAY); + if (!_mutex || xSemaphoreTake(_mutex, pdMS_TO_TICKS(ADCMANAGER_LOCK_TIMEOUT_MS)) != pdTRUE) return 0; + // check if continuous mode is running AFTER we take the semaphore, otherwise we could have a race with analogRead() + if (!_ctx || !_ctx->handle) { + xSemaphoreGive(_mutex); + return 0; + } uint16_t out = 0; // number of samples written to the buffer // check if any data was cached during an intermediate analogRead() @@ -324,7 +314,7 @@ int WLEDAdcManager::analogRead(uint8_t pin) { adc_channel_t ch; if (!_pinToChannel(pin, &ch)) return 0; - xSemaphoreTake(_mutex, portMAX_DELAY); + if (!_mutex || xSemaphoreTake(_mutex, pdMS_TO_TICKS(ADCMANAGER_LOCK_TIMEOUT_MS)) != pdTRUE) return 0; if (_ctx) { // continuous sampling is used _drainToCache(); _endContinuousADC(); // stop sampling and free the ADC hardware if in use From 208e2c7e5407dbdba10c509975c1d6c892313357 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 09:20:36 +0200 Subject: [PATCH 09/18] only include adcManager in V5 builds --- wled00/wled.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wled00/wled.h b/wled00/wled.h index 89194d1ddb..69392aa19d 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -196,7 +196,9 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #ifndef WLED_DISABLE_ESPNOW #include #endif -#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + #include +#endif #include "colors.h" #include "fcn_declare.h" #ifndef WLED_DISABLE_OTA From 9c52c8c4fb55517d62b2d9be26162265948ef2c0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 09:54:11 +0200 Subject: [PATCH 10/18] fix ifdefs to only compile adcManager if IDF >= V5.5 --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 9 ++++----- lib/wled_ADCmanager/wled_ADCmanager.h | 12 +++++------- wled00/wled.h | 4 +--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index 16bbaa9b4c..1cd42ab9aa 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -21,15 +21,13 @@ Note: C6 chip revision 0 and 1 have a hardware bug and the effective ADC resolut - there is an edge-case issue: when continuous sampling is running, several analog pins are configured and the pin-info page is open it can lead to crashes (some issue with semaphore) */ -#include "wled.h" +#ifdef ARDUINO_ARCH_ESP32 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +#include "wled.h" // prevent macro recursion of arduino overrides #undef analogRead -#if defined(ARDUINO_ARCH_ESP32) #undef analogReadMilliVolts -#endif - -#ifdef ARDUINO_ARCH_ESP32 #define ADCMANAGER_DMA_BLOCKSIZE 128 // DMA buffer block size, IDF driver uses 5 blocks under the hood, there is an ISR call each time a block finishes so dont make it too small #define ADCMANAGER_READBUFFERSAMPLES 128 // number of samples to read per chunk from the ADC buffer (stack buffer), do not set higher than 128 or stack overflow may occur @@ -353,4 +351,5 @@ int WLEDAdcManager::analogReadMilliVolts(uint8_t pin) { int mv = 0; return (adc_cali_raw_to_voltage(_cali, raw, &mv) == ESP_OK) ? mv : (raw * 3300) / 4095; } +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) #endif // ARDUINO_ARCH_ESP32 diff --git a/lib/wled_ADCmanager/wled_ADCmanager.h b/lib/wled_ADCmanager/wled_ADCmanager.h index 859bf2383e..f622a3a669 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.h +++ b/lib/wled_ADCmanager/wled_ADCmanager.h @@ -5,10 +5,10 @@ #pragma once -#include - #ifdef ARDUINO_ARCH_ESP32 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +#include #include #include #include @@ -59,15 +59,13 @@ WLEDAdcManager(); ContinuousCtx* _ctx; // nullptr when continuous mode is idle }; -#endif // ARDUINO_ARCH_ESP32 - // override of native Arduino functions for compatibility with external usermods -#if defined(ARDUINO_ARCH_ESP32) + #undef analogRead #define analogRead(pin) WLEDAdcManager::instance().analogRead(pin) #undef analogReadMilliVolts #define analogReadMilliVolts(pin) WLEDAdcManager::instance().analogReadMilliVolts(pin) -#else // ESP8266: do not override analogRead // we could add analogReadMilliVolts() which would just return (int)(analogRead(pin) / 1023.0f); -#endif +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +#endif // ARDUINO_ARCH_ESP32 diff --git a/wled00/wled.h b/wled00/wled.h index 69392aa19d..89194d1ddb 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -196,9 +196,7 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #ifndef WLED_DISABLE_ESPNOW #include #endif -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) - #include -#endif +#include #include "colors.h" #include "fcn_declare.h" #ifndef WLED_DISABLE_OTA From b6e9ac6382783e8a5f1a7db06ec18c6a954f5d59 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 10:15:20 +0200 Subject: [PATCH 11/18] fix include, add timeout guard for semaphore race in readsamples() --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index 1cd42ab9aa..9e3eb49403 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -18,20 +18,20 @@ Note: C6 chip revision 0 and 1 have a hardware bug and the effective ADC resolut - could add the option to use hardware IIR filter, although the lowest coefficient setting of 2 already has a 3dB cutoff around 2kHz (to be confirmed) at 20kHz sample rate - IIR filter are supported on all modern ESP32 but probably lacking on ESP32 classic, there we would need to do it in post-processing i.e. when writing the sample buffer - need to add a "buffer full" callback? -> is added but no longer needed with the bug being fixed in latest tasmota IDF - - there is an edge-case issue: when continuous sampling is running, several analog pins are configured and the pin-info page is open it can lead to crashes (some issue with semaphore) + - there is an edge-case issue: when continuous sampling is running, several analog pins are configured and the pin-info page is open it can lead to crashes (some issue with semaphore) -> cannot reproduce now, might be solved with added semaphore timeout */ +#include "wled.h" #ifdef ARDUINO_ARCH_ESP32 #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) -#include "wled.h" // prevent macro recursion of arduino overrides #undef analogRead #undef analogReadMilliVolts #define ADCMANAGER_DMA_BLOCKSIZE 128 // DMA buffer block size, IDF driver uses 5 blocks under the hood, there is an ISR call each time a block finishes so dont make it too small #define ADCMANAGER_READBUFFERSAMPLES 128 // number of samples to read per chunk from the ADC buffer (stack buffer), do not set higher than 128 or stack overflow may occur -#define ADCMANAGER_LOCK_TIMEOUT_MS 10 // timeout for mutex lock, this should be as short as possible but still allow for readSamples() to complete before doing analogRead(), may need tweaking +#define ADCMANAGER_LOCK_TIMEOUT_MS 10 // timeout for mutex lock, this should be as short as possible but still allow for readSamples() to complete before doing analogRead() #include @@ -217,8 +217,9 @@ void WLEDAdcManager::_drainToCache() { // read samples acquired in continuous mode. They are written as 12bit unsigned values into the passed buffer // tries to read "numSamples" and returns the actual number of samples written into the buffer -// it waits up to timeoutMs per fetch of tmpBfrSize (128) samples, if not enough samples are available, it returns what it got +// it waits up to timeoutMs total while fetching tmpBfrSize (128) samples at a time, if not enough samples are available, it returns what it got // to poll the buffer and "just give me what you got" use a timeout of 0. On read error, it restarts the driver so no action needed by caller. +// note: maximum timeout is ADCMANAGER_LOCK_TIMEOUT_MS-1, so make sure to call this function in reasonable intervals if you need all samples (or increase the lock time) uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs) { if (!buffer || !numSamples) return 0; // note: DMA uses SOC_ADC_DIGI_MAX_BITWIDTH which is 12bits on all checked units TODO: should make sure and handle this to future proof it @@ -229,6 +230,9 @@ uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint3 return 0; } + const uint32_t maxReadTimeoutMs = ADCMANAGER_LOCK_TIMEOUT_MS - 1; + const uint32_t readTimeoutMs = timeoutMs < maxReadTimeoutMs ? timeoutMs : maxReadTimeoutMs; + const uint32_t readStartMs = millis(); uint16_t out = 0; // number of samples written to the buffer // check if any data was cached during an intermediate analogRead() if (_ctx->cacheCount) { @@ -250,10 +254,14 @@ uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint3 adc_digi_output_data_t temp[tmpBfrSize]; while (out < numSamples) { + const uint32_t elapsedMs = millis() - readStartMs; + if (elapsedMs >= readTimeoutMs) break; + uint32_t n = 0; uint16_t want = (numSamples - out) < tmpBfrSize ? (numSamples - out) : tmpBfrSize; size_t wantBytes = want * sizeof(adc_digi_output_data_t); - esp_err_t err = adc_continuous_read(_ctx->handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(timeoutMs)); + uint32_t remainingMs = readTimeoutMs - elapsedMs; + esp_err_t err = adc_continuous_read(_ctx->handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(remainingMs)); // copy the data into 16bit buffer if (n > 0) { From bad484991c889ffb97834b9afb2d97016664736f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 10:40:03 +0200 Subject: [PATCH 12/18] add curve calibration support --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index 9e3eb49403..66d4f17c51 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -335,6 +335,19 @@ int WLEDAdcManager::analogRead(uint8_t pin) { bool WLEDAdcManager::_initCali() { if (_cali) return true; + +#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED + adc_cali_curve_fitting_config_t cfg = { + .unit_id = ADC_UNIT_1, + #if SOC_ADC_CALIB_CHAN_COMPENS_SUPPORTED + //.chan = adc_channel_t(channel); // per channel calibration is not implemented (C5, C6, P4 support it). trading complexity for a few mV of inaccuracy here. + #endif + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + if (adc_cali_create_scheme_curve_fitting(&cfg, &_cali) == ESP_OK) return true; +#endif + #if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED adc_cali_line_fitting_config_t cfg = { .unit_id = ADC_UNIT_1, @@ -354,7 +367,7 @@ int WLEDAdcManager::analogReadMilliVolts(uint8_t pin) { adc_channel_t ch; if (!_pinToChannel(pin, &ch)) return 0; int raw = analogRead(pin); - if (!_cali && !_initCali()) return (raw * 3300) / 4095; + if (!_cali && !_initCali()) return (raw * 3300) / 4095; // fallback to linear conversion if calibration fails int mv = 0; return (adc_cali_raw_to_voltage(_cali, raw, &mv) == ESP_OK) ? mv : (raw * 3300) / 4095; From ffd488b83c4b5b7cadcaf2757af5d7ece7d846ad Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 10:42:57 +0200 Subject: [PATCH 13/18] add libignore to fix builds --- platformio.ini | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8a060ba0fe..a711664867 100644 --- a/platformio.ini +++ b/platformio.ini @@ -224,7 +224,8 @@ lib_deps = https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f ${env.lib_deps} https://github.com/Makuna/NeoPixelBus.git#1d7ff38f14d04a976f9c6e365c83a232bcba04fc ;; standard NPB version used in main branch - +lib_ignore = + wled-ADCmanager ; ADCmanager is only available on ESP32, not on ESP8266 monitor_filters = esp8266_exception_decoder ;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 @@ -316,7 +317,8 @@ lib_deps = https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0 ${env.lib_deps} NeoPixelBus = git+https://github.com/Makuna/NeoPixelBus#1d7ff38f14d04a976f9c6e365c83a232bcba04f ;; latest release + properly inialized buffers -lib_ignore = +lib_ignore = + wled-ADCmanager ; ADCmanager is only available on IDF V5.5 [esp32_idf_V5] ;; build environment for ESP32 using ESP-IDF 5.3.4 / arduino-esp32 v3.1.10 From e8a6253d0914b617d0fc5cd8b337aca9bb8e9489 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 12:57:57 +0200 Subject: [PATCH 14/18] exclude ADCmanager for IDF < 5.5.0 --- wled00/wled.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wled00/wled.h b/wled00/wled.h index 89194d1ddb..aec3876902 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -115,6 +115,9 @@ #include #endif #include "esp_task_wdt.h" + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + #include + #endif #endif #include #include @@ -196,7 +199,6 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #ifndef WLED_DISABLE_ESPNOW #include #endif -#include #include "colors.h" #include "fcn_declare.h" #ifndef WLED_DISABLE_OTA From 8185539dc68e172fef911e0b0d9d903d14e62dcf Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 13:04:43 +0200 Subject: [PATCH 15/18] use milliseconds, not ticks --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index 66d4f17c51..ad1206a5e3 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -261,7 +261,7 @@ uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint3 uint16_t want = (numSamples - out) < tmpBfrSize ? (numSamples - out) : tmpBfrSize; size_t wantBytes = want * sizeof(adc_digi_output_data_t); uint32_t remainingMs = readTimeoutMs - elapsedMs; - esp_err_t err = adc_continuous_read(_ctx->handle, (uint8_t*)temp, wantBytes, &n, pdMS_TO_TICKS(remainingMs)); + esp_err_t err = adc_continuous_read(_ctx->handle, (uint8_t*)temp, wantBytes, &n, remainingMs); // copy the data into 16bit buffer if (n > 0) { From 68efe9b815bfc24b8fa11831764cde15e0e69fcc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 13:22:58 +0200 Subject: [PATCH 16/18] implement rabbits suggested nitpicks --- lib/wled_ADCmanager/library.json | 1 + lib/wled_ADCmanager/wled_ADCmanager.cpp | 10 +++------- lib/wled_ADCmanager/wled_ADCmanager.h | 2 +- platformio.ini | 2 -- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/wled_ADCmanager/library.json b/lib/wled_ADCmanager/library.json index 093d4028e8..7134127b62 100644 --- a/lib/wled_ADCmanager/library.json +++ b/lib/wled_ADCmanager/library.json @@ -1,4 +1,5 @@ { "name": "wled-ADCmanager", + "platforms": ["espressif32"], "build": { "libArchive": false } } diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index ad1206a5e3..eeee9b3072 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -363,14 +363,10 @@ bool WLEDAdcManager::_initCali() { #endif int WLEDAdcManager::analogReadMilliVolts(uint8_t pin) { - int result_mv = 0; - adc_channel_t ch; - if (!_pinToChannel(pin, &ch)) return 0; - int raw = analogRead(pin); - if (!_cali && !_initCali()) return (raw * 3300) / 4095; // fallback to linear conversion if calibration fails - + int raw = analogRead(pin); // returns 0 on an invalid pin or error int mv = 0; - return (adc_cali_raw_to_voltage(_cali, raw, &mv) == ESP_OK) ? mv : (raw * 3300) / 4095; + if ((_cali || _initCali()) && adc_cali_raw_to_voltage(_cali, raw, &mv) == ESP_OK) return mv; + return (raw * 3300) / 4095; // fallback to linear conversion if calibration fails } #endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) #endif // ARDUINO_ARCH_ESP32 diff --git a/lib/wled_ADCmanager/wled_ADCmanager.h b/lib/wled_ADCmanager/wled_ADCmanager.h index f622a3a669..1896c8db98 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.h +++ b/lib/wled_ADCmanager/wled_ADCmanager.h @@ -40,7 +40,7 @@ class WLEDAdcManager { //void checkADC(); // check ADC status, reset if overflow happened (watchdog function, needs to be called frequently if used, i.e. put this in main loop) private: -WLEDAdcManager(); + WLEDAdcManager(); ~WLEDAdcManager(); WLEDAdcManager(const WLEDAdcManager&) = delete; WLEDAdcManager& operator=(const WLEDAdcManager&) = delete; diff --git a/platformio.ini b/platformio.ini index a711664867..ffeb51b354 100644 --- a/platformio.ini +++ b/platformio.ini @@ -224,8 +224,6 @@ lib_deps = https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f ${env.lib_deps} https://github.com/Makuna/NeoPixelBus.git#1d7ff38f14d04a976f9c6e365c83a232bcba04fc ;; standard NPB version used in main branch -lib_ignore = - wled-ADCmanager ; ADCmanager is only available on ESP32, not on ESP8266 monitor_filters = esp8266_exception_decoder ;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 From 305af0f50955cb5034751777c2d738ea01bd638f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Aug 2026 19:21:12 +0200 Subject: [PATCH 17/18] cleanup --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 28 ++++++------------------- lib/wled_ADCmanager/wled_ADCmanager.h | 3 +-- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index eeee9b3072..f51106f1b3 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -1,5 +1,5 @@ /* - * ADC manager to handle continous ADC sampling in parallel with single-shot pin reads + * ADC manager to handle continuous ADC sampling in parallel with single-shot pin reads * by @dedehai (2026) licensed under EUPL 1.2 license * * supports sampling a single pin in continuous ADC mode @@ -69,6 +69,9 @@ WLEDAdcManager::WLEDAdcManager() WLEDAdcManager::~WLEDAdcManager() { end(); if (_mutex) vSemaphoreDelete(_mutex); +#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED + if (_cali) adc_cali_delete_scheme_curve_fitting(_cali); +#endif #if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED if (_cali) adc_cali_delete_scheme_line_fitting(_cali); #endif @@ -134,26 +137,6 @@ void WLEDAdcManager::end() { xSemaphoreGive(_mutex); } -/* -// buffer overflow callback, we need to watch this as permanent overflow causes stalls in combination with wifi -> it does not it was an IDF bug -volatile bool _overflow = false; -static bool IRAM_ATTR __attribute__((noinline)) _onPoolOvf(adc_continuous_handle_t handle, - const adc_continuous_evt_data_t* edata, - void* user_data) { - //auto* mgr = static_cast(user_data); - _overflow = true; // one word write, ISR-safe, no locks, no copying - return false; // nothing to wake -} - -void WLEDAdcManager::checkADC() { - if (_running && _overflow) { - adc_continuous_stop(_handle); - adc_continuous_flush_pool(_handle); // flush remaining data, we want fresh samples - adc_continuous_start(_handle); - _overflow = false; - } -} -*/ // initialize the hardware bool WLEDAdcManager::_initContinuousADC() { if (!_ctx) return false; // begin() not called @@ -166,7 +149,8 @@ bool WLEDAdcManager::_initContinuousADC() { }; if (adc_continuous_new_handle(&hcfg, &_ctx->handle) != ESP_OK) return false; - // register buffer overflow callback (sets flag, main loop needs to call checkADC() to clear overflow - this is to prevent wifi stalling due to a now fixed IDF bug causing a lockup) + // register callback, can be used to trigger a task. not implemented here. Callback can also be used to set a flag for polling + // for example implementation see https://github.com/espressif/esp-idf/blob/v5.5/examples/peripherals/adc/continuous_read/main/continuous_read_main.c //adc_continuous_evt_cbs_t cbs = { .on_conv_done = nullptr, .on_pool_ovf = _onPoolOvf }; //adc_continuous_register_event_callbacks(_handle, &cbs, this); diff --git a/lib/wled_ADCmanager/wled_ADCmanager.h b/lib/wled_ADCmanager/wled_ADCmanager.h index 1896c8db98..8d56ad63c7 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.h +++ b/lib/wled_ADCmanager/wled_ADCmanager.h @@ -1,5 +1,5 @@ /* - * ADC manager to handle continous ADC sampling in parallel with single-shot pin reads + * ADC manager to handle continuous ADC sampling in parallel with single-shot pin reads * by @dedehai (2026) licensed under EUPL 1.2 license */ @@ -37,7 +37,6 @@ class WLEDAdcManager { int analogRead(uint8_t pin); int analogReadMilliVolts(uint8_t pin); - //void checkADC(); // check ADC status, reset if overflow happened (watchdog function, needs to be called frequently if used, i.e. put this in main loop) private: WLEDAdcManager(); From 9ac00d698b5ccf61a2a331727e2b36e2e6e37cfc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 9 Aug 2026 16:16:21 +0200 Subject: [PATCH 18/18] add fix for driver stall (IDF bug still seems to be unsolved, just slightly less pronounced) --- lib/wled_ADCmanager/wled_ADCmanager.cpp | 28 ++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/wled_ADCmanager/wled_ADCmanager.cpp b/lib/wled_ADCmanager/wled_ADCmanager.cpp index f51106f1b3..b17c045b0e 100644 --- a/lib/wled_ADCmanager/wled_ADCmanager.cpp +++ b/lib/wled_ADCmanager/wled_ADCmanager.cpp @@ -53,6 +53,12 @@ bool WLEDAdcManager::_pinToChannel(uint8_t pin, adc_channel_t* ch) { return true; } +volatile bool _overflow = false; +// buffer overflow callback, we need to watch this as prolonged overflow state causes stalls (probably an IDF bug related to DMA ownership) +static bool IRAM_ATTR _onPoolOvf(adc_continuous_handle_t handle, const adc_continuous_evt_data_t* edata, void* user_data) { + _overflow = true; // set the flag + return false; // nothing to wake +} WLEDAdcManager& WLEDAdcManager::instance() { static WLEDAdcManager inst; @@ -143,16 +149,16 @@ bool WLEDAdcManager::_initContinuousADC() { if (_ctx->handle) return true; // already initialized size_t frameBytes = (size_t)_ctx->samplesPerFrame * sizeof(adc_digi_output_data_t); adc_continuous_handle_cfg_t hcfg = { - .max_store_buf_size = frameBytes, // hold single frame in buffer, caller needs to drain it fast enough to avoid data loss (can increase to avoid data loss but still need to drain fast enough at one point) + .max_store_buf_size = frameBytes + frameBytes/2, // hold one and a half frames in buffer, caller needs to drain it fast enough to avoid data loss (can increase to avoid data loss but still need to drain fast enough at one point) .conv_frame_size = ADCMANAGER_DMA_BLOCKSIZE, // use fixed DMA buffer size of 256 bytes (ADC driver creates 5 DMA descriptors with one buffer each, at 20kHz this means an interrupt every 1.4ms .flags = { .flush_pool = false }, // do not flush the store buffer on overrun but discard new samples (true means discard oldest, is much slower and can cause issues, do not set true) }; if (adc_continuous_new_handle(&hcfg, &_ctx->handle) != ESP_OK) return false; - // register callback, can be used to trigger a task. not implemented here. Callback can also be used to set a flag for polling - // for example implementation see https://github.com/espressif/esp-idf/blob/v5.5/examples/peripherals/adc/continuous_read/main/continuous_read_main.c - //adc_continuous_evt_cbs_t cbs = { .on_conv_done = nullptr, .on_pool_ovf = _onPoolOvf }; - //adc_continuous_register_event_callbacks(_handle, &cbs, this); + // register the overflow callback to catch buffer overflows before they case a stall + adc_continuous_evt_cbs_t cbs = { .on_conv_done = nullptr, .on_pool_ovf = _onPoolOvf }; + adc_continuous_register_event_callbacks(_ctx->handle, &cbs, this); + adc_digi_pattern_config_t pat = { .atten = ADC_ATTEN_DB_12, @@ -255,7 +261,7 @@ uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint3 } } - if (err == ESP_ERR_TIMEOUT) { + if (err == ESP_ERR_TIMEOUT && n > 0) { break; // not enough samples within timeout frame, return what we got } if (err != ESP_OK) { @@ -267,7 +273,15 @@ uint16_t WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint3 else if (n == 0) break; // should not happen, just in case (if no samples are read, it should not be ESP_OK) } - //adc_continuous_flush_pool(_handle); // flush remaining data -> no need, just let the samples accumulate, uncomment if you need freshest samples only + if (_overflow) { + // stop-flush-restart: this is a dirty workaround for a bug in the IDF driver that can cause de-sync and permanent firing of interrupts, + // stalling everything. stop-start takes about 0.3ms and resets the DMA so everything keeps working. + DEBUG_PRINTF_P(PSTR("ADC buffer overflow")); + _overflow = false; + adc_continuous_stop(_ctx->handle); + adc_continuous_flush_pool(_ctx->handle); // flush remaining data and make sure pool does not overflow + adc_continuous_start(_ctx->handle); + } xSemaphoreGive(_mutex); return out;