REV 1.3|2025-02-28

STM32 + FreeRTOS: Getting Started Guide

EmbeddedRTOSSTM32

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 solves these problems and is one of the most widely used open-source, certified (SAFERTOS derivative with IEC 61508 SIL 3 and ISO 26262 ASIL D) RTOS kernels in the world. It is supported by Amazon and delivered with AWS IoT integration.

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.
Example sensor reading task:
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):

SchemeFeatureUse Case
heap_1Allocate only, no freeStatic systems
heap_2Allocate + free (fragmentation risk)Fixed-size blocks
heap_3Standard malloc/free wrapperCompatibility requirements
heap_4Allocate + free + coalescingMost common choice
heap_5heap_4 + multiple memory regionsSystems using external RAM
Practical tip: STM32CubeMX uses heap_4 by default. For stack overflow detection, set 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() or osDelay() inside ISRs. Use FromISR suffixed 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.