Skip to content
C++
Domain Deep-Dive
Advanced

ARM Cortex-M C++ Specifics

"C++ features and pitfalls specific to ARM Cortex-M: Thumb-2 instruction set, ABI, bit-banding, CMSIS, FPU context, and compiler flags for optimal code generation."

TL;DR

ARM Cortex-M is the dominant architecture for embedded C++. The M0/M0+ use Thumb-only (16/32-bit), M3/M4/M33 add Thumb-2 (full 32-bit with DSP extensions), and M4F/M7F add a single-precision (M4) or double-precision (M7) FPU. Knowing which core you have determines which compiler flags and C++ features are cost-free vs. expensive.

Cortex-M core variants

cpp
Core      ISA         FPU              Typical use
─────────────────────────────────────────────────────────
M0        Thumb       None             Ultra-low-power
M0+       Thumb       None             Same + lower latency pipeline
M3        Thumb-2     None             General-purpose microcontrollers
M4        Thumb-2     Optional (SP)    DSP + control (STM32F4, NRF52)
M4F       Thumb-2     SP hard-float    Same with FPU always present
M7        Thumb-2     SP+DP optional   High-perf (STM32H7, iMXRT)
M33       Thumb-2     Optional         TrustZone security
M55       Thumb-2+MVE Optional         Helium SIMD

Compiler flags

cmake
# STM32F4 (Cortex-M4F with FPU)
target_compile_options(firmware PRIVATE
    -mcpu=cortex-m4            # generate M4 instructions
    -mthumb                    # Thumb-2 mode (required for M4)
    -mfpu=fpv4-sp-d16          # FP unit: single-precision, 16 D-regs
    -mfloat-abi=hard           # pass FP args in FP registers (ABI)
    -ffast-math                # allow FP reassociation, no NaN/Inf
    -ffunction-sections        # each function in own ELF section
    -fdata-sections            # each variable in own section
    # Linker will GC dead sections with --gc-sections
)

target_link_options(firmware PRIVATE
    -Wl,--gc-sections          # remove unused code/data
    -Wl,--print-memory-usage   # show flash/RAM usage after link
    -specs=nosys.specs         # no OS syscalls (newlib-nano)
    -specs=nano.specs          # smaller newlib (printf without %f if needed)
)

Float ABI: hard vs soft vs softfp

cpp
-mfloat-abi=soft    → all FP done in software, no FPU used
-mfloat-abi=softfp  → FPU used internally, but args passed in int regs (compatible with soft callers)
-mfloat-abi=hard    → FPU used + FP args in FP registers (fastest, not compatible with soft libs)

Mixing -mfloat-abi=hard and soft code at link time causes undefined behavior. All libraries must match.

FPU context in ISRs

The FPU has a lazy stacking behavior — M4F only saves FP registers to the exception stack on the first FP instruction inside the ISR:

cpp
// Lazy stacking: if ISR never uses FPU, no save penalty
// If ISR uses FPU: ~64 extra bytes pushed to stack, ~16 cycle overhead

// To guarantee no FPU context save penalty, avoid float in ISRs:
extern "C" void TIM2_IRQHandler() {
    // OK: integer-only, no FPU save needed
    ++g_tick_count;
    TIM2->SR &= ~TIM_SR_UIF;  // clear update interrupt flag
}

// If your ISR must use float, disable lazy stacking (always save upfront):
void disableFPULazyStacking() {
    FPU->FPCCR &= ~FPU_FPCCR_LSPEN_Msk;
}

CMSIS — Cortex Microcontroller Software Interface Standard

CMSIS provides standardized intrinsics for core features:

cpp
#include "cmsis_gcc.h"   // or cmsis_armcc.h for ARMCC

// CPU control
__enable_irq();          // CPSIE i — enable interrupts
__disable_irq();         // CPSID i — disable interrupts
uint32_t m = __get_PRIMASK();
__set_PRIMASK(m);        // restore previous state

// Memory barriers
__DSB();    // Data Synchronization Barrier
__DMB();    // Data Memory Barrier
__ISB();    // Instruction Synchronization Barrier

// Sleep
__WFI();    // Wait For Interrupt
__WFE();    // Wait For Event

// Bit manipulation (Thumb-2 instructions)
__REV(x)   // reverse byte order in 32-bit word
__CLZ(x)   // count leading zeros (→ log2)
__RBIT(x)  // reverse bits

// SIMD DSP extensions (Cortex-M4/M7 only)
__SADD8(a, b)   // parallel add of 4 signed bytes
__SMUAD(a, b)   // dual 16-bit multiply and accumulate (for FIR filters)

Bit-banding — atomic single-bit access

On Cortex-M3/M4, the bit-band region maps each bit to a full word address. Reading/writing that word atomically accesses one bit — no read-modify-write needed:

