| name | esp32-development |
| description | ESP32 memory management, ISRs, FreeRTOS tasks, SPI, power management, ESPHome components. Use when writing ESP32 code, handling interrupts, managing memory, configuring SPI, or implementing ESPHome components. |
ESP32 Development Best Practices
Use this skill when writing code for ESP32 microcontrollers, whether using ESP-IDF, Arduino framework, or ESPHome components.
Sources:
Memory Architecture
Memory Types
| Type | Size | Use For | Access |
|---|
| DRAM | 320 KB | Variables, heap, stack | Read/write, byte-aligned |
| IRAM | 128 KB | ISRs, timing-critical code | Execute, 4-byte aligned |
| IROM | Flash | Application code (via cache) | Execute only |
| DROM | Flash | Constants, strings (via cache) | Read only |
| RTC | 8 KB | Deep-sleep persistent data | Read/write |
Memory Placement Attributes
void IRAM_ATTR my_isr_handler() {
}
RTC_DATA_ATTR int boot_count = 0;
DMA_ATTR uint8_t dma_buffer[256];
WORD_ALIGNED_ATTR uint8_t aligned_buf[64];
Stack Considerations
void process() {
uint8_t buffer[8192];
}
void process() {
auto buffer = std::make_unique<std::array<uint8_t, 8192>>();
}
void process() {
static uint8_t buffer[8192];
}
Interrupt Handling (ISR)
ISR Rules
- Keep ISRs short — Set flags, defer work to tasks
- Use IRAM_ATTR — ISR code must be in IRAM
- No blocking calls — No
delay(), mutexes, or I/O
- Use
std::atomic for shared variables (ESP32 supports it)
ISR Flag Pattern (Recommended)
Use std::atomic<bool> with .exchange() to avoid the read-clear race condition:
#include <atomic>
std::atomic<bool> data_ready{false};
void IRAM_ATTR gpio_isr_handler(void* arg) {
data_ready.store(true, std::memory_order_release);
}
void loop() {
if (data_ready.exchange(false, std::memory_order_acquire)) {
process_data();
}
}
Multi-byte Atomics
std::atomic<uint32_t> counter{0};
void IRAM_ATTR isr() {
counter.fetch_add(1, std::memory_order_relaxed);
}
void loop() {
uint32_t val = counter.load(std::memory_order_acquire);
}
Note: std::atomic is lock-free on ESP32 for 8/16/32-bit types. Avoid on ESP8266/RP2040 (linker errors).
FreeRTOS Task Management
Task Creation
TaskHandle_t task_handle;
xTaskCreate(
task_function,
"TaskName",
4096,
nullptr,
5,
&task_handle
);
xTaskCreatePinnedToCore(
task_function, "TaskName", 4096, nullptr, 5, &task_handle,
1
);
Task Communication
QueueHandle_t event_queue;
event_queue = xQueueCreate(10, sizeof(Event));
void IRAM_ATTR isr() {
Event e = {.type = EVENT_DATA};
BaseType_t woken = pdFALSE;
xQueueSendFromISR(event_queue, &e, &woken);
if (woken) portYIELD_FROM_ISR();
}
void task(void* param) {
Event e;
while (true) {
if (xQueueReceive(event_queue, &e, portMAX_DELAY)) {
handle_event(e);
}
}
}
Synchronization
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
void access_shared() {
if (xSemaphoreTake(mutex, pdMS_TO_TICKS(100))) {
xSemaphoreGive(mutex);
}
}
SemaphoreHandle_t signal = xSemaphoreCreateBinary();
void IRAM_ATTR isr() {
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(signal, &woken);
if (woken) portYIELD_FROM_ISR();
}
GPIO Best Practices
Configuration
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << GPIO_NUM_4),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
pinMode(4, OUTPUT);
pinMode(5, INPUT_PULLUP);
Strapping Pins (Avoid or Use Carefully)
| GPIO | Function | Safe to Use? |
|---|
| GPIO0 | Boot mode | Avoid (needs HIGH at boot) |
| GPIO2 | Boot mode | Use with care |
| GPIO5 | SDIO timing | Use with care |
| GPIO12 | Flash voltage | Avoid |
| GPIO15 | SDIO timing | Use with care |
SPI Communication
Best Practices
spi_device_interface_config_t devcfg = {
.mode = 0,
.clock_speed_hz = 1000000,
.spics_io_num = CS_PIN,
.queue_size = 7,
};
class SpiTransaction {
public:
explicit SpiTransaction(int cs_pin) : cs_(cs_pin) {
gpio_set_level((gpio_num_t)cs_, 0);
}
~SpiTransaction() {
gpio_set_level((gpio_num_t)cs_, 1);
}
private:
int cs_;
};
spi_bus_config_t buscfg = {
.mosi_io_num = MOSI_PIN,
.miso_io_num = MISO_PIN,
.sclk_io_num = SCLK_PIN,
.max_transfer_sz = 4096,
};
Timing
void write_register(uint8_t addr, uint8_t data) {
{
SpiTransaction txn(cs_pin_);
spi_->transfer(addr);
spi_->transfer(data);
}
delayMicroseconds(15);
}
Power Management
Sleep Modes
| Mode | Wake Sources | Current | Use Case |
|---|
| Modem sleep | WiFi beacon | ~20 mA | Connected idle |
| Light sleep | Timer, GPIO, UART | ~0.8 mA | Short idle periods |
| Deep sleep | Timer, GPIO, ULP | ~10 µA | Long idle periods |
Deep Sleep Pattern
#include "esp_sleep.h"
RTC_DATA_ATTR int boot_count = 0;
void setup() {
boot_count++;
esp_sleep_enable_timer_wakeup(60 * 1000000);
esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 0);
esp_deep_sleep_start();
}
WiFi Best Practices
Connection Management
WiFi.onEvent([](WiFiEvent_t event) {
switch (event) {
case WIFI_EVENT_STA_CONNECTED:
ESP_LOGI(TAG, "Connected to AP");
break;
case WIFI_EVENT_STA_DISCONNECTED:
ESP_LOGW(TAG, "Disconnected, reconnecting...");
WiFi.reconnect();
break;
case IP_EVENT_STA_GOT_IP:
ESP_LOGI(TAG, "Got IP: %s", WiFi.localIP().toString().c_str());
break;
}
});
WiFi.begin(ssid, password);
Memory with WiFi
size_t free_heap = esp_get_free_heap_size();
if (free_heap < 20000) {
ESP_LOGW(TAG, "Low memory: %d bytes", free_heap);
}
Logging Best Practices
ESP-IDF Logging
#include "esp_log.h"
static const char* TAG = "my_component";
ESP_LOGE(TAG, "Error: %s", error_msg);
ESP_LOGW(TAG, "Warning: value=%d", val);
ESP_LOGI(TAG, "Info: started");
ESP_LOGD(TAG, "Debug: state=%d", state);
ESP_LOGV(TAG, "Verbose: raw=%02x", byte);
#if CONFIG_LOG_DEFAULT_LEVEL >= ESP_LOG_DEBUG
dump_buffer(data, len);
#endif
ESPHome Logging
ESP_LOGE("tag", "Error message");
ESP_LOGW("tag", "Warning message");
ESP_LOGI("tag", "Info message");
ESP_LOGD("tag", "Debug message");
ESP_LOGV("tag", "Verbose message");
ESP_LOGVV("tag", "Very verbose message");
Timing and Delays
Non-Blocking Patterns
void loop() {
do_work();
delay(1000);
}
uint32_t last_run = 0;
const uint32_t INTERVAL = 1000;
void loop() {
uint32_t now = millis();
if (now - last_run >= INTERVAL) {
last_run = now;
do_work();
}
}
Microsecond Timing
#include "esp_timer.h"
int64_t start = esp_timer_get_time();
int64_t elapsed_us = esp_timer_get_time() - start;
Safe Delays
delayMicroseconds(100);
ets_delay_us(100);
delay(10);
vTaskDelay(pdMS_TO_TICKS(10));
Persistent Storage
ESPHome Preferences (Recommended)
Always prefer ESPHome's preference system over raw NVS. It handles caching, batched writes, and wear leveling automatically.
Docs: developers.esphome.io/blog/2026/02/12/entity-preferences
#include "esphome/core/preferences.h"
#include "esphome/core/helpers.h"
struct MyConfig {
uint32_t address{0};
uint8_t channel{0};
char name[24]{};
bool is_valid() const { return address != 0; }
};
this->pref_ = this->make_entity_preference<MyConfig>(VERSION);
uint32_t hash = fnv1_hash("my_component_slot") + slot_index;
auto pref = global_preferences->make_preference<MyConfig>(hash);
MyConfig cfg{};
pref.save(&cfg);
pref.load(&cfg);
MyConfig empty{};
pref.save(&empty);
Key rules:
global_preferences is available after setup_priority::HARDWARE
- Hashes must be unique across all preferences in the firmware
- Use
fnv1_hash("descriptive_string") + index for stable, readable hashes
- The
version parameter in make_entity_preference<T>(version) is XOR'd into the hash — bumping it silently invalidates old data
- Writes are cached in RAM and flushed periodically — no flash wear concern for infrequent config changes
Raw NVS (Low-Level)
Only use raw NVS when ESPHome preferences don't fit (e.g., variable-length data, custom namespaces):
#include "nvs_flash.h"
#include "nvs.h"
nvs_handle_t handle;
nvs_open("storage", NVS_READWRITE, &handle);
nvs_set_i32(handle, "counter", 42);
nvs_commit(handle);
nvs_close(handle);
Wear Leveling
nvs_set_i32(handle, "counter", counter++);
if (counter - last_saved > 100) {
nvs_set_i32(handle, "counter", counter);
last_saved = counter;
}
ESPHome Component Guidelines
Component Lifecycle
class MyComponent : public Component {
public:
void setup() override {
}
void loop() override {
}
void dump_config() override {
ESP_LOGCONFIG(TAG, "MyComponent:");
ESP_LOGCONFIG(TAG, " Pin: %d", pin_);
}
float get_setup_priority() const override {
return setup_priority::DATA;
}
};
Setup Priorities
| Priority | Value | Use For |
|---|
BUS | 1000 | SPI, I2C buses |
IO | 900 | GPIO expanders |
HARDWARE | 800 | Sensors, displays |
DATA | 600 | Data processing |
PROCESSOR | 400 | After all hardware |
WIFI | 250 | Network-dependent |
AFTER_WIFI | 200 | Services requiring network |
Common Pitfalls
1. Watchdog Timeout
while (waiting) {
}
while (waiting) {
yield();
if (timeout_expired()) break;
}
2. Stack Overflow
void parse(Node* n) {
if (n->child) parse(n->child);
}
void parse(Node* n) {
while (n) {
process(n);
n = n->child;
}
}
3. Heap Fragmentation
for (int i = 0; i < 1000; i++) {
char* buf = (char*)malloc(10);
free(buf);
}
char buf[10];
for (int i = 0; i < 1000; i++) {
}
4. Flash Cache Miss in ISR
void IRAM_ATTR isr() {
Serial.println("ISR");
}
volatile bool flag = false;
void IRAM_ATTR isr() {
flag = true;
}
Debugging Tips
- Monitor heap:
ESP.getFreeHeap(), heap_caps_get_free_size()
- Monitor stack:
uxTaskGetStackHighWaterMark()
- Use assertions:
configASSERT(), ESP_ERROR_CHECK()
- Core dumps: Enable in menuconfig for crash analysis
- JTAG debugging: For step-through debugging