Skip to content

Commit 6c9922d

Browse files
Copilotsofthack007
andauthored
docs(esp-idf): add millis/micros internals, precision-wait pattern, PDM 16-bit note, ESP_ERROR_CHECK_WITHOUT_ABORT (#357)
* Clarified PDM microphone behavior: data unit width is effectively 16-bit in PDM mode * Updated microsecond timing with Arduino-ESP32 note about direct timer usage * Added "Precision waiting" subsection: coarse delay then busy-spin for microsecond accuracy * Expanded error-handling docs with non-aborting check example * Adjusted logging example presentation for human readers --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
1 parent 231373a commit 6c9922d

1 file changed

Lines changed: 49 additions & 4 deletions

File tree

.github/esp-idf.instructions.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,9 @@ The ESP32 has an audio PLL for precise sample rates. Rules:
499499

500500
- Not supported on ESP32-C3 (`SOC_I2S_SUPPORTS_PDM_RX` not defined).
501501
- ESP32-S3 PDM has known issues: sample rate at 50% of expected, very low amplitude.
502+
- **16-bit data width**: Espressif's IDF documentation states that in PDM mode the data unit width is always 16 bits, regardless of the configured `bits_per_sample`.
503+
- See [espressif/esp-idf#8660](https://github.com/espressif/esp-idf/issues/8660) for the upstream issue.
504+
- **Flag `bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT` in PDM mode** — this causes the S3 low-amplitude symptom.
502505
- No clock pin (`I2S_CKPIN = -1`) triggers PDM mode in WLED-MM.
503506

504507
---
@@ -579,13 +582,21 @@ if (!pinManager.allocatePin(myPin, true, PinOwner::UM_MyUsermod)) {
579582

580583
### Microsecond timing
581584

582-
For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolution, 64-bit) over `millis()` or `micros()`:
585+
For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolution, 64-bit) over `millis()` or `micros()`.
586+
<!-- HUMAN_ONLY_START -->
583587

584588
```cpp
585589
#include <esp_timer.h>
586590
int64_t now_us = esp_timer_get_time(); // monotonic, not affected by NTP
587591
```
588592

593+
> **Note**: In arduino-esp32, both `millis()` and `micros()` are thin wrappers around `esp_timer_get_time()` — they share the same monotonic clock source. Prefer the direct call when you need the full 64-bit value or ISR-safe access without truncation:
594+
> ```cpp
595+
> // arduino-esp32 internals (cores/esp32/esp32-hal-misc.c):
596+
> // unsigned long micros() { return (unsigned long)(esp_timer_get_time()); }
597+
> // unsigned long millis() { return (unsigned long)(esp_timer_get_time() / 1000ULL); }
598+
> ```
599+
<!-- HUMAN_ONLY_END -->
589600
<!-- HUMAN_ONLY_START -->
590601
### Periodic timers
591602
@@ -606,6 +617,27 @@ esp_timer_start_periodic(timer, 1000); // 1 ms period
606617
607618
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).
608619
620+
### Precision waiting: coarse delay then spin-poll
621+
622+
When waiting for a precise future deadline (e.g., FPS limiting, protocol timing), avoid spinning the entire duration — that wastes CPU and starves other tasks. Instead, yield to FreeRTOS while time allows, then spin only for the final window.
623+
<!-- HUMAN_ONLY_START -->
624+
```cpp
625+
// Wait until 'target_us' (a micros() / esp_timer_get_time() timestamp)
626+
long time_to_wait = (long)(target_us - micros());
627+
// Coarse phase: yield to FreeRTOS while we have more than ~2 ms remaining.
628+
// vTaskDelay(1) suspends the task for one RTOS tick, letting other task run freely.
629+
while (time_to_wait > 2000) {
630+
vTaskDelay(1);
631+
time_to_wait = (long)(target_us - micros());
632+
}
633+
// Fine phase: busy-poll the last ≤2 ms for microsecond accuracy.
634+
// micros() wraps esp_timer_get_time() so this is low-overhead.
635+
while ((long)(target_us - micros()) > 0) { /* spin */ }
636+
```
637+
<!-- HUMAN_ONLY_END -->
638+
639+
> The threshold (2000 µs as an example) should be at least one RTOS tick (default 1 ms on ESP32) plus some margin. A value of 1500–3000 µs works well in practice.
640+
609641
---
610642
611643
## ADC Best Practices
@@ -672,14 +704,15 @@ RMT drives NeoPixel LED output (via NeoPixelBus) and IR receiver input. Both use
672704
- New chips (C6, P4) have different RMT channel counts — use `SOC_RMT_TX_CANDIDATES_PER_GROUP` to check availability.
673705
- The new RMT API requires an "encoder" object (`rmt_encoder_t`) to translate data formats — this is more flexible but requires more setup code.
674706
707+
<!-- HUMAN_ONLY_END -->
675708
---
676709
677710
## Espressif Best Practices (from official examples)
678711
679712
### Error handling
680713
681-
Always check `esp_err_t` return values. Use `ESP_ERROR_CHECK()` in initialization code, but handle errors gracefully in runtime code:
682-
714+
Always check `esp_err_t` return values. Use `ESP_ERROR_CHECK()` in initialization code, but handle errors gracefully in runtime code.
715+
<!-- HUMAN_ONLY_START -->
683716
```cpp
684717
// Initialization — crash early on failure
685718
ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &config, 0, nullptr));
@@ -693,6 +726,17 @@ if (err != ESP_OK) {
693726
```
694727
<!-- HUMAN_ONLY_END -->
695728
729+
For situations between these two extremes — where you want the `ESP_ERROR_CHECK` formatted log message (file, line, error name) but must not abort — use `ESP_ERROR_CHECK_WITHOUT_ABORT()`.
730+
731+
<!-- HUMAN_ONLY_START -->
732+
```cpp
733+
// Logs in the same format as ESP_ERROR_CHECK, but returns the error code instead of aborting.
734+
// Useful for non-fatal driver calls where you want visibility without crashing.
735+
esp_err_t err = ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_set_clk(AR_I2S_PORT, rate, bits, ch));
736+
if (err != ESP_OK) return; // handle as needed
737+
```
738+
739+
<!-- HUMAN_ONLY_END -->
696740
### Logging
697741
698742
WLED-MM uses its own logging macros — **not** `ESP_LOGx()`. For application-level code, always use the WLED-MM macros defined in `wled.h`:
@@ -706,13 +750,14 @@ All of these wrap `Serial` output through the `DEBUGOUT` / `DEBUGOUTLN` / `DEBUG
706750
707751
**Exception — low-level driver code**: When writing code that interacts directly with ESP-IDF APIs (e.g., I2S initialization, RMT setup), use `ESP_LOGx()` macros instead. They support tag-based filtering and compile-time log level control:
708752
753+
<!-- HUMAN_ONLY_START -->
709754
```cpp
710755
static const char* TAG = "my_module";
711756
ESP_LOGI(TAG, "Initialized with %d buffers", count);
712757
ESP_LOGW(TAG, "PSRAM not available, falling back to DRAM");
713758
ESP_LOGE(TAG, "Failed to allocate %u bytes", size);
714759
```
715-
760+
<!-- HUMAN_ONLY_END -->
716761
### Task creation and pinning
717762
718763
<!-- HUMAN_ONLY_START -->

0 commit comments

Comments
 (0)