FreeRTOS Best Practices

Project architecture and task design

Design a Clear Task Architecture

  • Keep the number of tasks limited:

    • Prefer a few well-defined tasks with internal state machines instead of many tiny tasks.

    • Typical small/medium systems: 3–10 tasks.

  • Group responsibilities logically:

    • One task for communication (e.g. UART/USB).

    • One task for sensors / acquisition.

    • One task for control / application logic.

    • One “system” or “manager” task if needed.

  • Avoid tasks that just poll a flag and do a small job.

Define Clear Task Priorities

  • Start with a simple priority scheme:

    • High priority: Time-critical I/O and short-latency tasks.

    • Medium priority: Application logic.

    • Low priority: Logging, background calculations, housekeeping.

  • Common STM32 community advice:

    • Do not run multiple tasks at maximum priority.

    • Avoid “priority inflation”: if everything is high priority, nothing actually is.

    • Never create a high-priority task that runs a permanent busy loop without blocking.

Use Event-Driven Design, avoid Polling

  • Prefer event-driven mechanisms:

    • Queues, semaphores, direct-to-task notifications, event groups.

    • vTaskDelay() / vTaskDelayUntil() for periodic tasks.

  • Avoid:

    • Infinite loops with no blocking call.

    • Frequent polling with very short delays (e.g. vTaskDelay(1) in many tasks) unless justified.

  • Typical pattern for a periodic task:

    void PeriodicTask(void *argument)
    {
        TickType_t xLastWakeTime = xTaskGetTickCount();
        const TickType_t xPeriod = pdMS_TO_TICKS(10); /* 10 ms */
    
        for (;;)
        {
            /* Do work... */
            vTaskDelayUntil(&xLastWakeTime, xPeriod);
        }
    }
    

Memory management and configuration

Choose the right Heap implementation

  • On STM32, heap_4 is commonly recommended:

    • Dynamic allocation and free.

    • Coalescing of free blocks to reduce fragmentation.

  • Use heap_5 if:

    • User application requires multiple memory regions.

  • Avoid frequent dynamic allocations at runtime in tight loops; prefer:

    • Static allocation at startup.

    • Using FreeRTOS static allocation APIs where appropriate.

Use Static Allocation for critical objects

  • For critical or long-lived tasks, queues, and timers:

    • Prefer static allocation (config options and xTaskCreateStatic, xQueueCreateStatic, etc.).

  • Community pattern:

    • For Main system tasks (communication, control) use static allocation.

    • for less critical or optional tasks use dynamic allocation.

Size Task stacks and Main Stack carefully

  • Start with conservative stack sizes, then measure:

    • Use uxTaskGetStackHighWaterMark() for each task.

    • Leave some safety margin (e.g. 25–50%) above the worst-case usage.

  • Beware:

    • STM32 HAL drivers, especially with printf and floating-point, can consume significant stack.

    • Large local arrays should be moved to static/global or allocated on the heap.

  • Don’t forget main stack (MSP):

    • Used before scheduler start, and often in interrupts.

    • Increase via linker script or startup file if you see HardFaults during startup or heavy ISR usage.

Configure FreeRTOSConfig.h consistently with STM32

Key points:

  • configCPU_CLOCK_HZ must match the actual system clock.

  • configPRIO_BITS must match __NVIC_PRIO_BITS (from CMSIS).

  • Interrupt priority macros:

    • configLIBRARY_LOWEST_INTERRUPT_PRIORITY

    • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY

    • configKERNEL_INTERRUPT_PRIORITY

    Make sure these are consistent with NVIC configuration.

Interrupts and NVIC usage

Respect Interrupt priority rules

  • Any ISR that calls a FreeRTOS FromISR API must:

    • Have a numerical priority greater than or equal to configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY.

    • Not be a “higher urgency” interrupt than this threshold.

  • High-urgency interrupts that must not be delayed:

    • Should not call any FreeRTOS API.

    • Instead, set flags or write to circular buffers; a task with lower priority should process the data.

Keep ISRs short and simple

  • In STM32 HAL callbacks (e.g. DMA, UART, SPI):

    • Perform minimal work in ISRs.

    • Signal tasks via:

      • xSemaphoreGiveFromISR()

      • xQueueSendFromISR()

      • vTaskNotifyGiveFromISR()

  • Do not:

    • Call blocking HAL functions inside interrupts.

    • Perform long computations in ISRs.

  • Common pattern:

    • DMA complete ISR -> xTaskNotifyGiveFromISR().

    • Task waits with ulTaskNotifyTake(pdTRUE, portMAX_DELAY); and handles the buffer.

Synchronization and communication

