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
#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)
// 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
#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 automaticallyQueue wrapper
#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_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
CONFIG_CPP=y
CONFIG_STD_CPP17=y
CONFIG_NEWLIB_LIBC=y
CONFIG_CPLUSPLUS_EXCEPTIONS=n # Disable exceptions
CONFIG_RTTI=n # Disable RTTIZephyr thread in C++
#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
// 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:
// 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:
// In a periodic debug task
UBaseType_t watermark = uxTaskGetStackHighWaterMark(nullptr);
printf("Stack remaining: %u words\n", watermark);
// If watermark < 20, increase stack sizePriority inversion — use FreeRTOS mutexes (not binary semaphores) which implement priority inheritance:
// Priority-inheriting mutex
SemaphoreHandle_t mutex = xSemaphoreCreateMutex(); // use this
// NOT: xSemaphoreCreateBinary() // no priority inheritance