Skip to content
C++
Domain Deep-Dive
Advanced

RTOS Integration with C++ (FreeRTOS & Zephyr)

"Using C++ with FreeRTOS and Zephyr: task wrappers, ISR-safe patterns, C++ RAII for semaphores and mutexes, and avoiding common embedded C++ pitfalls."

TL;DR

FreeRTOS and Zephyr have C APIs but integrate well with C++. Wrap RTOS primitives in RAII classes to prevent resource leaks. Use static storage for task stacks and TCBs. Keep ISRs minimal — defer work to tasks via queues. Never use new/delete or exceptions in embedded C++.

FreeRTOS with C++

Task wrapper

cpp
#include "FreeRTOS.h"
#include "task.h"

class Task {
public:
    Task(const char* name, uint16_t stack_words, UBaseType_t priority) {
        xTaskCreate(
            &Task::taskEntry,  // static trampoline
            name,
            stack_words,
            this,              // pass 'this' as pvParameters
            priority,
            &handle_
        );
    }

    virtual ~Task() {
        if (handle_) vTaskDelete(handle_);
    }

    TaskHandle_t handle() const { return handle_; }

protected:
    virtual void run() = 0;  // override in derived class

private:
    static void taskEntry(void* arg) {
        static_cast<Task*>(arg)->run();
        // Task should not return; if it does, delete it
        vTaskDelete(nullptr);
    }

    TaskHandle_t handle_{nullptr};
};

// Usage
class LedTask : public Task {
public:
    LedTask() : Task("LED", 128, 1) {}

protected:
    void run() override {
        while (true) {
            toggleLED();
            vTaskDelay(pdMS_TO_TICKS(500));
        }
    }
};

// Statically allocated — no heap needed
static LedTask led_task;

Static allocation (no heap)

cpp
// FreeRTOS static allocation — deterministic, no heap fragmentation
class StaticTask {
public:
    StaticTask(const char* name, UBaseType_t priority) {
        handle_ = xTaskCreateStatic(
            &StaticTask::taskEntry,
            name,
            STACK_DEPTH,
            this,
            priority,
            stack_,   // stack buffer
            &tcb_     // TCB storage
        );
    }

protected:
    virtual void run() = 0;

private:
    static void taskEntry(void* arg) {
        static_cast<StaticTask*>(arg)->run();
        vTaskDelete(nullptr);
    }

    static constexpr uint32_t STACK_DEPTH = 256;
    StackType_t  stack_[STACK_DEPTH];
    StaticTask_t tcb_;
    TaskHandle_t handle_;
};

RAII mutex wrapper

cpp
#include "semphr.h"

class Mutex {
public:
    Mutex() : handle_(xSemaphoreCreateMutex()) {}
    ~Mutex() { vSemaphoreDelete(handle_); }

    Mutex(const Mutex&) = delete;
    Mutex& operator=(const Mutex&) = delete;

    bool lock(TickType_t timeout = portMAX_DELAY) {
        return xSemaphoreTake(handle_, timeout) == pdTRUE;
    }

    void unlock() { xSemaphoreGive(handle_); }

    SemaphoreHandle_t native() const { return handle_; }

private:
    SemaphoreHandle_t handle_;
};

class LockGuard {
public:
    explicit LockGuard(Mutex& m) : mutex_(m) { mutex_.lock(); }
    ~LockGuard() { mutex_.unlock(); }
    LockGuard(const LockGuard&) = delete;

private:
    Mutex& mutex_;
};

// Usage
Mutex sensor_mutex;

void readSensor() {
    LockGuard lock(sensor_mutex);  // locked here
    // ... critical section ...
}  // unlocked automatically

Queue wrapper

cpp
#include "queue.h"

template<typename T, size_t N>
class Queue {
public:
    Queue() : handle_(xQueueCreate(N, sizeof(T))) {}
    ~Queue() { vQueueDelete(handle_); }

    bool send(const T& item, TickType_t timeout = portMAX_DELAY) {
        return xQueueSend(handle_, &item, timeout) == pdTRUE;
    }

    bool sendFromISR(const T& item) {
        BaseType_t higher_prio_task_woken = pdFALSE;
        bool ok = xQueueSendFromISR(handle_, &item, &higher_prio_task_woken) == pdTRUE;
        portYIELD_FROM_ISR(higher_prio_task_woken);
        return ok;
    }

