C rehearsal - MarekBykowski/readme GitHub Wiki

C Embedded Cheat Sheet

C standard versions in order

C standard versions
GCC (GNU Compiler Collection)

Operator Precedence β€” the levels that bite

Level Operators Note
1 (highest) () [] -> . postfix++ -- always wins
2 prefix++ -- * & ! ~ sizeof right→left
3–4 * / % + - arithmetic
5 << >> shifts
6–7 < <= >= > == != == is above & !
8 & bitwise AND
9 ^ bitwise XOR
10 | bitwise OR
11–12 && || logical
14 (lowest) = += -= … assignment
// TRAP β€” == binds tighter than &
val & 0x01 == 0      // β†’ val & (0x01==0) β†’ val & 0 β†’ always 0!
(val & 0x01) == 0    // correct

// TRAP β€” postfix++ beats *
*p++    // β†’ *(p++) β€” deref old p, then advance
(*p)++  // increment the value p points to

πŸ”’ const and Pointers

const int *p1 = &x;        // pointer to const int
int *const p2 = &x;        // const pointer to int
const int *const p3 = &x;  // const pointer to const int
Pointer *p (value) p (address)
const int *p1 βœ— locked βœ“ allowed
int *const p2 βœ“ allowed βœ— locked
const int *const p3 βœ— locked βœ— locked

πŸ’‘ Read rightβ†’left: const int *p = "p is a pointer to const int"


βš™ Bit Operations β€” set / clear / toggle

// Macros (Linux kernel style)
#define BIT(n)       (1u << (n))
#define GENMASK(h,l) (((1u << ((h)-(l)+1))-1) << (l))

val |=  (1u << n);           // SET bit n
val &= ~(1u << n);           // CLEAR bit n
val ^=  (1u << n);           // TOGGLE bit n

// SET bits 3–7 to 'mode' β€” raw, no macros
val = (val & ~(0x1Fu << 3)) | ((mode & 0x1Fu) << 3);
//    step 1: build mask β†’ shift to pos 3 β†’ negate β†’ AND (clears bits 3-7)
//    step 2: shift mode to pos 3 β†’ OR (writes new value)

// READ bits 3–7
uint32_t m = (*reg >> 3) & 0x1F;

⚠️ Always clear first, then set. |= alone cannot zero bits already set to 1.


πŸ“¦ Struct Padding & Alignment

// Bad β€” 12 bytes (wasted padding)
struct foo { char a; int b; char c; };
// offsets: a=0, [3B pad], b=4, c=8, [3B pad] = 12B

// Good β€” 8 bytes (largest field first)
struct bar { int b; char a; char c; };
// offsets: b=0, a=4, c=5, [2B pad] = 8B
Tool What it does When to use
offsetof(T, f) field offset in bytes always safe
sizeof(struct T) total size with padding always
__attribute__((packed)) removes all padding protocols, not HW registers
_Alignas(N) forces alignment to N DMA buffers, cache lines

⚠️ packed = slower load (4Γ— LDRB instead of LDR). On Cortex-M0 β†’ HardFault on unaligned pointer access.


πŸ”„ Endianness

uint32_t x = 0x12345678

LITTLE ENDIAN (ARM / x86)     BIG ENDIAN (network / TCP)
addr  byte                    addr  byte
+0    0x78  ← *p  (LSB)       +0    0x12  ← *p  (MSB)
+1    0x56                    +1    0x34
+2    0x34                    +2    0x56
+3    0x12        (MSB)       +3    0x78        (LSB)
// Endianness test
uint32_t x = 0x12345678;
uint8_t *p = (uint8_t *)&x;
if (*p == 0x78) puts("little endian");
else             puts("big endian");

htonl(val);   // host β†’ network (big endian)
ntohl(val);   // network β†’ host

πŸ’‘ MAVLink = little endian Β· TCP/IP = big endian Β· ARM = little endian by default


⚑ volatile & Hardware Registers

// Bad β€” compiler caches the read at -O2
uint32_t *reg = (uint32_t *)0x40011004;
while (*reg & 0x01 == 0);    // also a precedence bug!

