Programming · 55 pages
- C language — 15 lessonstutorial
- Lesson 1 — Hello, C
- Lesson 2 — Types & literals
- Lesson 3 — Expressions & operators
- Lesson 4 — Control flow
- Lesson 5 — Functions
- Lesson 6 — Arrays
- Lesson 7 — Pointers
- Lesson 8 — Strings
- Lesson 9 — Structs & unions
- Lesson 10 — Dynamic memory
- Lesson 11 — Preprocessor
- Lesson 12 — Multi-file projects
- Lesson 13 — Standard library
- Lesson 14 — Undefined behavior
- Lesson 15 — Modern C (C11/17/23)
- C++ programming — 15 lessonstutorial
- Lesson 1 — Hello, C++
- Lesson 2 — References & namespaces
- Lesson 3 — Classes & RAII
- Lesson 4 — Inheritance & polymorphism
- Lesson 5 — Templates
- Lesson 6 — STL containers
- Lesson 7 — Iterators & algorithms
- Lesson 8 — Smart pointers
- Lesson 9 — Move semantics
- Lesson 10 — Lambdas
- Lesson 11 — Error handling
- Lesson 12 — Concurrency
- Lesson 13 — Modules (C++20)
- Lesson 14 — Modern C++23
- Lesson 15 — C++ on Cortex-M
- Bare-metal C tutorial — 10 lessonsembedded
- Lesson 1 — Toolchain setup
- Lesson 2 — Bare-metal blink
- Lesson 3 — Registers in C
- Lesson 4 — Memory map
- Lesson 5 — Linker script
- Lesson 6 — Interrupts & ISRs
- Lesson 7 — Bit patterns
- Lesson 8 — Fixed-point math
- Lesson 9 — State machines
- Lesson 10 — Ring buffers + DMA
- Embedded C patterns
- volatile & memory-mapped IO
- CMSIS-Core
- CMSIS-DSP
- Linker scripts
- Startup & vector table
- C++ on Cortex-M
- Rust on Cortex-Mmodern
- Inline assembly
- Bare-metal libc & newlib-nano
- Bootloaders & DFU
- OTA update flow
Embedded C patterns
The C patterns that show up in every Cortex-M codebase — volatile, register access, alignment, packed structs, weak symbols, and what they actually mean.
C on a microcontroller looks like C anywhere else, but the constraints are different. There's no OS to catch your bugs, no malloc safety net, no thread that "just" reschedules around your busy wait. The patterns below are what experienced firmware engineers reach for; ignoring them gets you ghost bugs that disappear when you add a debug print.
volatile — what it does and doesn't do
volatile uint32_t *const STATUS = (uint32_t *)0x40000000;
while (*STATUS & 1) { /* wait */ }volatile tells the compiler: do not optimise away accesses to this variable. Every read in the source becomes a load instruction; every write becomes a store. Without it, the compiler will hoist *STATUS out of the loop and your code spins forever.
What volatile does not do:
- It is not atomic.
volatile int x; x++;is still load-modify-store on Cortex-M, and an interrupt between them will corruptx. - It is not a memory barrier. It orders accesses to the same volatile variable, but the compiler is free to reorder volatile accesses relative to non-volatile data, and the CPU is free to reorder memory accesses generally (see memory model).
- It is not magic peripheral synchronisation. You still need
__DSB()after writing to a peripheral register if the next instruction depends on the side effect.
The rule: volatile only for hardware registers and variables shared with ISRs. Don't sprinkle it elsewhere — it disables useful optimisations.
Bit manipulation idioms
// Set bit
reg |= (1u << n);
// Clear bit
reg &= ~(1u << n);
// Toggle bit
reg ^= (1u << n);
// Test bit
if (reg & (1u << n)) { ... }
// Set a multi-bit field to a value
reg = (reg & ~(MASK << POS)) | ((value & MASK) << POS);The last one — clearing the field, then writing the new value — is the standard pattern for peripheral register fields. Don't write the simpler reg |= (value << POS) because if value has bits clear that were previously set, you won't actually clear them.
CMSIS provides _Msk and _Pos macros for every documented register field:
USART2->BRR = (USART2->BRR & ~USART_BRR_DIV_Mantissa_Msk)
| (mantissa << USART_BRR_DIV_Mantissa_Pos);Packed structures
A struct laid out by the compiler usually has padding for alignment:
struct frame {
uint8_t type; // 1 byte
uint32_t length; // 4 bytes — but offset 4, not 1, because of alignment
uint16_t crc;
};
sizeof(struct frame) == 12; // not 7!When the bytes have to match a wire format or memory-mapped layout, force packed:
struct __attribute__((packed)) frame {
uint8_t type;
uint32_t length;
uint16_t crc;
};
sizeof(struct frame) == 7;Alignment
__attribute__((aligned(64))) static uint8_t dma_buf[1024]; // GCC
alignas(64) static uint8_t dma_buf[1024]; // C11 stdalign.hDMA buffers should be aligned to cache lines (32 bytes on Cortex-M7) when the buffer will be flushed/invalidated, otherwise the maintenance op spills into adjacent data.
Stacks should be 8-byte aligned (AAPCS requirement). The startup code typically ensures this.
Weak symbols and the default handler
In every CMSIS startup file, every exception handler is declared weak:
__attribute__((weak, alias("Default_Handler"))) void NMI_Handler(void);
__attribute__((weak, alias("Default_Handler"))) void HardFault_Handler(void);
// ...
void Default_Handler(void) {
while (1) {}
}If you define HardFault_Handler in your code, the linker uses yours; otherwise it falls back to Default_Handler. This is how vendor startup files work without forcing you to implement every handler.
You can apply the same trick to library functions you want users to be able to override:
__attribute__((weak)) void on_button_press(void) { /* default no-op */ }
// User can define their own on_button_press elsewhereAtomicity on Cortex-M
For a read-modify-write on a shared variable:
Cortex-M0/M0+ — no LDREX/STREX. Disable interrupts:
uint32_t primask = __get_PRIMASK();
__disable_irq();
counter++;
__set_PRIMASK(primask);Cortex-M3 and up — use <stdatomic.h>:
#include <stdatomic.h>
atomic_uint_least32_t counter;
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);GCC emits a LDREX/STREX loop. For most counters, memory_order_relaxed is enough. For lock-free linked lists, you need memory_order_acquire/release.
Avoiding malloc
Most firmware codebases either:
- Don't use malloc at all — every buffer is statically allocated, sized at compile time.
- Use a fixed-size pool allocator —
freertos+CMSIS_OS2 osMemoryPoolNew()or similar. - Use FreeRTOS heap_4 as the global malloc — it's deterministic-ish, defrag-friendly, and trackable.
The reason: vanilla malloc() from newlib fragments after a few thousand allocations, and a 16 KB heap will silently start failing in a way that's nearly impossible to reproduce in debug. Static allocation makes worst-case memory use a compile-time property.
restrict and aliasing
If you tell the compiler that two pointers don't alias, it can keep values in registers across stores:
void copy(int * restrict dst, const int * restrict src, size_t n) {
for (size_t i = 0; i < n; i++) dst[i] = src[i];
}Without restrict, the compiler must assume src[i+1] might be the same address as dst[i], so it can't keep src values in registers across the store. With restrict, it can.
Especially impactful on Cortex-M7 (dual-issue) and M85 (Helium). Saves ~10–30% on tight loops with multiple arrays.
static const for tables in flash
static const uint8_t sine_table[256] = { /* values */ };The static const puts the table in .rodata, which the linker places in flash. Without const, it goes in .data, which is copied from flash to RAM at startup and lives in RAM forever. For a 256-byte table that's annoying; for a 64 KB voice prompt it's catastrophic.
See also
- CMSIS — the standard register definitions.
- Linker scripts — controlling where data lives.
- Memory model — what volatile doesn't give you.