Volume 05 Beginner 6 sub-modules ~20 min read

Functions, Scope and Storage

Three keywords separate C written for computers from C written for chips: static, const and volatile. This volume explains each one, shows the machine code volatile changes, and follows the stack as calls pile up - the memory a small chip has least of.

You will learn
  • Where a variable can be seen, and how long it lasts
  • What static does inside a function, and what it does outside one
  • How extern shares one variable between files, without copies
  • Why const tables can stay in flash, and what const really promises
  • What volatile changes in the generated code, shown in assembly
  • How much stack each call takes, and why recursion is rare in firmware
You need

5.1 Local and global variables

Where you declare a variable decides two things: who can see it (its scope), and how long it lasts. Firmware cares about both, because a chip has very little memory to spend.


boot_count starts at 0 (globals begin at zero)
after two calls, boot_count = 2
inside = 1, and it starts again at 0 every call
inside = 1, and it starts again at 0 every call
in the inner block, deeper = 2
outside the block, value = 1

Local: born and gone with the call

A variable declared inside a function exists only while that function runs. Call the function again and you get a fresh one, which is why inside printed 1 both times.

Global: visible everywhere, for the whole run

A global is declared outside every function. It lives from power-on to power-off, and it starts at zero unless you say otherwise - the start-up code clears it before main runs, as Volume 09 will show.

Common mistake

Reaching for a global because it is convenient. Every global is a variable that any function, and any interrupt, can change at any moment. When a value only matters inside one function, keep it local. When it must be shared, share it deliberately - and look at static in the next sub-module first.

In plain words

Locals live on the stack and are recycled constantly. Globals live in their own fixed place in RAM for the whole run. A small chip has kilobytes of RAM, so every global is a permanent cost.

Quick check

A function has int count = 0; count++; and prints count. What does it print on the third call?

Show the answer

Answer: B. The local is created fresh at every call and set to 0 again, so it always prints 1. Making it static would keep the value between calls, which is the next sub-module.

5.2 static and extern

static means two different things. Inside a function, it makes a local remember its value. Outside one, it hides a name from the rest of the program.


static uint32_t next_id(void)
{
    static uint32_t id = 0u;      /* set up once, and remembered between calls */
    id++;
    return id;
}

next_id gives 1
next_id gives 2
next_id gives 3
three calls in one printf came out as 6, 5, 4
private_counter = 5 (this file only)
shared_ticks = 3 (counted in the other file)

The static local lives for the whole run, like a global, but only this function can reach it by name. That is exactly what you want for a counter, a small state machine, or a cached reading.

Look at the fourth line again

Three calls inside one printf came out 6, 5, 4. C does not fix the order in which a function's arguments are worked out, and this compiler went right to left. Never write code whose answer depends on that order: make one call per statement.

static on a global: private to this file


static uint32_t private_counter = 0u;   /* this file only */
uint32_t        shared_ticks    = 0u;   /* any file that declares it extern */

A static global cannot be reached from another file, even by a file that declares the same name. That is how a driver keeps its own state private, and it is the closest thing C has to a private member.

extern: the same variable, seen from another file

One file defines the variable. Every other file declares it:


uint32_t shared_ticks = 0u;      /* the one definition */

void tick(void)
{
    shared_ticks++;
}

extern uint32_t shared_ticks;    /* someone else owns this */
void tick(void);

...
printf("%u\n", shared_ticks);    /* prints 3 after three ticks */
Remember

In real projects the extern line goes in a header file, and both .c files include it. That way the declaration exists once, and the compiler checks every file against the same promise. Volume 15 covers headers properly.

Common mistake

Putting a definition in a header. Writing uint32_t shared_ticks = 0; in a .h and including it from two files gives you two variables with one name - or a linker error about a duplicate symbol. Headers declare (extern); one .c file defines.

Quick check

What does static do to a variable declared inside a function?

Show the answer

Answer: C. A static local is created once, kept for the whole run, and can be reached by name only inside its own function. On a global, the same keyword does something different: it hides the name from other files.

5.3 const

const is a promise not to change something through that name. On a chip it has a second benefit: a const table can stay in flash instead of taking up RAM.


static const uint8_t gamma_table[8] = { 0u, 1u, 3u, 8u, 17u, 33u, 61u, 104u };
static const uint32_t baud_rate = 115200u;

static uint32_t sum_table(const uint8_t *table, unsigned count)   /* promises not to change it */
{
    uint32_t total = 0u;
    for (unsigned i = 0; i < count; i++) {
        total += table[i];
    }
    return total;
}

baud_rate = 115200
gamma_table[4] = 17
the whole table adds up to 227
limit = 100

Try to change it and the compiler stops you:


error: assignment of read-only location 'gamma_table[0]'

Why const matters more on a chip

A non-const table has to be copied from flash into RAM at start-up, because the program might change it. A const table can be left where it is and read straight from flash. On a chip with 2 KB of RAM and 32 KB of flash, that is the difference between fitting and not fitting.

Remember