// Correct
volatile uint32_t *reg = (volatile uint32_t *)0x40011004;
while ((*reg & 0x01) == 0);  // fresh load every iteration
Requirement Why
volatile compiler must not cache β€” every access = actual load/store
aligned address LDR requires addr % sizeof(type) == 0
~MASK before |= OR alone cannot clear bits already set to 1

🧩 Bitfield vs Packed

// PACKED β€” removes padding between fields
struct __attribute__((packed)) pkt {
    uint8_t  magic;   // offset 0
    uint32_t seq;     // offset 1 β€” no padding inserted
};  // sizeof = 5

// BITFIELD β€” controls field size in bits
struct uart_reg {
    uint32_t enable   : 1;   // 1 bit,  values 0–1
    uint32_t mode     : 3;   // 3 bits, values 0–7
    uint32_t baud     : 4;   // 4 bits, values 0–15
    uint32_t reserved : 24;  // 24 bits β€” 1+3+4+24 = 32 total
};  // sizeof = 4
packed bitfield
Controls padding between fields field size in bits
Min. field 1 byte 1 bit
&field works βœ— compile error
Bit order N/A unspecified by standard
Linux kernel network protocols IP header only

⚠️ Mixing container types (uint8_t + uint32_t) in bitfields β†’ unexpected padding between groups. Use one type throughout.


πŸ’€ Undefined Behaviour β€” the big ones

# Name What happens Fix
☠ Signed overflow INT_MAX + 1 β€” compiler may remove checks use unsigned or check before
☠ Strict aliasing *(uint32_t*)&float_var β€” wrong value at -O2 use memcpy() or union
☠ No sequence point printf("%d %d", *p++, *p++) β€” anything goes one *p++ per statement
☠ Null deref before check *p = 42; if(p==NULL) β€” branch eliminated check before deref
// ; is a sequence point β€” side effects committed before next statement
printf("%d ", *p++);   // ok β€” p++ committed after ;
printf("%d ", *p++);   // ok β€” p already advanced
printf("%d\n", *p);    // ok β€” no modification

πŸ“ Pointer Arithmetic

int a[] = {1,2,3,4,5};
int *p = &a[1];   // points to a[1]
int *q = &a[4];   // points to a[4]

q - p;   // β†’ 3  (elements, not bytes!)  type: ptrdiff_t
p + 2;   // β†’ &a[3], advances 2Γ—sizeof(int) bytes in memory

// Pointer TO an array (not to an element)
int (*ap)[3] = &a;   // type: int(*)[3]
(*ap)[0];            // β†’ 1: deref first (β†’ array), then index
ap++;                // advances sizeof(int[3]) = 12 bytes!

*ap[0]   // β†’ *(ap[0]) β€” [] beats * β€” DIFFERENT THING!
Type 32-bit 64-bit (Linux)
int 4 bytes 4 bytes
long 4 bytes 8 bytes
pointer 4 bytes 8 bytes
ptrdiff_t 4 bytes 8 bytes
uint32_t 4 bytes 4 bytes β€” always, use this in embedded!

πŸ”’ Two's Complement

How to compute -x: flip all bits, add 1

+3  =  0000 0011
    β†’  1111 1100  (flip bits)
    β†’  1111 1101  (+1) = -3

