Chapter 22: RTOS Footprint Case Study
Part VI: Embedded Constraints
"The best RTOS is the one that fits your constraints—memory, CPU, and development time." — Colin Walls
The Real Challenge of Choosing an RTOS
"Our MCU only has 64 KB flash and 8 KB RAM. Which RTOS should we choose?"
This is one of the most common questions embedded developers ask. The internet is full of comparison articles, but few answer this question in a data-driven way.
This chapter skips the marketing speak and feature lists. We measure with tools. We speak with data.
We'll analyze the footprint of three mainstream RTOSes:
- FreeRTOS: The most popular open-source RTOS
- Zephyr: A modern IoT RTOS
- RT-Thread: A highly popular RTOS from China
Measurement Methodology
Before comparing, we need to establish fair measurement conditions.
Test Platform
Hardware: QEMU emulator (to avoid hardware differences)
Target: ARM Cortex-M4 (no FPU)
Compiler: GCC 13.2 (arm-none-eabi)
Optimization: -Os -flto -ffunction-sections -fdata-sections -Wl,--gc-sections
Test Scenarios
We define three test scenarios:
Scenario 1: Minimal
- Kernel scheduler only
- 1 task
- No other features
Scenario 2: Basic
- Kernel + semaphore + queue
- 3 tasks
- Timer service
Scenario 3: Typical
- Scenario 2 + shell/console
- 5 tasks
- Dynamic memory allocation
Measurement Method
# For each RTOS and scenario:
$ arm-none-eabi-size firmware.elf
$ arm-none-eabi-nm -S --size-sort firmware.elf > symbols.txt
$ bloaty firmware.elf -d compileunits > modules.txt
FreeRTOS Analysis
Minimal Configuration
// FreeRTOS minimal configuration
#define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configMINIMAL_STACK_SIZE 64
#define configTOTAL_HEAP_SIZE 1024
#define configMAX_PRIORITIES 4
#define configUSE_MUTEXES 0
#define configUSE_SEMAPHORES 0
#define configUSE_TIMERS 0
#define configUSE_QUEUE_SETS 0
Measurement results:
$ arm-none-eabi-size freertos_minimal.elf
text data bss dec hex filename
3584 120 1152 4856 12f8 freertos_minimal.elf
Section breakdown:
.text = 3,584 bytes (kernel code)
.data = 120 bytes (initialized data)
.bss = 1,152 bytes (heap + TCB)
Main components:
$ arm-none-eabi-nm -S --size-sort freertos_minimal.elf | head -10
20000100 00000400 B ucHeap # 1024 bytes heap
08000a40 00000280 T xTaskCreate
08000cc0 00000200 T vTaskSwitchContext
08000ec0 00000180 T xTaskIncrementTick
08001040 00000140 T prvIdleTask
...
Feature vs Footprint Table
FreeRTOS feature impact on footprint:
Feature .text increase .bss increase
────────────────────────────────────────────────────────────
Basic kernel (1 task) 3,584 1,152
+ Semaphores +320 +0
+ Mutexes +480 +0
+ Queues +640 +0
+ Timers +1,024 +256
+ Task notifications +256 +0
+ Event groups +512 +64
────────────────────────────────────────────────────────────
Typical configuration ~6,500 ~1,500
Zephyr Analysis
Zephyr uses Kconfig for fine-grained configuration.
Minimal Configuration
# prj.conf for Zephyr minimal
CONFIG_KERNEL=y
CONFIG_MAIN_THREAD_PRIORITY=0
CONFIG_MAIN_STACK_SIZE=512
CONFIG_IDLE_STACK_SIZE=256
CONFIG_HEAP_MEM_POOL_SIZE=0
CONFIG_MINIMAL_LIBC=y
# Disable unneeded features
CONFIG_PRINTK=n
CONFIG_LOG=n
CONFIG_SHELL=n
Measurement results:
$ arm-none-eabi-size zephyr_minimal.elf
text data bss dec hex filename
5120 256 1280 6656 1a00 zephyr_minimal.elf
Analysis: Zephyr minimal is about 1.5 KB larger than FreeRTOS (.text). This is because Zephyr has a more complete abstraction layer and device model.
Feature vs Footprint Table
Zephyr feature impact on footprint:
Feature .text increase .bss increase
────────────────────────────────────────────────────────────
Basic kernel 5,120 1,280
+ Semaphores +128 +0
+ Mutexes +256 +0
+ Queues (k_msgq) +384 +0
+ Timers +512 +128
+ Shell +12,000+ +2,000+
+ Logging +4,000+ +1,000+
+ Networking +50,000+ +10,000+
────────────────────────────────────────────────────────────
Typical (no shell) ~7,000 ~1,500
Typical (with shell) ~20,000 ~4,000
RT-Thread Analysis
RT-Thread has a nano version specifically targeting minimal footprint.
Minimal Configuration (RT-Thread Nano)
// rtconfig.h for RT-Thread Nano
#define RT_THREAD_PRIORITY_MAX 8
#define RT_TICK_PER_SECOND 1000
#define RT_USING_OVERFLOW_CHECK
#define RT_USING_HOOK
#define RT_USING_IDLE_HOOK
// Disable most features
// #define RT_USING_SEMAPHORE
// #define RT_USING_MUTEX
// #define RT_USING_MAILBOX
// #define RT_USING_MESSAGEQUEUE
Measurement results:
$ arm-none-eabi-size rtthread_minimal.elf
text data bss dec hex filename
2816 96 896 3808 ee0 rtthread_minimal.elf
Analysis: RT-Thread Nano is the smallest of the three—only 2.8 KB .text.
Feature vs Footprint Table
RT-Thread feature impact on footprint:
Feature .text increase .bss increase
────────────────────────────────────────────────────────────
Basic kernel (Nano) 2,816 896
+ Semaphores +192 +0
+ Mutexes +256 +0
+ Mailbox +320 +0
+ Message queue +384 +0
+ Timer +512 +128
+ FinSH shell +10,000+ +2,000+
+ Device framework +3,000+ +500+
────────────────────────────────────────────────────────────
Typical (no shell) ~5,000 ~1,200
Typical (with shell) ~15,000 ~3,500
Comparison Summary
Minimal Configuration Comparison
RTOS .text .data .bss Total
────────────────────────────────────────────────────────────
RT-Thread Nano 2,816 96 896 3,808
FreeRTOS 3,584 120 1,152 4,856
Zephyr 5,120 256 1,280 6,656
Visualization:
.text size (bytes):
RT-Thread Nano ████████████████ 2,816
FreeRTOS ████████████████████ 3,584
Zephyr ████████████████████████████ 5,120
0 1K 2K 3K 4K 5K 6K
Typical Configuration Comparison
RTOS .text .data .bss Total
────────────────────────────────────────────────────────────
RT-Thread 5,000 128 1,200 6,328
FreeRTOS 6,500 150 1,500 8,150
Zephyr 7,000 300 1,500 8,800
Feature Richness vs Footprint Trade-off
Footprint
▲
│
Zephyr ● │ ← Most features, largest footprint
│
FreeRTOS ● │ ← Balanced
│
RT-Thread Nano ● ← Minimal footprint, fewer features
│
────────────────┼──────────────► Features
│
Selection Recommendations
When to Choose FreeRTOS
✅ Recommended when:
- Need mature, stable, widely-used solution
- Team already has FreeRTOS experience
- Need AWS IoT integration
- Memory constraints are moderate (32 KB+ flash)
❌ Not recommended when:
- Extremely tight memory (< 16 KB flash)
- Need advanced networking stack
- Need comprehensive device driver framework
When to Choose Zephyr
✅ Recommended when:
- Building IoT products with networking
- Need comprehensive device driver support
- Want modern build system (CMake + Kconfig)
- Have sufficient memory (64 KB+ flash)
❌ Not recommended when:
- Extremely tight memory constraints
- Simple bare-metal would suffice
- Team unfamiliar with Kconfig/devicetree
When to Choose RT-Thread
✅ Recommended when:
- Extremely tight memory (< 16 KB flash)
- Need minimal kernel (RT-Thread Nano)
- Chinese documentation/community preferred
- Need rich middleware (GUI, filesystem, etc.)
❌ Not recommended when:
- Need extensive English documentation
- Need AWS/Azure cloud integration
- Team unfamiliar with RT-Thread ecosystem
Optimization Techniques
Regardless of which RTOS you choose, these techniques help reduce footprint:
1. Disable Unused Features
// FreeRTOS example
#define configUSE_MUTEXES 0 // If not using mutexes
#define configUSE_RECURSIVE_MUTEXES 0
#define configUSE_COUNTING_SEMAPHORES 0
#define configUSE_QUEUE_SETS 0
#define configUSE_TASK_NOTIFICATIONS 0
2. Reduce Priority Levels
// Fewer priorities = smaller scheduler data structures
#define configMAX_PRIORITIES 4 // Instead of 32
3. Minimize Stack Sizes
// Measure actual usage, then add 25% margin
#define configMINIMAL_STACK_SIZE 64 // words
#define configTIMER_TASK_STACK_DEPTH 128
4. Use Static Allocation
// FreeRTOS static allocation (no heap overhead)
#define configSUPPORT_STATIC_ALLOCATION 1
#define configSUPPORT_DYNAMIC_ALLOCATION 0
StaticTask_t xTaskBuffer;
StackType_t xStack[128];
xTaskCreateStatic(task_func, "Task", 128, NULL, 1, xStack, &xTaskBuffer);
5. Compiler Optimization
# Always use these for release builds
$ arm-none-eabi-gcc -Os -flto \
-ffunction-sections -fdata-sections \
-Wl,--gc-sections \
--specs=nano.specs \
...
Case Study: Fitting into 32 KB Flash
Requirement: IoT sensor node with:
- 3 tasks (sensor, communication, LED)
- UART driver
- Simple protocol parsing
- 32 KB flash, 8 KB RAM
Initial attempt with FreeRTOS:
$ arm-none-eabi-size firmware.elf
text data bss dec hex filename
38912 512 4096 43520 aa00 firmware.elf
Problem: 38 KB > 32 KB limit!
Optimization steps:
Step 1: Disable unused features
configUSE_TIMERS = 0
configUSE_MUTEXES = 0
Result: 35,840 bytes (-3 KB)
Step 2: Use newlib-nano
--specs=nano.specs
Result: 28,672 bytes (-7 KB)
Step 3: Replace printf with custom
Custom uart_print_int()
Result: 26,624 bytes (-2 KB)
Step 4: Enable LTO
-flto
Result: 24,576 bytes (-2 KB)
Step 5: Static allocation
configSUPPORT_DYNAMIC_ALLOCATION = 0
Result: 23,552 bytes (-1 KB)
Final: 23.5 KB < 32 KB ✓
Summary
- Measurement methodology: Fair comparison requires identical conditions (compiler, optimization, platform)
- Minimal footprint ranking: RT-Thread Nano (2.8 KB) < FreeRTOS (3.6 KB) < Zephyr (5.1 KB)
- Feature trade-off: More features = larger footprint; choose based on actual needs
- Selection criteria:
- Extremely constrained: RT-Thread Nano
- Balanced: FreeRTOS
- Feature-rich IoT: Zephyr
- Optimization techniques:
- Disable unused features
- Reduce priority levels and stack sizes
- Use static allocation
- Compiler optimization (-Os, LTO, gc-sections)
- Use newlib-nano
- Key principle: Measure, compare, then decide—don't rely on marketing claims