FreeRTOS task scheduler — concurrent sensor, BLE, OTA, and watchdog tasks on a single ESP32-S3
FreeRTOS is the de-facto RTOS for IoT. ESP-IDF ships FreeRTOS built-in; STM32CubeMX generates FreeRTOS integration. Mastering it separates firmware that survives 3 years in production from firmware that only works in the lab.
void sensor_task(void *p) {
TickType_t last = xTaskGetTickCount();
while (1) {
sensor_data_t d = {.temp=read_temp(), .hum=read_hum()};
xQueueSend(sensor_q, &d, portMAX_DELAY);
vTaskDelayUntil(&last, pdMS_TO_TICKS(5000));
}
}
void ble_task(void *p) {
sensor_data_t d;
while (1) {
xQueueReceive(sensor_q, &d, portMAX_DELAY);
ble_transmit(&d);
xSemaphoreGive(watchdog_sem);
}
}
#define configCHECK_FOR_STACK_OVERFLOW 2
void vApplicationStackOverflowHook(TaskHandle_t t, char *name) {
ESP_LOGE(TAG, "STACK OVERFLOW: %s", name);
esp_restart();
}
uxTaskGetStackHighWaterMark() during testing to measure actual peak stack per task. Add 20% headroom. Never guess — measure under real-world workloads including worst-case BLE callbacks.Beyond basic queues, FreeRTOS provides event groups, stream buffers, and message buffers. Event groups are ideal for coordinating multiple tasks around a set of conditions — for example, an OTA task that waits until both a WiFi connection flag and a time synchronisation flag are set before downloading a firmware update. A single xEventGroupWaitBits() call replaces a complex polling loop.
Stream buffers are optimised for byte-stream data from interrupts — a UART receive ISR writing to a stream buffer that a parsing task reads from. The ISR never blocks; the parser blocks until data is available. This pattern handles variable-length sensor data packets efficiently without dynamic memory allocation.
A hardware watchdog that is not properly integrated with FreeRTOS is worse than useless — it will trigger randomly when a legitimate task takes longer than expected, causing unnecessary device restarts. ESP-IDF’s task watchdog (TWDT) monitors individual tasks, not the entire system. Configure it to monitor your highest-priority tasks and feed it in each task’s main loop.
esp_task_wdt_init(30, true); // 30s timeout, panic on trigger esp_task_wdt_add(sensor_task_h); // monitor sensor task esp_task_wdt_add(ble_task_h); // monitor BLE task // In each task's main loop: esp_task_wdt_reset(); // feed watchdog
Dynamic memory allocation (pvPortMalloc) in FreeRTOS uses a heap. ESP-IDF provides multiple heap implementations — heap_4 is the default, with first-fit allocation and coalescing of free blocks. For production firmware, avoid dynamic allocation in ISRs entirely, and minimise allocation after system initialisation. Fragmentation over days of continuous operation can cause allocation failures that are nearly impossible to reproduce in testing.
Use static allocation for queues, semaphores, and task stacks where predictability matters. FreeRTOS supports fully static allocation (no heap at all) via configSUPPORT_STATIC_ALLOCATION — useful for safety-critical applications where memory exhaustion must be provably impossible.
FreeRTOS APIs are identical on ESP32 (via ESP-IDF) and STM32 (via STM32CubeMX). Task creation, queue operations, and synchronisation primitives all have the same function signatures. If you design your application layer against FreeRTOS APIs and abstract hardware access behind a HAL, porting between MCU families is a matter of days rather than weeks. At FSS we maintain shared firmware components — MQTT client, OTA manager, sensor drivers — that run unchanged on both platforms.
FSS is a full-stack IoT engineering team — hardware, firmware, cloud, and mobile in one place.
FSS Technology designs and builds IoT products from silicon to cloud — embedded firmware, custom hardware, and Azure backends.
Talk to our team →