Embedded C

Storage Classes & Scope

Every variable in C has three properties that don't show up anywhere in its declared type: scope (where in the source it can be named), lifetime (how long its storage actually exists), and linkage (whether other files can see it). The storage-class keywords — static, extern, register, and the usually-omitted auto — are how you control all three, and on a memory-starved microcontroller with no OS to isolate a mistake, which one you picked is often the difference between a working driver and a corrupted stack.

Quick reference
What it is
The keywords and defaults that decide a variable's scope, lifetime, and cross-file visibility.
Mental model
Scope is who can call it by name; lifetime is how long its memory actually exists; linkage is whether other files are allowed to know about it at all.
Key numbers
4 storage-class keywords total (auto, static, extern, register) — auto is the default for locals and is almost never typed out.
Reach for it when
Deciding whether a variable should be a local, a file-scoped static, or a genuinely global — not an afterthought, a design decision made before writing the driver.

Local (automatic) variables

A variable declared inside a function with no storage-class keyword is auto by default — nobody writes auto int x; in practice, but that's what's happening. Scope is the enclosing block; lifetime is exactly the function call — the storage is carved out of the stack when the function is entered and is gone the instant it returns.

void read_sensor(void) {
    int raw = adc_read();       // lives on the stack — gone when this returns
    int scaled = raw * 3 / 2;
    send_over_uart(scaled);
}   // raw and scaled cease to exist here

Global variables

A variable declared outside any function has static storage duration — it exists for the entire life of the program, not just one function call — and, by default, external linkage, meaning another source file can see it too, by declaring it extern. Global state is the fastest way to let two completely unrelated pieces of code interact by accident, which gets sharply worse the moment one of the writers is an interrupt handler — the exact shared-state hazard covered in Interrupts.

volatile uint32_t system_tick_ms = 0;   // written by an ISR, read everywhere

void SysTick_Handler(void) {
    system_tick_ms++;                    // fires every 1ms, forever
}

static inside a function

A local variable declared static is a genuine hybrid: block scope (only nameable inside that function, same as a normal local) but static lifetime (its storage exists for the whole program, and its value persists between calls instead of being reinitialized). It's the standard way to give one function its own private, persistent state without polluting file or global scope.

int next_request_id(void) {
    static int counter = 0;   // initialized once, ever — not on every call
    return ++counter;
}

static at file scope

The same keyword means something different outside a function: static on a file-scope variable or function restricts its linkage to internal — only this translation unit (this .c file, after preprocessing) can see it, even though C provides no private keyword. In a real firmware project with dozens of driver files, marking file-local helpers static is what prevents a linker error the moment two unrelated files both happen to define a function called init().

// uart_driver.c
static uint8_t rx_buffer[64];     // invisible outside this file
static void flush_rx(void) { ... } // ditto

void uart_init(void) { flush_rx(); }  // the file's actual public API

extern

extern declares a variable or function without defining it — it tells the compiler "this exists somewhere, the linker will find it," so other files can reference a global without each one allocating its own copy. The definition (the one place storage is actually reserved) lives in exactly one .c file.

// error_codes.h
extern const char *error_table[16];   // declaration — no storage here

// error_codes.c
const char *error_table[16] = { "OK", "TIMEOUT", "CRC_FAIL", ... }; // definition

register

register is a hint asking the compiler to keep a frequently-used local variable in an actual CPU register instead of memory, for speed — a hint modern optimizing compilers almost always ignore in favor of their own register-allocation decisions, which are generally better than a programmer's guess. Its one remaining practical effect: the address-of operator is disallowed on a register variable (&x won't compile), since there may be no memory address to take. Mostly a historical relic, occasionally still seen in older embedded codebases predating good optimizers.

Where each one actually lives

A program's address space from low to high: compiled code and constants, initialized globals, uninitialized globals, the heap growing upward, free space, and the stack growing downward from the top High address Stack local/auto variables, call frames grows down unused (free space) Heap malloc / calloc / realloc .bss uninitialized globals & statics (zeroed at startup) .data initialized globals & statics .text / .rodata compiled code + string/const literals Low address
Heap and stack deliberately grow toward each other from opposite ends of the free space between them — on a tiny microcontroller with only a few KB total, they can actually collide, which is exactly the kind of failure a memory pool (see Dynamic Memory) is chosen to avoid.

Reading the diagram against the keywords above: a plain local variable lives on the Stack. An uninitialized global or static (static int count;) lives in .bss — the compiler doesn't even store its zero value in the binary, it just records "zero this many bytes at startup." An initialized global or static (static int count = 5;) lives in .data, and its initial value is stored in the binary, copied into RAM by startup code before main() runs (see Compilation & Linking). A const global typically lands in .rodata, often placed in flash directly rather than copied to RAM at all, since it's read-only.

Real-world applications

Gotchas

Field notes