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

@@ -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.
> **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_*`
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_ESP32P4` | ESP32-P4 | RISC-V dual-core | High performance. Future target |
<!-- HUMAN_ONLY_END -->
### 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:
<!-- HUMAN_ONLY_START -->
```cpp
#if defined(CONFIG_IDF_TARGET_ESP32)
// classic ESP32 path
@@ -39,6 +46,7 @@ WLED validates at compile time that exactly one target is defined and that it is
#endif
```
<!-- HUMAN_ONLY_END -->
### Guidelines
- **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.
<!-- HUMAN_ONLY_START -->
### Important `SOC_*` macros used in WLED-MM
| 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_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
| 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_CPU_CORES_NUM` | Core count (1 or 2) — useful for task pinning decisions |
<!-- HUMAN_ONLY_END -->
### Best practices
```cpp
@@ -92,6 +108,7 @@ WLED validates at compile time that exactly one target is defined and that it is
#endif
```
<!-- HUMAN_ONLY_START -->
### PSRAM capability macros
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_MEMSPI_FLASH_PSRAM_INDEPENDENT` | SPI flash and PSRAM on separate buses (no speed contention) |
<!-- HUMAN_ONLY_END -->
#### 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:
@@ -129,6 +148,7 @@ On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself
## ESP-IDF Version Conditionals
<!-- HUMAN_ONLY_START -->
### Checking the IDF version
```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.3+** | arduino-esp32 v3.x compatibility; C6/P4 support |
<!-- HUMAN_ONLY_END -->
### Guidelines
- 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.
<!-- HUMAN_ONLY_START -->
### Compiler changes
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 |
| Implicit `this` capture in lambdas | Accepted in `[=]` | Deprecated warning; error in C++20 mode | Use `[=, this]` or `[&]` |
<!-- HUMAN_ONLY_END -->
#### 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.
- **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.
- **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.
- **Mark intentional fallthrough** with `[[fallthrough]]` — GCC 14 warns on unmarked fallthrough by default.
<!-- HUMAN_ONLY_END -->
### Deprecated and removed APIs
#### 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 |
|---|---|---|
@@ -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_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.
#### 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 |
|---|---|---|
| `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
#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.
<!-- HUMAN_ONLY_START -->
#### ADC (Analog-to-Digital Converter)
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
```
<!-- HUMAN_ONLY_END -->
### 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.
@@ -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.
<!-- HUMAN_ONLY_START -->
### Memory regions
| 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 |
| 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 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.
- **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
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);
```
<!-- HUMAN_ONLY_END -->
---
## 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
### Prefer `gpio_config()` over individual calls
@@ -518,6 +560,7 @@ gpio_config(&io_conf);
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
gpio_set_pull_mode(pin, GPIO_FLOATING);
```
<!-- HUMAN_ONLY_END -->
### 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
```
<!-- HUMAN_ONLY_START -->
### Periodic timers
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_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).
@@ -565,6 +610,7 @@ Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR
## ADC Best Practices
<!-- HUMAN_ONLY_START -->
### Version-aware ADC code
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);
#endif
```
<!-- HUMAN_ONLY_END -->
### 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
### Current usage in WLED
@@ -643,6 +691,7 @@ if (err != ESP_OK) {
return;
}
```
<!-- HUMAN_ONLY_END -->
### Logging
@@ -666,6 +715,7 @@ ESP_LOGE(TAG, "Failed to allocate %u bytes", size);
### Task creation and pinning
<!-- HUMAN_ONLY_START -->
On dual-core chips (ESP32, S3, P4), pin latency-sensitive tasks to a specific core:
```cpp
@@ -679,6 +729,7 @@ xTaskCreatePinnedToCore(
0 // core ID (0 = protocol core, 1 = app core)
);
```
<!-- HUMAN_ONLY_END -->
Guidelines:
- 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.
<!-- HUMAN_ONLY_START -->
| Call | What it does | Reaches IDLE (priority 0)? |
|---|---|---|
| `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 |
| 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.
**`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
<!-- 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:
@@ -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.
- **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.
### 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`.
<!-- HUMAN_ONLY_START -->
---
## 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 |
| Timer | `driver/timer.h` | `driver/gptimer.h` | General-purpose timer handles |
| PCNT | `driver/pcnt.h` | `driver/pulse_cnt.h` | Handle-based API |
<!-- HUMAN_ONLY_END -->