Skip to content

Fix for ESP32C3 Glitching on Long Strips#5683

Closed
bigfella5000 wants to merge 3 commits into
wled:mainfrom
bigfella5000:fix/esp32c3-heap-glitch
Closed

Fix for ESP32C3 Glitching on Long Strips#5683
bigfella5000 wants to merge 3 commits into
wled:mainfrom
bigfella5000:fix/esp32c3-heap-glitch

Conversation

@bigfella5000

@bigfella5000 bigfella5000 commented Jun 16, 2026

Copy link
Copy Markdown

Hardware

  • Seeed XIAO ESP32-C3
  • 833x WS2815 ECO LEDs

Fix

Made wait time before calling getContiguousFreeHeap() on ESP32C3 to be a function of the number of LEDs to avoid interrupting RMT transmissions, which corrupts end of frame during animations every 5 seconds. At 800kHz with 24 bits per LED, a full frame takes N × 30µs. For strips longer than ~500 LEDs the fixed 15ms wait is insufficient, causing getContiguousFreeHeap() to interrupt an in-progress RMT transmission and corrupt the last portion of the frame (visible as a white flash every 5 seconds). This PR replaces the hardcoded value with a calculation based on actual strip length with a safety margin, floored at the original 15ms so short strips are unaffected.

Ongoing Issue

When running the effect given in "Wavesins API Command," my solution stopped the glitching. And in all other effects I tested, the issue is fixed. However, when running the effect given in "Stream API Command," the issue is still persisting. Additionally, the glitching occurs not just in the last quarter of the frame as it did with the other effects, but instead on the whole frame. And instead of glitching every 5 seconds, it's glitching every 6 seconds. Even when I raised the margin in the waitMS calculation, the glitch still occurred the exact same way on the Stream effect.

Wavesins API Command
{"on":true,"bri":255,"transition":0,"bs":0,"mainseg":0,"seg":[{"id":0,"start":0,"stop":832,"grp":1,"spc":0,"of":0,"on":true,"frz":false,"bri":255,"cct":127,"set":0,"lc":1,"n":"","col":[[115,0,255],[255,4,0],[0,0,0]],"fx":184,"sx":15,"ix":245,"pal":18,"c1":124,"c2":34,"c3":1,"sel":true,"rev":false,"mi":false,"o1":false,"o2":false,"o3":false,"si":0,"m12":0,"bm":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0}]}

Stream API Command
{"on":true,"bri":255,"transition":0,"bs":0,"mainseg":0,"seg":[{"id":0,"start":0,"stop":832,"grp":1,"spc":0,"of":0,"on":true,"frz":false,"bri":255,"cct":127,"set":0,"lc":1,"n":"","col":[[115,0,255],[255,4,0],[0,0,0]],"fx":39,"sx":255,"ix":39,"pal":48,"c1":128,"c2":128,"c3":16,"sel":true,"rev":false,"mi":false,"o1":false,"o2":false,"o3":false,"si":0,"m12":0,"bm":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0},{"stop":0}]}

Summary by CodeRabbit

  • Bug Fixes
    • Improved responsiveness on ESP32C3 devices by optimizing memory health monitoring to prevent blocking during LED strip updates. The system defers heap checks when the strip is actively updating and resumes normal monitoring intervals when complete. All memory safety checks remain intact.

…a function of the number of LEDs to avoid interrupting RMT transmissions, which corrupts end of frame during animations every 5 seconds
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 10e29103-e419-40fb-a749-aff8cb666cb3

📥 Commits

Reviewing files that changed from the base of the PR and between 3835df0 and c282d21.

📒 Files selected for processing (1)
  • wled00/wled.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • wled00/wled.cpp

Walkthrough

In WLED::loop(), the ESP32C3 heap-check block replaces a blocking waitForLEDs(15) call with a non-blocking deferral: when strip.isUpdating() is true, heapTime is backdated so the next heap evaluation is delayed by the normal 5-second interval; when the strip is idle, the existing contiguous-heap read and heapDanger escalation logic executes unchanged.

Changes

ESP32C3 heap-check deferral

