Skip to content

Commit a718caf

Browse files
authored
Update FreeRTOS task management instructions
Clarify usage of delay() and yield() in FreeRTOS tasks, emphasizing the differences between ESP32 and ESP8266. Update instructions on task management and watchdog behavior.
1 parent 6c9922d commit a718caf

1 file changed

Lines changed: 19 additions & 14 deletions

File tree

.github/cpp.instructions.md

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -518,22 +518,28 @@ Always pair every `esp32SemTake` with a matching `esp32SemGive`. Choose a timeou
518518
**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.
519519

520520
### `delay()` vs `yield()` in FreeRTOS Tasks
521+
<!-- HUMAN_ONLY_START -->
522+
* 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.
523+
* The Arduino `loop()` function runs inside `loopTask`. Calling `delay()` there does *not* block the network stack, audio FFT, LED DMA, nor any other FreeRTOS task.
524+
* This differs from ESP8266, where `delay()` stalled the entire system unless `yield()` was called inside.
525+
<!-- HUMAN_ONLY_END -->
521526

522-
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.
523-
524-
**`delay()` in `loopTask` is allowed.** 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.
525-
526-
**`yield()` is a no-op in WLED-MM on ESP32.** `WLEDMM_FASTPATH` redefines `yield()` to an empty macro:
527+
- On ESP32, `delay()` is generally allowed, as it helps to efficiently manage CPU usage of all tasks.
528+
- On ESP8266, only use `delay()` and `yield()` in the main `loop()` context. If not sure, protect with `if (can_yield()) ...`.
529+
- Do *not* use `delay()` in effects (FX.cpp) or in the hot pixel path.
530+
- `delay()` on ``busses`` level is allowed, it might be needed to achieve exact timing in LED drivers.
531+
- **`yield()` is a no-op in WLED-MM on ESP32.** `WLEDMM_FASTPATH` redefines `yield()` to an empty macro.
532+
```cpp
533+
#define yield() {} // WLEDMM: yield() is completely unnecessary on ESP32
534+
```
527535
528-
```cpp
529-
#define yield() {} // WLEDMM: yield() is completely unnecessary on ESP32
530-
```
536+
### IDLE Watchdog and Custom Tasks on ESP32
531537
532-
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.
533-
**Do not use `yield()` to pace ESP32 tasks or assume it feeds any watchdog**.
538+
- **Do NOT use `yield()` to pace ESP32 tasks or assume it feeds any watchdog**.
534539
535-
**Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved. The IDLE watchdog panic is the first visible symptom — but the damage starts earlier: deleted task memory leaks, software timers stop firing, light sleep is disabled, and Wi-Fi/BT idle hooks don't run. See `esp-idf.instructions.md` for a full explanation of what IDLE does. Structure custom tasks like this:
540+
- 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.
536541
542+
- **Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved. The IDLE watchdog panic is the first visible symptom — but the damage starts earlier: deleted task memory leaks, software timers stop firing, light sleep is disabled, and Wi-Fi/BT idle hooks don't run. See `esp-idf.instructions.md` for a full explanation of what IDLE does. Structure custom tasks like this:
537543
```cpp
538544
// WRONG — IDLE task is never scheduled; yield() does not feed the idle task watchdog.
539545
void myTask(void*) {
@@ -552,9 +558,8 @@ void myTask(void*) {
552558
}
553559
```
554560

555-
Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed.
556-
557-
**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.
561+
- Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed.
562+
- **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.
558563

559564
## General
560565

0 commit comments

Comments
 (0)