cpp
// SRAM bit-band alias: 0x22000000 + (byte_offset × 32) + (bit_number × 4)
#define BITBAND_SRAM(var, bit) \
    (*reinterpret_cast<volatile uint32_t*>( \
        0x22000000 + (reinterpret_cast<uint32_t>(&(var)) - 0x20000000) * 32 \
        + (bit) * 4))

// Peripheral bit-band alias
#define BITBAND_PERIPH(addr, bit) \
    (*reinterpret_cast<volatile uint32_t*>( \
        0x42000000 + ((addr) - 0x40000000) * 32 + (bit) * 4))

// Usage: atomic single-bit toggle without masking
volatile uint8_t g_flags = 0;
volatile uint32_t& flag0 = BITBAND_SRAM(g_flags, 0);  // address of bit 0

flag0 = 1;  // atomic set
flag0 = 0;  // atomic clear
if (flag0)  // atomic read
    doSomething();

Stack alignment and ABI

ARM EABI requires 8-byte stack alignment at public function boundaries:

cpp
// The linker script should align _estack to 8 bytes
// Startup code must ensure 8-byte alignment before calling main()

// Check if you have unaligned access fault enabled
void enableUnalignedFaultDetection() {
    SCB->CCR |= SCB_CCR_UNALIGN_TRP_Msk;  // trap on unaligned access
}

// Packed structs break alignment — use carefully
struct __attribute__((packed)) ProtocolHeader {
    uint8_t  magic;
    uint32_t length;  // may be at unaligned address — ARM may fault
};

// Better: use memcpy to load from potentially-unaligned memory
uint32_t readUnaligned(const uint8_t* p) {
    uint32_t val;
    __builtin_memcpy(&val, p, sizeof(val));  // compiler generates LDR or LDRB×4
    return val;
}

Memory layout tips

cpp
// Place read-only data in flash (already there by default for const)
const uint8_t lut[256] = { /* ... */ };  // in flash, no RAM cost

// Force a function into RAM for faster execution (at RAM cost)
__attribute__((section(".ramfunc")))
void criticalISR() {
    // executes from RAM — no flash wait states
}

// On STM32F4, CCMRAM (Core Coupled Memory) at 0x10000000
// is accessible only by the CPU (not DMA), zero wait states
__attribute__((section(".ccmram")))
static uint8_t isr_stack[2048];

__attribute__((section(".ccmram")))
extern "C" void USART1_IRQHandler() { /* fast path */ }

Add to linker script:

.ramfunc : {
    *(.ramfunc)
    *(.ramfunc*)
} > RAM AT > FLASH

.ccmram : {
    *(.ccmram)
    *(.ccmram*)
} > CCMRAM

SWO trace output

Serial Wire Output (SWO) is a one-pin debug trace available on Cortex-M3/M4/M7. It's faster than UART and doesn't require dedicated pins:

cpp
// ITM (Instrumentation Trace Macrocell) printf
int ITM_SendChar(uint32_t ch) {
    if ((ITM->TCR & ITM_TCR_ITMENA_Msk) &&
        (ITM->TER & 1)) {           // stimulus port 0 enabled
        while (ITM->PORT[0].u32 == 0);   // wait if full
        ITM->PORT[0].u8 = static_cast<uint8_t>(ch);
    }
    return ch;
}

// Redirect printf to ITM (implement in your syscalls.c)
int _write(int fd, char* ptr, int len) {
    for (int i = 0; i < len; ++i)
        ITM_SendChar(*ptr++);
    return len;
}

The STM32CubeIDE and Ozone debugger both display SWO output in real-time without pausing execution.

Printf debugging safely

cpp
// Avoid printf in ISRs — it can block on UART TX
// Use a ring buffer + flush in main loop
class DebugLog {
    static constexpr size_t BUF_SIZE = 512;
    char buf_[BUF_SIZE];
    volatile size_t head_ = 0, tail_ = 0;

public:
    void write(char c) {
        size_t next = (head_ + 1) % BUF_SIZE;
        if (next != tail_) { buf_[head_] = c; head_ = next; }
    }

    void flush(UART_HandleTypeDef* huart) {
        while (tail_ != head_) {
            HAL_UART_Transmit(huart, (uint8_t*)&buf_[tail_], 1, 1);
            tail_ = (tail_ + 1) % BUF_SIZE;
        }
    }
};

static DebugLog g_debug;

// ISR — just buffer the message
extern "C" void TIM2_IRQHandler() {
    g_debug.write('T');
}

// Main loop — safe to flush
void mainLoop() {
    while (true) {
        g_debug.flush(&huart1);
        // ... other work ...
    }
}
Edit on GitHubUpdated 2026-05-01T00:00:00.000Z