const on a parameter is a message to the reader as much as to the compiler: this function will not change what you hand it. Driver interfaces use it constantly - send(const uint8_t *data, uint16_t length) says the data is safe.

Going deeper: const does not always mean flash

It is the compiler and the linker that decide where a const object goes, following the linker script. A const local inside a function still lives on the stack. A static const at file scope is the one that normally lands in flash, and on Arm toolchains you will see it in the .rodata section - read-only data. Volume 09 reads a map file and finds it.

Quick check

Why is static const uint8_t table[256] better than static uint8_t table[256] for a lookup table that never changes?

Show the answer

Answer: A. A writable table must be copied into RAM at start-up so that it could be changed. A const table is read straight from flash, which on a small chip saves the scarcest resource there is.

5.4 volatile

volatile tells the compiler: this value can change on its own, so read it from memory every single time. Without it, an optimiser is free to read once and reuse the copy for ever.

Here are two identical-looking wait loops, one on a plain variable and one on a volatile:


static uint8_t          plain_flag    = 1u;
static volatile uint8_t volatile_flag = 1u;

unsigned wait_plain(void)
{
    unsigned turns = 0u;
    while (plain_flag != 0u && turns < 3u) { turns++; }
    return turns;
}

unsigned wait_volatile(void)
{
    unsigned turns = 0u;
    while (volatile_flag != 0u && turns < 3u) { turns++; }
    return turns;
}

Run them on a computer and both return 3. The difference is in the machine code the compiler writes. This is the real output of gcc -O2 -S:


wait_plain:
        endbr64
        movl    $3, %eax
        ret

wait_volatile:
        endbr64
        movzbl  volatile_flag(%rip), %eax
        testb   %al, %al
        je      .L6
        movzbl  volatile_flag(%rip), %eax
        testb   %al, %al
        je      .L7
        movzbl  volatile_flag(%rip), %eax
        ...

Look at what happened. Without volatile, the compiler never read the flag at all: it worked the answer out while compiling and returned the constant 3. With volatile, there is a fresh movzbl - a load from memory - before every test.

In plain words

The compiler assumes it is the only thing changing your variables. On a chip that is untrue: an interrupt, a timer or a pin can change memory between two lines of your code. volatile is how you say so.

Where volatile is required

Case Why
A hardware register The hardware changes it, not your code
A flag set by an interrupt handler The handler runs between any two instructions
Memory shared with DMA The DMA engine writes it while you are reading
A variable read in a wait loop Otherwise the loop may be optimised into nothing
Common mistake

Thinking volatile makes something safe to share. It does not. It stops the compiler caching the value, and nothing more. It does not make a read-modify-write atomic, and it does not order access between two processors. Volume 11 covers what else a shared flag needs.

Quick check

An interrupt sets flag = 1, and the main loop spins on while (flag == 0) { }. The program hangs at -O2. What is missing?

Show the answer

Answer: B. Nothing in the loop changes the flag, so the optimiser may read it once and loop on the copy for ever. The volatile keyword forces a fresh read on every turn. That is exactly what the assembly above shows.

5.5 The stack and function calls

Every call takes a slice of the stack: its locals, its arguments and the address to come back to. The stack is the memory a small chip has least of, and nothing warns you when it runs out.


five nested calls moved the stack by 160 bytes
that is about 32 bytes per call, on this machine
each call also stores the return address and any saved registers

Each call adds a stack frame and each return takes one away. The frames pile up, so the deepest chain of calls in your program decides how much stack you need.

Three nested calls, each with its own frame on the stack the stack, while three calls are running read_sensor() update_display() main() its locals, and where to return to its locals, and where to return to its locals newest frame, on top oldest frame grows your variables live here - the stack must not reach them About 32 bytes per call here, plus any arrays you declare.
Figure 5.1 - The stack while three calls are in progress. Each frame holds that call's locals, its arguments and the address to return to. When a function returns, its frame is simply forgotten - which is why a local cannot be used after the call that made it has finished.
The big local array

void parse(void)
{
    uint8_t buffer[1024];      /* all on the stack */
    ...
}

On a chip with 2 KB of SRAM, that single line takes half the memory. It is also invisible in the map file, because the stack is sized once for the whole program. Big buffers belong in static storage, where the linker counts them.

Common mistake

Returning a pointer to a local. The frame is gone the moment the function returns, so the address points at memory that the next call will reuse. gcc catches the obvious cases:


error: storing the address of local variable 'here' in 'deepest' [-Werror=dangling-pointer=]

Volume 07 comes back to this once pointers are properly introduced.

Quick check

Which of these costs stack space every time the function runs?

Show the answer

Answer: C. Only the plain local array is created on the stack at each call. A static local, a file-scope const and a global all live in their own fixed memory, counted once by the linker.

5.6 Recursion, and why firmware avoids it

Recursion is elegant, and firmware mostly avoids it. Each call takes another frame, and the depth often depends on data you cannot see from the code.


uint32_t factorial_recursive(uint32_t n)
{
    if (n <= 1u) {
        return 1u;
    }
    return n * factorial_recursive(n - 1u);    /* one more frame, every time */
}

