C Refresher

ESP-IDF Patterns

This section highlights the practical differences between writing C for a desktop application and writing C for an embedded system like the ESP-IDF.


Normal C vs Embedded C — Key Differences

AspectNormal C (Desktop)Embedded C (ESP-IDF)
Entry pointmain() — runs onceapp_main() — runs forever
Exitreturn 0 / exit()Never exits — esp_restart() to reboot
MemoryAbundant (GBs), OS-managedLimited (~320KB internal + 8MB PSRAM)
Typesint, float, doubleint32_t, uint8_t, float only
I/Oprintf, scanf, fprintfGPIO, I2C, SPI, UART drivers
Error handlingexit(1) — OS cleans upMust handle and continue or reboot
Timingsleep(), usleep() — imprecisevTaskDelay() — precise, cooperative
Concurrencypthread — OS scheduledFreeRTOS tasks — priority scheduled
DebuggingGDB, Valgrind, ASANSerial output, ESP_LOGx, JTAG (rare)
Compilationgcc nativextensa-esp32s3-elf-gcc cross-compiler

Program Structure

c
1// ── Normal C (desktop) ──
2// Program runs once and exits
3int main(int argc, char *argv[]) {
4 printf("Hello!\n");
5 process_data();
6 return 0; // OS cleans up everything
7}
8
9// ── Embedded C (ESP-IDF) ──
10// Program runs forever — there is no OS to return to
11void app_main(void) {
12 init_hardware();
13
14 while (1) { // super loop — or use FreeRTOS tasks
15 read_sensors();
16 process_data();
17 actuate_outputs();
18 vTaskDelay(pdMS_TO_TICKS(100)); // yield CPU — don't busy-wait!
19 }
20 // never reaches here
21}

Error Handling

c
1// ── Normal C (desktop) ──
2// Print error and exit — OS handles cleanup
3FILE *f = fopen("data.txt", "r");
4if (f == NULL) {
5 perror("Failed to open file");
6 exit(1); // just quit
7}
8
9// ── Embedded C (ESP-IDF) ──
10// Must handle error and continue — there is no OS to exit to
11esp_err_t ret = i2c_master_init();
12if (ret != ESP_OK) {
13 ESP_LOGE("MAIN", "I2C init failed: %s", esp_err_to_name(ret));
14 // Options: retry, use fallback, or restart the device
15 esp_restart(); // last resort — reboot the chip
16}

Preprocessor Macros in ESP-IDF

Macros are heavily used in ESP-IDF for configuration, logging, and conditional compilation.

c
1// Object-like macro — constant replacement
2#define LED_GPIO 2
3
4// Conditional compilation (controlled by menuconfig)
5#define CONFIG_ENABLE_WIFI 1
6
7#if CONFIG_ENABLE_WIFI
8 void wifi_init(void) { /* ... */ }
9#endif
10
11// ESP-IDF logging macros (you'll use these constantly)
12#include "esp_log.h"
13#define TAG "MAIN"
14
15ESP_LOGI(TAG, "Temperature: %.1f", 24.5); // Info (Green)
16ESP_LOGW(TAG, "Low battery: %d%%", 15); // Warning (Yellow)
17ESP_LOGE(TAG, "Sensor read failed!"); // Error (Red)

Core Patterns You Will See

1. The Error Checking Pattern

Almost every ESP-IDF API returns an esp_err_t. You must check it!

c
1esp_err_t ret = nvs_flash_init();
2
3if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
4 ESP_ERROR_CHECK(nvs_flash_erase());
5 ret = nvs_flash_init();
6}
7
8// ESP_ERROR_CHECK will immediately crash and reboot the board if ret != ESP_OK
9ESP_ERROR_CHECK(ret);

2. The Configuration Struct Pattern

Instead of passing 10 arguments into a function, ESP-IDF uses "init config" structs extensively.

c
1// 1. Create and fill the configuration struct
2gpio_config_t io_conf = {
3 .pin_bit_mask = (1ULL << 2),
4 .mode = GPIO_MODE_OUTPUT,
5 .pull_up_en = GPIO_PULLUP_DISABLE,
6 .pull_down_en = GPIO_PULLDOWN_DISABLE,
7 .intr_type = GPIO_INTR_DISABLE,
8};
9
10// 2. Pass a pointer to the struct to apply the configuration
11gpio_config(&io_conf);

3. The Handle Pattern

Many ESP-IDF peripherals use opaque handles to keep track of state behind the scenes.

c
1TaskHandle_t my_task;
2
3xTaskCreate(
4 my_task_function,
5 "Task Name",
6 4096,
7 NULL,
8 1,
9 &my_task // Pass the address so FreeRTOS can give you the handle
10);
11
12// Later, you can use that handle to suspend or delete the task
13vTaskDelete(my_task);

If you understand pointers, structs, and these three patterns, you are fully prepared to tackle the ESP-IDF API!

Previous
Multi-File Projects