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