On this page
- The Wrong Question
- Use Bare Metal When the Firmware Is Naturally Simple
- Use STM32 HAL When Vendor Support Saves Time
- Use Project-Specific Drivers When Boundaries Repeat
- Use FreeRTOS When Scheduling Becomes the Problem
- The Cost of FreeRTOS
- Decision Matrix
- Example: LED and Button
- Example: Sensor Polling and Display Refresh
- Example: Connected or Multi-Rate Firmware
- Migration Path: Loop to Drivers to RTOS
- Common Decision Mistakes
- Final Checklist
- Series Wrap-Up
The previous articles ported FreeRTOS into an STM32 project step by step. They covered the build, portable layer, configuration, tick, exception handlers, first tasks, debugging, queues, delays, and moving peripheral work into tasks.
That raises the practical question this series has been building toward: when should an STM32 project use FreeRTOS at all?
The answer is not "always." FreeRTOS is useful, but it is not a badge of professionalism. A simple loop can be the right design. STM32 HAL can be the right tool. A small project-specific driver can solve a problem without adding a scheduler. FreeRTOS earns its place when scheduling, blocking, communication, and independent timing domains become the real complexity.
This article is a decision guide for choosing the simplest structure that still fits the firmware problem.
The Wrong Question
The wrong question is: should this project be bare metal or RTOS?
That sounds clear, but it hides too much. Real STM32 firmware usually mixes several layers:
- Startup code and linker scripts define how the firmware boots and uses memory.
- HAL or LL code may configure clocks and peripherals.
- Project-specific drivers may wrap repeated device behavior.
- Interrupt handlers may capture urgent events.
- A main loop or FreeRTOS tasks may own application behavior.
The better question is: where should each responsibility live?
FreeRTOS is one possible owner of scheduling and blocking behavior. It does not replace good startup code, clear driver boundaries, short interrupt handlers, or explicit peripheral ownership.
Use Bare Metal When the Firmware Is Naturally Simple
Bare metal is a good fit when the firmware has one clear control flow.
Examples include:
- Blink an LED.
- Read a button and debounce it.
- Poll a sensor at a fixed rate.
- Update a small display from one loop.
- Teach startup code, linker scripts, registers, and interrupts.
- Build a small deterministic firmware path with few moving parts.
A simple loop is easy to inspect:
int main(void)
{
board_init();
while (1)
{
button_update();
sensor_update();
display_update();
}
}
This style works well while each operation is short and the timing relationships are obvious.
Do not add FreeRTOS just to make this loop look more advanced. A scheduler adds stacks, priority rules, tick behavior, synchronization objects, and new failure modes. If the loop is clear and reliable, keep it.
Use STM32 HAL When Vendor Support Saves Time
STM32 HAL is useful when vendor-supported setup and peripheral access save time.
HAL can help with:
- Clock configuration.
- GPIO setup.
- UART, I2C, SPI, ADC, DMA, and timer initialization.
- Board bring-up from CubeMX-generated configuration.
- Peripheral examples that match ST documentation and community knowledge.
HAL is not the same thing as FreeRTOS. HAL is a peripheral support layer. FreeRTOS is a scheduler and kernel.
Using HAL does not require using FreeRTOS. Using FreeRTOS does not require putting HAL calls everywhere. They solve different problems.
HAL can hide low-level details, which is useful when the goal is fast peripheral bring-up. It can also make debugging harder when the goal is understanding registers, timing, interrupt paths, or DMA behavior. Use it deliberately.
Use Project-Specific Drivers When Boundaries Repeat
A project-specific driver is useful when behavior repeats and deserves a stable interface.
Examples:
- A debounced button module.
- An OLED display driver.
- A MAX7219 matrix driver.
- A UART logging wrapper.
- A sensor readout module.
- A small timer abstraction for one board.
Drivers are not automatically RTOS objects. A good driver can often be used from a bare-metal loop or from a FreeRTOS task.
For example:
void display_init(void);
void display_write_text(const char *text);
void display_refresh(void);
Those functions do not need to know whether the caller is main() or a FreeRTOS task.
Put RTOS-specific timing and queue behavior around the driver when possible. Keep the driver focused on the device or peripheral behavior.
Use FreeRTOS When Scheduling Becomes the Problem
FreeRTOS becomes useful when the firmware has multiple independent activities that need to block, wait, or communicate without turning the main loop into a manual scheduler.
Examples include:
- One task samples sensors periodically.
- Another task refreshes a display.
- Another task handles UART logging.
- An interrupt sends events into a queue.
- A communication task waits for incoming messages.
- Different activities have different timing requirements.
Without an RTOS, this can become a fragile loop with many flags and counters:
while (1)
{
if (sensor_due)
{
sensor_due = false;
sensor_read();
}
if (display_due)
{
display_due = false;
display_refresh();
}
if (log_pending)
{
log_flush();
}
}
That can still be valid. But when the timing and blocking rules become hard to reason about, tasks and queues may make the design clearer.
FreeRTOS helps most when it removes accidental complexity rather than adding structure for its own sake.
The Cost of FreeRTOS
FreeRTOS has real costs.
It adds:
- Per-task stacks.
- Heap or static allocation decisions.
- Tick configuration.
- Interrupt priority rules.
- Context-switching behavior.
- Scheduler-aware debugging.
- Queue, mutex, and blocking semantics.
- More ways for timing bugs to hide.
Those costs are manageable, but they are not free.
A task that blocks forever because the tick is broken is harder to debug than a simple polling loop. A queue that silently fills can hide lost data. A mutex around a shared SPI bus can introduce priority inversion if the design grows. An interrupt with the wrong priority can trip FreeRTOS assertions or corrupt assumptions.
Use FreeRTOS when its structure pays for those costs.
Decision Matrix
Use this as a starting point:
Situation Better fit
Single LED/button demo Bare metal or HAL
Learning registers/startup/interrupts Bare metal
Fast vendor peripheral bring-up HAL or CubeMX-generated support
Simple periodic sensor polling Bare-metal loop or timer callback
Repeated device behavior Project-specific driver
Multiple independent periodic jobs FreeRTOS may help
Blocking UART/display/network work FreeRTOS often helps
ISR hands work to foreground code Queue or flag, depending on complexity
Shared I2C/SPI across activities Single owner task or clear locking model
Hard real-time interrupt response Short ISR, careful priorities, RTOS or not
The matrix is not a rulebook. It is a way to ask the right questions before adding architecture.
Example: LED and Button
An LED and button project usually does not need FreeRTOS.
A simple loop can read the button, debounce it, and update the LED. A timer interrupt can provide a periodic tick if needed. HAL can configure GPIO quickly. Direct register access can teach the hardware path.
Adding FreeRTOS here mostly adds overhead.
Use this kind of project to learn fundamentals: GPIO modes, pullups, debounce, interrupts, and timer basics. Those concepts remain useful later inside RTOS projects.
Example: Sensor Polling and Display Refresh
Sensor polling plus display refresh sits near the boundary.
If the firmware reads one sensor every 100 ms and updates one display from the same loop, bare metal is probably fine.
If the display refresh takes variable time, the sensor has a strict sample cadence, UART logging can block, and button events need to be handled without polling everywhere, FreeRTOS may help.
A possible RTOS shape is:
- A sensor task samples periodically with
vTaskDelayUntil(). - A display task receives state updates through a queue.
- A logging task owns UART output.
- Interrupts send compact events into task context.
The same low-level drivers can still stay RTOS-independent. The tasks own scheduling and communication.
Example: Connected or Multi-Rate Firmware
FreeRTOS becomes more attractive when firmware has multiple timing domains.
For example:
- A network or serial protocol waits for input.
- A display refreshes at one rate.
- A sensor samples at another rate.
- A control output must update predictably.
- Logging should not block the control path.
This is where a main loop can become a manual scheduler. FreeRTOS gives each activity a clearer place to live.
That does not remove the need for design. Task priorities, queue lengths, shared bus ownership, and interrupt boundaries still matter. FreeRTOS gives you tools, not automatic architecture.
Migration Path: Loop to Drivers to RTOS
A healthy migration path is incremental.
Start with a simple loop:
while (1)
{
app_run();
}
When repeated behavior appears, create small drivers or modules:
button_update();
sensor_read();
display_refresh();
When timing and blocking behavior become the main problem, move application behavior into tasks:
sensor_task();
display_task();
log_task();
This path avoids two common mistakes: staying in one giant loop too long, and jumping to an RTOS before there is enough complexity to justify it.
Common Decision Mistakes
One mistake is adding FreeRTOS because the project feels more professional with tasks. Professional firmware is understandable and reliable. Sometimes that means no RTOS.
Another mistake is staying bare-metal after the main loop has become a pile of manual scheduling rules, blocking calls, and shared flags. At that point, an RTOS may simplify the design.
A third mistake is making every driver depend on FreeRTOS. Keep drivers reusable unless they truly need RTOS features internally.
A fourth mistake is treating HAL callbacks as application architecture. Callbacks and interrupts should stay short. They can wake tasks or record events, but they should not become hidden application loops.
A fifth mistake is using queues, mutexes, and priorities without an ownership model. Synchronization tools are not a replacement for clear boundaries.
Final Checklist
Before choosing FreeRTOS, ask:
- Are there multiple independent activities?
- Does any activity need to block without stopping the rest of the firmware?
- Are timing domains becoming hard to manage in one loop?
- Would queues make producer/consumer boundaries clearer?
- Are interrupts currently doing too much work?
- Can drivers remain RTOS-independent?
- Is there enough RAM for task stacks and kernel objects?
- Is the team ready to debug scheduler, priority, and stack problems?
If the answer is mostly no, keep the firmware simpler.
If the answer is mostly yes, FreeRTOS may be the right structure.
Series Wrap-Up
This series ported FreeRTOS to STM32 from the bottom up.
The path started with motivation, then prepared the CMake project, added kernel source files, explained the portable layer, configured FreeRTOSConfig.h, connected the tick, wired context-switching exceptions, created the first tasks, debugged scheduler bring-up, used queues and delays, and moved peripheral work into tasks.
The point was not only to run FreeRTOS. The point was to know what FreeRTOS is connected to: the build system, startup code, vector table, Cortex-M exceptions, interrupt priorities, stacks, heap, tasks, queues, and project boundaries.
That understanding makes generated projects less mysterious, manual ports less fragile, and firmware architecture choices more deliberate.
The best STM32 structure is the one that makes the firmware clear enough to build, debug, and maintain. Sometimes that is a loop. Sometimes it is HAL plus a few modules. Sometimes it is project-specific drivers. Sometimes it is FreeRTOS.
Choose the simplest one that actually fits the problem.