Why it works: 2ⁿ - x  (complement to a power of two)
2⁴ - 3 = 13 = 1101
check: 0011 + 1101 = 10000 β†’ carry discarded β†’ 0000 βœ“
4-bit value 4-bit value
0111 +7 (MAX) 1000 -8 (MIN)
0001 +1 1111 -1
0000 0 1001 -7
// Trap: INT_MIN has no positive counterpart!
INT_MAX =  2147483647  =  0x7FFFFFFF
INT_MIN = -2147483648  =  0x80000000
abs(INT_MIN)  β†’  UB!  (result doesn't fit in int)

🦾 ARM LDR β€” Alignment & Packed

Instruction Size Required alignment
LDR X0, [X1] 8 bytes (64-bit) addr % 8 == 0
LDR W0, [X1] 4 bytes (32-bit) addr % 4 == 0
LDRH W0, [X1] 2 bytes addr % 2 == 0
LDRB W0, [X1] 1 byte none
; packed: int at offset 1 (unaligned)
; compiler replaces one LDR with 4Γ— LDRB + bit-shifts:
LDRB W0, [X1, #1]   ; byte 0 of int
LDRB W1, [X1, #2]   ; byte 1 of int
LDRB W2, [X1, #3]   ; byte 2 of int
LDRB W3, [X1, #4]   ; byte 3 of int
Architecture Unaligned access result
ARMv8-A Linux (SCTLR.A=0) hardware fixup, slower
Cortex-M0 / M0+ HardFault!
Cortex-M3 / M4 / M7 partial fixup (LDR ok, LDM/STM no)
x86 works, slightly slower

🧠 Memory Layout β€” where things live

High address
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   stack         β”‚  local vars, function args, return addr β€” grows DOWN
β”‚        ↓        β”‚  fixed size (usually 8MB on Linux), overflow = segfault
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   (gap)         β”‚
β”‚        ↑        β”‚
β”‚   heap          β”‚  malloc/free β€” grows UP, you manage it manually
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   BSS           β”‚  uninitialised globals & statics β†’ zero-initialised by OS
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   data          β”‚  initialised globals & statics (e.g. int x = 42;)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   text (code)   β”‚  executable instructions β€” read-only
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Low address
int g_init   = 42;      // data segment
int g_uninit;           // BSS β€” zeroed automatically

void foo() {
    int local  = 1;     // stack
    int *p     = malloc(4); // p on stack, *p on heap
    static int s = 0;   // BSS (uninit static) or data (init static)
}

πŸ’‘ In embedded (no OS): BSS is zeroed by startup code (crt0), heap may not exist at all β€” everything on stack or statically allocated.


πŸ—‘οΈ malloc / free β€” and what goes wrong

// Basic usage
int *p = malloc(sizeof(int) * 10);
if (!p) { /* always check! malloc returns NULL on failure */ }
memset(p, 0, sizeof(int) * 10);   // heap memory is NOT zeroed
free(p);
p = NULL;   // good habit β€” prevents use-after-free

// calloc β€” allocates AND zeroes
int *p2 = calloc(10, sizeof(int));

// realloc β€” resize existing allocation
p2 = realloc(p2, 20 * sizeof(int));  // may move the block!
Bug What happens How to detect
Memory leak malloc without free β€” heap grows until OOM Valgrind, AddressSanitizer
Double-free free(p); free(p); β†’ UB, heap corruption ASan, guard pages
Use-after-free access *p after free(p) β†’ UB, stale data or crash ASan
Buffer overflow write past end of malloc'd block β†’ heap corruption ASan, bounds checking
NULL deref skipping the if (!p) check β†’ crash code review
// Safe pattern
void *safe_malloc(size_t n) {
    void *p = malloc(n);
    if (!p) { perror("malloc"); exit(EXIT_FAILURE); }
    return p;
}

⚠️ In embedded: avoid dynamic allocation entirely if possible. Heap fragmentation on a microcontroller with 64KB RAM can cause allocation failures hours into a flight.


πŸ“‹ memcpy vs memmove

void *memcpy (void *dst, const void *src, size_t n);  // UB if regions overlap
void *memmove(void *dst, const void *src, size_t n);  // safe if regions overlap
memcpy  β€” assumes NO overlap, may use SIMD/vectorised copy β†’ faster
memmove β€” handles overlap: copies via temp buffer or adjusts direction
char buf[] = "hello world";

// Safe β€” regions don't overlap
memcpy(buf + 6, "there", 5);    // "hello there"

// UNSAFE with memcpy β€” overlapping regions
memcpy(buf + 1, buf, 5);        // UB! use memmove instead
memmove(buf + 1, buf, 5);       // correct

// Classic use: shifting array elements
memmove(&a[1], &a[0], n * sizeof(int));  // insert at front

πŸ’‘ Rule of thumb: if in doubt, use memmove. The performance difference is negligible unless you're in a hot loop.


πŸ”§ Function Pointers β€” syntax, callbacks, dispatch tables

// Declaration: pointer to function taking int, returning int
int (*fp)(int);

// Assign and call
int square(int x) { return x * x; }
fp = square;
int result = fp(5);    // β†’ 25
int result = (*fp)(5); // same β€” both are valid

// typedef makes it readable
typedef int (*transform_fn)(int);
transform_fn fn = square;

// As function parameter (callback)
void apply(int *arr, int n, transform_fn fn) {
    for (int i = 0; i < n; i++) arr[i] = fn(arr[i]);
}

// Dispatch table β€” replaces switch/if-else chains
typedef void (*cmd_handler)(void);
cmd_handler dispatch[] = {
    [CMD_ARM]    = handle_arm,
    [CMD_DISARM] = handle_disarm,
    [CMD_TAKEOFF]= handle_takeoff,
};
dispatch[cmd]();  // O(1) dispatch, no branching

πŸ’‘ Dispatch tables are the embedded alternative to virtual functions in C++. Used heavily in drone firmware for command handling.


πŸ”— static, inline, extern β€” linkage and visibility

// static at file scope β€” internal linkage (invisible outside this .c file)
static int counter = 0;        // not accessible from other .c files
static void helper(void) {}    // private function

// static at function scope β€” persistent across calls
void count(void) {
    static int n = 0;   // initialised once, lives for program lifetime
    printf("%d\n", ++n);
}

// extern β€” declared here, defined in another .c file
extern int g_system_state;     // tells compiler "trust me, it exists"
extern void uart_init(void);

// inline β€” hint to compiler to expand inline (no function call overhead)
static inline uint32_t clamp(uint32_t v, uint32_t lo, uint32_t hi) {
    return v < lo ? lo : v > hi ? hi : v;
}
// static inline = inline + internal linkage β€” the correct pattern for headers
Keyword Scope Effect
static (file) translation unit internal linkage β€” invisible to linker
static (local) function persistent storage, init once
extern file external linkage β€” defined elsewhere
inline function hint to expand inline, no call overhead
static inline header inline + no multiple-definition linker error

βš›οΈ volatile vs atomics β€” difference and when each is needed

// volatile β€” prevents compiler caching, does NOT guarantee atomicity
volatile uint32_t *reg = (volatile uint32_t *)0x40011004;
while ((*reg & 0x01) == 0);   // compiler must re-read every iteration

// C11 atomics β€” guarantee both visibility AND atomicity
#include <stdatomic.h>
atomic_int shared = ATOMIC_VAR_INIT(0);
atomic_fetch_add(&shared, 1);                         // thread-safe increment
int v = atomic_load_explicit(&shared, memory_order_acquire);
volatile atomic
Prevents compiler caching βœ“ βœ“
Prevents CPU reordering βœ— βœ“
Atomic read-modify-write βœ— βœ“
Use for HW registers βœ“ βœ—
Use for shared variables (threads/ISR) βœ— alone βœ“
// Classic ISR pattern β€” volatile is correct here (single read/write, no RMW)
volatile bool flag = false;

void ISR_handler(void) {
    flag = true;   // single store β€” atomic on most architectures
}

void main_loop(void) {
    while (!flag);   // volatile ensures re-read every time
    flag = false;
    process();
}

⚠️ volatile is NOT a substitute for atomics in multi-threaded code. On multi-core ARM, you need memory barriers too.


⚑ Interrupt Handlers β€” volatile, reentrancy, critical sections

// Variables shared between ISR and main must be volatile
volatile uint32_t tick_count = 0;

void SysTick_Handler(void) {   // ISR β€” called by hardware
    tick_count++;               // write
}

void main(void) {
    while (1) {
        uint32_t t = tick_count;  // volatile β€” reads fresh value
        do_work(t);
    }
}

Critical section β€” disable interrupts for atomic read-modify-write:

// ARM Cortex-M
uint32_t save = __get_PRIMASK();
__disable_irq();           // enter critical section
shared_var += 1;           // safe β€” interrupts disabled
__set_PRIMASK(save);       // restore (don't just re-enable blindly)

// Linux kernel equivalent
unsigned long flags;
spin_lock_irqsave(&lock, flags);
shared_var += 1;
spin_unlock_irqrestore(&lock, flags);

ISR rules:

Rule Why
Keep ISR short interrupts blocked while ISR runs
No malloc/free in ISR heap is not reentrant
No blocking calls (printf, sleep) will deadlock or corrupt
Set a flag, process in main loop standard pattern
All shared vars must be volatile or compiler optimises reads away

πŸ• Watchdog Timer

A hardware counter that resets the CPU if not "petted" (reset) within a timeout. Prevents firmware from hanging silently.

// Initialise watchdog with 1s timeout
watchdog_init(1000);  // 1000ms

// Main loop β€” must pet before timeout
while (1) {
    watchdog_kick();   // reset the counter ("I'm alive")
    do_work();
}

// If do_work() hangs β†’ watchdog fires β†’ CPU reset β†’ system recovers

πŸ’‘ In drone firmware: watchdog is critical. A hung flight controller that doesn't reset is more dangerous than one that reboots. Always enable it in production.


πŸ›‘οΈ Include Guards vs #pragma once

// Traditional include guard β€” portable, standard C
#ifndef MY_HEADER_H
#define MY_HEADER_H

// ... header content ...

#endif  // MY_HEADER_H

// #pragma once β€” simpler, supported by GCC/Clang/MSVC, not standard C
#pragma once

// ... header content ...
Include guard #pragma once
Standard C βœ“ βœ— (compiler extension)
GCC / Clang / MSVC βœ“ βœ“
Handles symlinks/copies βœ“ βœ— (may include twice)
Less boilerplate βœ— βœ“
Linux kernel uses βœ“ guards β€”

πŸ’‘ For embedded/cross-compiler work: use include guards. #pragma once is fine for most projects but fails edge cases with symlinked headers.


πŸ”¨ Preprocessor β€” #define, args, token pasting

// Object-like macro
#define MAX_DRONES  16
#define PI          3.14159f

// Function-like macro β€” parenthesise everything!
#define MAX(a, b)   ((a) > (b) ? (a) : (b))  // safe
#define SQUARE(x)   ((x) * (x))               // safe
#define BAD(x)      x * x                     // unsafe: BAD(1+2) β†’ 1+2*1+2 = 5!

// Stringification β€” # turns token into string literal
#define STRINGIFY(x)   #x
STRINGIFY(hello)  β†’  "hello"

// Token pasting β€” ## concatenates tokens
#define REG(n)   reg_##n
REG(status)  β†’  reg_status

// Conditional compilation
#ifdef DEBUG
    #define LOG(msg)  printf("[DBG] %s\n", msg)
#else
    #define LOG(msg)  ((void)0)   // compiles to nothing
#endif

// X-macro pattern β€” define list once, generate multiple things
#define STATES(X)  X(IDLE) X(ARMED) X(FLYING) X(FAULT)

typedef enum {
#define ENTRY(n) STATE_##n,
    STATES(ENTRY)
#undef ENTRY
} state_t;

static const char *state_names[] = {
#define ENTRY(n) [STATE_##n] = #n,
    STATES(ENTRY)
#undef ENTRY
};

πŸ”§ Makefile Basics β€” compilation pipeline

The pipeline:

source.c  β†’  [preprocessor]  β†’  source.i
source.i  β†’  [compiler]      β†’  source.s  (assembly)
source.s  β†’  [assembler]     β†’  source.o  (object file)
source.o  β†’  [linker]        β†’  binary / .elf / .so
CC      = arm-none-eabi-gcc
CFLAGS  = -Wall -Wextra -O2 -mcpu=cortex-m4 -mthumb
LDFLAGS = -T linker.ld -lc

# Pattern rule β€” compile any .c to .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Link all objects into final binary
firmware.elf: main.o uart.o mavlink.o
	$(CC) $(LDFLAGS) $^ -o $@

# Useful targets
clean:
	rm -f *.o *.elf

# Force rebuild if header changes
main.o: main.c uart.h mavlink.h

Key flags to know:

Flag Meaning
-c compile only, don't link β†’ produces .o
-o output filename
-Wall -Wextra enable warnings (always use)
-O0 no optimisation (debug)
-O2 optimise (production)
-g include debug symbols
-I path add include search path
-L path add library search path
-l name link library libname.a
-D NAME define preprocessor macro
-mcpu=cortex-m4 target CPU (embedded)
-mthumb use Thumb instruction set
-fstack-usage report stack usage per function

πŸ’‘ For embedded: always check .map file after linking β€” it shows exactly where each symbol lands in flash/RAM. Critical for catching BSS/data overflows on small MCUs.


Generated during prep session Β· Swarmer Integration Engineer Β· 2026

Co-authored by Claude Sonnet 2026

⚠️ **GitHub.com Fallback** ⚠️