uint32_t factorial_loop(uint32_t n)
{
    uint32_t result = 1u;
    for (uint32_t i = 2u; i <= n; i++) {
        result *= i;
    }
    return result;                             /* one frame, always */
}

recursive 10! = 3628800, and it went 10 calls deep
loop      10! = 3628800, with one stack frame
both agree for 0! to 12!
13! would overflow a uint32_t, whichever way it is written

Same answers. One of them used ten stack frames to get there; the other used one.

Why firmware says no

Going deeper: when recursion is still fine

Walking a tree of known depth is the classic exception - a menu structure four levels deep, or a parser for a format with a fixed nesting limit. The test is whether you can write down the maximum depth and multiply it by the frame size. If you can, and the answer fits with room to spare, the recursion is safe and often clearer. If you cannot, write the loop.

Quick check

Why is a recursive function risky on a chip with 2 KB of RAM?

Show the answer

Answer: B. Each call adds a frame. With only kilobytes of RAM and no memory protection, a deep chain of calls silently overwrites whatever sits below the stack, and the fault shows up far from the cause.

What you learned

Key words from this volume

Every word below has a plain-English entry in the glossary.

Practice

Practice 1

Choose the storage

Pick local, static local, file-static or global for each of these. A loop counter. The number of times a driver has been called. A 256-byte lookup table that never changes. A flag an interrupt sets for the main loop.

Show the solution
  • Loop counter: local. It matters for three lines and should disappear afterwards.
  • Call count: static local. It must survive between calls, and nothing outside the function has any business touching it.
  • Lookup table: static const at file scope. It never changes, so it belongs in flash, and nothing outside the file needs it.
  • Interrupt flag: global - and volatile. Two different pieces of code must see it, and it changes on its own.

The pattern to notice: start as local as possible, and widen only when something forces you to.

Practice 2

Find the missing keyword

This loop never ends when built with optimisation, although the interrupt really does set the flag. What is missing, and why does it matter?


uint8_t data_ready = 0u;

void ISR_uart(void)     { data_ready = 1u; }

void wait_for_data(void)
{
    while (data_ready == 0u) {
        /* wait */
    }
}
Show the solution

data_ready needs to be volatile:


volatile uint8_t data_ready = 0u;

Nothing inside wait_for_data changes the flag, so the optimiser is entitled to read it once before the loop and spin on that copy for ever. The assembly in this volume shows exactly that: the non-volatile version never loaded the variable at all.

volatile fixes the caching. It does not make the flag *safe* in general - for anything wider than a byte, or for read-modify-write, you also need the critical sections from Volume 11.

Practice 3

Count the stack

A function has uint8_t buf[200]; and calls another function that has uint8_t line[80];, which calls a third with uint32_t work[16];. Roughly how much stack is in use at the deepest point, on a machine that costs about 32 bytes per frame in overhead?

Show the solution

Add the locals and the overhead for the three frames that are alive at once:

  • 200 + 32 = 232 bytes
  • 80 + 32 = 112 bytes
  • 16 × 4 + 32 = 96 bytes

That is about 440 bytes at the deepest point. On a chip with 2 KB of SRAM, one such chain is already a fifth of the memory. Interrupts run on the same stack too, adding their own frames on top at any moment.

This is why firmware reviews look for large locals, and why buffers are usually declared static so the linker can count them.

Practice 4

Make it private

A UART driver in uart.c has a buffer and a helper function that nothing outside the file should touch, plus one function the rest of the program calls. How do you write that?

Show the solution

static uint8_t rx_buffer[64];          /* private to this file */
static uint16_t rx_count;              /* private to this file */

static void push_byte(uint8_t b)       /* private helper */
{
    ...
}

void uart_send(const uint8_t *data, uint16_t length)   /* the public one */
{
    ...
}

Everything private is static; only uart_send is left visible. Its declaration goes in uart.h, which other files include. That single habit - static by default, public only on purpose - is most of what module structure means in C.

Interview corner

Interview question 1

What does volatile do?

"What does the volatile keyword do, and where would you use it?"

Show the solution

"It tells the compiler that a variable can change outside the program's control, so every read must actually happen and cannot be replaced by a cached copy. I use it for hardware registers, for flags written by an interrupt handler, and for buffers shared with DMA. It is easy to show why: without it, gcc at -O2 compiles a wait loop on a flag into a constant, because nothing in the loop changes the flag. The thing to be clear about is what it does not do - it is not atomic, and it is not a memory barrier between cores."

Interview question 2

static, three ways

"Give me the three meanings of static in C."

Show the solution

"On a local variable, it moves the variable out of the stack frame so it keeps its value between calls, while the name stays private to the function. On a global variable, it limits the name to that one file, so the linker will not let another file reach it. On a function, the same thing: the function can only be called from within its own file. In firmware I use the last two by default - a driver exposes a handful of functions and keeps everything else static."

Next, Volume 06 takes on arrays and strings: how they sit in memory, how to walk them safely, and how a buffer overflow really happens.