STM32 + FreeRTOS: Getting Started Guide
Introduction: Why RTOS?
In embedded systems development, there are two fundamental approaches: bare-metal (superloop) and RTOS (Real-Time Operating System). In the bare-metal approach, all operations are executed sequentially within a single infinite loop. While this is sufficient for simple applications, serious problems arise as system complexity increases:
- Timing violations: A long-running operation prevents other operations from executing on time.
- No priority management: An urgent event (e.g., a motor stop signal) must wait for other operations to complete.
- Increased code complexity: State machines become nested, maintenance becomes difficult.
FreeRTOS Configuration with STM32CubeMX
The easiest way to get started with the STM32 family is to enable the CMSIS-RTOS v2 API through STM32CubeMX (or STM32CubeIDE).
Step 1: Project Creation
Create a new project in STM32CubeIDE and select your target MCU (e.g., STM32F407VG). In the Middleware section, find the FREERTOS tab and select the CMSIS_V2 interface.
Step 2: Task Definition
Define tasks from the "Tasks and Queues" tab in the CubeMX interface:
Task Name | Priority | Stack (words) | Entry Function
-------------|-----------------------|---------------|----------------
defaultTask | osPriorityNormal | 256 | StartDefaultTask
sensorTask | osPriorityHigh | 512 | StartSensorTask
commsTask | osPriorityAboveNormal | 384 | StartCommsTask
Step 3: Code Generation
Project files are generated with "Generate Code." The FreeRTOS configuration is found in FreeRTOSConfig.h. Critical parameters:
#define configTICK_RATE_HZ ((TickType_t)1000) // 1 ms tick
#define configMAX_PRIORITIES (56)
#define configMINIMAL_STACK_SIZE ((uint16_t)128)
#define configTOTAL_HEAP_SIZE ((size_t)15360)
#define configUSE_PREEMPTION 1
#define configUSE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
Task Lifecycle
In FreeRTOS, each task is an independent execution unit. A task can be in the following states:
- Running: Using the processor (only one task at a time on a single core).
- Ready: Runnable, waiting to be selected by the scheduler.
- Blocked: Waiting for an event (delay, queue, semaphore, etc.).
- Suspended: Manually suspended.
void StartSensorTask(void *argument)
{
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS(100); // 100 ms period
for(;;) { // Read sensor data float temperature = ReadTemperatureSensor(); float humidity = ReadHumiditySensor();
// Send data to queue SensorData_t data = { .temperature = temperature, .humidity = humidity, .timestamp = xTaskGetTickCount() }; xQueueSend(sensorQueueHandle, &data, pdMS_TO_TICKS(10));
// Precise timing for periodic execution vTaskDelayUntil(&xLastWakeTime, xPeriod); } }
Important: Note the difference between vTaskDelay() and vTaskDelayUntil(). vTaskDelay() waits for the specified duration from the point of call; however, the task's own execution time is not accounted for. vTaskDelayUntil() works based on absolute time, thus guaranteeing periodic execution at a fixed frequency.
Synchronization Mechanisms
Mutex (Mutual Exclusion)
Mutexes are used to protect access to shared resources (e.g., UART, SPI, I2C):
osMutexId_t uartMutexHandle;
const osMutexAttr_t uartMutex_attr = {
.name = "uartMutex"
};
// Creation (inside main) uartMutexHandle = osMutexNew(&uartMutex_attr);
// Usage (inside any task) if (osMutexAcquire(uartMutexHandle, osWaitForever) == osOK) { HAL_UART_Transmit(&huart2, data, len, HAL_MAX_DELAY); osMutexRelease(uartMutexHandle); }
FreeRTOS mutexes support priority inheritance: if a low-priority task holds the mutex while a high-priority task is waiting, the low-priority task's priority is temporarily elevated. This prevents the priority inversion problem.
Binary Semaphore
Ideal for sending notifications from ISR (Interrupt Service Routine) to a task:
osSemaphoreId_t gpioSemHandle;
// ISR callback void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == BUTTON_Pin) { osSemaphoreRelease(gpioSemHandle); } }
// Task void StartButtonTask(void *argument) { for(;;) { if (osSemaphoreAcquire(gpioSemHandle, osWaitForever) == osOK) { // Button pressed, handle the event ToggleLED(); } } }
Message Queue
The most reliable method for data transfer between tasks. Data is transferred by copying, avoiding shared memory issues:
typedef struct {
uint8_t commandId;
float value;
uint32_t timestamp;
} Command_t;
osMessageQueueId_t cmdQueueHandle;
// Sending Command_t cmd = { .commandId = CMD_SET_SPEED, .value = 1500.0f }; osMessageQueuePut(cmdQueueHandle, &cmd, 0, pdMS_TO_TICKS(100));
// Receiving Command_t received; if (osMessageQueueGet(cmdQueueHandle, &received, NULL, osWaitForever) == osOK) { ProcessCommand(&received); }
Memory Management
FreeRTOS offers five different heap management schemes (heap_1.c - heap_5.c):
| Scheme | Feature | Use Case |
|---|---|---|
| heap_1 | Allocate only, no free | Static systems |
| heap_2 | Allocate + free (fragmentation risk) | Fixed-size blocks |
| heap_3 | Standard malloc/free wrapper | Compatibility requirements |
| heap_4 | Allocate + free + coalescing | Most common choice |
| heap_5 | heap_4 + multiple memory regions | Systems using external RAM |
configCHECK_FOR_STACK_OVERFLOW to 2 and implement the vApplicationStackOverflowHook() function.
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
{
// Stack overflow detected!
// pcTaskName contains the name of the offending task
__disable_irq();
while(1)
{
HAL_GPIO_TogglePin(ERROR_LED_GPIO_Port, ERROR_LED_Pin);
HAL_Delay(100); // Scheduler is not running at this point
}
}
Debugging Tips
1. Task Statistics
By enabling configGENERATE_RUN_TIME_STATS and configUSE_TRACE_FACILITY, per-task CPU usage can be monitored:
char statsBuffer[512];
vTaskGetRunTimeStats(statsBuffer);
// statsBuffer: CPU usage percentage for each task
2. Segger SystemView
Segger SystemView allows visual monitoring of task switches, ISR entry/exit, and synchronization events. Data is captured via SWO (Serial Wire Output) or UART.
3. Common Mistakes
- Calling blocking APIs inside ISRs: Never call
osMutexAcquire()orosDelay()inside ISRs. UseFromISRsuffixed functions for ISRs. - Insufficient stack size: Functions like printf and sprintf consume large amounts of stack. Check with uxTaskGetStackHighWaterMark().
- Defining configASSERT macro: Critical for catching errors early during development.
Advanced: Software Timers
FreeRTOS software timers allow you to run periodic or one-shot operations without creating a task:
osTimerId_t heartbeatTimerHandle;
void HeartbeatCallback(void *argument) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); }
// Creation const osTimerAttr_t heartbeat_attr = { .name = "heartbeat" }; heartbeatTimerHandle = osTimerNew(HeartbeatCallback, osTimerPeriodic, NULL, &heartbeat_attr); osTimerStart(heartbeatTimerHandle, pdMS_TO_TICKS(500)); // 500 ms
Conclusion
FreeRTOS significantly improves code modularity, timing reliability, and system scalability in STM32-based embedded systems. With proper task prioritization, appropriate synchronization mechanism selection, and careful memory management, robust and maintainable embedded software can be developed. As next steps, FreeRTOS+TCP, FreeRTOS+CLI, or AWS IoT integration can be explored.