C Language Essentials for Embedded

C is the lingua franca of embedded systems. Learn the parts of C that matter most on microcontrollers: fixed-width types from <stdint.h>, integer promotion and overflow, signed vs unsigned, operators and bitwise manipulation, pointers and arrays, structs, unions, enums, padding and alignment, the preprocessor, storage classes (static, extern, const, volatile) and the undefined behaviours that silently break firmware.

C gives you direct access to memory and hardware with almost no runtime overhead — exactly what a 64 KB microcontroller needs. The price is that C trusts you completely: it will happily overflow a buffer, read an uninitialised variable or truncate a value without warning.

This lesson focuses on the C you will write every day in firmware and the traps that cause most embedded bugs. Later lessons build on it for register access, drivers and RTOS code.

An analogy

C is a manual-transmission race car: no automatic safety features, but total control. You can shift at exactly the right moment — or grind the gears if you are careless.

Use exact-width types

int is 16 bits on some MCUs (AVR, MSP430) and 32 bits on ARM. Registers and protocols need exact widths, so embedded code uses uint8_t, int16_t, uint32_t from <stdint.h> and bool from <stdbool.h>. Use size_t for sizes and indexes.

Pointers and arrays

A pointer holds an address. *p reads/writes the object it points to; p + 1 advances by sizeof(*p) bytes. Arrays decay to pointers when passed to functions, so always pass the length too. Pointers are how C reaches hardware: a register is just a memory address cast to a pointer.

Storage classes and qualifiers

Where a variable lives and how it may change:

Structs, unions, enums and the preprocessor

Structs group related data (a sensor's config); unions overlay types on the same bytes (packet parsing); enums name states and error codes; #define and inline functions create constants and small helpers. Use include guards (#ifndef FOO_H) or #pragma once in headers, and prefer static inline functions over complex macros.

Undefined behaviour and coding standards

Signed overflow, out-of-bounds access, using uninitialised variables, shifting by ≥ the type width, and misaligned or aliased pointer accesses are undefined behaviour — the optimiser may assume they never happen and generate surprising code. MISRA C and CERT C restrict the language to a safer subset; static analysers (cppcheck, PC-lint, Polyspace) and -Wall -Wextra -Werror catch many issues before hardware testing.

Formulas

Worked example: Unsigned wrap-around

Given: uint8_t c = 250; c += 10;

Find: c.

Answer: c = 4 (defined behaviour for unsigned)

Worked example: Extracting a field

Given: status = 0x3A5, bits [7:4] hold a 4-bit code

Find: The code.

Answer: code = 10

Worked example: Struct padding

Given: struct { uint8_t a; uint32_t b; uint8_t c; }

Find: sizeof on Cortex-M.

Answer: 12 bytes (8 if reordered b, a, c)

Types, promotion, bit fields and padding

#include <stdio.h>
#include <stdint.h>

struct bad  { uint8_t a; uint32_t b; uint8_t c; };   /* 1 + 3 pad + 4 + 1 + 3 pad */
struct good { uint32_t b; uint8_t a; uint8_t c; };   /* 4 + 1 + 1 + 2 pad        */

int main(void)
{
    uint8_t x = 200, y = 100;
    printf("x + y as int     = %d\n", x + y);        /* promoted to int: 300 */
    uint8_t z = x + y;                                /* truncated mod 256    */
    printf("stored in uint8  = %u\n", z);

    uint32_t reg = 0;
    reg |= (1u << 5);                                 /* set bit 5            */
    printf("set bit 5        = 0x%08X\n", reg);
    reg = (reg & ~(0x7u << 8)) | (0x5u << 8);         /* write 3-bit field    */
    printf("field[10:8] = 5  = 0x%08X\n", reg);
    printf("bit 5 is %s\n", (reg & (1u << 5)) ? "set" : "clear");
    reg &= ~(1u << 5);                                /* clear bit 5          */
    printf("clear bit 5      = 0x%08X\n", reg);

    printf("sizeof bad = %u  good = %u\n",
           (unsigned)sizeof(struct bad), (unsigned)sizeof(struct good));
    printf("-1 < 1u is %s\n", (-1 < 1u) ? "true" : "false");   /* surprise! */
    return 0;
}
/* Output (32-bit Cortex-M, also x86-64):
x + y as int     = 300
stored in uint8  = 44
set bit 5        = 0x00000020
field[10:8] = 5  = 0x00000520
bit 5 is set
clear bit 5      = 0x00000500
sizeof bad = 12  good = 8
-1 < 1u is false                                                          */

Arithmetic on small types is done in int, then truncated on assignment. Comparing signed −1 with an unsigned value converts −1 to 0xFFFFFFFF — a classic bug. Ordering struct members from largest to smallest removes padding (12 → 8 bytes).

Common mistakes

Applications

Key takeaways

Going further