laurigates/mcu-tinkering-lab

embedded-best-practices

Embedded systems development best practices for ESP32, FreeRTOS, and ESP-IDF.

Voir la source
Document Skill original

Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.

Embedded Best Practices Skill

This skill provides comprehensive guidance for embedded systems development with a focus on ESP32 and ESP-IDF.

When to Use

  • Writing new firmware code
  • Reviewing implementation approaches
  • Learning embedded patterns
  • Debugging issues
  • Optimizing code

ESP-IDF Project Structure

Recommended Layout

project/
├── main/
│   ├── CMakeLists.txt
│   ├── main.c
│   ├── Kconfig.projbuild
│   └── include/
│       └── project.h
├── components/
│   └── custom_component/
│       ├── CMakeLists.txt
│       ├── component.c
│       └── include/
│           └── component.h
├── CMakeLists.txt
├── sdkconfig.defaults
├── partitions.csv
└── README.md

Component Organization

  • One responsibility per component
  • Clear public interface in include/
  • Private implementation in src/
  • Document dependencies

FreeRTOS Best Practices

Task Design

c
// Good: Proper task function
void sensor_task(void *pvParameters) {
    sensor_config_t *config = (sensor_config_t *)pvParameters;

    while (1) {
        // Do work
        read_sensor(config);

        // Must yield to prevent watchdog
        vTaskDelay(pdMS_TO_TICKS(100));
    }

    // Tasks should never return, but if they do:
    vTaskDelete(NULL);
}

// Create with appropriate stack
xTaskCreate(sensor_task, "sensor", 4096, &config, 5, &task_handle);

Stack Sizing

  • Start with 4096 bytes for typical tasks
  • Use uxTaskGetStackHighWaterMark() to measure actual usage
  • Add 25% safety margin
  • Camera/network tasks may need 8192+

Synchronization

c
// Mutex for shared resource protection
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();

// Use with timeout, never infinite wait in production
if (xSemaphoreTake(mutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
    // Access shared resource
    xSemaphoreGive(mutex);
} else {
    ESP_LOGE(TAG, "Failed to acquire mutex");
}

Queue Usage

c
// Prefer queues for inter-task communication
QueueHandle_t data_queue = xQueueCreate(10, sizeof(sensor_data_t));

// Send with timeout
sensor_data_t data = {.value = 42};
if (xQueueSend(data_queue, &data, pdMS_TO_TICKS(100)) != pdTRUE) {
    ESP_LOGW(TAG, "Queue full, dropping data");
}

// Receive
sensor_data_t received;
if (xQueueReceive(data_queue, &received, portMAX_DELAY) == pdTRUE) {
    process_data(&received);
}

Memory Management

Static vs Dynamic Allocation

c
// Prefer static for fixed resources
static StaticTask_t task_buffer;
static StackType_t task_stack[4096];
TaskHandle_t task = xTaskCreateStatic(
    task_func, "task", 4096, NULL, 5,
    task_stack, &task_buffer
);

// Dynamic for variable-size resources
char *buffer = heap_caps_malloc(size, MALLOC_CAP_DEFAULT);
if (buffer == NULL) {
    ESP_LOGE(TAG, "Allocation failed");
    return ESP_ERR_NO_MEM;
}
// ... use buffer ...
free(buffer);

String Handling

c
// Bad
char buf[64];
sprintf(buf, "Value: %d", value);

// Good - prevents buffer overflow
char buf[64];
snprintf(buf, sizeof(buf), "Value: %d", value);

// For const strings, keep in flash
static const char *TAG = "mymodule";
ESP_LOGI(TAG, "Starting");

Error Handling

ESP-IDF Error Pattern

c
esp_err_t initialize_peripheral(void) {
    esp_err_t ret;

    ret = gpio_config(&io_conf);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret));
        return ret;
    }

    ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, DMA_CHAN);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "SPI init failed: %s", esp_err_to_name(ret));
        // Clean up GPIO if needed
        return ret;
    }

    return ESP_OK;
}

// Use ESP_ERROR_CHECK for fatal errors only
ESP_ERROR_CHECK(nvs_flash_init());

Graceful Degradation

c
// Don't crash on non-fatal errors
if (wifi_connect() != ESP_OK) {
    ESP_LOGW(TAG, "WiFi failed, running in offline mode");
    run_offline_mode();
}

Peripheral Initialization

GPIO Configuration

c
gpio_config_t io_conf = {
    .pin_bit_mask = (1ULL << GPIO_NUM_2),
    .mode = GPIO_MODE_OUTPUT,
    .pull_up_en = GPIO_PULLUP_DISABLE,
    .pull_down_en = GPIO_PULLDOWN_DISABLE,
    .intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));

I2C Setup

c
i2c_config_t conf = {
    .mode = I2C_MODE_MASTER,
    .sda_io_num = GPIO_NUM_21,
    .scl_io_num = GPIO_NUM_22,
    .sda_pullup_en = GPIO_PULLUP_ENABLE,
    .scl_pullup_en = GPIO_PULLUP_ENABLE,
    .master.clk_speed = 400000,
};
ESP_ERROR_CHECK(i2c_param_config(I2C_NUM_0, &conf));
ESP_ERROR_CHECK(i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0));

Interrupt Handlers

Keep ISRs Minimal

c
// ISR - keep it SHORT
static void IRAM_ATTR gpio_isr_handler(void *arg) {
    uint32_t gpio_num = (uint32_t)arg;
    // Just signal, don't process
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

// Process in task
void gpio_task(void *arg) {
    uint32_t io_num;
    while (1) {
        if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
            // Heavy processing here, not in ISR
            process_gpio_event(io_num);
        }
    }
}

IRAM Considerations

  • Mark ISR handlers with IRAM_ATTR
  • Functions called from ISR also need IRAM_ATTR
  • Minimize IRAM usage (limited to ~128KB)

WiFi Best Practices

Connection Handling

c
// Use event loop for WiFi events
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data) {
    if (event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGI(TAG, "Disconnected, retrying...");
        esp_wifi_connect();
    }
}

// Register handler
ESP_ERROR_CHECK(esp_event_handler_instance_register(
    WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));

PSRAM and WiFi

  • WiFi uses significant memory
  • Enable PSRAM for memory-intensive applications
  • Use CONFIG_SPIRAM_USE_MALLOC to extend heap

Logging

Log Levels

c
ESP_LOGE(TAG, "Error: critical failure");      // Always shown
ESP_LOGW(TAG, "Warning: unusual condition");   // Important
ESP_LOGI(TAG, "Info: normal operation");       // Default
ESP_LOGD(TAG, "Debug: detailed info");         // Development
ESP_LOGV(TAG, "Verbose: very detailed");       // Tracing

Production Logging

  • Set log level via menuconfig
  • Reduce logging in production (ESP_LOGW minimum)
  • Log strings consume flash space

Power Management

Light Sleep

c
// Enable automatic light sleep
esp_pm_config_esp32_t pm_config = {
    .max_freq_mhz = 240,
    .min_freq_mhz = 80,
    .light_sleep_enable = true,
};
ESP_ERROR_CHECK(esp_pm_configure(&pm_config));

Deep Sleep

c
// Configure wakeup source
esp_sleep_enable_timer_wakeup(60 * 1000000);  // 60 seconds

// Enter deep sleep
esp_deep_sleep_start();

Additional Resources

For more detailed information on specific topics, consult:

  • ESP-IDF Programming Guide
  • FreeRTOS documentation
  • ESP32 Technical Reference Manual