Table of Contents
KEY TAKEAWAYS
- FreeRTOS is a lightweight real-time operating system for microcontrollers with preemptive scheduling
- Tasks are independent threads of execution, each with its own stack and priority level
- Synchronization primitives (semaphores, mutexes, queues) coordinate communication between tasks
- FreeRTOS enables concurrent operations that would be complex to implement in a bare-metal super-loop
What is FreeRTOS?
FreeRTOS is the most popular real-time operating system (RTOS) for microcontrollers. It is free, open-source (MIT license), and runs on nearly every microcontroller architecture: ARM Cortex-M, AVR, PIC, RISC-V, ESP32, and more.FreeRTOS provides:- Multitasking: Run multiple tasks concurrently on a single CPU
- Task scheduling: Priority-based preemptive scheduling
- Synchronization: Semaphores, mutexes, and event groups
- Communication: Queues and stream buffers for inter-task data exchange
- Timers: Software timers for periodic or one-shot events
- Memory management: Multiple heap allocation schemes
Why Use an RTOS?
Without an RTOS, embedded firmware typically runs in a super loop:// Super loop (bare-metal) approach
int main(void) {
system_init();
while (1) {
read_sensors(); // Takes 5ms
update_display(); // Takes 20ms
check_buttons(); // Takes 1ms
send_data_to_cloud(); // Takes 200ms (blocks!)
blink_led(); // Takes 1ms
}
}The problem: while send_data_to_cloud() blocks for 200ms, nothing else runs. The display freezes, buttons are unresponsive, and sensor readings are delayed.With FreeRTOS, each of these becomes an independent task:// FreeRTOS approach - each task runs independently
void vSensorTask(void *params) { while(1) { read_sensors(); vTaskDelay(100); } }
void vDisplayTask(void *params) { while(1) { update_display(); vTaskDelay(50); } }
void vButtonTask(void *params) { while(1) { check_buttons(); vTaskDelay(10); } }
void vCloudTask(void *params) { while(1) { send_data_to_cloud(); vTaskDelay(5000); } }
void vLedTask(void *params) { while(1) { blink_led(); vTaskDelay(500); } }Now when the cloud task blocks waiting for a network response, the scheduler switches to other tasks. The system remains responsive.Core FreeRTOS Concepts
Tasks
A task is a function that runs in an infinite loop with its own stack. You create tasks withxTaskCreate():#include "FreeRTOS.h"
#include "task.h"
void vBlinkTask(void *pvParameters) {
while (1) {
gpio_toggle(LED_PIN);
vTaskDelay(pdMS_TO_TICKS(500)); // Delay 500ms
}
}
int main(void) {
// Create a task with:
// - Function: vBlinkTask
// - Name: "Blink" (for debugging)
// - Stack size: 256 words
// - Parameters: NULL
// - Priority: 1
// - Handle: NULL
xTaskCreate(vBlinkTask, "Blink", 256, NULL, 1, NULL);
// Start the scheduler - this never returns
vTaskStartScheduler();
// Should never reach here
while (1);
}Task Priorities
Each task has a priority (0 = lowest). The scheduler always runs the highest-priority task that is ready. If two tasks have equal priority, they share CPU time using round-robin scheduling.// Priority assignment xTaskCreate(vMotorControl, "Motor", 256, NULL, 4, NULL); // Highest xTaskCreate(vSensorRead, "Sensor", 256, NULL, 3, NULL); xTaskCreate(vDisplay, "Display", 256, NULL, 2, NULL); xTaskCreate(vLogging, "Log", 256, NULL, 1, NULL); // Lowest
Delays: vTaskDelay vs vTaskDelayUntil
vTaskDelay() delays relative to when it is called. vTaskDelayUntil() delays until an absolute time, providing more precise periodic execution:// Precise 100ms period regardless of task execution time
void vPeriodicTask(void *params) {
TickType_t xLastWakeTime = xTaskGetTickCount();
while (1) {
// Do work here (takes variable time)
read_and_process_sensor();
// Wait until exactly 100ms from last wake
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));
}
}Queues
Queues are the primary way to send data between tasks safely:#include "queue.h"
QueueHandle_t xSensorQueue;
// Producer task: reads sensor and sends to queue
void vSensorTask(void *params) {
xSensorQueue = xQueueCreate(10, sizeof(float));
while (1) {
float temperature = read_temperature();
xQueueSend(xSensorQueue, &temperature, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// Consumer task: receives from queue and displays
void vDisplayTask(void *params) {
float received_temp;
while (1) {
// Block until data is available
if (xQueueReceive(xSensorQueue, &received_temp, portMAX_DELAY)) {
display_temperature(received_temp);
}
}
}Semaphores and Mutexes
Binary Semaphore: Used for task synchronization, typically to signal a task from an ISR:SemaphoreHandle_t xButtonSemaphore;
// ISR: signal that button was pressed
void EXTI_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xButtonSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// Task: wait for button press
void vButtonTask(void *params) {
xButtonSemaphore = xSemaphoreCreateBinary();
while (1) {
// Block until semaphore is given (button pressed)
xSemaphoreTake(xButtonSemaphore, portMAX_DELAY);
handle_button_press();
}
}Mutex: Used to protect shared resources from simultaneous access by multiple tasks:SemaphoreHandle_t xUartMutex;
void vTask1(void *params) {
while (1) {
xSemaphoreTake(xUartMutex, portMAX_DELAY);
uart_send_string("Message from Task 1rn");
xSemaphoreGive(xUartMutex);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vTask2(void *params) {
while (1) {
xSemaphoreTake(xUartMutex, portMAX_DELAY);
uart_send_string("Message from Task 2rn");
xSemaphoreGive(xUartMutex);
vTaskDelay(pdMS_TO_TICKS(1500));
}
}A Complete Example: Temperature Monitor
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
QueueHandle_t xTempQueue;
SemaphoreHandle_t xUartMutex;
void vSensorTask(void *params) {
TickType_t xLastWake = xTaskGetTickCount();
while (1) {
float temp = read_temperature_sensor();
xQueueSend(xTempQueue, &temp, 0);
vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(2000));
}
}
void vDisplayTask(void *params) {
float temp;
while (1) {
if (xQueueReceive(xTempQueue, &temp, portMAX_DELAY)) {
xSemaphoreTake(xUartMutex, portMAX_DELAY);
printf("Temperature: %.1f Crn", temp);
xSemaphoreGive(xUartMutex);
display_show_value(temp);
}
}
}
void vAlarmTask(void *params) {
while (1) {
float temp;
if (xQueuePeek(xTempQueue, &temp, portMAX_DELAY)) {
if (temp > 40.0f) {
buzzer_on();
led_set(LED_RED, ON);
} else {
buzzer_off();
led_set(LED_RED, OFF);
}
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main(void) {
hardware_init();
xTempQueue = xQueueCreate(5, sizeof(float));
xUartMutex = xSemaphoreCreateMutex();
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 3, NULL);
xTaskCreate(vDisplayTask, "Display", 256, NULL, 2, NULL);
xTaskCreate(vAlarmTask, "Alarm", 256, NULL, 4, NULL);
vTaskStartScheduler();
while (1);
}FreeRTOS Configuration
FreeRTOS is configured through a file calledFreeRTOSConfig.h. Key settings include:// FreeRTOSConfig.h (key entries) #define configUSE_PREEMPTION 1 // Enable preemptive scheduling #define configCPU_CLOCK_HZ 72000000 // MCU clock speed #define configTICK_RATE_HZ 1000 // 1ms tick (1000 Hz) #define configMAX_PRIORITIES 5 // Priority levels (0-4) #define configMINIMAL_STACK_SIZE 128 // Minimum task stack (words) #define configTOTAL_HEAP_SIZE (8 * 1024) // 8KB heap for tasks #define configUSE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configUSE_QUEUE_SETS 1
Common Mistakes
- Stack overflow: Tasks have fixed-size stacks. If a task uses more stack than allocated, it corrupts memory. Enable
configCHECK_FOR_STACK_OVERFLOWduring development. - Calling blocking functions from ISR: Never use
xQueueSend()orxSemaphoreTake()in an ISR. Use theirFromISRvariants:xQueueSendFromISR(),xSemaphoreGiveFromISR(). - Priority inversion: A low-priority task holds a mutex that a high-priority task needs. Use
xSemaphoreCreateMutex()(which has built-in priority inheritance) instead of binary semaphores for resource protection. - Forgetting to start the scheduler: Always call
vTaskStartScheduler()after creating tasks.
Summary
FreeRTOS transforms how you structure embedded firmware. Instead of a single super loop trying to do everything, you break your system into independent tasks that the scheduler manages. Queues pass data between tasks safely. Semaphores and mutexes handle synchronization. The result is cleaner code, better responsiveness, and easier maintenance. Start with simple tasks and delays, then add queues and semaphores as your project grows in complexity.
Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.






