Bitronix Lab Topic

Embedded C

The language of firmware. Learn the patterns, pitfalls and idioms that turn standard C into reliable, deterministic embedded code.

PointersRegistersvolatileBit-fieldsDrivers
Bitronix Lab logo Innovating ideas, empowering future

Embedded C: Writing Firmware That Lasts

Why C still rules embedded

C gives you direct access to memory, near-zero runtime overhead and predictable timing. There is no garbage collector that can pause you mid-ISR, no hidden allocations, and the generated assembly looks roughly like the source. That is exactly what you want when a watchdog is breathing down your neck.

Embedded C ≠ standard C. Same syntax, very different mindset: every byte matters and every cycle is visible.

Types & stdint.h

Never use plain int or long in firmware — their sizes change between platforms. Always use the fixed-width types from <stdint.h>:

#include <stdint.h>
#include <stdbool.h>

uint8_t   counter   = 0;       // 0..255
int16_t   temp_x10  = -125;    // -32k..32k, °C * 10
uint32_t  ticks_ms  = 0;
bool      ready     = false;

Bit manipulation

You will spend more time twiddling bits than anything else. Memorize these four patterns:

/* Set bit n */
reg |=  (1U << n);

/* Clear bit n */
reg &= ~(1U << n);

/* Toggle bit n */
reg ^=  (1U << n);

/* Check bit n */
if (reg & (1U << n)) { /* it is set */ }

volatile & memory-mapped I/O

Hardware registers and variables shared with ISRs must be declared volatile, otherwise the compiler will happily optimize your code into something that does not work.

/* Memory-mapped GPIO register */
#define GPIOA_ODR  (*(volatile uint32_t *)0x40020014U)

/* Shared with ISR */
volatile uint32_t systick_ms = 0;

void SysTick_Handler(void) {
    systick_ms++;          // updated in interrupt
}

void delay_ms(uint32_t ms) {
    uint32_t start = systick_ms;
    while ((systick_ms - start) < ms) { }
}

Forget volatile on a shared variable and your program may work in debug, then mysteriously freeze in release. This is the #1 firmware bug.

The embedded memory model

An MCU binary lives in a few sections defined by the linker script:

  • .text — code, in Flash.
  • .rodata — constants (const), in Flash.
  • .data — initialized globals, copied from Flash → RAM at boot.
  • .bss — zero-initialized globals, in RAM.
  • Stack — function calls, local variables.
  • Heap — dynamic allocation. Usually avoided.

Most production firmware avoids malloc() entirely — fragmentation in a long-running embedded system is a one-way ticket to a hard fault.

Patterns every firmware engineer needs

1. Finite State Machine

typedef enum { ST_IDLE, ST_HEATING, ST_HOLD, ST_FAULT } state_t;
static state_t state = ST_IDLE;

void controller_step(void) {
    switch (state) {
        case ST_IDLE:    if (start_pressed())   state = ST_HEATING; break;
        case ST_HEATING: if (temp_reached())    state = ST_HOLD;    break;
        case ST_HOLD:    if (stop_pressed())    state = ST_IDLE;    break;
        case ST_FAULT:   /* require reset */                        break;
    }
}

2. Lock-free ring buffer

#define RB_SIZE 64
static volatile uint8_t  buf[RB_SIZE];
static volatile uint16_t head = 0, tail = 0;

bool rb_push(uint8_t b) {
    uint16_t next = (head + 1) % RB_SIZE;
    if (next == tail) return false;   // full
    buf[head] = b;
    head = next;
    return true;
}

bool rb_pop(uint8_t *b) {
    if (head == tail) return false;   // empty
    *b = buf[tail];
    tail = (tail + 1) % RB_SIZE;
    return true;
}

3. Driver = function table

typedef struct {
    int  (*init)(void);
    int  (*write)(const uint8_t *data, size_t len);
    int  (*read) (uint8_t *data, size_t len);
} comm_driver_t;

extern const comm_driver_t uart_driver;
extern const comm_driver_t usb_driver;

Common pitfalls

  • Forgetting volatile on ISR-shared variables.
  • Using float/double on an MCU without an FPU.
  • Calling printf() from an ISR — never do that.
  • Stack overflow because of large local arrays — use static.
  • Implicit integer promotion to int hiding overflow bugs.
  • Trusting malloc() in a long-running system.

Master these and you are 80% of the way to writing professional firmware.