Domain Track
Difficulty 3/5Embedded 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 allocationCRTP 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 timeStack 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
)