@polderlabs/bizar 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/audit.mjs +144 -0
- package/cli/banner.mjs +41 -0
- package/cli/bin.mjs +186 -0
- package/cli/copy.mjs +508 -0
- package/cli/export.mjs +87 -0
- package/cli/init.mjs +147 -0
- package/cli/install.mjs +390 -0
- package/cli/plan-templates.mjs +523 -0
- package/cli/plan.mjs +2087 -0
- package/cli/prompts.mjs +163 -0
- package/cli/update.mjs +273 -0
- package/cli/utils.mjs +153 -0
- package/config/AGENTS.md +282 -0
- package/config/agents/baldr.md +148 -0
- package/config/agents/forseti.md +112 -0
- package/config/agents/frigg.md +101 -0
- package/config/agents/heimdall.md +157 -0
- package/config/agents/hermod.md +144 -0
- package/config/agents/mimir.md +115 -0
- package/config/agents/odin.md +309 -0
- package/config/agents/quick.md +78 -0
- package/config/agents/semble-search.md +44 -0
- package/config/agents/thor.md +97 -0
- package/config/agents/tyr.md +96 -0
- package/config/agents/vidarr.md +100 -0
- package/config/agents/vor.md +140 -0
- package/config/commands/audit.md +1 -0
- package/config/commands/explain.md +1 -0
- package/config/commands/init.md +1 -0
- package/config/commands/learn.md +1 -0
- package/config/commands/pr-review.md +1 -0
- package/config/commands/tailscale-serve.md +96 -0
- package/config/hooks/README.md +29 -0
- package/config/hooks/post-tool-use.md +16 -0
- package/config/hooks/pre-tool-use.md +16 -0
- package/config/opencode.json +52 -0
- package/config/opencode.json.template +52 -0
- package/config/rules/general.md +8 -0
- package/config/rules/git.md +11 -0
- package/config/rules/javascript.md +10 -0
- package/config/rules/python.md +10 -0
- package/config/rules/testing.md +10 -0
- package/config/skills/bizar/README.md +9 -0
- package/config/skills/bizar/SKILL.md +187 -0
- package/config/skills/cpp-coding-standards/README.md +28 -0
- package/config/skills/cpp-coding-standards/SKILL.md +634 -0
- package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
- package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
- package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
- package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
- package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
- package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
- package/config/skills/cpp-testing/README.md +28 -0
- package/config/skills/cpp-testing/SKILL.md +304 -0
- package/config/skills/cpp-testing/agents/openai.yaml +4 -0
- package/config/skills/cpp-testing/references/coverage.md +370 -0
- package/config/skills/cpp-testing/references/framework-compare.md +175 -0
- package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
- package/config/skills/cpp-testing/references/mocking.md +364 -0
- package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
- package/config/skills/embedded-esp-idf/README.md +41 -0
- package/config/skills/embedded-esp-idf/SKILL.md +439 -0
- package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
- package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
- package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
- package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
- package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
- package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
- package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
- package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
- package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
- package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
- package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
- package/config/skills/self-improvement/SKILL.md +64 -0
- package/package.json +47 -0
- package/templates/plan/htmx.min.js +1 -0
- package/templates/plan/library/bug-investigation.mdx +79 -0
- package/templates/plan/library/decision-record.mdx +71 -0
- package/templates/plan/library/feature-design.mdx +92 -0
- package/templates/plan/meta.json.template +8 -0
- package/templates/plan/plan.canvas.template +1711 -0
- package/templates/plan/plan.html.template +937 -0
- package/templates/plan/plan.mdx.template +46 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: embedded-esp-idf
|
|
3
|
+
description: Write, review, or debug ESP-IDF v5.x C++ firmware. Triggers on idf.py build/flash/monitor/menuconfig/size, FreeRTOS tasks and ISRs, IRAM/DRAM/PSRAM budgeting, packed binary protocols with static_assert on sizeof, NVS, BLE/NimBLE, ESP-NOW, deep-sleep/light-sleep, Kconfig option design, or host-side C++ unit tests that compile firmware headers without idf.py. AMS7 ambulatory-monitoring firmware conventions (SD-first storage, low-rate aggregate logs, CONFIG_AMS7_* options, ESP-NOW framing discipline) are flagged as project-specific extensions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Embedded ESP-IDF
|
|
7
|
+
|
|
8
|
+
ESP-IDF v5.x C++ firmware patterns: build/flash/monitor workflow, FreeRTOS, IRAM/DRAM memory model, packed binary protocols, drivers, and host-side testing without idf.py. Universal ESP-IDF rules are the default. Anything tagged **(AMS7)** is a project-specific extension for the `/projects/ams7_esp32` ambulatory-monitoring firmware — do not silently generalize it.
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 1. Source the ESP-IDF environment (required once per shell)
|
|
14
|
+
source esp/esp-idf/export.sh
|
|
15
|
+
|
|
16
|
+
# 2. Build / flash / monitor
|
|
17
|
+
idf.py build # compile firmware
|
|
18
|
+
idf.py -p /dev/ttyUSB0 flash # flash (Linux; COMx on Windows)
|
|
19
|
+
idf.py -p /dev/ttyUSB0 monitor # serial monitor (Ctrl-] to exit)
|
|
20
|
+
|
|
21
|
+
# 3. Configure / inspect
|
|
22
|
+
idf.py menuconfig # TUI Kconfig editor
|
|
23
|
+
idf.py size # IRAM/DRAM/flash breakdown (text)
|
|
24
|
+
idf.py size --format json # machine-readable for tooling
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`idf.py` is the single entry point: build, flash, monitor, menuconfig, size, clean, full-clean, set-target, partition-table, and OTA-related commands are all subcommands. See `references/idf-py-commands.md` for the full list and exit codes.
|
|
28
|
+
|
|
29
|
+
A helper that validates your environment is at `scripts/idf_env.sh`:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bash scripts/idf_env.sh # prints the source line and verifies idf.py is reachable
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Project layout (typical ESP-IDF + AMS7)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
project-root/
|
|
39
|
+
├── CMakeLists.txt # top-level: project(<name>), C++ standard, EXTRA_COMPONENT_DIRS
|
|
40
|
+
├── sdkconfig # PRIMARY build configuration (Kconfig output)
|
|
41
|
+
├── sdkconfig.<profile> # SECONDARY overlays (per-flow variants) — AMS7 uses these
|
|
42
|
+
├── partitions.csv # flash partition table
|
|
43
|
+
├── main/ # app component (idf_component_register lives here)
|
|
44
|
+
│ ├── CMakeLists.txt # main component sources and INCLUDE_DIRS
|
|
45
|
+
│ ├── Kconfig # app-level CONFIG_* options
|
|
46
|
+
│ ├── ams7_esp32.cpp # app_main(), task creation, system bring-up
|
|
47
|
+
│ └── ...
|
|
48
|
+
├── components/ # first-party shared components
|
|
49
|
+
├── module/ # submodules vendored into the build (set EXTRA_COMPONENT_DIRS)
|
|
50
|
+
├── managed_components/ # populated by `idf.py add-dependency` (idf-component-manager)
|
|
51
|
+
├── docs/ # design notes, memory budget, runbooks
|
|
52
|
+
└── tools/ # project-specific helpers (size budget, etc.)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`(AMS7)` In the AMS7 project the top-level `CMakeLists.txt` sets:
|
|
56
|
+
|
|
57
|
+
```cmake
|
|
58
|
+
set(EXTRA_COMPONENT_DIRS "./module") # pull ./module/* into the component search path
|
|
59
|
+
set(CMAKE_CXX_STANDARD 20) # C++20 across all components
|
|
60
|
+
set(PROJECT_VER "beta.studio.core.v1")
|
|
61
|
+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
|
62
|
+
project(ams7_esp32)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Build profiles
|
|
66
|
+
|
|
67
|
+
`sdkconfig` is the primary, always-applied configuration. Project overlays under `sdkconfig.<name>` are layered on top for variant builds:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# (AMS7) Build a flow-specific firmware variant in a separate build directory
|
|
71
|
+
idf.py -B build_espnow_flow -C . \
|
|
72
|
+
--sdkconfig sdkconfig.espnow_flow \
|
|
73
|
+
build flash
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`(AMS7)` AMS7 ships `sdkconfig.espnow_flow`, `sdkconfig.espnow_flow_minadv`, `sdkconfig.espnow_flow_studylock`. These toggle `CONFIG_AMS7_*` options (e.g., `CONFIG_AMS7_SUMMARY_ENABLE=y`, `CONFIG_AMS7_ESPNOW_WIFI_CHANNEL=6`) without touching the primary `sdkconfig`. Never hand-edit `sdkconfig` for flow-specific tweaks — add a profile file and merge with `--sdkconfig`.
|
|
77
|
+
|
|
78
|
+
## Kconfig
|
|
79
|
+
|
|
80
|
+
Add options under `main/Kconfig` (or a component's `Kconfig` file). ESP-IDF auto-sources any `Kconfig` file inside a registered component.
|
|
81
|
+
|
|
82
|
+
```kconfig
|
|
83
|
+
config AMS7_SUMMARY_ENABLE
|
|
84
|
+
bool "Enable ESP-NOW summary workflow"
|
|
85
|
+
default n
|
|
86
|
+
help
|
|
87
|
+
Master switch for the recording workflow that starts with BLE control
|
|
88
|
+
and then switches to ESP-NOW summary mode after acquisition begins.
|
|
89
|
+
|
|
90
|
+
config AMS7_BLE_VITALS_ENABLE
|
|
91
|
+
bool "Enable custom BLE vitals characteristic"
|
|
92
|
+
depends on AMS7_VITALS_ENABLE
|
|
93
|
+
default y
|
|
94
|
+
|
|
95
|
+
config AMS7_HR_HOLD_MS
|
|
96
|
+
int "Hold time for fused HR before fallback"
|
|
97
|
+
depends on AMS7_VITALS_ENABLE
|
|
98
|
+
range 0 60000
|
|
99
|
+
default 10000
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Key operators:
|
|
103
|
+
|
|
104
|
+
- `bool` / `int` / `string` / `hex` — value type
|
|
105
|
+
- `default y|n|"value"` — default when not explicitly set
|
|
106
|
+
- `depends on FOO` — option only visible when `FOO` is set
|
|
107
|
+
- `select BAR` — when this option is enabled, force `BAR = y`
|
|
108
|
+
- `imply BAR` — soft suggestion; user can still disable
|
|
109
|
+
- `range MIN MAX` — integer range constraint
|
|
110
|
+
- `choice ... endchoice` — radio group with mutually-exclusive values
|
|
111
|
+
|
|
112
|
+
`(AMS7)` AMS7 namespacing uses the `AMS7_*` prefix on every project-defined symbol. Treat `CONFIG_AMS7_*` as AMS7-specific — do not propose `CONFIG_AMS7_*` symbols for a different ESP-IDF project.
|
|
113
|
+
|
|
114
|
+
See `references/kconfig.md` for menus, sourcing custom Kconfig files, and component-level isolation.
|
|
115
|
+
|
|
116
|
+
## Component CMakeLists.txt
|
|
117
|
+
|
|
118
|
+
Each component has a `CMakeLists.txt` with one call to `idf_component_register`:
|
|
119
|
+
|
|
120
|
+
```cmake
|
|
121
|
+
idf_component_register(
|
|
122
|
+
SRCS
|
|
123
|
+
"ams7fileheader.cpp"
|
|
124
|
+
"ams7fileheader.h"
|
|
125
|
+
INCLUDE_DIRS
|
|
126
|
+
"."
|
|
127
|
+
REQUIRES # public link deps (visible in component.h)
|
|
128
|
+
nvs_flash
|
|
129
|
+
esp_wifi
|
|
130
|
+
PRIV_REQUIRES # private link deps (only used in component .cpp)
|
|
131
|
+
mbedtls
|
|
132
|
+
app_update
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- `SRCS` — sources for this component.
|
|
137
|
+
- `INCLUDE_DIRS` — headers exported to dependents.
|
|
138
|
+
- `REQUIRES` — other components whose public headers are used here (public dependency).
|
|
139
|
+
- `PRIV_REQUIRES` — used only in `.cpp` (private dependency; smaller rebuild blast radius).
|
|
140
|
+
|
|
141
|
+
`(AMS7)` The main component in AMS7 (`main/CMakeLists.txt`) registers ~50 sources and lists ~25 components under `REQUIRES` (drivers, BLE, NVS, OTA, etc.). Keep this list explicit — do not switch to glob patterns.
|
|
142
|
+
|
|
143
|
+
## FreeRTOS patterns
|
|
144
|
+
|
|
145
|
+
ESP-IDF uses FreeRTOS. The universal rules:
|
|
146
|
+
|
|
147
|
+
- **Tasks**: `xTaskCreatePinnedToCore` to pin to core 0 or 1. App/main runs on the PRO CPU (core 1) by default; pin acquisition ISRs and time-critical tasks deliberately.
|
|
148
|
+
- **Priorities**: 0 is idle. 1–4 normal tasks. Real-time acquisition can use 5–24. `(AMS7)` AMS7 acquisition tasks run at priority 20+.
|
|
149
|
+
- **Blocking calls**: `vTaskDelay(pdMS_TO_TICKS(N))`, `xQueueReceive(..., portMAX_DELAY)`.
|
|
150
|
+
- **Queues**: prefer a queue over a shared buffer. Producer/consumer with `xQueueSend` / `xQueueReceive`.
|
|
151
|
+
- **Semaphores / mutexes**: binary/counting semaphores for ISR-to-task signaling; mutexes for shared resources. Prefer `xSemaphoreCreateBinary` and give it from `xQueueSendFromISR`/`xTaskNotifyFromISR`.
|
|
152
|
+
- **Timers**: `xTimerCreate` for periodic callbacks. One-shot timers for deferred work.
|
|
153
|
+
- **Event groups**: `xEventGroupWaitBits` for "wait until several subsystems ready" patterns.
|
|
154
|
+
- **Stream/ring buffers**: `xStreamBufferCreate` for variable-length byte streams; `xRingbufferCreate` for the IDF lockless variant.
|
|
155
|
+
- **ISR rules**: never block in an ISR. Use `xQueueSendFromISR`, `xTaskNotifyFromISR`, or `xTimerPendFunctionCallFromISR` and check the returned `BaseType_t xHigherPriorityTaskWoken` to force a context switch with `portYIELD_FROM_ISR`.
|
|
156
|
+
|
|
157
|
+
`portTICK_PERIOD_MS` is `10` by default (10 ms tick). Always convert via `pdMS_TO_TICKS(ms)` rather than multiplying by hand.
|
|
158
|
+
|
|
159
|
+
See `references/freertos-patterns.md` for task lifecycle, pinning to cores, watchdog setup, and ISR-to-task handoff patterns.
|
|
160
|
+
|
|
161
|
+
## Memory model
|
|
162
|
+
|
|
163
|
+
ESP32 (Xtensa LX6/LX7, or RISC-V on S3/C3) has distinct address spaces:
|
|
164
|
+
|
|
165
|
+
- **IRAM** (instruction RAM, ~128 KB on classic ESP32): code marked with `IRAM_ATTR` lives here. Used for ISR handlers, flash-cache-disabled paths, and time-critical code. **Tight budget.**
|
|
166
|
+
- **DRAM** (data RAM, ~120 KB on classic ESP32): `.data`, `.bss`, heap, stack. Default for all variables and most code.
|
|
167
|
+
- **Flash** (4–16 MB): program text, read-only data (`.rodata`), filesystem. Code lives here by default; XIP cache fetches it.
|
|
168
|
+
- **PSRAM** (when chip has it): slow external RAM via SPI. Use `MALLOC_CAP_SPIRAM` for large buffers you cannot fit in DRAM.
|
|
169
|
+
|
|
170
|
+
Universal guidance:
|
|
171
|
+
|
|
172
|
+
- **Prefer `static` over heap.** DRAM fragmentation on ESP32 is severe; long-running firmware that `malloc`s in a loop will eventually fail.
|
|
173
|
+
- **Mark only what must run from IRAM**: ISR handlers, flash-cache-disabled paths (e.g., SPI drivers during DMA), and a few hot loops. Most code is fine in flash.
|
|
174
|
+
- **`MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT`** for buffers that must not touch PSRAM (DMA-capable memory).
|
|
175
|
+
- **`MALLOC_CAP_SPIRAM`** for big buffers that need not be DMA-capable (logs, JSON caches).
|
|
176
|
+
- **`heap_caps_print_heap_info(MALLOC_CAP_8BIT)`** in a debug build to confirm.
|
|
177
|
+
|
|
178
|
+
`(AMS7)` **IRAM is the tightest budget on AMS7.** Reference baseline (see `docs/firmware/memory_budget.md`):
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
IRAM: 108,726 used, 22,346 free, 131,072 total
|
|
182
|
+
DRAM: 43,152 used, 81,428 free, 124,580 total
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Keep IRAM remaining ≥ 20 KiB; below 24 KiB prints a warning because new `IRAM_ATTR` code can hurt quickly. Do not add `IRAM_ATTR` unless the function is required in ISR/cache-disabled paths. Always re-run the size check after acquisition-path changes.
|
|
186
|
+
|
|
187
|
+
See `references/memory-and-iram.md` for IRAM_ATTR placement rules, PSRAM trade-offs, and the things that blow the IRAM budget (logging, format strings in IRAM, ISR deep paths).
|
|
188
|
+
|
|
189
|
+
## Packed binary protocols
|
|
190
|
+
|
|
191
|
+
Wire-format frames over ESP-NOW, BLE, or any byte-oriented transport must be deterministic in size. AMS7 uses the same shape across all frames — see `main/connectivity/espnow_imu_frame.hpp` for a clean example:
|
|
192
|
+
|
|
193
|
+
```cpp
|
|
194
|
+
#define CORE_ESPNOW_IMU_MAGIC 0x494dU // 'IM'
|
|
195
|
+
#define CORE_ESPNOW_IMU_VERSION 1U
|
|
196
|
+
#define CORE_ESPNOW_FRAME_IMU_RAW 1U
|
|
197
|
+
|
|
198
|
+
typedef struct __attribute__((packed)) {
|
|
199
|
+
uint16_t magic; // first 2 bytes identify the frame family
|
|
200
|
+
uint8_t version; // protocol version of this struct
|
|
201
|
+
uint8_t frame_type; // sub-type within family
|
|
202
|
+
uint16_t id; // sender / subject
|
|
203
|
+
uint16_t seq; // monotonic sequence
|
|
204
|
+
uint16_t age_ms; // capture age in ms (sender-stamped)
|
|
205
|
+
int16_t accel_x, accel_y, accel_z;
|
|
206
|
+
int16_t gyro_x, gyro_y, gyro_z;
|
|
207
|
+
} core_espnow_imu_frame_v1_t;
|
|
208
|
+
|
|
209
|
+
// Compile-time size lock — breaks the build if the struct grows or shrinks.
|
|
210
|
+
static_assert(sizeof(core_espnow_imu_frame_v1_t) == 22,
|
|
211
|
+
"core_espnow_imu_frame_v1_t must be 22 bytes");
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Universal rules:
|
|
215
|
+
|
|
216
|
+
- Always `__attribute__((packed))` and always back it with `static_assert(sizeof(T) == N)` (or `_Static_assert` in C linkage). Catch drift at compile time, not over the air.
|
|
217
|
+
- Fixed-width types only (`uint16_t`, `int16_t`, not `int`).
|
|
218
|
+
- First fields are `magic` (16-bit ASCII-ish) + `version` + `frame_type`. Magic is how receivers reject foreign frames; version is how they migrate.
|
|
219
|
+
- All multi-byte fields are **little-endian on the wire** — ESP32 is LE natively, but make byte order explicit when porting.
|
|
220
|
+
- Wire framing is **length-prefixed**: receiver gets a 2-byte length, then exactly that many bytes. No delimiter-scanning, no escape bytes.
|
|
221
|
+
- For variable payloads, define `frame_header_t` once and reuse — don't reinvent per frame family.
|
|
222
|
+
|
|
223
|
+
`(AMS7)` The naming convention `core_<transport>_<family>_frame_vN_t` and the `static_assert` on `sizeof` are non-negotiable in AMS7. Every frame header under `main/connectivity/` follows this pattern (imu, resp, resp_icg, beat_event, capability, name, protocol_status, gateway_command, marker).
|
|
224
|
+
|
|
225
|
+
See `references/packed-structs.md` for the full pattern, capability bits, and migration across versions.
|
|
226
|
+
|
|
227
|
+
## Logging
|
|
228
|
+
|
|
229
|
+
```cpp
|
|
230
|
+
static const char *TAG = "ams7driver";
|
|
231
|
+
ESP_LOGI(TAG, "Start acquisition stack_hw=%lu", (unsigned long)stack_hw);
|
|
232
|
+
ESP_LOGW(TAG, "Skipping channel-info drain wait for device without ringbuffer");
|
|
233
|
+
ESP_LOGE(TAG, "Failed to start stop finalization task");
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Universal guidance:
|
|
237
|
+
|
|
238
|
+
- Use `ESP_LOGE` for failures, `ESP_LOGW` for recoverable issues, `ESP_LOGI` for state transitions, `ESP_LOGD` for verbose debug (off by default), `ESP_LOGV` for trace.
|
|
239
|
+
- Pick a stable per-module tag — file-scoped `static const char *TAG = "modname";`.
|
|
240
|
+
- Format strings are validated against the args at compile time — `%lu` for `unsigned long`, `%" PRIu32 "` for `uint32_t`, etc.
|
|
241
|
+
- Gating with `CONFIG_*_LOG_ENABLE` switches lets you build a release binary with no debug logs at all (`idf.py menuconfig → Component config → Log output → Default log verbosity`).
|
|
242
|
+
|
|
243
|
+
`(AMS7)` **Logging discipline.** Acquisition builds must not log per-sample or per-frame — at 125–500 Hz that is unprintable and useless. Acceptable log rates:
|
|
244
|
+
|
|
245
|
+
- Per-event: BLE connect, ESP-NOW peer add, acquisition start/stop, file open/close.
|
|
246
|
+
- Periodic aggregate: low-rate counters via a `pl:` console line every N ms (e.g., `ESP_LOGI(TAG, "pl: beats=%lu rr_ms=%lu q=%u", ...)`).
|
|
247
|
+
- Trial-debug toggles (e.g., `CONFIG_AMS7_RESP_TRIAL_DEBUG_LOG_ENABLE=y`) gate extra lines.
|
|
248
|
+
|
|
249
|
+
For full AMS7 logging rules see `references/logging-discipline.md`.
|
|
250
|
+
|
|
251
|
+
## Drivers
|
|
252
|
+
|
|
253
|
+
ESP-IDF v5 unified drivers (used by AMS7 modules):
|
|
254
|
+
|
|
255
|
+
- **I2C**: `i2c_master_bus_config_t` + `i2c_master_bus_handle_t`, then per-device `i2c_device_config_t`. `i2c_master_transmit`, `i2c_master_receive`, `i2c_master_transmit_receive`.
|
|
256
|
+
- **SPI**: `spi_bus_initialize` + `spi_device_interface_config_t` + `spi_device_queue_transfer`. Use `IRAM_ATTR` on `queueTransfer`/`getTransferResult` for DMA-completion paths.
|
|
257
|
+
- **GPIO**: `gpio_config`, `gpio_set_level`, `gpio_install_isr_service` (zero-arg ISR service install once at boot, then `gpio_isr_handler_add`).
|
|
258
|
+
- **ADC**: `esp_adc/adc_oneshot.h` for one-shot reads; `esp_adc/adc_continuous.h` for DMA-driven streams.
|
|
259
|
+
- **RTC**: `esp_sleep_enable_timer_wakeup`, `esp_sleep_enable_ext0_wakeup` (single GPIO), `esp_sleep_enable_ext1_wakeup` (multiple GPIOs), `esp_deep_sleep_start`.
|
|
260
|
+
- **NVS**: see next section.
|
|
261
|
+
- **BLE**: NimBLE (`nimble/`), or the Bluedroid stack. AMS7 uses NimBLE via `components/bt`.
|
|
262
|
+
- **Wi-Fi / ESP-NOW**: `esp_wifi_set_mode(WIFI_MODE_STA)`, `esp_wifi_set_promiscuous`, `esp_wifi_set_channel`, then `esp_now_init`, `esp_now_register_send_cb`, `esp_now_send`.
|
|
263
|
+
|
|
264
|
+
## NVS (preferences)
|
|
265
|
+
|
|
266
|
+
`nvs_flash_init()` once at boot. Per-namespace open/get/set/commit:
|
|
267
|
+
|
|
268
|
+
```cpp
|
|
269
|
+
nvs_handle_t h;
|
|
270
|
+
esp_err_t err = nvs_open("storage", NVS_READWRITE, &h);
|
|
271
|
+
if (err != ESP_OK) { /* handle */ }
|
|
272
|
+
|
|
273
|
+
uint8_t mac[6] = {};
|
|
274
|
+
size_t len = sizeof(mac);
|
|
275
|
+
err = nvs_get_blob(h, "peer_mac", mac, &len);
|
|
276
|
+
|
|
277
|
+
uint32_t counter = 0;
|
|
278
|
+
nvs_set_u32(h, "boot_count", counter + 1);
|
|
279
|
+
nvs_commit(h);
|
|
280
|
+
nvs_close(h);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- Namespaces (`"storage"`, `"wifi"`, `"ams7cfg"`) isolate unrelated settings.
|
|
284
|
+
- Blobs for binary data; `nvs_set_str` / `nvs_get_str` for short strings; `nvs_set_u8/u16/u32/u64/i32` for integers.
|
|
285
|
+
- `nvs_commit` is required after writes; reads do not need commit.
|
|
286
|
+
- Flash wear is real — prefer batched writes. Use a memory cache and flush on shutdown or every N minutes.
|
|
287
|
+
|
|
288
|
+
`(AMS7)` AMS7 stores device-name, hardware-rev, last-acquisition metadata in NVS under the `"ams7cfg"` namespace, and persists peer / study-locks state.
|
|
289
|
+
|
|
290
|
+
For NVS review tasks (most common: catching `nvs_flash_init()` anti-patterns, swallowed errors, missing commits, wrong namespaces), see `references/nvs.md`. NVS issues are the highest-yield review finding for non-driver firmware code, so this reference is worth loading early.
|
|
291
|
+
|
|
292
|
+
## Power management
|
|
293
|
+
|
|
294
|
+
- `esp_sleep_enable_timer_wakeup(seconds)` for timed wakes.
|
|
295
|
+
- `esp_sleep_enable_ext0_wakeup(gpio_num, level)` for one external wake pin.
|
|
296
|
+
- `esp_sleep_enable_ext1_wakeup(bitmask, mode)` for multiple pins (EXT1 only on classic ESP32; on S3 use `gpio_wakeup`).
|
|
297
|
+
- `esp_deep_sleep_start()` — full power-off; only RTC fast memory survives (`RTC_DATA_ATTR` / `RTC_IRAM_ATTR`).
|
|
298
|
+
- `esp_light_sleep_start()` — pause CPU, peripherals resume on wake; lower latency than deep sleep.
|
|
299
|
+
- `esp_sleep_pd_domain_config` to power down unused domains.
|
|
300
|
+
- `RTC_DATA_ATTR static uint32_t boot_count;` survives deep sleep.
|
|
301
|
+
- `esp_wifi_set_ps(WIFI_PS_MIN_MODEM)` for Wi-Fi connected/idle power save.
|
|
302
|
+
|
|
303
|
+
`(AMS7)` AMS7 enters deep sleep after acquisition stop with a wake-on-tap (`esp_sleep_enable_ext0_wakeup` on ICM-42688-P INT1) plus a wake-after-N-seconds safety timer.
|
|
304
|
+
|
|
305
|
+
## Size budget
|
|
306
|
+
|
|
307
|
+
`idf.py size` shows IRAM / DRAM / flash app image. Read it after every change to acquisition-path code.
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
idf.py size # text summary
|
|
311
|
+
idf.py size --format json # for tooling
|
|
312
|
+
idf.py size --output-file size.json
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
What to watch:
|
|
316
|
+
|
|
317
|
+
- **IRAM used**: this is the dangerous one. Adding `IRAM_ATTR` to a large function or pulling logging into ISR paths inflates it.
|
|
318
|
+
- **DRAM used**: static `.bss` and `.data`. Large `static` buffers add here.
|
|
319
|
+
- **Flash app binary**: usually comfortable; check `0xfb650 bytes, 0xf49b0 bytes free in the smallest app partition`.
|
|
320
|
+
|
|
321
|
+
`(AMS7)` AMS7 ships `tools/check_idf_size_budget.py` — a hard gate that runs `idf.py size --format json` and fails if IRAM remaining drops below 20 KiB or DRAM remaining below 40 KiB. Always run it after changes:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
source esp/esp-idf/export.sh
|
|
325
|
+
tools/check_idf_size_budget.py --min-iram-remain 20480 --min-dram-remain 40960
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
A non-project wrapper that delegates to it when present (and falls back to `idf.py size`) is at `scripts/size_check.sh`:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
bash scripts/size_check.sh # uses current dir as project
|
|
332
|
+
bash scripts/size_check.sh --project-dir build_espnow_flow
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Host-side tests
|
|
336
|
+
|
|
337
|
+
Policy modules, payload encoders/decoders, and pure-C++ algorithms can be unit-tested on the host **without `idf.py`**. ESP-IDF firmware that depends on FreeRTOS / drivers stays out of these tests.
|
|
338
|
+
|
|
339
|
+
`(AMS7)` AMS7's host tests are deliberately minimal: a `tests/<area>/<name>_test.cpp` source file plus a `run_<name>_test.sh` shell wrapper that compiles with `g++ -std=c++17`, links the firmware `.cpp` source files directly, and runs. Example:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
#!/usr/bin/env bash
|
|
343
|
+
set -euo pipefail
|
|
344
|
+
g++ -std=c++17 \
|
|
345
|
+
tests/connectivity/espnow_imu_frame_test.cpp \
|
|
346
|
+
main/connectivity/espnow_imu_frame.cpp \
|
|
347
|
+
-I main/connectivity \
|
|
348
|
+
-o /tmp/espnow_imu_frame_test
|
|
349
|
+
/tmp/espnow_imu_frame_test
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Two key distinctions vs the firmware build:
|
|
353
|
+
|
|
354
|
+
- **No `idf.py`**: pure `g++` (or `cmake --build` for larger suites). Fast, runs anywhere.
|
|
355
|
+
- **No FreeRTOS or driver code**: the host testable surface should not include `<freertos/FreeRTOS.h>`, `esp_log.h`, or anything that pulls in `driver/`. If a header you want to test transitively includes those, factor the pure logic out.
|
|
356
|
+
|
|
357
|
+
For larger host-test projects with fakes and a CMake build, see the **`cpp-testing`** skill — it covers GoogleTest, Catch2, host fakes for FreeRTOS, and fixture patterns. See `references/host-tests.md` for the AMS7 host-test layout and pitfalls.
|
|
358
|
+
|
|
359
|
+
## Do / Don't
|
|
360
|
+
|
|
361
|
+
**Do**
|
|
362
|
+
|
|
363
|
+
```cpp
|
|
364
|
+
// Mark an ISR handler and let it defer work to a task
|
|
365
|
+
void IRAM_ATTR gpio_isr(void *arg) {
|
|
366
|
+
BaseType_t hpw = pdFALSE;
|
|
367
|
+
xQueueSendFromISR(gpio_evt_queue, &pin, &hpw);
|
|
368
|
+
portYIELD_FROM_ISR(hpw);
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```cpp
|
|
373
|
+
// Lock the wire size at compile time
|
|
374
|
+
typedef struct __attribute__((packed)) { /* ... */ } my_frame_v1_t;
|
|
375
|
+
static_assert(sizeof(my_frame_v1_t) == N, "wire size must match");
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
```cpp
|
|
379
|
+
// Aggregate, low-rate log instead of per-frame spam
|
|
380
|
+
ESP_LOGI(TAG, "pl: beats=%lu rr_ms=%lu q=%u", beats, rr_ms, quality);
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Don't**
|
|
384
|
+
|
|
385
|
+
```cpp
|
|
386
|
+
// Don't malloc in a hot path
|
|
387
|
+
while (running) {
|
|
388
|
+
auto *p = new uint8_t[256]; // DRAM fragmentation, leaks on errors
|
|
389
|
+
// ...
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Don't block inside an ISR
|
|
393
|
+
void IRAM_ATTR bad_isr(void *a) {
|
|
394
|
+
vTaskDelay(pdMS_TO_TICKS(10)); // illegal — use xTaskNotifyFromISR + task
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Don't add IRAM_ATTR unless required
|
|
398
|
+
void IRAM_ATTR slow_json_formatter(...); // pulls a big function into IRAM
|
|
399
|
+
|
|
400
|
+
// Don't hand-roll endianness
|
|
401
|
+
uint32_t be = (raw[0] << 24) | (raw[1] << 16) | ...; // use explicit <bit> byte-swap helpers
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Resources
|
|
405
|
+
|
|
406
|
+
### Task-to-reference index
|
|
407
|
+
|
|
408
|
+
For most C++ review tasks on non-driver firmware code (NVS, logging, Kconfig, BLE/ESP-NOW data paths), the highest-yield references are:
|
|
409
|
+
|
|
410
|
+
| Task | Load first | Also useful |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| Reviewing NVS / persistence code | `references/nvs.md` | `logging-discipline.md` |
|
|
413
|
+
| Reviewing logging or `ESP_LOG*` calls | `references/logging-discipline.md` | `kconfig.md` |
|
|
414
|
+
| Reviewing Kconfig options | `kconfig.md` | (inline guidance in SKILL.md) |
|
|
415
|
+
| Reviewing packed binary frames / structs | `packed-structs.md` | (inline guidance in SKILL.md) |
|
|
416
|
+
| Reviewing task/queue/ISR code | `freertos-patterns.md` | `memory-and-iram.md` |
|
|
417
|
+
| Reviewing IRAM/disk usage after a build | `memory-and-iram.md` | `scripts/size_check.sh` |
|
|
418
|
+
| Building, flashing, monitoring | `idf-py-commands.md` | `scripts/idf_env.sh` |
|
|
419
|
+
| Writing a host-side C++ test | `host-tests.md` | `$cpp-testing` skill |
|
|
420
|
+
|
|
421
|
+
### references/
|
|
422
|
+
|
|
423
|
+
Load on demand for deeper detail:
|
|
424
|
+
|
|
425
|
+
- `idf-py-commands.md` — full `idf.py` subcommand list, exit codes, monitor filters, partition-table flow.
|
|
426
|
+
- `freertos-patterns.md` — task lifecycle, pinning, priorities, queue + ISR-to-task handoff, ringbuffer for streams, watchdog, ISR rules.
|
|
427
|
+
- `memory-and-iram.md` — IRAM vs DRAM vs flash vs PSRAM, `iram_attr` placement, what blows the IRAM budget.
|
|
428
|
+
- `kconfig.md` — adding options, menus, `depends on` / `select` / `imply` / `range` / `choice`, component-level Kconfig isolation.
|
|
429
|
+
- `nvs.md` — NVS init/handle/error pattern, namespace hygiene, `nvs_commit`, `nvs_flash_init()` anti-patterns, AMS7 `"ams7cfg"` namespace. **High-yield for review tasks.**
|
|
430
|
+
- `packed-structs.md` — `__attribute__((packed))`, byte order, `static_assert`, versioning, capability bits, magic byte, length-prefixed framing. Mirrors AMS7's `main/connectivity/espnow_*_frame.hpp`.
|
|
431
|
+
- `logging-discipline.md` — `ESP_LOG*`, log levels, tagging, no-per-frame logs, `pl:` aggregate lines, gating with `CONFIG_*_LOG_ENABLE`. **High-yield for review tasks.**
|
|
432
|
+
- `host-tests.md` — pure-CMake host tests that include firmware `.cpp` from `main/`, fakes for FreeRTOS, common pitfalls.
|
|
433
|
+
|
|
434
|
+
### scripts/
|
|
435
|
+
|
|
436
|
+
Small, runnable helpers:
|
|
437
|
+
|
|
438
|
+
- `scripts/idf_env.sh` — prints the `source esp/esp-idf/export.sh` line and validates that `idf.py` is reachable. Exits 0 if the env is good, 1 otherwise.
|
|
439
|
+
- `scripts/size_check.sh` — runs AMS7's `tools/check_idf_size_budget.py` if available, else falls back to `idf.py size`. Exits 0 on pass, 1 on size violation.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Embedded ESP-IDF"
|
|
3
|
+
short_description: "ESP-IDF v5.3 firmware C++ patterns and idf.py"
|
|
4
|
+
default_prompt: "Use $embedded-esp-idf to write, review, or debug ESP-IDF C++ firmware, including FreeRTOS tasks, packed structs, IRAM/DRAM budget, and the idf.py build/flash/monitor workflow."
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# FreeRTOS patterns
|
|
2
|
+
|
|
3
|
+
ESP-IDF v5 uses FreeRTOS. Most of these patterns are the same as FreeRTOS elsewhere; the ESP32-specific bits are task pinning, ISR rules (no blocking), and `portTICK_PERIOD_MS`.
|
|
4
|
+
|
|
5
|
+
## Task lifecycle
|
|
6
|
+
|
|
7
|
+
Create, run, delete:
|
|
8
|
+
|
|
9
|
+
```cpp
|
|
10
|
+
void acquisition_task(void *arg) {
|
|
11
|
+
while (true) {
|
|
12
|
+
// ... do work, sleep, wait on a queue ...
|
|
13
|
+
}
|
|
14
|
+
vTaskDelete(nullptr); // never reached in a normal acquisition loop
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
void start() {
|
|
18
|
+
xTaskCreatePinnedToCore(
|
|
19
|
+
acquisition_task, // task function
|
|
20
|
+
"acq", // name (shown in runtime stats)
|
|
21
|
+
4096, // stack depth in WORDS (not bytes) on ESP32
|
|
22
|
+
nullptr, // arg
|
|
23
|
+
20, // priority (0 = idle, higher = more preemptive)
|
|
24
|
+
nullptr, // optional task handle
|
|
25
|
+
1 // core ID (0 = APP CPU on classic ESP32, 1 = PRO CPU)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
|
|
32
|
+
- Stack depth is in **words**, not bytes. On ESP32 (32-bit) `4096` = 16 KB. Allocate generously for tasks with deep call chains; oversized stack wastes DRAM but undersized stack corrupts memory silently.
|
|
33
|
+
- Pass `nullptr` if you do not need the handle. Use the handle later for `vTaskSuspend` / `vTaskResume` / `vTaskDelete`.
|
|
34
|
+
- Pinning is mandatory for acquisition paths — non-pinned tasks can bounce between cores and lose cache locality.
|
|
35
|
+
|
|
36
|
+
`(AMS7)` AMS7 acquisition tasks run at priority 20+, pinned to core 0 (APP CPU). The PRO CPU (core 1) handles BLE/Wi-Fi by default.
|
|
37
|
+
|
|
38
|
+
## Priorities
|
|
39
|
+
|
|
40
|
+
| Range | Use for |
|
|
41
|
+
|--------|------------------------------------------------------------|
|
|
42
|
+
| 0 | Idle task only — do not use |
|
|
43
|
+
| 1–4 | Normal app tasks, command handling, BLE host |
|
|
44
|
+
| 5–19 | Latency-sensitive but not real-time (rate control, queues) |
|
|
45
|
+
| 20–24 | Real-time acquisition, time-critical ISRs |
|
|
46
|
+
|
|
47
|
+
Do not stack many tasks at priority 20+ — round-robin between them defeats the point.
|
|
48
|
+
|
|
49
|
+
## Pinning to cores
|
|
50
|
+
|
|
51
|
+
ESP32 dual-core (Xtensa LX6):
|
|
52
|
+
|
|
53
|
+
- **Core 0 (APP CPU)** — protocol stacks (Wi-Fi, BLE), low-level drivers.
|
|
54
|
+
- **Core 1 (PRO CPU)** — `app_main` runs here by default.
|
|
55
|
+
|
|
56
|
+
Pick a split that puts time-critical work on the under-used core. `xTaskCreatePinnedToCore(handle, name, stack, arg, prio, handle_out, core_id)`.
|
|
57
|
+
|
|
58
|
+
ESP32-S3 / C3 / H2 are single-core: pinning is a no-op but the API still works.
|
|
59
|
+
|
|
60
|
+
## Queues
|
|
61
|
+
|
|
62
|
+
Producer/consumer with bounded queues. Decouples ISR rate from task rate.
|
|
63
|
+
|
|
64
|
+
```cpp
|
|
65
|
+
QueueHandle_t evt_q = xQueueCreate(16, sizeof(uint32_t));
|
|
66
|
+
|
|
67
|
+
// Producer (task context)
|
|
68
|
+
uint32_t pin = read_pin();
|
|
69
|
+
xQueueSend(evt_q, &pin, pdMS_TO_TICKS(10));
|
|
70
|
+
|
|
71
|
+
// Consumer
|
|
72
|
+
uint32_t pin;
|
|
73
|
+
if (xQueueReceive(evt_q, &pin, portMAX_DELAY) == pdTRUE) {
|
|
74
|
+
handle(pin);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use `portMAX_DELAY` only when the consumer must never return. Use `pdMS_TO_TICKS(N)` when the consumer should time out (e.g., to check a "stop" flag).
|
|
79
|
+
|
|
80
|
+
## ISR → task handoff
|
|
81
|
+
|
|
82
|
+
ISRs must not block. The standard handoff uses `xQueueSendFromISR` and the higher-priority-task-woken flag:
|
|
83
|
+
|
|
84
|
+
```cpp
|
|
85
|
+
static QueueHandle_t gpio_evt_q;
|
|
86
|
+
|
|
87
|
+
void IRAM_ATTR gpio_isr(void *arg) {
|
|
88
|
+
uint32_t pin = (uint32_t)arg;
|
|
89
|
+
BaseType_t hpw = pdFALSE;
|
|
90
|
+
xQueueSendFromISR(gpio_evt_q, &pin, &hpw);
|
|
91
|
+
if (hpw == pdTRUE) portYIELD_FROM_ISR();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void consumer_task(void *arg) {
|
|
95
|
+
uint32_t pin;
|
|
96
|
+
while (true) {
|
|
97
|
+
if (xQueueReceive(gpio_evt_q, &pin, portMAX_DELAY) == pdTRUE) {
|
|
98
|
+
// process pin (NOT in ISR context — can block here)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Alternatives:
|
|
105
|
+
|
|
106
|
+
- `xTaskNotifyFromISR` / `xTaskNotifyWait` — single-task notifications, very fast.
|
|
107
|
+
- `xTimerPendFunctionCallFromISR` — defer a callback into the timer service task (useful for one-shot IRAM work).
|
|
108
|
+
- Direct event bits with `xEventGroupSetBitsFromISR` — broadcast to multiple waiters.
|
|
109
|
+
|
|
110
|
+
## Stream and ring buffers
|
|
111
|
+
|
|
112
|
+
For variable-length byte streams (e.g., SPI DMA into a parser):
|
|
113
|
+
|
|
114
|
+
```cpp
|
|
115
|
+
StreamBufferHandle_t sb = xStreamBufferCreate(2048, 256); // total, trigger
|
|
116
|
+
// Producer (DMA ISR)
|
|
117
|
+
xStreamBufferSendFromISR(sb, data, len, &hpw);
|
|
118
|
+
// Consumer task
|
|
119
|
+
uint8_t buf[256];
|
|
120
|
+
size_t got = xStreamBufferReceive(sb, buf, sizeof(buf), pdMS_TO_TICKS(100));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`xRingbufferCreate` (IDF) is the lock-free variant — better for high-rate ISR streams where the producer must never wait.
|
|
124
|
+
|
|
125
|
+
## Semaphores and mutexes
|
|
126
|
+
|
|
127
|
+
```cpp
|
|
128
|
+
SemaphoreHandle_t ready = xSemaphoreCreateBinary();
|
|
129
|
+
xSemaphoreGive(ready);
|
|
130
|
+
xSemaphoreTake(ready, portMAX_DELAY);
|
|
131
|
+
|
|
132
|
+
SemaphoreHandle_t in_flight = xSemaphoreCreateCounting(4, 0); // max 4, starts empty
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Mutexes for shared resources:
|
|
136
|
+
|
|
137
|
+
```cpp
|
|
138
|
+
SemaphoreHandle_t cfg_lock = xSemaphoreCreateMutex();
|
|
139
|
+
if (xSemaphoreTake(cfg_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
140
|
+
// ... access shared resource ...
|
|
141
|
+
xSemaphoreGive(cfg_lock);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Recursive mutexes** (`xSemaphoreCreateRecursiveMutex`) for code that may take the same lock twice in a call chain.
|
|
146
|
+
|
|
147
|
+
## Timers
|
|
148
|
+
|
|
149
|
+
```cpp
|
|
150
|
+
TimerHandle_t t = xTimerCreate("rate", pdMS_TO_TICKS(1000), pdTRUE, nullptr, [](TimerHandle_t x) {
|
|
151
|
+
// fired every 1000 ms; called in the timer service task (NOT ISR context)
|
|
152
|
+
});
|
|
153
|
+
xTimerStart(t, 0);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
One-shot for delayed work:
|
|
157
|
+
|
|
158
|
+
```cpp
|
|
159
|
+
TimerHandle_t oneshot = xTimerCreate("once", pdMS_TO_TICKS(50), pdFALSE, nullptr, cb);
|
|
160
|
+
xTimerStart(oneshot, 0);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Event groups
|
|
164
|
+
|
|
165
|
+
Useful when a task must wait for several subsystems to be ready before proceeding:
|
|
166
|
+
|
|
167
|
+
```cpp
|
|
168
|
+
EventGroupHandle_t eg = xEventGroupCreate();
|
|
169
|
+
xEventGroupSetBits(eg, BIT_SD_READY | BIT_BLE_READY | BIT_ACQ_READY);
|
|
170
|
+
|
|
171
|
+
EventBits_t bits = xEventGroupWaitBits(
|
|
172
|
+
eg,
|
|
173
|
+
BIT_SD_READY | BIT_BLE_READY | BIT_ACQ_READY,
|
|
174
|
+
pdFALSE, // don't clear on exit
|
|
175
|
+
pdTRUE, // wait for ALL bits
|
|
176
|
+
portMAX_DELAY
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Watchdog
|
|
181
|
+
|
|
182
|
+
The IDF task watchdog resets the system if a task does not feed it within a window. Always feed:
|
|
183
|
+
|
|
184
|
+
```cpp
|
|
185
|
+
void heavy_task(void *arg) {
|
|
186
|
+
while (true) {
|
|
187
|
+
// chunk of work
|
|
188
|
+
vTaskDelay(pdMS_TO_TICKS(10)); // yields — watchdog is fed by idle
|
|
189
|
+
// OR if you spin:
|
|
190
|
+
// vTaskDelay(1); // minimum yield
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
If a task really must spin for >1 second (rare), subscribe it to the watchdog and call `esp_task_wdt_reset()` periodically. Long ISR-disabled windows also trip the watchdog — keep them < one tick (`portTICK_PERIOD_MS` = 10 ms by default).
|
|
196
|
+
|
|
197
|
+
## portTICK_PERIOD_MS
|
|
198
|
+
|
|
199
|
+
Always convert via `pdMS_TO_TICKS(ms)`:
|
|
200
|
+
|
|
201
|
+
```cpp
|
|
202
|
+
vTaskDelay(pdMS_TO_TICKS(100)); // 100 ms regardless of tick rate
|
|
203
|
+
xQueueReceive(q, &x, pdMS_TO_TICKS(50));
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Never multiply by hand (`ms * portTICK_PERIOD_MS`) — it lies when the tick rate changes.
|
|
207
|
+
|
|
208
|
+
## Common pitfalls
|
|
209
|
+
|
|
210
|
+
- **Blocking in an ISR.** Symptom: hard fault or watchdog reset. Fix: defer to a task via `xQueueSendFromISR` / `xTaskNotifyFromISR`.
|
|
211
|
+
- **Stack too small.** Symptom: random crashes after a few minutes, often deep in call chains. Fix: bump stack depth, or enable `CONFIG_COMPILER_STACK_CHECK_MODE_NORM` to catch overflows at runtime.
|
|
212
|
+
- **Priority inversion.** Symptom: low-priority task holds a mutex, blocks a high-priority task. Fix: `xSemaphoreCreateRecursiveMutex` and keep critical sections short.
|
|
213
|
+
- **Forgetting `portYIELD_FROM_ISR`.** Symptom: producer wakes the consumer but the consumer doesn't run until the next tick. Fix: check the `xHigherPriorityTaskWoken` returned from `*FromISR` and yield.
|
|
214
|
+
- **Non-pinned tasks bouncing cores.** Symptom: jittery timing, occasional cache misses. Fix: pin acquisition and protocol tasks explicitly.
|