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
printfand 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_HZmust match the actual system clock.configPRIO_BITSmust match__NVIC_PRIO_BITS(from CMSIS).Interrupt priority macros:
configLIBRARY_LOWEST_INTERRUPT_PRIORITYconfigLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITYconfigKERNEL_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()withvTaskDelay().
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 beforevTaskStartScheduler().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_InitTickis 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.hdefine:#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_IDLEand 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.
Links ¶
What is FreeRTOS? : What is FreeRTOS ?
FreeRTOS FAQ: FreeRTOS FAQ