Skip to content

Commit 7d29c8a

Browse files
Copilotsofthack007
andauthored
docs: add delay/yield/looptask/IDLE guidance to cpp.instructions.md and esp-idf.instructions.md
Agent-Logs-Url: https://github.com/MoonModules/WLED-MM/sessions/e6fa4128-6712-4700-b716-3c5194ca2286 Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
1 parent 0e508ed commit 7d29c8a

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

.github/cpp.instructions.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,44 @@ Always pair every `esp32SemTake` with a matching `esp32SemGive`. Choose a timeou
449449

450450
**Important**: Not every shared resource needs a mutex. Some synchronization is guaranteed by the overall control flow. For example, `volatile bool` flags like `suspendStripService`, `doInitBusses`, `loadLedmap`, and `OTAisRunning` (declared in `wled.h`) are checked sequentially in the main loop (`wled.cpp`), so they serialize access without requiring a semaphore. Use mutexes when true concurrent access from multiple FreeRTOS tasks is possible and race-conditions can lead to unexpected behaviour. Rely on control-flow ordering when operations are sequenced within the same loop iteration.
451451

452+
### `delay()` vs `yield()` in FreeRTOS Tasks
453+
454+
On ESP32, `delay(ms)` calls `vTaskDelay(ms / portTICK_PERIOD_MS)`, which **suspends only the calling task**. The FreeRTOS scheduler immediately runs all other ready tasks. This differs from ESP8266, where `delay()` stalled the entire system unless `yield()` was called inside.
455+
456+
**`delay()` in `loopTask` is acceptable.** The Arduino `loop()` function runs inside `loopTask`. Calling `delay()` there does not block the network stack, audio FFT, LED DMA, or any other FreeRTOS task.
457+
458+
**`yield()` is a no-op in WLED-MM on ESP32.** `WLEDMM_FASTPATH` redefines `yield()` to an empty macro:
459+
460+
```cpp
461+
#define yield() {} // WLEDMM: yield() is completely unnecessary on ESP32
462+
```
463+
464+
Even in stock arduino-esp32, `yield()` calls `vTaskDelay(0)`, which only switches to tasks at equal or higher priority — the IDLE task (priority 0) is never reached. Do not use `yield()` to pace ESP32 tasks or assume it feeds any watchdog.
465+
466+
**Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved and the IDLE watchdog may panic:
467+
468+
```cpp
469+
// WRONG — IDLE task is never scheduled; yield() is a no-op in WLEDMM_FASTPATH
470+
void myTask(void*) {
471+
for (;;) {
472+
doWork();
473+
yield();
474+
}
475+
}
476+
477+
// CORRECT — delay(1) suspends the task for ≥1 ms, IDLE task runs, IDLE watchdog is fed
478+
void myTask(void*) {
479+
for (;;) {
480+
doWork();
481+
delay(1); // DO NOT REMOVE — lets IDLE(0) run and feeds its watchdog
482+
}
483+
}
484+
```
485+
486+
Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed.
487+
488+
**Watchdog note.** WLED-MM disables the Task Watchdog by default (`WLED_WATCHDOG_TIMEOUT 0` in `wled.h`). When enabled, `esp_task_wdt_reset()` is called at the end of each `loop()` iteration. Long blocking operations inside `loop()` — such as OTA downloads or slow file I/O — must call `esp_task_wdt_reset()` periodically, or be restructured so the main loop is not blocked for longer than the configured timeout.
489+
452490
## General
453491

454492
- Follow the existing style in the file you are editing

.github/esp-idf.instructions.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,21 @@ Guidelines:
682682
#endif
683683
```
684684

685+
### `delay()`, `yield()`, and the IDLE task
686+
687+
FreeRTOS on ESP32 is **preemptive** — all tasks are scheduled by priority regardless of `yield()` calls. This is fundamentally different from ESP8266 cooperative multitasking.
688+
689+
| Call | What it does | Reaches IDLE (priority 0)? |
690+
|---|---|---|
691+
| `delay(ms)` / `vTaskDelay(ticks)` | Suspends calling task; scheduler runs all other ready tasks | ✅ Yes |
692+
| `yield()` / `vTaskDelay(0)` | Hint to switch to tasks at **equal or higher** priority only | ❌ No |
693+
| `taskYIELD()` | Same as `vTaskDelay(0)` | ❌ No |
694+
| Blocking API (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) | Suspends task until event or timeout; IDLE runs freely | ✅ Yes |
695+
696+
**`delay()` in `loopTask` is safe.** Arduino's `loop()` runs inside `loopTask`. Calling `delay()` suspends only `loopTask` — all other FreeRTOS tasks (Wi-Fi stack, audio FFT, LED DMA) continue uninterrupted on either core.
697+
698+
**`yield()` does not yield to IDLE.** Any task that loops with only `yield()` calls will starve the IDLE task, causing the IDLE watchdog to fire. Always use `delay(1)` (or a blocking FreeRTOS call) in tight task loops. Note: WLED-MM redefines `yield()` as an empty macro on ESP32 WLEDMM_FASTPATH builds — see `cpp.instructions.md`.
699+
685700
### Watchdog management
686701

687702
Long-running operations may trigger the task watchdog. Feed it explicitly:

0 commit comments

Comments
 (0)