Skip to content
C++
Domain Track
Difficulty 3/5

Embedded Systems C++ Patterns

C++ for embedded systems — no heap, no exceptions, constexpr configuration, CRTP hardware abstraction, fixed-size buffers, and interrupt-safe code.

TL;DR

Embedded C++ avoids heap allocation, exceptions, and RTTI. Use constexpr for configuration, fixed-size containers, CRTP for zero-cost hardware abstraction, and volatile/atomics for registers and ISRs.


No Heap — Fixed-Size Containers

cpp
// Replace std::vector with fixed-capacity alternative
template<typename T, size_t Capacity>
class StaticVector {
    alignas(T) std::byte storage_[sizeof(T) * Capacity];
    size_t size_ = 0;

public:
    template<typename... Args>
    bool push_back(Args&&... args) {
        if (size_ >= Capacity) return false;
        ::new(&storage_[size_ * sizeof(T)]) T(std::forward<Args>(args)...);
        ++size_;
        return true;
    }

    T& operator[](size_t i) { return reinterpret_cast<T*>(storage_)[i]; }
    size_t size() const { return size_; }
    bool full()   const { return size_ == Capacity; }
    bool empty()  const { return size_ == 0; }
    static constexpr size_t capacity() { return Capacity; }

    ~StaticVector() {
        for (size_t i = 0; i < size_; ++i)
            reinterpret_cast<T*>(storage_)[i].~T();
    }
};

StaticVector<int, 16> buf;
buf.push_back(42);  // no heap allocation

CRTP Hardware Abstraction Layer

Zero-cost polymorphism — no vtable, full inlining:

cpp
// Generic GPIO interface via CRTP
template<typename Derived>
class GPIO {
public:
    void set_high()  { static_cast<Derived*>(this)->set_pin(true); }
    void set_low()   { static_cast<Derived*>(this)->set_pin(false); }
    bool read()      { return static_cast<Derived*>(this)->read_pin(); }

    void toggle() {
        if (read()) set_low();
        else        set_high();
    }
};

// STM32 concrete implementation
struct STM32_PA5 : GPIO<STM32_PA5> {
    void set_pin(bool high) {
        if (high) GPIOA->BSRR = (1 << 5);
        else      GPIOA->BRR  = (1 << 5);
    }
    bool read_pin() {
        return (GPIOA->IDR >> 5) & 1;
    }
};

// Completely inlined at compile time
STM32_PA5 led;
led.set_high();
led.toggle();

Register Access with volatile

cpp
// Map hardware registers to typed pointers
struct UART_Regs {
    volatile uint32_t DR;    // data register
    volatile uint32_t SR;    // status register
    volatile uint32_t BRR;   // baud rate register
    volatile uint32_t CR1;   // control register 1
};

constexpr auto* UART1 = reinterpret_cast<UART_Regs*>(0x40011000);

// Write register
UART1->CR1 |= (1 << 3);   // enable TX

// Read register (prevents optimization away)
while (!(UART1->SR & (1 << 7))) {}  // wait for TX empty

UART1->DR = 'A';  // send character

// Bit-field register model (type-safe)
struct ControlReg {
    uint32_t enable   : 1;
    uint32_t mode     : 2;
    uint32_t reserved : 29;
};
volatile ControlReg& ctrl = *reinterpret_cast<volatile ControlReg*>(0x40000000);
ctrl.enable = 1;
ctrl.mode   = 2;

Interrupt-Safe Code

cpp
#include <atomic>

// Use std::atomic for ISR-shared variables
std::atomic<uint32_t> tick_count{0};

// Interrupt Service Routine (ISR)
extern "C" void SysTick_Handler() {
    tick_count.fetch_add(1, std::memory_order_relaxed);
}

// Main thread reads atomically
uint32_t get_ticks() {
    return tick_count.load(std::memory_order_relaxed);
}