Prefer Direct-to-task notifications for simple signaling

  • Direct-to-task notifications:

    • Very lightweight, faster than semaphores.

    • Ideal for one-to-one events (one ISR/task signals one receiving task).

  • Example:

    void DMA_IRQHandler(void)
    {
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        vTaskNotifyGiveFromISR(xDmaTaskHandle, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
    
    void DmaTask(void *arg)
    {
        for (;;)
        {
            ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
            /* Process DMA buffer */
        }
    }
    

Use Queues for Data, Mutexes for Shared Resources

  • Queues:

    • For transferring data or messages between tasks and ISRs.

    • Size them based on worst-case traffic, not only average load.

  • Mutexes:

    • For protecting shared resources (e.g. a peripheral or shared buffer).

    • Use FreeRTOS mutex (not binary semaphores) when you rely on priority inheritance.

  • Avoid:

    • Long sections under a mutex that perform heavy computations or blocking I/O.

    • Nested mutex usage without clear design (risk of deadlocks).

Event Groups for Multi-condition Waits

  • Use event groups when a task must wait for multiple conditions.

  • Good practice:

    • Define bits in an enumeration or macros for readability.

    • Consider whether bits must be manually cleared after reading.

Integration with STM32 HAL/LL

Centralize Peripheral Access

  • Common community pattern:

    • One owner task per peripheral:

      • For example, one “UART task” handles all UART TX/RX.

      • Other tasks send requests via queues or messages.

  • Benefits:

    • Avoids concurrent HAL calls from multiple tasks.

    • Simplifies synchronization and error handling.

Avoid HAL_Delay() Once FreeRTOS Is Running

  • After the scheduler starts:

    • Replace HAL_Delay() with vTaskDelay().

  • Problems with HAL_Delay():

    • It blocks the calling context without awareness of task scheduling.

    • It can conflict with SysTick usage by FreeRTOS.

  • If necessary:

    • Restrict HAL_Delay() to early initialization before vTaskStartScheduler().

    • Keep such delays as short as possible.

Configure SysTick Correctly

  • Let FreeRTOS own SysTick after kernel start.

  • Ensure:

    • No other part of your code (including HAL) reconfigures SysTick.

    • HAL_InitTick is adapted if needed (or replaced) in a FreeRTOS project.

  • If using tickless idle or low-power modes:

    • Use the FreeRTOS port’s recommended method for tick suppression.

Debugging, diagnostics and safety hooks

Always Enable configASSERT

  • In FreeRTOSConfig.h define:

    #define configASSERT(x)  if ((x) == 0) { taskDISABLE_INTERRUPTS(); for(;;); }
    
  • Add a breakpoint or logging in that path.

  • Many FreeRTOS configuration or API misuse issues are caught early by asserts (e.g. wrong interrupt priorities, invalid parameters).

Use Stack Overflow and Malloc Failed Hooks

  • In FreeRTOSConfig.h:

    #define configCHECK_FOR_STACK_OVERFLOW 2
    #define configUSE_MALLOC_FAILED_HOOK   1
    
    void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
    {
        /* Breakpoint, log, reset, etc. */
        for (;;);
    }
    
    void vApplicationMallocFailedHook(void)
    {
        /* Breakpoint, log, reset, etc. */
        for (;;);
    }
    
  • This is a common STM32 community recommendation to quickly detect:

    • Stack overflows (typical cause of random HardFaults).

    • Heap exhaustion.

Add a Watchdog and Supervision Task

  • Use the independent or window watchdog (IWDG/WWDG):

    • Feed it in a supervision task that: - Checks system health (queues not full, tasks responsive). - Resets the watchdog only if everything is OK.

  • Good practice:

    • Each critical task periodically signals the supervision task (e.g. via notification or event).

    • The supervision task expects all heartbeats within a specified time window.

Low-Power and Tickless mode

Use Tickless Idle for STM32 Devices

  • Enable configUSE_TICKLESS_IDLE and use the provided vPortSuppressTicksAndSleep() or implement a custom version.

  • Combine with:

    • RTC or low-power timer to wake at specific times.

    • STOP modes for deeper sleep when possible.

  • Check ST application notes and Cube examples for family-specific guidance (clock reconfiguration, wake-up sources).

Design tasks for Low Power

  • Ensure that when there is no work:

    • Tasks are blocked on queues/semaphores, or use (long) delays.

    • No “busy” tasks prevent the scheduler from entering idle.

  • Avoid:

    • Frequent wake-ups for trivial checks that could be event-driven.

    • Use of long, active loops instead of timed waits.

  • Consider:

    • Grouping operations in bursts, then letting the MCU sleep for longer periods.

Coding style and process

Avoid Blocking Calls Without Timeouts

  • For calls like:

    • xQueueReceive() / xSemaphoreTake() / ulTaskNotifyTake()

  • Use timeouts instead of indefinite waits when appropriate:

    • Detect communication failures.

    • Allow graceful recovery instead of permanent deadlocks.

Separate RTOS-Dependent and RTOS-Independent Code

  • Structure your code so that:

    • Drivers and low-level processing are RTOS-agnostic when possible.

    • RTOS-specific logic (task creation, queues, etc.) is kept in higher-level modules.

  • Benefits:

    • Easier unit testing and reuse.

    • Future flexibility (migration to a different RTOS or bare metal, if needed).

Key Takeaways

  • Assign each middleware a clear owner task and well-defined communication interface.

  • Keep ISRs and middleware callbacks thin, delegating work to tasks via FreeRTOS primitives.

  • Carefully manage priorities, locks, and dynamic memory across all components to avoid deadlocks and timing issues.

  • Replace blocking, busy-wait HAL behavior with RTOS-aware patterns using DMA, interrupts, and task notifications.

  • Continuously measure and validate with trace tools, asserts, and stress tests to ensure your design behaves correctly in real conditions.