    bool receive(T& item, TickType_t timeout = portMAX_DELAY) {
        return xQueueReceive(handle_, &item, timeout) == pdTRUE;
    }

private:
    QueueHandle_t handle_;
};

// Usage
struct SensorData { int32_t temperature; uint32_t timestamp; };

Queue<SensorData, 16> sensor_queue;

// In ISR — minimal work, just enqueue
extern "C" void SENSOR_IRQHandler() {
    SensorData data{readRegister(), getTimestamp()};
    sensor_queue.sendFromISR(data);
}

// In task — process data
class ProcessingTask : public Task {
    void run() override {
        SensorData data;
        while (sensor_queue.receive(data)) {
            processReading(data);
        }
    }
};

Zephyr with C++

Zephyr has first-class C++ support from Zephyr 3.0+.

CMakeLists.txt for C++

cmake
cmake_minimum_required(VERSION 3.20)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_firmware CXX C)

# Enable C++17
set(CMAKE_CXX_STANDARD 17)

target_sources(app PRIVATE src/main.cpp src/sensor.cpp)

prj.conf

cpp
CONFIG_CPP=y
CONFIG_STD_CPP17=y
CONFIG_NEWLIB_LIBC=y
CONFIG_CPLUSPLUS_EXCEPTIONS=n  # Disable exceptions
CONFIG_RTTI=n                   # Disable RTTI

Zephyr thread in C++

cpp
#include <zephyr/kernel.h>

class ZephyrTask {
public:
    ZephyrTask(k_thread_entry_t entry, int priority) {
        tid_ = k_thread_create(
            &thread_data_,
            stack_,
            STACK_SIZE,
            entry,
            this, nullptr, nullptr,
            priority,
            0,           // options
            K_NO_WAIT    // start immediately
        );
    }

private:
    static constexpr size_t STACK_SIZE = 1024;
    K_THREAD_STACK_MEMBER(stack_, STACK_SIZE);
    struct k_thread thread_data_;
    k_tid_t tid_;
};

// Zephyr mutex RAII
class ZMutex {
    struct k_mutex m_;
public:
    ZMutex() { k_mutex_init(&m_); }
    void lock() { k_mutex_lock(&m_, K_FOREVER); }
    void unlock() { k_mutex_unlock(&m_); }
};

ISR best practices

cpp
// Rule: ISRs must be short and deterministic
// - No blocking calls (no mutex lock, no vTaskDelay)
// - No dynamic memory allocation
// - No logging (typically)
// - Minimal computation

// Pattern: ISR posts to queue, task does the work
extern "C" void UART_IRQHandler() {
    uint8_t byte = UART->DR;
    BaseType_t woken = pdFALSE;
    xQueueSendFromISR(rx_queue, &byte, &woken);
    portYIELD_FROM_ISR(woken);  // yield if higher-priority task unblocked
}

// Task processes bytes
void uartTask(void*) {
    uint8_t byte;
    while (true) {
        if (xQueueReceive(rx_queue, &byte, portMAX_DELAY))
            protocol.consume(byte);
    }
}

Common pitfalls

Global constructors run before main() — RTOS isn't started yet. Don't create RTOS objects (tasks, queues, mutexes) as global objects:

cpp
// BAD — constructor runs before FreeRTOS scheduler starts
Mutex global_mutex;  // xSemaphoreCreateMutex() called before vTaskStartScheduler()

// OK — use lazy initialization or init in main()
Mutex* global_mutex = nullptr;

int main() {
    global_mutex = new Mutex();  // but avoid heap...
    // Better: static with explicit init
    static std::aligned_storage_t<sizeof(Mutex), alignof(Mutex)> mutex_storage;
    global_mutex = new (&mutex_storage) Mutex();

    vTaskStartScheduler();
}

Stack overflow — embedded stacks are tiny. Use uxTaskGetStackHighWaterMark() to check:

cpp
// In a periodic debug task
UBaseType_t watermark = uxTaskGetStackHighWaterMark(nullptr);
printf("Stack remaining: %u words\n", watermark);
// If watermark < 20, increase stack size

Priority inversion — use FreeRTOS mutexes (not binary semaphores) which implement priority inheritance:

cpp
// Priority-inheriting mutex
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();  // use this
// NOT: xSemaphoreCreateBinary()  // no priority inheritance
Edit on GitHubUpdated 2026-05-01T00:00:00.000Z