Layer / File(s) Summary
Non-blocking heap-check deferral
wled00/wled.cpp
Replaces the blocking strip.waitForLEDs(15) wait with a conditional: if strip.isUpdating(), backdate heapTime to defer the check by 5s; otherwise read contiguous free heap and apply the same heapDanger segment-purge and strip-reset escalation logic.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • wled/WLED#4939: Modifies the same WLED::loop() heap-checker path and RAM-recovery actions that this PR adjusts for ESP32C3.
  • wled/WLED#5435: Directly modifies the same strip.isUpdating() branch in wled00/wled.cpp by adding strip.waitForLEDs(15) before ESP.getFreeHeap(), which is the exact call this PR removes in favor of deferral.

Suggested reviewers

  • DedeHai
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: addressing ESP32C3 glitching on long LED strips by modifying heap-check behavior during strip updates.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@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 `@wled00/wled.cpp`:
- Line 183: The timeout calculation in the waitMs assignment at line 183
performs integer division (/ 1000) before applying the 1.3 margin multiplier,
causing the margin to be applied to an already-truncated value. This results in
shorter-than-intended waits for longer strips. Rearrange the arithmetic
operations so that the 1.3 margin is applied before the division by 1000, or
ensure floating-point arithmetic is preserved throughout the calculation until
the final cast to unsigned, so the margin factor is applied to the full value
rather than the truncated intermediate result.
🪄 Autofix (Beta)

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

Run ID: a4d3a621-6da2-496e-8798-07f63c8fd8ff

📥 Commits

Reviewing files that changed from the base of the PR and between 8dfd2cf and 46f3b66.

📒 Files selected for processing (1)
  • wled00/wled.cpp

Comment thread wled00/wled.cpp Outdated
@softhack007 softhack007 added the AI Partly generated by an AI. Make sure that the contributor fully understands the code! label Jun 16, 2026
Comment thread wled00/wled.cpp Outdated
while (strip.isUpdating() && (millis() - t0 < 15)) delay(1); // be nice, but not too nice. Waits up to 15ms
// Most strips run at 800kHz, each LED needs 24 bits = 30µs per LED
// Add 30% margin and convert to ms, minimum 15ms
uint32_t waitMs = max(15u, (unsigned)(strip.getLengthTotal() * 30 / 1000 * 1.3));

@DedeHai DedeHai Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

in general has a point BUT:

  • don't use floats
  • only calculate if we actually need to wait
  • getLengthTotal() is not the length of the longest strip but total configred LEDs, including gaps

in general, waiting on a single core MCU is bad. the proper way to do it is to defer this to a loop when LEDs are not updating.

edit:
even the original wait is a hack and a workaround for unfixable deeper bugs.

@softhack007

softhack007 commented Jun 16, 2026

Copy link
Copy Markdown
Member

@bigfella5000 I think you're only scratching on the surface - the real problem on -C3 is the shared "RMT refill" interrupt. This interrupt can be stalled by many FreeRTOS or hardware driver activities, like file access or calculating unused memory.

You can have a look at this PR for a more complete picture

