ESP-IDF in depth: projects, tasks, logs and partitions

A practical guide to ESP-IDF: project layout, idf.py, menuconfig, FreeRTOS tasks, logs, OTA partitions and when it is worth it over Arduino.

Also available in: EN, ES

Series Introduction to the ESP32 Part 12 View the series →
ESP-IDF on the ESP32

If the Arduino core hides the ESP32, ESP-IDF shows you everything. It is Espressif’s official SDK and the base everything else is built on. It has a steeper curve, yes, but it also gives you the control a product needs.

Let’s cover what matters without drowning in docs.

Project layout

my-project/
├── CMakeLists.txt          # project (almost always the same)
├── sdkconfig               # generated configuration
└── main/
    ├── CMakeLists.txt      # registers main
    └── main.c

The root CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my-project)

And the one in main/:

idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS ".")

The commands you will use

idf.py set-target esp32s3   # pick the chip
idf.py menuconfig           # configure (Kconfig)
idf.py build                # compile
idf.py flash monitor        # flash and open the monitor

menuconfig is one of the best things in ESP-IDF: you change options (Wi-Fi, logs, partitions, PSRAM) without touching code, and they are stored in sdkconfig.

Hello world with logs

#include "esp_log.h"

static const char *TAG = "app";

void app_main(void) {
    ESP_LOGI(TAG, "Starting…");
    ESP_LOGW(TAG, "This is a warning");
    ESP_LOGE(TAG, "And this an error");
}

No printf: use log levels. You can raise or lower the level per component from menuconfig, and turn almost all of them off in production.

Tasks: the heart of ESP-IDF

Here is the big difference from Arduino. Instead of a loop(), you create FreeRTOS tasks with their own priority and stack size:

void sensor_task(void *arg) {
    while (1) {
        read_sensor();
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void) {
    xTaskCreate(sensor_task, "sensor", 4096, NULL, 5, NULL);
}

Real benefits:

  • True parallelism: one task measures, another publishes over MQTT.
  • vTaskDelay does not block the rest of the system.
  • You control stack and priority, which is what prevents weird reboots.

Partitions and OTA

Flash is not a single block: it is partitioned (a partitions.csv table). By default there are two application slots (app0 and app1) so you can do OTA (Wi-Fi updates) with rollback if something fails.

# Name,   Type, SubType, Offset,  Size
nvs,      data, nvs,     0x9000,  0x4000
factory,  app,  factory, 0x10000, 1M

An OTA-style partition table:

# Name,   Type, SubType,  Size
nvs,      data, nvs,      0x4000
otadata,  data, ota,      0x2000
phy_init, data, phy,      0x1000
factory,  app,  factory,  1M
ota_0,    app,  ota_0,    1M
ota_1,    app,  ota_1,    1M

Wiring: Arduino or ESP-IDF?

Arduino coreESP-IDF
CurveLowHigh
ControlMediumTotal
Tasks1 loopN tasks
MemoryAutomaticYou decide
OTAEasyComplete (rollback)
LogsSerialPer-component levels

When it is worth it

  • Yes: products, battery, fine timing, serious OTA, many peripherals.
  • No: if you just want to read a sensor and publish, Arduino gets you there in an afternoon and is more than enough.

Tip: you can mix them. The Arduino core is ESP-IDF underneath, so from a sketch you can call ESP-IDF APIs when you need them.

Summary

  • ESP-IDF is the official SDK: CMake + idf.py + menuconfig.
  • Use FreeRTOS tasks, not a loop().
  • Log levels with ESP_LOGx instead of printf.
  • Flash is partitioned; OTA with rollback comes almost for free.
  • Current version: 6.1.

Next step: MicroPython and Lua for the opposite extreme: zero compiling. Or go back to the series hub.

Related posts

↑↓ navigate ↵ open Esc close