// For complex data: disable interrupts around critical section
void critical_update() {
    __disable_irq();   // ARM: CPSID i
    // ... update shared data ...
    __enable_irq();    // ARM: CPSIE i
}

// Or RAII interrupt lock
struct CriticalSection {
    CriticalSection()  { __disable_irq(); }
    ~CriticalSection() { __enable_irq();  }
};

{
    CriticalSection cs;
    shared_data.update();
}

constexpr Configuration

cpp
// All configuration at compile time — no runtime overhead
struct HardwareConfig {
    static constexpr uint32_t CPU_FREQ_HZ    = 168'000'000;
    static constexpr uint32_t UART_BAUD      = 115'200;
    static constexpr uint32_t FLASH_BASE     = 0x08000000;
    static constexpr uint32_t SRAM_SIZE      = 192 * 1024;

    static constexpr uint32_t uart_brr() {
        return CPU_FREQ_HZ / UART_BAUD;
    }
};

// Checked at compile time
static_assert(HardwareConfig::SRAM_SIZE >= 64 * 1024,
    "Need at least 64KB SRAM");

// Constexpr lookup tables (no flash penalty from runtime init)
constexpr std::array<uint8_t, 256> build_crc_table() {
    std::array<uint8_t, 256> tbl{};
    for (int i = 0; i < 256; ++i) {
        uint8_t crc = i;
        for (int j = 0; j < 8; ++j)
            crc = crc & 1 ? (crc >> 1) ^ 0x8C : crc >> 1;
        tbl[i] = crc;
    }
    return tbl;
}
constexpr auto crc_table = build_crc_table();  // in flash, computed at compile time

Stack Allocation Patterns

cpp
// Object pool for ISR-safe allocation
template<typename T, size_t N>
class ISR_Pool {
    T objects_[N];
    std::atomic<uint32_t> bitmask_{0};  // one bit per object

public:
    T* acquire() {
        uint32_t mask = bitmask_.load(std::memory_order_relaxed);
        while (true) {
            int free_idx = __builtin_ctz(~mask);  // find first free bit
            if (free_idx >= N) return nullptr;
            uint32_t new_mask = mask | (1u << free_idx);
            if (bitmask_.compare_exchange_weak(mask, new_mask,
                    std::memory_order_acquire))
                return &objects_[free_idx];
        }
    }

    void release(T* p) {
        int idx = p - objects_;
        bitmask_.fetch_and(~(1u << idx), std::memory_order_release);
    }
};

Linker Script Integration

cpp
// Symbols from linker script
extern "C" {
    extern uint32_t _sidata;   // init data source (flash)
    extern uint32_t _sdata;    // data section start (RAM)
    extern uint32_t _edata;    // data section end
    extern uint32_t _sbss;     // bss start
    extern uint32_t _ebss;     // bss end
}

// C++ startup (called before main)
void system_init() {
    // Copy initialized data from flash to RAM
    uint32_t* src = &_sidata;
    for (uint32_t* dst = &_sdata; dst < &_edata; ++dst)
        *dst = *src++;

    // Zero BSS
    for (uint32_t* p = &_sbss; p < &_ebss; ++p)
        *p = 0;

    // Call C++ global constructors
    // (typically handled by _init/_fini or __libc_init_array)
}

Compile Flags for Embedded

cmake
# Bare-metal ARM Cortex-M4 example
target_compile_options(firmware PRIVATE
    -mcpu=cortex-m4
    -mthumb
    -mfpu=fpv4-sp-d16
    -mfloat-abi=hard
    -Os           # optimize for size
    -fno-exceptions
    -fno-rtti
    -ffunction-sections
    -fdata-sections
    -ffreestanding
)

target_link_options(firmware PRIVATE
    -T linker.ld
    -Wl,--gc-sections    # remove unused code
    -Wl,-Map=output.map  # generate map file
    -nostdlib
    -specs=nosys.specs
)