The wled dev team is aware of the problem, and the only real solution for -C3 is to rewrite the leds driver, or implement a high-priority RMT driver (similar to #4890) for RISC-V chips. Both options are ongoing experiments, but the code is not stable yet - @DedeHai and @willmmiles might be able to tell more.

Your idea to make the wait time depend on number of leds is interesting, and could be combined with #5435 for a more robust solution. However I see a fundamental flaw: strip.getLengthTotal() does not relate to the number of leds driven by a single RMT pin. On esp32, I can have 4096 leds distributed over 8 pins (just one example) - in this example each RMT output drives 512 leds, not 4096.

Edit: with 4096 LEDs, your patch would calculate a wait time of ~160ms, which is totally unacceptable as a delay that happens each 5 seconds.

Comment thread wled00/wled.cpp Outdated
@bigfella5000

bigfella5000 commented Jun 16, 2026

Copy link
Copy Markdown
Author

Thanks for pointing to #5435. The 0_15_x build solved the issue on all effects I tested including the Stream effect. One issue I'm having with it, which is likely well known, is that when the esp32c3 boots after uploading the 0_15_x build, I'm still seeing some glitching for the first few seconds. I'm assuming this is because there's plenty of other tasks interfering on boot. One other potential solution for avoiding the interference between the heap check and led updates would be the following:

#ifdef CONFIG_IDF_TARGET_ESP32C3
// calling getContiguousFreeHeap() during led update causes glitches on C3
// this can (probably) be removed once RMT driver for C3 is fixed
// If strip updating, defer heap check until future
if (strip.isUpdating()) {
  heapTime = millis() - 4999;
} else
#endif
{
  uint32_t heap = getContiguousFreeHeap(); // ESP32 family needs ~10k of contiguous free heap for UI to work properly
  #endif
  if (heap < MIN_HEAP_SIZE - 1024) heapDanger+=5; // allow 1k of "wiggle room" for things that do not respect min heap limits
  else heapDanger = 0;
  switch (heapDanger) {
    case 15: // 15 consecutive seconds
      DEBUG_PRINTLN(F("Heap low, purging segments"));
      strip.purgeSegments();
      strip.setTransition(0); // disable transitions
      for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {
        strip.getSegments()[i].setMode(FX_MODE_STATIC); // set static mode to free effect memory
      }
      errorFlag = ERR_NORAM; // alert UI  TODO: make this a distinct error: segment reset
      break;
    case 30: // 30 consecutive seconds
      DEBUG_PRINTLN(F("Heap low, reset segments"));
      strip.resetSegments(); // remove all but one segments from memory
      errorFlag = ERR_NORAM; // alert UI  TODO: make this a distinct error: segment reset
      break;
    case 45: // 45 consecutive seconds
      DEBUG_PRINTF_P(PSTR("Heap panic! Reset strip, reset connection\n"));
      strip.~WS2812FX();      // deallocate strip and all its memory
      new(&strip) WS2812FX(); // re-create strip object, respecting current memory limits
      if (!Update.isRunning()) forceReconnect = true; // in case wifi is broken, make sure UI comes back, set disableForceReconnect = true to avert
      errorFlag = ERR_NORAM; // alert UI  TODO: make this a distinct error: strip reset
      break;
    default:
      break;
  }
  heapTime = (uint16_t)millis();
}

However, the 0_15_x handles the interference more generally and in more locations by using waitForLEDs(), so I'm assuming that's a better solution.

@bigfella5000 bigfella5000 force-pushed the fix/esp32c3-heap-glitch branch from daeb462 to 3835df0 Compare June 17, 2026 00:00
coderabbitai[bot]

This comment was marked as outdated.

@bigfella5000 bigfella5000 force-pushed the fix/esp32c3-heap-glitch branch from 3835df0 to c282d21 Compare June 17, 2026 00:15
@bigfella5000 bigfella5000 deleted the fix/esp32c3-heap-glitch branch June 17, 2026 01:25
Comment thread wled00/wled.cpp
// calling getContiguousFreeHeap() during led update causes glitches on C3
// this can (probably) be removed once RMT driver for C3 is fixed
unsigned t0 = millis();
while (strip.isUpdating() && (millis() - t0 < 15)) delay(1); // be nice, but not too nice. Waits up to 15ms

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Second thought - as 15ms is just the upper limit for the waiting time - it would be sufficient here to change 15 into 50, and drop all the other changes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

that is what I thought too, however, it may be better to give the main loop several tries to get a slot where the strip is idle. Otherwise, this can result in unnecessary FPS hickups every 5s. I don't think there is a universal solution though, if target FPS is higher than actual FPS the main loop may not/never be running at the right moment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree. A limited number of retries - maybe an extra 2seconds when a new measurement is due - could reduce hickups. If the main loops runs strip.show() in each iteration (high FPS), this will not help much. But users could still reduce target FPS to prevent glitches.

Comment thread wled00/wled.cpp
while (strip.isUpdating() && (millis() - t0 < 15)) delay(1); // be nice, but not too nice. Waits up to 15ms
// If strip updating, defer heap check until future
if (strip.isUpdating()) {
heapTime = millis() - 4999;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not a good idea - this basicially postpones the heap check to infinite future, especially when target FPS is high.

@softhack007

softhack007 commented Jun 17, 2026

Copy link
Copy Markdown
Member
Screenshot_20260617-091606_Samsung Internet

@bigfella5000 For any future PR you may do - please don't force-push, or add a comment that explains why you had to push --force-with-lease.

See https://github.com/wled/WLED?tab=contributing-ov-file#updating-your-code

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

Labels

AI Partly generated by an AI. Make sure that the contributor fully understands the code!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants