Revise *.instructions.md: separate AI-relevant special knowledge from background information for contributors (#354)

Revising AI review instructions:
* Reduce context window use for AI review tools, by avoiding repeating common knowledge and API information that is (usually) part of the AI training datasets any way.
* Introduce a mechanism to maintain both parts in single files, to avoid "silent diversion" over time
* Adding a coderabbit path instruction that ensures cross-checking of both parts whenever a PR modifies instruction files

Objectives:
* Primary Goal: only inject content in AI-visible areas that are WLED-MM–specific or which deviate from general knowledge (the context window "token cost" of true false-positive suppressors is always worth it).
* Soft goal: keep each file's AI-facing section lean enough that the signal-to-noise ratio in the attention layer stays high — around 1,500–2,000 words per file type is a reasonable practical ceiling for current models.
* Aspirational: 500 words per file if achievable without sacrificing review quality.

This is an evolution of #353, based on the discussion in https://github.com/MoonModules/WLED-MM/pull/353#issuecomment-4186989873

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Frank Möhle
2026-04-04 22:58:30 +02:00
committed by GitHub
parent e969a9b272
commit 1243c7b1fa
5 changed files with 161 additions and 27 deletions

View File

@@ -66,3 +66,19 @@ reviews:
scoped to least privilege. Never interpolate github.event.* values directly scoped to least privilege. Never interpolate github.event.* values directly
into run: steps — pass them through an env: variable to prevent script into run: steps — pass them through an env: variable to prevent script
injection. Do not use pull_request_target unless fully justified. injection. Do not use pull_request_target unless fully justified.
- path: ".github/*.instructions.md"
instructions: |
This file contains both AI-facing rules and human-only reference sections.
Human-only sections are enclosed in `<!-- HUMAN_ONLY_START -->` /
`<!-- HUMAN_ONLY_END -->` HTML comment markers and should not be used as
actionable review criteria.
When this file is modified in a PR, perform the following alignment check:
1. For each `<!-- HUMAN_ONLY_START --> ... <!-- HUMAN_ONLY_END -->` block,
verify that its examples and guidance are consistent with (and do not
contradict) the AI-facing rules stated in the same file.
2. Flag any HUMAN_ONLY section whose content has drifted from the surrounding
AI-facing rules due to edits introduced in this PR.
3. If new AI-facing rules were added without updating a related HUMAN_ONLY
reference section, note this as a suggestion (not a required fix).

View File

@@ -3,6 +3,12 @@ applyTo: ".github/workflows/*.yml,.github/workflows/*.yaml"
--- ---
# CI/CD Conventions — GitHub Actions Workflows # CI/CD Conventions — GitHub Actions Workflows
> **Note for AI review tools**: sections enclosed in
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
> contributor reference material. Do **not** use that content as actionable review
> criteria — treat it as background context only.
<!-- HUMAN_ONLY_START -->
## YAML Style ## YAML Style
- Indent with **2 spaces** (no tabs) - Indent with **2 spaces** (no tabs)
@@ -60,6 +66,7 @@ schedule:
- Name artifacts with enough context to be unambiguous (e.g., `firmware-${{ matrix.environment }}`) - Name artifacts with enough context to be unambiguous (e.g., `firmware-${{ matrix.environment }}`)
- Avoid uploading artifacts that will never be consumed downstream - Avoid uploading artifacts that will never be consumed downstream
<!-- HUMAN_ONLY_END -->
--- ---
## Security ## Security

View File

@@ -5,12 +5,19 @@ WLED-MM is a fork focused on higher performance (ESP32, ESP32-S3, PSRAM boards),
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
> **Note for AI review tools**: sections enclosed in
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
> contributor reference material. Do **not** use that content as actionable review
> criteria — treat it as background context only.
<!-- HUMAN_ONLY_START -->
## Setup ## Setup
- Node.js 20+ (see `.nvmrc`) - Node.js 20+ (see `.nvmrc`)
- Install dependencies: `npm ci` - Install dependencies: `npm ci`
- PlatformIO (required only for firmware compilation): `pip install -r requirements.txt` - PlatformIO (required only for firmware compilation): `pip install -r requirements.txt`
<!-- HUMAN_ONLY_END -->
## Hardware Targets ## Hardware Targets
| Target | Status | | Target | Status |
@@ -21,6 +28,7 @@ Always reference these instructions first and fallback to search or bash command
| ESP32-P4/-C5/-C6 | Will be supported in the future | | ESP32-P4/-C5/-C6 | Will be supported in the future |
| ESP8266 | Deprecated — should still compile, but not actively maintained | | ESP8266 | Deprecated — should still compile, but not actively maintained |
<!-- HUMAN_ONLY_START -->
## Build and Test ## Build and Test
| Command | Purpose | Typical Time | | Command | Purpose | Typical Time |
@@ -36,8 +44,15 @@ Common firmware environments: `esp32_4MB_V4_M`, `esp32_16MB_V4_S_HUB75`, `esp32S
For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build-instructions.md](agent-build-instructions.md). For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build-instructions.md](agent-build-instructions.md).
<!-- HUMAN_ONLY_END -->
## Repository Structure ## Repository Structure
tl;dr: Firmware source: `wled00/` (C++). Web UI source: `wled00/data/`. Auto-generated headers: `wled00/html_*.h`**never edit or commit**.
Usermods: `usermods/` (`.h` files, included via `usermods_list.cpp`). Build targets: `platformio.ini`. CI/CD: `.github/workflows/`.
<!-- HUMAN_ONLY_START -->
Detailed overview:
```text ```text
wled00/ # Firmware source (C++) wled00/ # Firmware source (C++)
├── data/ # Web UI source (HTML, CSS, JS) ├── data/ # Web UI source (HTML, CSS, JS)
@@ -57,7 +72,7 @@ tools/cdata-test.js # Test suite
package.json # Node.js scripts and release ID package.json # Node.js scripts and release ID
.github/workflows/ # CI/CD pipelines .github/workflows/ # CI/CD pipelines
``` ```
<!-- HUMAN_ONLY_END -->
Main development branch: `mdev` Main development branch: `mdev`
## General Guidelines ## General Guidelines
@@ -68,7 +83,7 @@ Main development branch: `mdev`
- **When unsure, say so.** Gather more information rather than guessing. - **When unsure, say so.** Gather more information rather than guessing.
- **Acknowledge good patterns** when you see them. Summarize good practices as part of your review - positive feedback always helps. - **Acknowledge good patterns** when you see them. Summarize good practices as part of your review - positive feedback always helps.
- **Provide references** when making analyses or recommendations. Base them on the correct branch or PR. - **Provide references** when making analyses or recommendations. Base them on the correct branch or PR.
- **Look for user-visible "breaking" changes**. Ask for confirmation that these were made intentionally. - **Look for user-visible breaking changes and ripple effects**. Ask for confirmation that these were introduced intentionally.
- **Unused / dead code must be justified or removed**. This helps to keep the codebase clean, maintainable and readable. - **Unused / dead code must be justified or removed**. This helps to keep the codebase clean, maintainable and readable.
- **C++ formatting available**: `clang-format` is installed but not in CI - **C++ formatting available**: `clang-format` is installed but not in CI
- No automated linting is configured — match existing code style in files you edit. See `cpp.instructions.md` and `web.instructions.md` for language-specific conventions, and `cicd.instructions.md` for GitHub Actions workflows. - No automated linting is configured — match existing code style in files you edit. See `cpp.instructions.md` and `web.instructions.md` for language-specific conventions, and `cicd.instructions.md` for GitHub Actions workflows.
@@ -77,7 +92,8 @@ Main development branch: `mdev`
Using AI-generated code can hide the source of the inspiration / knowledge / sources it used. Using AI-generated code can hide the source of the inspiration / knowledge / sources it used.
- Document attribution of inspiration / knowledge / sources used in the code, e.g. link to GitHub repositories or other websites describing the principles / algorithms used. - Document attribution of inspiration / knowledge / sources used in the code, e.g. link to GitHub repositories or other websites describing the principles / algorithms used.
- When a larger block of code is generated by an AI tool, mark it with an `// AI: below section was generated by an AI` comment (see C++ guidelines). - When a larger block of code is generated by an AI tool, mark it with an `// AI: below section was generated by an AI` comment (see C++ guidelines).
- Make sure AI-generated code is well documented. - Every non-trivial AI-generated function should have a brief comment describing what it does. Explain parameters when their names alone are not self-explanatory.
- AI-generated code must be well documented; comment-to-code ratio > 15% is expected. Do not rephrase source code, but explain the concepts/logic behind the code.
### Pull Request Expectations ### Pull Request Expectations

View File

@@ -3,6 +3,11 @@ applyTo: "**/*.cpp,**/*.h,**/*.hpp, **/*.ino"
--- ---
# C++ Coding Conventions # C++ Coding Conventions
> **Note for AI review tools**: sections enclosed in
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
> contributor reference material. Do **not** use that content as actionable review
> criteria — treat it as background context only.
See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines that apply to all contributors. See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines that apply to all contributors.
## Formatting ## Formatting
@@ -13,6 +18,7 @@ See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines tha
- Space between keyword and parenthesis: `if (...)`, `for (...)`. No space between function name and parenthesis: `doStuff(a)` - Space between keyword and parenthesis: `if (...)`, `for (...)`. No space between function name and parenthesis: `doStuff(a)`
- No enforced line-length limit; wrap when a line exceeds your editor width - No enforced line-length limit; wrap when a line exceeds your editor width
<!-- HUMAN_ONLY_START -->
## Naming ## Naming
- **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent` - **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent`
@@ -29,18 +35,21 @@ Most headers use `#ifndef` / `#define` guards. Some newer headers add `#pragma o
// ... // ...
#endif // WLED_EXAMPLE_H #endif // WLED_EXAMPLE_H
``` ```
<!-- HUMAN_ONLY_END -->
## Comments ## Comments
- `//` for inline comments, `/* ... */` for block comments. Always put a space after `//` - `//` for inline comments, `/* ... */` for block comments. Always put a space after `//`
- Mark WLED-MM-specific changes with `// WLEDMM` or `// WLEDMM: description`: - Mark WLED-MM-specific changes with `// WLEDMM` or `// WLEDMM: description`:
<!-- HUMAN_ONLY_START -->
```cpp ```cpp
// WLEDMM: increased max bus count for larger installs // WLEDMM: increased max bus count for larger installs
#ifndef WLED_MAX_BUSSES #ifndef WLED_MAX_BUSSES
#define WLED_MAX_BUSSES 20 // WLEDMM default (upstream: 10) #define WLED_MAX_BUSSES 20 // WLEDMM default (upstream: 10)
#endif #endif
``` ```
<!-- HUMAN_ONLY_END -->
- **AI attribution:** When a larger block of code is generated by an AI tool, mark it with an `// AI:` comment so reviewers know to scrutinize it: - **AI attribution:** When a larger block of code is generated by an AI tool, mark it with an `// AI:` comment so reviewers know to scrutinize it:
@@ -54,6 +63,7 @@ void calculateCRC(const uint8_t* data, size_t len) {
Single-line AI-assisted edits do not need the marker — use it when the AI produced a contiguous block that a human did not write line-by-line. Single-line AI-assisted edits do not need the marker — use it when the AI produced a contiguous block that a human did not write line-by-line.
<!-- HUMAN_ONLY_START -->
- **Function & feature comments:** Every non-trivial function should have a brief comment above it describing what it does. Include a note about each parameter when the names alone are not self-explanatory: - **Function & feature comments:** Every non-trivial function should have a brief comment above it describing what it does. Include a note about each parameter when the names alone are not self-explanatory:
```cpp ```cpp
@@ -65,6 +75,8 @@ void calculateCRC(const uint8_t* data, size_t len) {
***** */ ***** */
uint8_t gammaCorrect(uint8_t value, float gamma); uint8_t gammaCorrect(uint8_t value, float gamma);
``` ```
<!-- HUMAN_ONLY_END -->
Short accessor-style functions (getters/setters, one-liners) may skip this if their purpose is obvious from the name. Short accessor-style functions (getters/setters, one-liners) may skip this if their purpose is obvious from the name.
@@ -92,13 +104,18 @@ uint8_t gammaCorrect(uint8_t value, float gamma);
## Memory ## Memory
- **PSRAM-aware allocation**: use `d_malloc()` (prefer DRAM), `p_malloc()` (prefer PSRAM) from `util.h` - **PSRAM-aware allocation**: use `d_malloc()` (prefer DRAM), `p_malloc()` (prefer PSRAM) from `util.h`
- **Avoid Variable Length Arrays (VLAs)**: GCC/Clang support VLAs as an extension (they are not part of the C++ standard), so they look like a legitimate feature — but they are allocated on the stack at runtime. On ESP32/ESP8266, FreeRTOS task stacks are typically only 28 KB; a VLA whose size depends on a runtime parameter (segment dimensions, pixel counts, etc.) can silently exhaust the stack and cause the program to behave in unexpected ways or crash. Prefer a fixed-size array with a compile-time bound, or heap allocation (`d_malloc` / `p_malloc`) for dynamically sized buffers. **Any VLA must be explicitly justified in the source code or PR.** - **Avoid Variable Length Arrays (VLAs)**: FreeRTOS task stacks are typically 28 KB. A runtime-sized VLA can silently exhaust the stack. Use fixed-size arrays or heap allocation (`d_malloc` / `p_malloc`). Any VLA must be explicitly justified in source or PR.
<!-- HUMAN_ONLY_START -->
GCC/Clang support VLAs as an extension (they are not part of the C++ standard), so they look like a legitimate feature — but they are allocated on the stack at runtime. On ESP32/ESP8266, a VLA whose size depends on a runtime parameter (segment dimensions, pixel counts, etc.) can silently exhaust the stack and cause the program to behave in unexpected ways or crash.
<!-- HUMAN_ONLY_END -->
- **Larger buffers** (LED data, JSON documents) should use PSRAM when available and technically feasible - **Larger buffers** (LED data, JSON documents) should use PSRAM when available and technically feasible
- **Hot-path**: some data should stay in DRAM or IRAM for performance reasons - **Hot-path**: some data should stay in DRAM or IRAM for performance reasons
- Memory efficiency matters, but is less critical on boards with PSRAM - Memory efficiency matters, but is less critical on boards with PSRAM
## `const` and `constexpr` ## `const` and `constexpr`
Add `const` to cached locals in hot-path code (helps the compiler keep values in registers). Pass and store objects by `const&` to avoid copies in loops.
<!-- HUMAN_ONLY_START -->
`const` is a promise to the compiler that a value (or object) will not change - a function declared with a `const char* message` parameter is not allowed to modify the content of `message`. `const` is a promise to the compiler that a value (or object) will not change - a function declared with a `const char* message` parameter is not allowed to modify the content of `message`.
This pattern enables optimizations and makes intent clear to reviewers. This pattern enables optimizations and makes intent clear to reviewers.
@@ -110,11 +127,12 @@ Adding `const` to a local variable that is only assigned once is not necessary
const uint_fast16_t cols = virtualWidth(); const uint_fast16_t cols = virtualWidth();
const uint_fast16_t rows = virtualHeight(); const uint_fast16_t rows = virtualHeight();
``` ```
<!-- HUMAN_ONLY_END -->
### `const` references to avoid copies ### `const` references to avoid copies
Pass and store objects by `const &` (or `&`) instead of copying them implicitly. This avoids constructing temporary objects on every access — especially important in loops: Pass and store objects by `const &` (or `&`) instead of copying them implicitly. This avoids constructing temporary objects on every access — especially important in loops.
<!-- HUMAN_ONLY_START -->
```cpp ```cpp
const auto &m = _mappings[i]; // reference, not a copy (bus_manager.cpp) const auto &m = _mappings[i]; // reference, not a copy (bus_manager.cpp)
const CRGB& c = ledBuffer[pix]; // alias — avoids creating a temporary CRGB instance const CRGB& c = ledBuffer[pix]; // alias — avoids creating a temporary CRGB instance
@@ -125,8 +143,10 @@ For function parameters that are read-only, prefer `const &`:
```cpp ```cpp
BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com); BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com);
``` ```
<!-- HUMAN_ONLY_END -->
### `constexpr` over `#define` ### `constexpr` over `#define`
<!-- HUMAN_ONLY_START -->
Prefer `constexpr` for compile-time constants. Unlike `#define`, `constexpr` respects scope and type safety, keeping the global namespace clean: Prefer `constexpr` for compile-time constants. Unlike `#define`, `constexpr` respects scope and type safety, keeping the global namespace clean:
@@ -155,6 +175,10 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#error "WLED_MAX_BUSSES exceeds hard limit" #error "WLED_MAX_BUSSES exceeds hard limit"
#endif #endif
``` ```
<!-- HUMAN_ONLY_END -->
Prefer `constexpr` over `#define` for typed constants (scope-safe, debuggable). Use `static_assert` instead of `#if … #error` for compile-time validation.
Exception: `#define` is required for conditional-compilation guards and build-flag-overridable values.
### `static` and `const` class methods ### `static` and `const` class methods
@@ -166,16 +190,18 @@ Marking a member function `const` tells the compiler that it does not modify the
uint16_t length() const { return _len; } uint16_t length() const { return _len; }
bool isActive() const { return _active; } bool isActive() const { return _active; }
``` ```
<!-- HUMAN_ONLY_START -->
Benefits for GCC/Xtensa/RISC-V: Benefits for GCC/Xtensa/RISC-V:
- The compiler knows the method cannot write to `this`, so it is free to **keep member values in registers** across the call and avoid reload barriers. - The compiler knows the method cannot write to `this`, so it is free to **keep member values in registers** across the call and avoid reload barriers.
- `const` methods can be called on `const` objects and `const` references — essential when passing large objects as `const &` to avoid copying. - `const` methods can be called on `const` objects and `const` references — essential when passing large objects as `const &` to avoid copying.
- `const` allows the compiler to **eliminate redundant loads**: if a caller already has a member value cached, the compiler can prove the `const` call cannot invalidate it. - `const` allows the compiler to **eliminate redundant loads**: if a caller already has a member value cached, the compiler can prove the `const` call cannot invalidate it.
<!-- HUMAN_ONLY_END -->
Declare every getter, query, or inspection method `const`. If you need to mark a member `mutable` to work around this (e.g. for a cache or counter), document the reason. Declare getter, query, or inspection methods `const`. If you need to mark a member `mutable` to work around this (e.g. for a cache or counter), document the reason.
#### `static` member functions #### `static` member functions
<!-- HUMAN_ONLY_START -->
A `static` member function has no implicit `this` pointer. This has two distinct advantages: A `static` member function has no implicit `this` pointer. This has two distinct advantages:
1. **Smaller code, faster calls**: no `this` is passed in a register. On Xtensa and RISC-V, this removes one register argument from every call site and prevents the compiler from emitting `this`-preservation code around inlined blocks. 1. **Smaller code, faster calls**: no `this` is passed in a register. On Xtensa and RISC-V, this removes one register argument from every call site and prevents the compiler from emitting `this`-preservation code around inlined blocks.
@@ -191,8 +217,9 @@ static BusConfig fromJson(JsonObject obj);
static uint8_t gamma8(uint8_t val); static uint8_t gamma8(uint8_t val);
static uint32_t colorBalance(uint32_t color, uint8_t r, uint8_t g, uint8_t b); static uint32_t colorBalance(uint32_t color, uint8_t r, uint8_t g, uint8_t b);
``` ```
<!-- HUMAN_ONLY_END -->
`static` also communicates intent clearly: a reviewer immediately knows the method is stateless and safe to call without a fully constructed object. `static` communicates intent clearly: a reviewer immediately knows the method is stateless and safe to call without a fully constructed object.
> **Rule of thumb**: if a method does not read or write any member variable, make it `static`. If it only reads member variables, make it `const`. Note: `static` methods cannot also be `const`-qualified because there is no implicit `this` pointer to be const — just use `static`. Both qualifiers reduce coupling and improve generated code on all ESP32 targets. > **Rule of thumb**: if a method does not read or write any member variable, make it `static`. If it only reads member variables, make it `const`. Note: `static` methods cannot also be `const`-qualified because there is no implicit `this` pointer to be const — just use `static`. Both qualifiers reduce coupling and improve generated code on all ESP32 targets.
@@ -225,6 +252,7 @@ Example signature:
void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(int i, uint32_t col) void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(int i, uint32_t col)
``` ```
<!-- HUMAN_ONLY_START -->
### Use `uint_fast` Types for Locals ### Use `uint_fast` Types for Locals
Use `uint_fast8_t` and `uint_fast16_t` for loop counters, indices, and temporary calculations in hot paths. These let the compiler pick the CPU's native word size (32-bit on ESP32), avoiding unnecessary narrow-type masking: Use `uint_fast8_t` and `uint_fast16_t` for loop counters, indices, and temporary calculations in hot paths. These let the compiler pick the CPU's native word size (32-bit on ESP32), avoiding unnecessary narrow-type masking:
@@ -249,6 +277,7 @@ for (uint_fast8_t i = 0; i < count; i++) {
... ...
} }
``` ```
<!-- HUMAN_ONLY_END -->
### Unsigned Range Check ### Unsigned Range Check
@@ -270,8 +299,8 @@ if (unsigned(i) >= virtualLength()) return; // bounds check (catches negative i
### Avoid Nested Calls — Fast Path / Complex Path ### Avoid Nested Calls — Fast Path / Complex Path
Avoid calling non-inline functions or making complex decisions inside per-pixel hot-path code. When a function has both a common simple case and a rare complex case, split it into two variants and choose once per frame rather than per pixel: Avoid calling non-inline functions or making complex decisions inside per-pixel hot-path code. When a function has both a common simple case and a rare complex case, split it into two variants and choose once per frame rather than per pixel.
<!-- HUMAN_ONLY_START -->
```cpp ```cpp
// Decision made once per frame in startFrame(), stored in a bool // Decision made once per frame in startFrame(), stored in a bool
bool simpleSegment = _isSuperSimpleSegment; bool simpleSegment = _isSuperSimpleSegment;
@@ -288,16 +317,18 @@ The same principle applies to color utilities — `color_add()` accepts a `fast`
```cpp ```cpp
uint32_t color_add(uint32_t c1, uint32_t c2, bool fast=false); uint32_t color_add(uint32_t c1, uint32_t c2, bool fast=false);
``` ```
<!-- HUMAN_ONLY_END -->
General rules: General rules:
- Keep the per-pixel fast path free of non-inline function calls, multi-way branches and complex switch-case decisions. - Keep fast-path functions free of non-inline calls, multi-way branches, and complex switch-case decisions.
- Hoist the "which path?" decision out of the inner loop (once per frame or per segment) - Hoist per-frame decisions (e.g. simple vs. complex segment) out of the per-pixel loop.
- It is acceptable to duplicate some code between fast and complex variants to keep the fast path lean - Code duplication between fast/slow variants is acceptable to keep the fast path lean.
### Function Pointers to Eliminate Repeated Decisions ### Function Pointers to Eliminate Repeated Decisions
When the same decision (e.g. "which drawing routine?") would be evaluated for every pixel, assign the chosen variant to a function pointer once and let the inner loop call through the pointer. This removes the branch entirely — the calling code (e.g. the GIF decoder loop) only ever invokes one function per frame, with no per-pixel decision. When the same decision (e.g. "which drawing routine?") would be evaluated for every pixel, assign the chosen variant to a function pointer once and let the inner loop call through the pointer. This removes the branch entirely — the calling code (e.g. the GIF decoder loop) only ever invokes one function per frame, with no per-pixel decision.
<!-- HUMAN_ONLY_START -->
`image_loader.cpp` demonstrates the pattern: `calculateScaling()` picks the best drawing callback once per frame based on segment dimensions and GIF size, then passes it to the decoder via `setDrawPixelCallback()`: `image_loader.cpp` demonstrates the pattern: `calculateScaling()` picks the best drawing callback once per frame based on segment dimensions and GIF size, then passes it to the decoder via `setDrawPixelCallback()`:
```cpp ```cpp
@@ -309,7 +340,8 @@ else
``` ```
Each callback is a small, single-purpose function with no internal branching — the decoder's per-pixel loop never re-evaluates which strategy to use. Each callback is a small, single-purpose function with no internal branching — the decoder's per-pixel loop never re-evaluates which strategy to use.
<!-- HUMAN_ONLY_END -->
<!-- HUMAN_ONLY_START -->
### Template Specialization (Advanced) ### Template Specialization (Advanced)
Templates can eliminate runtime decisions by generating separate code paths at compile time. For example, a pixel setter could be templated on color order or channel count so the compiler removes dead branches and produces tight, specialized machine code: Templates can eliminate runtime decisions by generating separate code paths at compile time. For example, a pixel setter could be templated on color order or channel count so the compiler removes dead branches and produces tight, specialized machine code:
@@ -349,10 +381,11 @@ if (!guard) return; // another task is already sending
This avoids FreeRTOS semaphore overhead and the risk of forgetting `esp32SemGive`. There are no current examples of this pattern in the codebase — consult with maintainers before introducing it in new code, to ensure it aligns with the project's synchronization conventions. This avoids FreeRTOS semaphore overhead and the risk of forgetting `esp32SemGive`. There are no current examples of this pattern in the codebase — consult with maintainers before introducing it in new code, to ensure it aligns with the project's synchronization conventions.
<!-- HUMAN_ONLY_END -->
### Pre-Compute Outside Loops ### Pre-Compute Outside Loops
Move invariant calculations before the loop. Pre-compute reciprocals to replace division with multiplication: Move invariant calculations before the loop. Pre-compute reciprocals to replace division with multiplication.
<!-- HUMAN_ONLY_START -->
```cpp ```cpp
const uint_fast16_t cols = virtualWidth(); const uint_fast16_t cols = virtualWidth();
const uint_fast16_t rows = virtualHeight(); const uint_fast16_t rows = virtualHeight();
@@ -360,6 +393,7 @@ uint_fast8_t fadeRate = (255 - rate) >> 1;
float mappedRate_r = 1.0f / (float(fadeRate) + 1.1f); // reciprocal — avoid division inside loop float mappedRate_r = 1.0f / (float(fadeRate) + 1.1f); // reciprocal — avoid division inside loop
``` ```
<!-- HUMAN_ONLY_END -->
### Parallel Channel Processing ### Parallel Channel Processing
Process R+B and W+G channels simultaneously using the two-channel mask pattern: Process R+B and W+G channels simultaneously using the two-channel mask pattern:
@@ -374,11 +408,10 @@ return rb | wg;
### Bit Shifts Over Division (mainly for RISC-V boards) ### Bit Shifts Over Division (mainly for RISC-V boards)
ESP32 and ESP32-S3 (Xtensa core) have a fast "integer divide" instruction, so manual shifts rarely help. ESP32 and ESP32-S3 (Xtensa core) have a fast "integer divide" instruction, so manual shifts rarely help.
The compiler already converts power-of-two unsigned divisions to shifts at `-O2`. On RISC-V targets (ESP32-C3/C6/P4), prefer explicit bit-shifts for power-of-two arithmetic — the compiler does **not** always convert divisions to shifts on RISC-V at `-O2`. Always use unsigned operands; signed right-shift is implementation-defined.
On RISC-V-based boards (ESP32-C3, ESP32-C6, ESP32-C5) explicit shifts can be beneficial:
Prefer bit shifts for power-of-two operations:
<!-- HUMAN_ONLY_START -->
On RISC-V-based boards (ESP32-C3, ESP32-C6, ESP32-C5) explicit shifts can be beneficial.
```cpp ```cpp
position >> 3 // instead of position / 8 position >> 3 // instead of position / 8
(255U - rate) >> 1 // instead of (255 - rate) / 2 (255U - rate) >> 1 // instead of (255 - rate) / 2
@@ -387,6 +420,7 @@ i & 0x0007 // instead of i % 8
**Important**: The bit-shifted expression should be unsigned. On some MCUs, "signed right-shift" is implemented by an "arithmetic shift right" that duplicates the sign bit: ``0b1010 >> 1 = 0b1101``. **Important**: The bit-shifted expression should be unsigned. On some MCUs, "signed right-shift" is implemented by an "arithmetic shift right" that duplicates the sign bit: ``0b1010 >> 1 = 0b1101``.
<!-- HUMAN_ONLY_END -->
### Static Caching for Expensive Computations ### Static Caching for Expensive Computations
Cache results in static locals when the input rarely changes between calls: Cache results in static locals when the input rarely changes between calls:
@@ -406,6 +440,7 @@ if (lastKelvin != kelvin) {
- Use `static inline` for file-local helpers - Use `static inline` for file-local helpers
- On ESP32 with `WLEDMM_FASTPATH`, color utilities are inlined from `colorTools.hpp`; on ESP8266 or `WLEDMM_SAVE_FLASH`, they fall back to `colors.cpp` - On ESP32 with `WLEDMM_FASTPATH`, color utilities are inlined from `colorTools.hpp`; on ESP8266 or `WLEDMM_SAVE_FLASH`, they fall back to `colors.cpp`
<!-- HUMAN_ONLY_START -->
### Colors ### Colors
- Store and pass colors as `uint32_t` (0xWWRRGGBB) - Store and pass colors as `uint32_t` (0xWWRRGGBB)
@@ -416,6 +451,7 @@ if (lastKelvin != kelvin) {
uint16_t r1 = R(color1); // 16-bit intermediate keeps the multiply result in 32 bits, avoiding 64-bit promotion uint16_t r1 = R(color1); // 16-bit intermediate keeps the multiply result in 32 bits, avoiding 64-bit promotion
``` ```
<!-- HUMAN_ONLY_END -->
## Multi-Task Synchronization ## Multi-Task Synchronization
ESP32 runs multiple FreeRTOS tasks concurrently (e.g. network handling, LED output, JSON parsing). Use the WLED-MM mutex macros for synchronization — they expand to FreeRTOS recursive semaphore calls on ESP32 and compile to no-ops on ESP8266: ESP32 runs multiple FreeRTOS tasks concurrently (e.g. network handling, LED output, JSON parsing). Use the WLED-MM mutex macros for synchronization — they expand to FreeRTOS recursive semaphore calls on ESP32 and compile to no-ops on ESP8266:

View File

@@ -7,8 +7,14 @@ WLED-MM runs on the Arduino-ESP32 framework, which wraps ESP-IDF. Understanding
> **Scope**: This file is an optional review guideline. It applies when touching chip-specific code, peripheral drivers, memory allocation, or platform conditionals. > **Scope**: This file is an optional review guideline. It applies when touching chip-specific code, peripheral drivers, memory allocation, or platform conditionals.
> **Note for AI review tools**: sections enclosed in
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
> contributor reference material. Do **not** use that content as actionable review
> criteria — treat it as background context only.
--- ---
<!-- HUMAN_ONLY_START -->
## Identifying the Build Target: `CONFIG_IDF_TARGET_*` ## Identifying the Build Target: `CONFIG_IDF_TARGET_*`
Use `CONFIG_IDF_TARGET_*` macros to gate chip-specific code at compile time. These are set by the build system and are mutually exclusive — exactly one is defined per build. Use `CONFIG_IDF_TARGET_*` macros to gate chip-specific code at compile time. These are set by the build system and are mutually exclusive — exactly one is defined per build.
@@ -23,10 +29,11 @@ Use `CONFIG_IDF_TARGET_*` macros to gate chip-specific code at compile time. The
| `CONFIG_IDF_TARGET_ESP32C6` | ESP32-C6 | RISC-V single-core | Wi-Fi 6, Thread/Zigbee. Future target | | `CONFIG_IDF_TARGET_ESP32C6` | ESP32-C6 | RISC-V single-core | Wi-Fi 6, Thread/Zigbee. Future target |
| `CONFIG_IDF_TARGET_ESP32P4` | ESP32-P4 | RISC-V dual-core | High performance. Future target | | `CONFIG_IDF_TARGET_ESP32P4` | ESP32-P4 | RISC-V dual-core | High performance. Future target |
<!-- HUMAN_ONLY_END -->
### Build-time validation ### Build-time validation
WLED validates at compile time that exactly one target is defined and that it is a supported chip (`wled.cpp` lines 3961). Follow this pattern when adding new chip-specific branches: WLED validates at compile time that exactly one target is defined and that it is a supported chip (`wled.cpp` lines 3961). Follow this pattern when adding new chip-specific branches:
<!-- HUMAN_ONLY_START -->
```cpp ```cpp
#if defined(CONFIG_IDF_TARGET_ESP32) #if defined(CONFIG_IDF_TARGET_ESP32)
// classic ESP32 path // classic ESP32 path
@@ -39,6 +46,7 @@ WLED validates at compile time that exactly one target is defined and that it is
#endif #endif
``` ```
<!-- HUMAN_ONLY_END -->
### Guidelines ### Guidelines
- **Always test on the actual chip** before claiming support. Simulators and cross-compilation can hide peripheral differences. - **Always test on the actual chip** before claiming support. Simulators and cross-compilation can hide peripheral differences.
@@ -52,6 +60,7 @@ WLED validates at compile time that exactly one target is defined and that it is
`SOC_*` macros (from `soc/soc_caps.h`) describe what the current chip supports. They are the correct way to check for peripheral features — they stay accurate when new chips are added, unlike `CONFIG_IDF_TARGET_*` checks. `SOC_*` macros (from `soc/soc_caps.h`) describe what the current chip supports. They are the correct way to check for peripheral features — they stay accurate when new chips are added, unlike `CONFIG_IDF_TARGET_*` checks.
<!-- HUMAN_ONLY_START -->
### Important `SOC_*` macros used in WLED-MM ### Important `SOC_*` macros used in WLED-MM
| Macro | Type | Used in | Purpose | | Macro | Type | Used in | Purpose |
@@ -65,6 +74,11 @@ WLED validates at compile time that exactly one target is defined and that it is
| `SOC_UART_NUM` | `int` | `dmx_input.cpp` | Number of UART peripherals | | `SOC_UART_NUM` | `int` | `dmx_input.cpp` | Number of UART peripherals |
| `SOC_DRAM_LOW` / `SOC_DRAM_HIGH` | `addr` | `util.cpp` | DRAM address boundaries for validation | | `SOC_DRAM_LOW` / `SOC_DRAM_HIGH` | `addr` | `util.cpp` | DRAM address boundaries for validation |
<!-- HUMAN_ONLY_END -->
### Key pitfall
`SOC_ADC_MAX_BITWIDTH` (ADC resolution 12 or 13 bits) was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5.
<!-- HUMAN_ONLY_START -->
### Less commonly used but valuable ### Less commonly used but valuable
| Macro | Purpose | | Macro | Purpose |
@@ -76,6 +90,8 @@ WLED validates at compile time that exactly one target is defined and that it is
| `SOC_SPIRAM_SUPPORTED` | Whether PSRAM interface exists | | `SOC_SPIRAM_SUPPORTED` | Whether PSRAM interface exists |
| `SOC_CPU_CORES_NUM` | Core count (1 or 2) — useful for task pinning decisions | | `SOC_CPU_CORES_NUM` | Core count (1 or 2) — useful for task pinning decisions |
<!-- HUMAN_ONLY_END -->
### Best practices ### Best practices
```cpp ```cpp
@@ -92,6 +108,7 @@ WLED validates at compile time that exactly one target is defined and that it is
#endif #endif
``` ```
<!-- HUMAN_ONLY_START -->
### PSRAM capability macros ### PSRAM capability macros
For PSRAM presence, mode, and DMA access patterns: For PSRAM presence, mode, and DMA access patterns:
@@ -105,6 +122,8 @@ For PSRAM presence, mode, and DMA access patterns:
| `CONFIG_SOC_PSRAM_DMA_CAPABLE` | PSRAM buffers can be used with DMA (ESP32-S3 with octal PSRAM) | | `CONFIG_SOC_PSRAM_DMA_CAPABLE` | PSRAM buffers can be used with DMA (ESP32-S3 with octal PSRAM) |
| `CONFIG_SOC_MEMSPI_FLASH_PSRAM_INDEPENDENT` | SPI flash and PSRAM on separate buses (no speed contention) | | `CONFIG_SOC_MEMSPI_FLASH_PSRAM_INDEPENDENT` | SPI flash and PSRAM on separate buses (no speed contention) |
<!-- HUMAN_ONLY_END -->
#### Detecting octal/hex flash #### Detecting octal/hex flash
On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself runs in Octal-PI mode), the build system sets: On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself runs in Octal-PI mode), the build system sets:
@@ -129,6 +148,7 @@ On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself
## ESP-IDF Version Conditionals ## ESP-IDF Version Conditionals
<!-- HUMAN_ONLY_START -->
### Checking the IDF version ### Checking the IDF version
```cpp ```cpp
@@ -155,6 +175,7 @@ On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself
| **5.1.0** | Matter protocol support; new `esp_flash` API stable | | **5.1.0** | Matter protocol support; new `esp_flash` API stable |
| **5.3+** | arduino-esp32 v3.x compatibility; C6/P4 support | | **5.3+** | arduino-esp32 v3.x compatibility; C6/P4 support |
<!-- HUMAN_ONLY_END -->
### Guidelines ### Guidelines
- When adding a version guard, **always include a comment** explaining *what* changed and *why* the guard is needed. - When adding a version guard, **always include a comment** explaining *what* changed and *why* the guard is needed.
@@ -174,6 +195,7 @@ On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself
The jump from IDF v4.4 (arduino-esp32 v2.x) to IDF v5.x (arduino-esp32 v3.x) is the largest API break in ESP-IDF history. This section documents the critical changes and recommended migration patterns based on the upstream WLED `V5-C6` branch (`https://github.com/wled/WLED/tree/V5-C6`). Note: WLED-MM has not yet migrated to IDF v5 — these patterns prepare for the future migration. The jump from IDF v4.4 (arduino-esp32 v2.x) to IDF v5.x (arduino-esp32 v3.x) is the largest API break in ESP-IDF history. This section documents the critical changes and recommended migration patterns based on the upstream WLED `V5-C6` branch (`https://github.com/wled/WLED/tree/V5-C6`). Note: WLED-MM has not yet migrated to IDF v5 — these patterns prepare for the future migration.
<!-- HUMAN_ONLY_START -->
### Compiler changes ### Compiler changes
IDF v5.x ships a much newer GCC toolchain. Key versions: IDF v5.x ships a much newer GCC toolchain. Key versions:
@@ -245,19 +267,25 @@ These work on both IDF v4.4 and v5.x — prefer them now:
| Narrowing in aggregate init | Warning | Error | Use explicit cast or wider type | | Narrowing in aggregate init | Warning | Error | Use explicit cast or wider type |
| Implicit `this` capture in lambdas | Accepted in `[=]` | Deprecated warning; error in C++20 mode | Use `[=, this]` or `[&]` | | Implicit `this` capture in lambdas | Accepted in `[=]` | Deprecated warning; error in C++20 mode | Use `[=, this]` or `[&]` |
<!-- HUMAN_ONLY_END -->
#### Recommendations #### Recommendations
- **Do not raise the minimum C++ standard yet.** WLED-MM must still build on IDF v4.4 (GCC 8.4, C++17). Use `#if __cplusplus > 201703L` to gate C++20 features. - **Do not raise the minimum C++ standard yet.** WLED-MM must still build on IDF v4.4 (GCC 8.4, C++17). Use `#if __cplusplus > 201703L` to gate C++20 features.
- **Mark intentional fallthrough** with `[[fallthrough]]` — GCC 14 warns on unmarked fallthrough by default.
<!-- HUMAN_ONLY_START -->
- **Prefer `std::optional` over sentinel values** (e.g., `-1` for "no pin") in new code — it works on both compilers. - **Prefer `std::optional` over sentinel values** (e.g., `-1` for "no pin") in new code — it works on both compilers.
- **Use `std::string_view`** for read-only string parameters instead of `const char*` or `const String&` — zero-copy and works on GCC 8+. - **Use `std::string_view`** for read-only string parameters instead of `const char*` or `const String&` — zero-copy and works on GCC 8+.
- **Avoid raw `union` type punning** — prefer `memcpy` (GCC 8) or `std::bit_cast` (GCC 13+) for strict-aliasing safety. - **Avoid raw `union` type punning** — prefer `memcpy` (GCC 8) or `std::bit_cast` (GCC 13+) for strict-aliasing safety.
- **Mark intentional fallthrough** with `[[fallthrough]]` — GCC 14 warns on unmarked fallthrough by default.
<!-- HUMAN_ONLY_END -->
### Deprecated and removed APIs ### Deprecated and removed APIs
#### RMT (Remote Control Transceiver) #### RMT (Remote Control Transceiver)
The legacy `rmt_*` functions are removed in IDF v5. The new API is channel-based: The legacy `rmt_*` functions are removed in IDF v5. Do not introduce new legacy RMT calls.
<!-- HUMAN_ONLY_START -->
The new API is channel-based:
| IDF v4 (legacy) | IDF v5 (new) | Notes | | IDF v4 (legacy) | IDF v5 (new) | Notes |
|---|---|---| |---|---|---|
@@ -266,12 +294,15 @@ The legacy `rmt_*` functions are removed in IDF v5. The new API is channel-based
| `rmt_set_idle_level()` | Configure in channel config | Set at creation time | | `rmt_set_idle_level()` | Configure in channel config | Set at creation time |
| `rmt_item32_t` | `rmt_symbol_word_t` | Different struct layout | | `rmt_item32_t` | `rmt_symbol_word_t` | Different struct layout |
<!-- HUMAN_ONLY_END -->
**WLED impact**: NeoPixelBus LED output and IR receiver both use legacy RMT. The upstream `V5-C6` branch adds `-D WLED_USE_SHARED_RMT` and disables IR until the library is ported. **WLED impact**: NeoPixelBus LED output and IR receiver both use legacy RMT. The upstream `V5-C6` branch adds `-D WLED_USE_SHARED_RMT` and disables IR until the library is ported.
#### I2S (Inter-IC Sound) #### I2S (Inter-IC Sound)
Legacy `i2s_driver_install()` + `i2s_read()` API is deprecated. The new API uses channel handles: Legacy `i2s_driver_install()` + `i2s_read()` API is deprecated. When touching audio source code, wrap legacy I2S init and reading in `#if ESP_IDF_VERSION_MAJOR < 5` / `#else`.
<!-- HUMAN_ONLY_START -->
The new API uses channel handles:
| IDF v4 (legacy) | IDF v5 (new) | Notes | | IDF v4 (legacy) | IDF v5 (new) | Notes |
|---|---|---| |---|---|---|
| `i2s_driver_install()` | `i2s_channel_init_std_mode()` | Separate STD/PDM/TDM modes | | `i2s_driver_install()` | `i2s_channel_init_std_mode()` | Separate STD/PDM/TDM modes |
@@ -299,9 +330,10 @@ Legacy `i2s_driver_install()` + `i2s_read()` API is deprecated. The new API uses
// Legacy i2s_driver_install() path // Legacy i2s_driver_install() path
#endif #endif
``` ```
<!-- HUMAN_ONLY_END -->
**WLED impact**: The audioreactive usermod (`audio_source.h`) heavily uses legacy I2S. Migration requires rewriting the `I2SSource` class for channel-based API. **WLED impact**: The audioreactive usermod (`audio_source.h`) heavily uses legacy I2S. Migration requires rewriting the `I2SSource` class for channel-based API.
<!-- HUMAN_ONLY_START -->
#### ADC (Analog-to-Digital Converter) #### ADC (Analog-to-Digital Converter)
Legacy `adc1_get_raw()` and `esp_adc_cal_*` are deprecated: Legacy `adc1_get_raw()` and `esp_adc_cal_*` are deprecated:
@@ -341,6 +373,7 @@ The upstream `V5-C6` branch explicitly disables features with incompatible libra
-D WLED_USE_SHARED_RMT # Use new shared RMT driver for NeoPixel output -D WLED_USE_SHARED_RMT # Use new shared RMT driver for NeoPixel output
``` ```
<!-- HUMAN_ONLY_END -->
### Migration checklist for new code ### Migration checklist for new code
1. **Never use a removed API without a version guard.** Always provide both old and new paths, or disable the feature on IDF v5. 1. **Never use a removed API without a version guard.** Always provide both old and new paths, or disable the feature on IDF v5.
@@ -354,6 +387,7 @@ The upstream `V5-C6` branch explicitly disables features with incompatible libra
ESP32 has multiple memory regions with different capabilities. Using the right allocator is critical for performance and stability. ESP32 has multiple memory regions with different capabilities. Using the right allocator is critical for performance and stability.
<!-- HUMAN_ONLY_START -->
### Memory regions ### Memory regions
| Region | Flag | Speed | DMA | Size | Use for | | Region | Flag | Speed | DMA | Size | Use for |
@@ -363,6 +397,8 @@ ESP32 has multiple memory regions with different capabilities. Using the right a
| PSRAM (SPIRAM) | `MALLOC_CAP_SPIRAM \| MALLOC_CAP_8BIT` | Slower | Chip-dependent | 216 MB | Large buffers, JSON documents, image data | | PSRAM (SPIRAM) | `MALLOC_CAP_SPIRAM \| MALLOC_CAP_8BIT` | Slower | Chip-dependent | 216 MB | Large buffers, JSON documents, image data |
| RTC RAM | `MALLOC_CAP_RTCRAM` | Moderate | No | 8 KB | Data surviving deep sleep; small persistent buffers | | RTC RAM | `MALLOC_CAP_RTCRAM` | Moderate | No | 8 KB | Data surviving deep sleep; small persistent buffers |
<!-- HUMAN_ONLY_END -->
### WLED-MM allocation wrappers ### WLED-MM allocation wrappers
WLED-MM provides convenience wrappers with automatic fallback. **Always prefer these over raw `heap_caps_*` calls**: WLED-MM provides convenience wrappers with automatic fallback. **Always prefer these over raw `heap_caps_*` calls**:
@@ -385,8 +421,12 @@ WLED-MM provides convenience wrappers with automatic fallback. **Always prefer t
``` ```
- **Fragmentation**: PSRAM allocations fragment less than DRAM because the region is larger. But avoid mixing small and large allocations in PSRAM — small allocations waste the MMU page granularity. - **Fragmentation**: PSRAM allocations fragment less than DRAM because the region is larger. But avoid mixing small and large allocations in PSRAM — small allocations waste the MMU page granularity.
- **Heap validation**: use `d_measureHeap()` and `d_measureContiguousFreeHeap()` to monitor remaining DRAM. Allocations that would drop free DRAM below `MIN_HEAP_SIZE` should go to PSRAM instead. - **Heap validation**: use `d_measureHeap()` and `d_measureContiguousFreeHeap()` to monitor remaining DRAM. Allocations that would drop free DRAM below `MIN_HEAP_SIZE` should go to PSRAM instead.
- **Performance**: PSRAM access is 310× slower than DRAM on ESP32/S2 (quad-SPI bus). On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), the penalty is smaller (~2×) because the 8-line DTR bus runs at up to 80 MHz (120 MHz is possible with CONFIG_SPIRAM_SPEED_120M, which requires enabling experimental ESP-IDF features). On ESP32-P4 with hex PSRAM (`CONFIG_SPIRAM_MODE_HEX`), the 16-line bus runs at 200 MHz, further reducing the gap. Keep hot-path data in DRAM regardless. - **Performance**: Keep hot-path data in DRAM. Prefer PSRAM for capacity-oriented buffers and monitor contiguous DRAM headroom.
<!-- HUMAN_ONLY_START -->
PSRAM access is up to 15× slower than DRAM on ESP32, 310× slower than DRAM on ESP32-S3/-S2 with quad-SPI bus. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), the penalty is smaller (~2×) because the 8-line DTR bus can transfer 8 bits in parallel at 80 MHz (120 MHz is possible with CONFIG_SPIRAM_SPEED_120M, which requires enabling experimental ESP-IDF features). On ESP32-P4 with hex PSRAM (`CONFIG_SPIRAM_MODE_HEX`), the 16-line bus runs at 200 MHz which brings it on-par with DRAM. Keep hot-path data in DRAM regardless, but consider that ESP32 often crashes when the largest DRAM chunk gets below 10 KB.
<!-- HUMAN_ONLY_END -->
<!-- HUMAN_ONLY_START -->
### Pattern: preference-based allocation ### Pattern: preference-based allocation
When you need a buffer that works on boards with or without PSRAM: When you need a buffer that works on boards with or without PSRAM:
@@ -400,6 +440,7 @@ uint8_t* buf = (uint8_t*)heap_caps_malloc_prefer(bufSize, 2,
uint8_t* buf = (uint8_t*)p_malloc(bufSize); uint8_t* buf = (uint8_t*)p_malloc(bufSize);
``` ```
<!-- HUMAN_ONLY_END -->
--- ---
## I2S Audio: Best Practices ## I2S Audio: Best Practices
@@ -499,6 +540,7 @@ The driver dynamically reduces color depth for larger displays to stay within DM
--- ---
<!-- HUMAN_ONLY_START -->
## GPIO Best Practices ## GPIO Best Practices
### Prefer `gpio_config()` over individual calls ### Prefer `gpio_config()` over individual calls
@@ -518,6 +560,7 @@ gpio_config(&io_conf);
gpio_set_direction(pin, GPIO_MODE_OUTPUT); gpio_set_direction(pin, GPIO_MODE_OUTPUT);
gpio_set_pull_mode(pin, GPIO_FLOATING); gpio_set_pull_mode(pin, GPIO_FLOATING);
``` ```
<!-- HUMAN_ONLY_END -->
### Pin manager integration ### Pin manager integration
@@ -543,6 +586,7 @@ For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolutio
int64_t now_us = esp_timer_get_time(); // monotonic, not affected by NTP int64_t now_us = esp_timer_get_time(); // monotonic, not affected by NTP
``` ```
<!-- HUMAN_ONLY_START -->
### Periodic timers ### Periodic timers
For periodic tasks with sub-millisecond precision, use `esp_timer`: For periodic tasks with sub-millisecond precision, use `esp_timer`:
@@ -558,6 +602,7 @@ esp_timer_create_args_t args = {
esp_timer_create(&args, &timer); esp_timer_create(&args, &timer);
esp_timer_start_periodic(timer, 1000); // 1 ms period esp_timer_start_periodic(timer, 1000); // 1 ms period
``` ```
<!-- HUMAN_ONLY_END -->
Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR-level latency — ISR callbacks have severe restrictions (no logging, no heap allocation, no FreeRTOS API calls). Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR-level latency — ISR callbacks have severe restrictions (no logging, no heap allocation, no FreeRTOS API calls).
@@ -565,6 +610,7 @@ Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR
## ADC Best Practices ## ADC Best Practices
<!-- HUMAN_ONLY_START -->
### Version-aware ADC code ### Version-aware ADC code
ADC is one of the most fragmented APIs across IDF versions: ADC is one of the most fragmented APIs across IDF versions:
@@ -585,6 +631,7 @@ ADC is one of the most fragmented APIs across IDF versions:
int raw = adc1_get_raw(ADC1_CHANNEL_0); int raw = adc1_get_raw(ADC1_CHANNEL_0);
#endif #endif
``` ```
<!-- HUMAN_ONLY_END -->
### Bit width portability ### Bit width portability
@@ -611,6 +658,7 @@ WLED-MM's `util.cpp` uses the IDF v4 form (`SOC_ADC_MAX_BITWIDTH`) — this will
--- ---
<!-- HUMAN_ONLY_START -->
## RMT Best Practices ## RMT Best Practices
### Current usage in WLED ### Current usage in WLED
@@ -643,6 +691,7 @@ if (err != ESP_OK) {
return; return;
} }
``` ```
<!-- HUMAN_ONLY_END -->
### Logging ### Logging
@@ -666,6 +715,7 @@ ESP_LOGE(TAG, "Failed to allocate %u bytes", size);
### Task creation and pinning ### Task creation and pinning
<!-- HUMAN_ONLY_START -->
On dual-core chips (ESP32, S3, P4), pin latency-sensitive tasks to a specific core: On dual-core chips (ESP32, S3, P4), pin latency-sensitive tasks to a specific core:
```cpp ```cpp
@@ -679,6 +729,7 @@ xTaskCreatePinnedToCore(
0 // core ID (0 = protocol core, 1 = app core) 0 // core ID (0 = protocol core, 1 = app core)
); );
``` ```
<!-- HUMAN_ONLY_END -->
Guidelines: Guidelines:
- Pin network/protocol tasks to core 0 (where Wi-Fi runs). - Pin network/protocol tasks to core 0 (where Wi-Fi runs).
@@ -699,6 +750,7 @@ Guidelines:
FreeRTOS on ESP32 is **preemptive** — all tasks are scheduled by priority regardless of `yield()` calls. This is fundamentally different from ESP8266 cooperative multitasking. FreeRTOS on ESP32 is **preemptive** — all tasks are scheduled by priority regardless of `yield()` calls. This is fundamentally different from ESP8266 cooperative multitasking.
<!-- HUMAN_ONLY_START -->
| Call | What it does | Reaches IDLE (priority 0)? | | Call | What it does | Reaches IDLE (priority 0)? |
|---|---|---| |---|---|---|
| `delay(ms)` / `vTaskDelay(ticks)` | Suspends calling task; scheduler runs all other ready tasks | ✅ Yes | | `delay(ms)` / `vTaskDelay(ticks)` | Suspends calling task; scheduler runs all other ready tasks | ✅ Yes |
@@ -706,11 +758,14 @@ FreeRTOS on ESP32 is **preemptive** — all tasks are scheduled by priority rega
| `taskYIELD()` | Same as `vTaskDelay(0)` | ❌ No | | `taskYIELD()` | Same as `vTaskDelay(0)` | ❌ No |
| Blocking API (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) | Suspends task until event or timeout; IDLE runs freely | ✅ Yes | | Blocking API (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) | Suspends task until event or timeout; IDLE runs freely | ✅ Yes |
<!-- HUMAN_ONLY_END -->
**`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. **`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.
**`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`. **`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`.
#### Why the IDLE task is not optional #### Why the IDLE task is not optional
<!-- HUMAN_ONLY_START -->
The FreeRTOS IDLE task (one per core on dual-core ESP32 and ESP32-S3; single instance on single-core chips) is not idle in the casual sense — it performs essential system housekeeping: The FreeRTOS IDLE task (one per core on dual-core ESP32 and ESP32-S3; single instance on single-core chips) is not idle in the casual sense — it performs essential system housekeeping:
@@ -719,6 +774,8 @@ The FreeRTOS IDLE task (one per core on dual-core ESP32 and ESP32-S3; single ins
- **Implements tickless idle / light sleep**: on battery-powered devices, IDLE is the entry point for low-power sleep. A permanently starved IDLE task disables light sleep entirely. - **Implements tickless idle / light sleep**: on battery-powered devices, IDLE is the entry point for low-power sleep. A permanently starved IDLE task disables light sleep entirely.
- **Runs registered idle hooks**: ESP-IDF components register callbacks via `esp_register_freertos_idle_hook()` (e.g., Wi-Fi background maintenance, Bluetooth housekeeping). These only fire when IDLE runs. - **Runs registered idle hooks**: ESP-IDF components register callbacks via `esp_register_freertos_idle_hook()` (e.g., Wi-Fi background maintenance, Bluetooth housekeeping). These only fire when IDLE runs.
<!-- HUMAN_ONLY_END -->
In short: **starving IDLE corrupts memory cleanup, breaks background activities, disables low-power sleep, and prevents Wi-Fi/BT maintenance.** The IDLE watchdog panic is a symptom — the real damage happens before the watchdog fires. In short: **starving IDLE corrupts memory cleanup, breaks background activities, disables low-power sleep, and prevents Wi-Fi/BT maintenance.** The IDLE watchdog panic is a symptom — the real damage happens before the watchdog fires.
### Watchdog management ### Watchdog management
@@ -740,6 +797,7 @@ esp_task_wdt_add(NULL); // re-register
> **IDF v5 note**: In IDF v5, `esp_task_wdt_add()` and `esp_task_wdt_delete()` require an explicit `TaskHandle_t`. Use `xTaskGetCurrentTaskHandle()` instead of `NULL`. > **IDF v5 note**: In IDF v5, `esp_task_wdt_add()` and `esp_task_wdt_delete()` require an explicit `TaskHandle_t`. Use `xTaskGetCurrentTaskHandle()` instead of `NULL`.
<!-- HUMAN_ONLY_START -->
--- ---
## Quick Reference: IDF v4 → v5 API Mapping ## Quick Reference: IDF v4 → v5 API Mapping
@@ -754,3 +812,4 @@ esp_task_wdt_add(NULL); // re-register
| GPIO | `driver/gpio.h` | `driver/gpio.h` | `gpio_pad_select_gpio()` removed | | GPIO | `driver/gpio.h` | `driver/gpio.h` | `gpio_pad_select_gpio()` removed |
| Timer | `driver/timer.h` | `driver/gptimer.h` | General-purpose timer handles | | Timer | `driver/timer.h` | `driver/gptimer.h` | General-purpose timer handles |
| PCNT | `driver/pcnt.h` | `driver/pulse_cnt.h` | Handle-based API | | PCNT | `driver/pcnt.h` | `driver/pulse_cnt.h` | Handle-based API |
<!-- HUMAN_ONLY_END -->