Embedded systems are everywhere — from smart thermostats to automotive electronics — and many require precise control over multiple tasks running simultaneously.
FreeRTOS is a lightweight, open-source real-time operating system (RTOS) designed to help developers achieve efficient multitasking on microcontrollers with minimal overhead.
This post breaks down how FreeRTOS works and adds a hands-on setup guide for popular microcontrollers, including a Blinky example you can compile today.
⚙️ What Is FreeRTOS?
FreeRTOS is a deterministic kernel that manages multiple independent tasks (threads), each with its own stack, priority, and state.
It focuses on predictable timing, small memory footprint, and simple APIs—ideal for resource-constrained MCUs.
🧩 Why Multitasking Matters
A single, sequential while(1) loop can cause jitter and missed deadlines. FreeRTOS lets you split logic into independent tasks and schedule them predictably based on priority and timing.
🔁 Scheduling & Context Switching (Quick Recap)
- Preemptive scheduling: high-priority tasks preempt lower ones.
- Cooperative: tasks yield explicitly.
- Context switch saves/restores CPU registers and stack for each task—done in microseconds.
📨 Communication Primitives (Quick Recap)
- Queues for passing data.
- Semaphores for signaling.
- Mutexes for protecting shared resources.
Use these to avoid races and blocking in high-priority code.
🛠️ Set Up FreeRTOS on Microcontrollers (Step-by-Step)
Below are universal steps plus platform-specific notes for STM32, ESP32, and Arduino-class boards.
1) Get the Code
- Option A (vendor-integrated):
- STM32CubeIDE: enable FreeRTOS in CubeMX middleware.
- Downloads the correct FreeRTOS kernel
- Picks the right port layer (for STM32F4 →
portable/GCC/ARM_CMx) - Generates the full FreeRTOSConfig.h
- ESP-IDF: FreeRTOS is built in (no extra setup).
- Arduino:
- For AVR (Uno/Nano): install
FreeRTOS by Richard Barry(Arduino port) - For SAMD (MKR/Zero) or RP2040 (Pico): install
Arduino_FreeRTOSorFreeRTOS-RP2040(both use the same API).
- For AVR (Uno/Nano): install
- STM32CubeIDE: enable FreeRTOS in CubeMX middleware.
- Option B (source):
- Download from the official FreeRTOS repo. Add
Source/and the correctportable/port to your project. Refer to the References section below.
- Download from the official FreeRTOS repo. Add
2) Choose the Port Layer
Pick the correct port for your MCU/CPU:
- ARM Cortex-M:
portable/GCC/ARM_CMxis selected automatically if you are using cubeMX middleware. - ESP32: ESP-IDF already ships with an integrated FreeRTOS SMP (Symmetric Multi-Processing) port.
- AVR/SAMD: use the FreeRTOS by Richard Barry library → it automatically includes the AVR or SAMD port.
3) Create/Configure FreeRTOSConfig.h
This file tunes the kernel. Start minimal:
// FreeRTOSConfig.h (Cortex-M example)
#define configUSE_PREEMPTION 1
#define configCPU_CLOCK_HZ (SystemCoreClock)
#define configTICK_RATE_HZ ((TickType_t)1000) // 1ms tick
#define configMAX_PRIORITIES 5
#define configMINIMAL_STACK_SIZE ((unsigned short)128) // tune per MCU
#define configTOTAL_HEAP_SIZE ((size_t)(16*1024)) // tune per project
#define configUSE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY 2
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 256
// Debug
#define configCHECK_FOR_STACK_OVERFLOW 2
#define configUSE_MALLOC_FAILED_HOOK 1
Timing tips
configTICK_RATE_HZ: 1 kHz (1 ms tick) is common; reduce to 100–250 Hz to save power if you can.- Interrupt priorities (Cortex-M): set configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY and keep ISRs that call FreeRTOS APIs at/under that priority. Never call FreeRTOS APIs from very high-priority ISRs.
Select a Heap Scheme
- FreeRTOS provides heap allocators in
portable/MemMang/: heap_4.c: general-purpose, coalescing (good default)heap_5.c: like heap_4 but supports multiple regionsheap_1.c: very simple, no free- Include exactly one in your build and set
configTOTAL_HEAP_SIZE.
Hardware Tick & Startup
- Cortex-M: the port uses
SysTickor a timer for the RTOS tick. EnsureSysTick_Handleror port-specific tick handler is linked (CubeMX handles this). - ESP32: tick and SMP scheduling are built into ESP-IDF.
- Arduino: library sets up the tick; avoid long blocking in
loop().
Build Integration
- STM32CubeIDE: CubeMX generates FreeRTOS files and includes automatically.
- CMake (generic):
# CMakeLists.txt (snippet)
add_subdirectory(FreeRTOS-Kernel) # if you vendored the kernel as a submodule
target_link_libraries(my_firmware PRIVATE freertos_kernel)
target_include_directories(my_firmware PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/FreeRTOS-Kernel/include)- ESP-IDF: FreeRTOS is part of the SDK—no extra linking.
Sanity Checks
- Enable stack overflow and malloc failed hooks.
- Start with few tasks, small stacks (256–512 bytes), and measure high-water marks with
uxTaskGetStackHighWaterMark().
💡 Blinky: The Smallest Useful FreeRTOS App
Below are three versions of Blinky so you can run it on your favorite platform.
A) STM32 (HAL) — Two FreeRTOS Tasks on STM32 (C + HAL)
#include "main.h"
#include "cmsis_os.h"
#include "FreeRTOS.h"
#include "task.h"
/* --- Configure your LED pins here --- */
#define LED1_PIN GPIO_PIN_5 // Example: Nucleo onboard LED (PA5)
#define LED1_PORT GPIOA
#define LED2_PIN GPIO_PIN_6 // Second LED (connect manually if needed)
#define LED2_PORT GPIOA
extern UART_HandleTypeDef huart2; // UART2 handle from CubeMX
/* --- Task Handles (optional) --- */
TaskHandle_t BlinkLED1Handle = NULL;
TaskHandle_t BlinkLED2Handle = NULL;
/* --- Task 1: Blink LED1 --- */
void TaskBlinkLED1(void *pvParameters)
{
(void) pvParameters;
for (;;)
{
HAL_GPIO_TogglePin(LED1_PORT, LED1_PIN);
vTaskDelay(pdMS_TO_TICKS(500)); // Blink every 500ms
}
}
/* --- Task 2: Blink LED2 and print --- */
void TaskBlinkLED2(void *pvParameters)
{
(void) pvParameters;
const char *taskName = pcTaskGetName(NULL);
for (;;)
{
HAL_GPIO_TogglePin(LED2_PORT, LED2_PIN);
char msg[50];
snprintf(msg, sizeof(msg), "Running: %s\r\n", taskName);
HAL_UART_Transmit(&huart2, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1000)); // Blink every 1 second
}
}
/* --- Main Setup --- */
int main(void)
{
HAL_Init();
SystemClock_Config();
/* --- GPIO & UART init (CubeMX-generated normally) --- */
MX_GPIO_Init();
MX_USART2_UART_Init();
/* --- Create Tasks --- */
xTaskCreate(TaskBlinkLED1, "LED1", 128, NULL, 1, &BlinkLED1Handle);
xTaskCreate(TaskBlinkLED2, "LED2", 256, NULL, 1, &BlinkLED2Handle);
/* --- Start Scheduler --- */
vTaskStartScheduler();
/* --- Should never reach here --- */
for (;;);
}
Notes (STM32):
- If using CubeMX, enable FreeRTOS middleware and it will autogenerate kernel files.
- Make sure the SysTick is owned by FreeRTOS (Cube handles this).
- Tune stack sizes; 256–512 bytes is typical for simple tasks.
B) ESP32 (ESP-IDF) — GPIO 2 (built-in LED on many boards)
// blinky_freertos_esp32.c
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define LED_PIN GPIO_NUM_2
void vTaskBlink(void *pvParameters) {
(void) pvParameters;
bool on = false;
for (;;) {
gpio_set_level(LED_PIN, on);
on = !on;
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void app_main(void) {
gpio_config_t io = {
.pin_bit_mask = 1ULL << LED_PIN,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = 0,
.pull_down_en = 0,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io);
xTaskCreate(vTaskBlink, "blink", 2048, NULL, 1, NULL);
}
Notes (ESP32):
- Build with idf.py build flash monitor.
- Task stacks are larger on ESP32 due to newlib/printf; 2048 is a safe starting point.
C) Arduino-Class Boards (AVR/SAMD/RP2040) — LED on LED_BUILTIN
// blinky_freertos_arduino.ino
#include <Arduino_FreeRTOS.h>
// LED pin
#define LED_PIN LED_BUILTIN
// Handle for tasks (optional)
TaskHandle_t blink1Handle = NULL;
TaskHandle_t blink2Handle = NULL;
void TaskBlink1(void *pvParameters) {
(void) pvParameters;
pinMode(LED_PIN, OUTPUT);
for (;;) {
digitalWrite(LED_PIN, HIGH);
vTaskDelay(pdMS_TO_TICKS(500)); // 500 ms
digitalWrite(LED_PIN, LOW);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void TaskBlinkAndPrint(void *pvParameters) {
(void) pvParameters;
pinMode(LED_PIN, OUTPUT);
for (;;) {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
Serial.print("Running: ");
Serial.println(pcTaskGetName(NULL));
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void setup() {
Serial.begin(115200);
while (!Serial) ; // wait for serial port (needed on SAMD/RP2040)
xTaskCreate(
TaskBlink1,
"Blink1",
128, // stack words (not bytes)
NULL,
1, // priority
&blink1Handle
);
xTaskCreate(
TaskBlinkAndPrint,
"BlinkPrint",
256,
NULL,
1,
&blink2Handle
);
vTaskStartScheduler(); // never returns
}
void loop() {
// not used when scheduler is running
}
Notes (Arduino):
- Install Arduino_FreeRTOS (if not included in your core).
- Keep stacks small; avoid heavy printf.
- Some cores start the scheduler for you; others require vTaskStartScheduler().
🧱 Extend Blinky with a Second Task (Optional)
Add a heartbeat logger to demonstrate multitasking:
void vTaskHeartbeat(void *pvParameters) {
(void) pvParameters;
for (;;) {
// e.g., toggle a second LED or print a dot
// printf(".\n"); // avoid in tiny MCUs
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main(void) {
// ...hardware init...
xTaskCreate(vTaskBlink, "Blink", 256, NULL, 1, NULL);
xTaskCreate(vTaskHeartbeat, "Heartbeat", 256, NULL, 1, NULL);
vTaskStartScheduler();
while (1) {}
}
Common Pitfalls & Pro Tips
- Stack overflows: enable
configCHECK_FOR_STACK_OVERFLOW=2and implementvApplicationStackOverflowHook(). - Heap exhaustion: set
configTOTAL_HEAP_SIZErealistically; implementvApplicationMallocFailedHook(). - ISRs using FreeRTOS: only call
xxxFromISR()APIs; end withportYIELD_FROM_ISR()as required. - Priorities: don’t make everything priority 3+. Reserve high priorities for hard real-time loops.
- Timing: use
vTaskDelayUntil()for periodic tasks to avoid drift.
🧠 Summary
You now have:
- A solid conceptual model of tasks, priorities, delays, and IPC in FreeRTOS.
- A repeatable setup flow for STM32, ESP32, and Arduino-class boards.
- A Blinky you can build and extend into a multi-task demo.
- FreeRTOS gives you determinism without bloat—perfect for professional embedded systems that must be responsive and maintainable.


