Skip to content

Adding ADC manager in preparation for AR update to IDF V5 - #5773

Open
DedeHai wants to merge 18 commits into
wled:mainfrom
DedeHai:ADC_manager
Open

Adding ADC manager in preparation for AR update to IDF V5#5773
DedeHai wants to merge 18 commits into
wled:mainfrom
DedeHai:ADC_manager

Conversation

@DedeHai

@DedeHai DedeHai commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

This adds an ADCmanager library like I mentioned in #5764 which makes it possible to use continuous ADC sampling along side single-shot pin sampling. What this means is that we get the best of both worlds for the AR usermod: one pin can be sampled "in the background" with high sampling rate. If a different pin needs to be sampled (analog button, battery usermod etc.) it will pause the sampling for ~1ms, saving whatever samples were already acquired, read the single-shot pin and continue sampling in continuous mode.
Tested this extensively on differend MCUs and found no issue (well, found many, but all are fixed)

  • Requires the IDF bugfix which is available in latest tasmota (i.e. Update to IDF 5.5.4 #5769)
    edit: latest version still seems to have that bug, it seems less severe though and I added a fix that works well.

  • Uses some RAM for the static manager - if no continuous sampling is used, its minimal (mostly for the semaphore, maybe 32bytes?)

  • Only single pin continuous sampling is supported

The Arduino functions analogRead() and analogReadMilliVolts() are overriden with the ADCmanager's functions, so full backwards compatibility (and any usermod that uses those calls). On ESP8266 the manager is not doing anything, it keeps working as it was.

How it works:
A caller can simply use the function

WLEDAdcManager::begin(uint8_t pin, uint32_t sampleRateHz, uint16_t samplesPerFrame)

To start sampling a pin. It will fill the lower level drivers buffer up to samplesPerFrame, any additional samples are dropped. The samples can be read back using

WLEDAdcManager::readSamples(int16_t* buffer, uint16_t numSamples, uint32_t timeoutMs)

To read the full buffer or just a part of it. If timeout is set to 0 (or low enough) it will return the samples that are currently available.

So in a nutshell, this is like "I2S_GRAB_ADC1_COMPLETELY" in AR but without grabbing it completely.

Tested on C3, C6, S3 and classic ESP32

I only found one issue so far and that is if the pin-info page is open and analog pins are configured and the continuous sampling is active it can sometimes crash, could not find the exact reason (crash log:
`LoadProhibited (Exception 28: Access to invalid address: LOAD (wild pointer?))

0 0x4008baff xTaskRemoveFromEventList ??

1 0x401c13e3 xQueueGenericSend ??

2 0x400d4e76 WLEDAdcManager::analogRead(unsigned char) wled_ADCmanager.cpp:336

3 0x400f3958 handleAnalog(unsigned char) button.cpp:182`

Summary by CodeRabbit

New Features

  • Added continuous ADC sampling on ESP32 devices with configurable sample rates and frame sizes.
  • Added buffered sample retrieval with timeout support.
  • Added calibrated millivolt readings alongside standard analog readings.
  • Added automatic recovery when ADC sampling encounters read errors.
  • Added one-shot analog reads while continuous sampling remains active.
  • Added managed ADC startup and shutdown controls.
  • Preserved existing analog-reading behavior on ESP8266 devices.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds an ESP32-only WLEDAdcManager singleton for continuous ADC1 sampling, cached reads, one-shot reads, synchronization, lifecycle management, and calibrated millivolt conversion. ESP32 analog-read calls route through the manager.

Changes

ESP32 ADC management

Layer / File(s) Summary
ADC manager interface and integration
lib/wled_ADCmanager/wled_ADCmanager.h, wled00/wled.h, lib/wled_ADCmanager/library.json
Defines the manager API, target-specific ADC formats, compatibility macros, platform guards, WLED integration, and library metadata.
Continuous sampling lifecycle
lib/wled_ADCmanager/wled_ADCmanager.cpp
Adds ADC1 pin validation, session setup, DMA configuration, sample caching, synchronized reads and teardown, and driver restart recovery.
One-shot and calibrated reads
lib/wled_ADCmanager/wled_ADCmanager.cpp
Adds normalized one-shot reads, pause-and-resume handling, optional line-fitting calibration, and millivolt conversion fallback.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant WLEDAdcManager
  participant ESP32ADCDriver
  Caller->>WLEDAdcManager: begin(pin, sampleRateHz, samplesPerFrame)
  WLEDAdcManager->>ESP32ADCDriver: Configure and start ADC1
  Caller->>WLEDAdcManager: readSamples(buffer, numSamples, timeoutMs)
  WLEDAdcManager->>ESP32ADCDriver: Read sample frames
  ESP32ADCDriver-->>WLEDAdcManager: Return ADC samples
  WLEDAdcManager-->>Caller: Return cached and continuous samples
Loading
sequenceDiagram
  participant Caller
  participant WLEDAdcManager
  participant ESP32ADCDriver
  Caller->>WLEDAdcManager: analogRead(pin)
  WLEDAdcManager->>ESP32ADCDriver: Pause continuous sampling
  WLEDAdcManager->>ESP32ADCDriver: Perform one-shot ADC read
  ESP32ADCDriver-->>WLEDAdcManager: Return raw conversion
  WLEDAdcManager->>ESP32ADCDriver: Resume continuous sampling
  WLEDAdcManager-->>Caller: Return normalized ADC value
Loading

Possibly related PRs

  • wled/WLED#5764: Both changes address ESP32 continuous ADC sampling and analogRead() coordination.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an ADC manager to support the planned IDF V5 update.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
lib/wled_ADCmanager/library.json (1)

1-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider restricting the library to ESP32 platforms.

The manifest has no platforms field, so PlatformIO builds this library for ESP8266 environments too. The source compiles to nothing there because of the ARDUINO_ARCH_ESP32 guard, but declaring the platform makes the intent explicit and skips the compile step.

♻️ Proposed manifest addition
 {
   "name": "wled-ADCmanager",
+  "platforms": "espressif32",
   "build": { "libArchive": false }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/wled_ADCmanager/library.json` around lines 1 - 4, Update the library
manifest near the existing build configuration to declare an ESP32-only platform
restriction using the manifest’s platforms field. Preserve the current library
name and libArchive setting.
lib/wled_ADCmanager/wled_ADCmanager.h (1)

40-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clean up the commented-out declaration and fix the indentation.

Line 40 keeps a disabled checkADC() declaration. Line 43 has no indentation. The project requires 2-space indentation and removal of dead code.

♻️ Proposed cleanup
   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();
+  WLEDAdcManager();
   ~WLEDAdcManager();

As per coding guidelines: "Use 2-space indentation and no tabs in C++ files" and "Remove dead or unused code".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/wled_ADCmanager/wled_ADCmanager.h` around lines 40 - 44, Remove the
commented-out checkADC declaration and indent the private constructor and
destructor declarations by two spaces within the WLEDAdcManager class.

Source: Coding guidelines

lib/wled_ADCmanager/wled_ADCmanager.cpp (2)

297-320: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Check the return value of adc_oneshot_config_channel().

Line 307 discards the return value. If the configuration fails, adc_oneshot_read() runs on an unconfigured channel and the caller receives a value that looks valid. Return false on failure and delete the unit.

♻️ Proposed change
-  adc_oneshot_config_channel(h, ch, &ccfg);
+  if (adc_oneshot_config_channel(h, ch, &ccfg) != ESP_OK) {
+    adc_oneshot_del_unit(h);
+    return false;
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/wled_ADCmanager/wled_ADCmanager.cpp` around lines 297 - 320, Update
_oneshotRead to check the result of adc_oneshot_config_channel() before calling
adc_oneshot_read(); on configuration failure, delete the ADC unit with
adc_oneshot_del_unit(h) and return false so no unconfigured read occurs.

153-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the dead code and the unused variables.

Three items are dead:

  • Lines 153-172: the commented-out overflow callback and checkADC() implementation. The header comment on line 20 states the workaround is no longer needed.
  • Lines 353-354: an empty #if SOC_ADC_DIG_IIR_FILTER_SUPPORTED / #endif pair.
  • Lines 357-359: result_mv is never read, and ch is assigned but never used. analogRead(pin) performs its own pin validation, so only the return check is needed.

ContinuousCtx::pin (line 56) is also written in begin() and never read.

♻️ Proposed cleanup for `analogReadMilliVolts()`
 int WLEDAdcManager::analogReadMilliVolts(uint8_t pin) {
-  int result_mv = 0;
-  adc_channel_t ch;
-  if (!_pinToChannel(pin, &ch)) return 0;
+  adc_channel_t ch;
+  if (!_pinToChannel(pin, &ch)) return 0; // reject non-ADC1 pins before reading
   int raw = analogRead(pin);

As per coding guidelines: "Remove dead or unused code". As per path instructions: "CHECK for singleton data (defined but never used) and for dead/disabled code, and suggest to remove them."

Also applies to: 353-354, 356-360

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/wled_ADCmanager/wled_ADCmanager.cpp` around lines 153 - 172, Remove the
obsolete commented overflow callback and checkADC implementation, the empty
SOC_ADC_DIG_IIR_FILTER_SUPPORTED conditional, and unused result_mv/ch
declarations in analogReadMilliVolts(), retaining only the analogRead(pin)
return check. Remove ContinuousCtx::pin and its assignment in begin() since it
is never read.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/wled_ADCmanager/wled_ADCmanager.cpp`:
- Around line 327-337: Update analogRead() to check the return value of
_initContinuousADC() after restarting continuous sampling and handle failure by
preventing subsequent use of the invalid continuous context. Add a defensive
null check for _ctx->handle in readSamples() before calling
adc_continuous_read(), returning through the existing safe failure path when the
handle is unavailable.
- Around line 220-233: Update WLEDAdcManager::_drainToCache() to preserve unread
samples already stored in _ctx->cache: remove the unconditional reset of
_ctx->cacheCount and append newly drained samples from the current count, while
retaining the existing cache-size bounds.
- Around line 48-49: Update the ESP32-C6 branch in the ADC pin validation logic
to use the ADC channel range, accepting channels 0 through 6 inclusive. Replace
the current pin-based limit in the relevant conditional while preserving the
existing handling of ch and other target branches.
- Line 242: Replace the unbounded _mutex waits in readSamples()
(lib/wled_ADCmanager/wled_ADCmanager.cpp:242-242), begin() (95-95), end()
(143-143), and analogRead() (327-327) with bounded waits, checking _mutex for
null before each take; return 0 from readSamples() and analogRead(), return
false from begin(), and return early from end() when acquisition fails.
- Around line 340-351: Update WLEDAdcManager::_initCali() to support
ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED by creating _cali with
adc_cali_create_scheme_curve_fitting(), while preserving the existing
line-fitting path as appropriate. In the WLEDAdcManager destructor, release
curve-fitting calibration handles with
adc_cali_delete_scheme_curve_fitting(_cali), ensuring the scheme-specific
cleanup matches the creation path.
- Around line 177-182: Update the adc_continuous_handle_cfg_t initialization in
the ADC manager to make max_store_buf_size consistent with the documented
two-frame capacity, using frameBytes * 2 and ensuring the result exceeds
ADCMANAGER_DMA_BLOCKSIZE for small frames. Correct the conv_frame_size comment
to remove the incorrect 256-byte claim and state the actual 128-byte block size.

---

Nitpick comments:
In `@lib/wled_ADCmanager/library.json`:
- Around line 1-4: Update the library manifest near the existing build
configuration to declare an ESP32-only platform restriction using the manifest’s
platforms field. Preserve the current library name and libArchive setting.

In `@lib/wled_ADCmanager/wled_ADCmanager.cpp`:
- Around line 297-320: Update _oneshotRead to check the result of
adc_oneshot_config_channel() before calling adc_oneshot_read(); on configuration
failure, delete the ADC unit with adc_oneshot_del_unit(h) and return false so no
unconfigured read occurs.
- Around line 153-172: Remove the obsolete commented overflow callback and
checkADC implementation, the empty SOC_ADC_DIG_IIR_FILTER_SUPPORTED conditional,
and unused result_mv/ch declarations in analogReadMilliVolts(), retaining only
the analogRead(pin) return check. Remove ContinuousCtx::pin and its assignment
in begin() since it is never read.

In `@lib/wled_ADCmanager/wled_ADCmanager.h`:
- Around line 40-44: Remove the commented-out checkADC declaration and indent
the private constructor and destructor declarations by two spaces within the
WLEDAdcManager class.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fb8a355f-c017-4a73-a330-95f8519821a4

📥 Commits

Reviewing files that changed from the base of the PR and between c1838ed and 445e77f.

📒 Files selected for processing (4)
  • lib/wled_ADCmanager/library.json
  • lib/wled_ADCmanager/wled_ADCmanager.cpp
  • lib/wled_ADCmanager/wled_ADCmanager.h
  • wled00/wled.h

Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp Outdated
Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp
Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp
Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp Outdated
Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp Outdated
Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/wled_ADCmanager/wled_ADCmanager.cpp`:
- Line 36: Update ADCManager’s mutex arbitration around readSamples() and
analogRead() so continuous reads cannot hold _mutex beyond
ADCMANAGER_LOCK_TIMEOUT_MS, or replace timeout-based contention handling with
bounded arbitration that never returns 0 solely because the lock is unavailable.
Preserve valid zero-valued ADC results while honoring the caller’s timeoutMs and
numSamples constraints.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b0a9001a-d3cc-4102-b313-aea697c5a475

📥 Commits

Reviewing files that changed from the base of the PR and between 445e77f and feab0b1.

📒 Files selected for processing (1)
  • lib/wled_ADCmanager/wled_ADCmanager.cpp

Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/wled_ADCmanager/wled_ADCmanager.cpp (1)

220-234: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document readSamples() capping the requested timeout.

WLEDAdcManager::readSamples() declares timeoutMs = 100, but the implementation caps every read at ADCMANAGER_LOCK_TIMEOUT_MS - 1 and returns after 9 ms if fewer samples are ready. The current internal comment documents this only from the implementation side. Add the effective timeout limit to the public readSamples() API documentation and confirm callers handle partial returns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/wled_ADCmanager/wled_ADCmanager.cpp` around lines 220 - 234, Update the
public API documentation for WLEDAdcManager::readSamples() to state that
timeoutMs is capped at ADCMANAGER_LOCK_TIMEOUT_MS - 1 and reads may return fewer
samples when the limit is reached. Review callers of readSamples() and ensure
they correctly handle partial returns rather than assuming numSamples are always
produced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/wled_ADCmanager/wled_ADCmanager.cpp`:
- Around line 25-34: Update the ESP32/ESP-IDF guard surrounding the ADC manager
definitions to use the same include-state protection pattern as wled.h, ensuring
ESP_IDF_VERSION_VAL is safely available before evaluating the version check.
Preserve the existing analogRead macro undefinitions and ADCMANAGER constants
while preventing preprocessor or include-path failures when the default
definition is absent.
- Around line 233-234: Update the adc_continuous_read() calls in the ADC read
flow to pass remainingMs directly as the timeout argument, rather than
converting it with pdMS_TO_TICKS(). Apply the same change to both affected call
sites while preserving the existing millisecond calculations and timeout bounds.

---

Nitpick comments:
In `@lib/wled_ADCmanager/wled_ADCmanager.cpp`:
- Around line 220-234: Update the public API documentation for
WLEDAdcManager::readSamples() to state that timeoutMs is capped at
ADCMANAGER_LOCK_TIMEOUT_MS - 1 and reads may return fewer samples when the limit
is reached. Review callers of readSamples() and ensure they correctly handle
partial returns rather than assuming numSamples are always produced.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 30f8fc41-1f43-4e59-8d91-1daf2b07a225

📥 Commits

Reviewing files that changed from the base of the PR and between 9c52c8c and b6e9ac6.

📒 Files selected for processing (1)
  • lib/wled_ADCmanager/wled_ADCmanager.cpp

Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp
Comment thread lib/wled_ADCmanager/wled_ADCmanager.cpp
@netmindz

netmindz commented Aug 8, 2026

Copy link
Copy Markdown
Member

@softhack007 is probably best to review this rather than me

@DedeHai

DedeHai commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

@softhack007 when reviewing this, check if the mutex is sound and will work with AR. Everything low level / hardware I tested in many combinations

  • I2S LED output on ESP32 in parallel
  • multiple button reads
  • fast analogRead intervals (50ms)
  • not fetching ADC data at all or in long intervals (1s)
  • pin info page also fetching ADC values in parallel (did crash on earlier version but seems fixed now that I added semaphore timeouts)
  • using analogRead on the pin being sampled continuously

what I did not test:

  • ISR latency issues when using more than 2 RMT channels (could cause samples being lost if ADC DMA ISR is delayed more than a few milliseconds)
  • frequency artefacts due to interrupted sampling
  • fetching continuous data from a task (other than main loop)

edit:
I now tested in combination with AR and added the "bug workaround" back in, it is still needed - it would frequently hang in AR. I also checked for the frequency artefacts and they were there, a simple low-pass (same as it was) takes care of that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants