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
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 SIMDCompiler flags
# 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
-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:
// 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:
#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:
// 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:
// 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
// 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*)
} > CCMRAMSWO 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:
// 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
// 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 ...
}
}