Part 1 · The C Language30 min read
Variables, Scope, Lifetime, and Storage Classes
Every variable in C has three independent properties: its scope (where in the source its name can be used), its linkage (whether the same name in another file refers to the same object), and its storage duration (when it exists at run time). Together they decide whether a variable sits on the stack, in zero-filled RAM, in RAM initialized from flash, or purely in flash. On a microcontroller with 128 KB of RAM and no memory protection, getting this right is the difference between a firmware that fits and behaves, and one that randomly corrupts its own state. This lesson gives you precise control over all three properties and introduces the qualifiers const and volatile.
Declaration versus definition
Section titled “Declaration versus definition”A declaration tells the compiler that a name exists and what type it has. A definition is a declaration that also creates the thing: it allocates storage for an object or provides the body of a function.
#include <stdint.h>
extern uint32_t g_tick_count; /* declaration only: no storage allocated */uint32_t g_tick_count = 0; /* definition: storage allocated, initialized */
int32_t clamp(int32_t v, int32_t lo, int32_t hi); /* function declaration (prototype) */
int32_t clamp(int32_t v, int32_t lo, int32_t hi) /* function definition */{ if (v < lo) { return lo; } return (v > hi) ? hi : v;}You may declare a name as many times as you like, as long as the declarations agree. Each object or function with external linkage must be defined exactly once in the whole program. You saw the linker enforce this in Hello, C: the compilation pipeline: zero definitions give “undefined reference”, two give “multiple definition”.
Tentative definitions and the -fno-common change
Section titled “Tentative definitions and the -fno-common change”A file-scope declaration with no initializer and no extern, such as uint32_t g_errors;, is a tentative definition. If the file contains no other definition of g_errors, it becomes a definition with value zero.
Historically, Unix compilers let two files both contain int g_errors; and silently merged them into one object (a “common” symbol, shown by nm as C). GCC 10 changed the default to -fno-common, so this now fails with “multiple definition”. You will hit this when building old vendor code with a modern compiler. The fix is not -fcommon; it is to define the variable in one file and declare it extern everywhere else.
Scope: where a name is visible
Section titled “Scope: where a name is visible”C has four kinds of scope.
Block scope. A name declared inside a block (between braces) or in a function’s parameter list is visible from its declaration to the end of that block. Each for statement also forms a block, so a loop variable declared in the for exists only inside the loop.
File scope. A name declared outside every function is visible from its declaration to the end of the translation unit (the .c file after preprocessing).
Function prototype scope. Parameter names in a declaration that is not a definition are visible only until the end of that declaration. They are documentation: int32_t clamp(int32_t v, int32_t lo, int32_t hi); and int32_t clamp(int32_t, int32_t, int32_t); declare exactly the same function, and the definition may use different names. Always write the names anyway; they tell the reader which argument is which.
Function scope. Only goto labels have it: a label is visible throughout the function that contains it, even before the line where it appears. Control flow that holds up in production shows why that matters.
#include <stdint.h>
static uint32_t total_samples; /* file scope */
int32_t sum_of_squares(uint32_t n) /* n: block scope, whole body */{ int32_t sum = 0; /* block scope, from here to the closing brace */
for (uint32_t i = 1; i <= n; i++) { /* i: only inside the for statement */ int32_t sq = (int32_t)(i * i); /* sq: only inside the loop body */ sum += sq; } /* i and sq are not visible here */
total_samples += n; return sum;}Declare variables in the smallest scope that works, as close as possible to their first use. A narrow scope means fewer lines where the variable can be misused, and it tells the compiler the value is dead afterwards, so the register can be reused.
Linkage: one name, one object?
Section titled “Linkage: one name, one object?”Scope is about the source text of one file. Linkage is about whether the same name in different scopes or different files refers to the same object or function:
- External linkage: the name refers to the same entity across the whole program. Non-
staticfile-scope variables and functions have it. These are the uppercase letters (T,D,B) innmoutput. - Internal linkage: the name refers to the same entity everywhere within one translation unit, but is invisible to the linker from other files. File-scope
staticgives it. These are the lowercase letters. - No linkage: each declaration is a separate entity. Local variables and parameters have it.
Storage duration: when a variable exists
Section titled “Storage duration: when a variable exists”| Storage duration | Lifetime | Who has it | Where it lives on an MCU |
|---|---|---|---|
| Automatic | From entry to the enclosing block until it exits | Local variables and parameters without static | Registers or the stack |
| Static | The entire run of the program | Every file-scope variable, and locals declared static | .data, .bss (RAM) or .rodata (flash) |
| Allocated | From malloc until free | Memory you get from malloc and friends | The heap, if one exists |
| Thread | The life of a thread (_Thread_local) | Rarely used in firmware | Depends on the RTOS |
An automatic variable comes into existence each time its block is entered and stops existing when the block ends. The memory is then reused by the next function call. That is why returning the address of a local variable is a bug: the pointer outlives the object. Pointers II: function pointers, declarations, and bugs dissects that failure.
A static variable exists before main starts and keeps its value until the device resets.
The two meanings of static
Section titled “The two meanings of static”The keyword static does two completely different jobs depending on where it appears.
At block scope: persistence
Section titled “At block scope: persistence”Inside a function, static changes the storage duration from automatic to static. The name is still visible only inside the block, but the object lives for the whole program and keeps its value between calls.
#include <stdio.h>#include <stdint.h>#include <inttypes.h>
static uint32_t next_id(void){ static uint32_t id = 100; /* initialized once, before main runs */ return id++;}
int main(void){ for (int i = 0; i < 3; i++) { printf("%" PRIu32 "\n", next_id()); } return 0;}100101102Remove the inner static and the program prints 100 three times, because a fresh automatic id is created and initialized on every call.
Two details matter. First, the initializer = 100 is applied once, conceptually before the program starts, not each time execution passes the line. In C (unlike C++) there is no “first call” initialization, so the initializer must be a constant expression. Second, the object is stored exactly like a file-scope variable: here in .data, because its initial value is nonzero. In nm output it appears as a local symbol with a suffix, such as id.0, so that several functions can each have their own id.
Firmware uses static locals for state that belongs to one function, such as a debouncer:
#include <stdbool.h>#include <stdint.h>
#define DEBOUNCE_SAMPLES 5u
/* Call every millisecond with the raw pin level. Returns the debounced level. */bool debounce(bool raw){ static bool stable; /* zero-initialized: false */ static uint8_t count;
if (raw == stable) { count = 0; } else if (++count >= DEBOUNCE_SAMPLES) { stable = raw; count = 0; } return stable;}The convenience has costs you must understand:
- The function is not reentrant. If it is called from both
mainand an interrupt handler, or for two different buttons, they share onestableand onecount. For two buttons you need the state outside the function, one copy per button (a struct, as in Structs, unions, enums, and memory layout). - The state is hidden, which makes the function harder to unit test: there is no way to reset it between tests.
A second firmware use: moving a large buffer off the stack. uint8_t frame[2048]; inside a function puts 2 KB on the stack on every call, which may overflow a small task stack. static uint8_t frame[2048]; moves it to .bss, where its RAM cost is visible in the map file at link time, at the price of reentrancy.
At file scope: privacy
Section titled “At file scope: privacy”At file scope every variable already has static storage duration, so static there means something else: it gives the name internal linkage. The variable or function becomes private to its .c file.
#ifndef LED_H#define LED_H
#include <stdint.h>
void led_toggle(void);uint32_t led_toggle_count(void);
#endif#include <stdbool.h>#include <stdint.h>#include "led.h"
static bool led_on; /* private state */static uint32_t toggle_count;
static void led_hw_write(bool on) /* private helper */{ (void)on; /* will write GPIOD BSRR here once we have the register header */}
void led_toggle(void){ led_on = !led_on; toggle_count++; led_hw_write(led_on);}
uint32_t led_toggle_count(void){ return toggle_count;}Making everything static that does not need to be public is the single most valuable habit in C module design:
- No name collisions. Ten modules can each have a private
static uint32_t state;without conflict. - Clear ownership. Only
led.ccan modifytoggle_count; other modules must go throughled_toggle_count(). When the value is wrong, you know which file to read. - Better code. The compiler sees every use of a
staticfunction in this file, so it can inline it and delete the original. For astaticvariable that is never written after initialization, it can even replace reads with the constant.
The full module pattern (headers, opaque types, private state structs) is the subject of Organizing a real C codebase.
extern: using a variable defined elsewhere
Section titled “extern: using a variable defined elsewhere”extern declares a name without defining it. The standard pattern for a genuinely shared variable puts the declaration in a header and the single definition in one source file:
#ifndef CONFIG_H#define CONFIG_H
#include <stdint.h>
extern uint32_t g_console_baud; /* declaration for every user */
#endif#include "config.h" /* lets the compiler check the definition matches */
uint32_t g_console_baud = 115200; /* the one definition */Any file that includes config.h can read and write g_console_baud. Including the header in config.c itself is important: if someone later changes the definition to uint16_t, the compiler reports a conflict with the declaration. Without that, a file declaring extern uint16_t g_console_baud; by hand would read only half the variable, and neither the compiler nor the linker would notice, because the linker matches names, not types.
extern is also implied for function declarations: void led_toggle(void); and extern void led_toggle(void); mean the same thing. Most code omits it for functions.
Initialization rules
Section titled “Initialization rules”The rules differ sharply between static and automatic storage, and the difference explains a whole category of bugs.
Objects with static storage duration (file scope, or static locals):
- With no initializer, they are zero-initialized: integers 0, floats 0.0, pointers null.
- Their initializers must be constant expressions, computable at compile time. You cannot write
static uint32_t start = get_time();. - The values are baked into the image. On a microcontroller, initialized variables go to
.data, whose initial values are stored in flash and copied into RAM by the startup code; zero-initialized ones go to.bss, which startup code fills with zeros. That happens beforemainruns. (On a PC the operating system loader does the equivalent.) If the startup code is broken, “zero-initialized” variables are not zero, which is one of the first things to check on a new board.
Objects with automatic storage duration (ordinary locals):
- With no initializer, the value is indeterminate. In practice it is whatever bits the previous function left in that register or stack slot. Using it is undefined behavior.
- Initializers can be any expression, and they run every time the declaration is reached.
#include <stdint.h>
uint32_t count_ones(uint32_t v){ uint32_t count; /* bug: never initialized */
while (v != 0u) { count += v & 1u; v >>= 1; } return count;}warning: 'count' may be used uninitialized [-Wmaybe-uninitialized]-Wall catches this case, reported as either “is used uninitialized” or “may be used uninitialized” depending on the GCC version and optimization level. The detection works best with optimization enabled, because it relies on the compiler’s data-flow analysis. It does not catch every case, for example when the variable is initialized on only some paths through a complicated function or passed by address to another function. On a microcontroller this bug is nasty because the stack contents are often the same on every boot, so the code works on the bench, then breaks when an unrelated change alters what ran before. Initialize every local at its declaration unless you have a reason not to.
Aggregates (arrays and structs) follow one more rule: if you provide an initializer for some elements, all the remaining elements are zero-initialized. So uint8_t buf[64] = { 0 }; zeroes the whole array, and uint16_t pins[8] = { 12, 13 }; sets the first two and zeroes the other six. For an automatic array this initialization costs time on every call (the compiler emits a memset or a copy), so do not initialize large local buffers you are about to overwrite anyway.
const: read-only, not constant
Section titled “const: read-only, not constant”const qualifies an object as read-only: the compiler rejects any attempt to modify it through that name.
#include <stdint.h>
const uint32_t sysclk_hz = 16000000u; /* HSI after reset */
void f(void){ sysclk_hz = 168000000u; /* error: assignment of read-only variable 'sysclk_hz' */}On a microcontroller, const at file scope (or static const in a function) has a major practical benefit: the object goes to .rodata, which the linker places in flash. A 1 KB lookup table declared const costs 1 KB of flash and zero RAM. The same table without const costs 1 KB of flash for its initial values and 1 KB of RAM to hold the copy. Arrays, strings, and tables exploits this heavily.
A const local without static is still an automatic variable: it lives on the stack or in a register and is initialized each time. const only promises you will not change it.
const is not a constant expression
Section titled “const is not a constant expression”This surprises everyone coming from C++. In C, a const variable is a read-only variable, not a compile-time constant. The places where C demands a constant expression reject it:
#include <stdint.h>
const uint32_t BUF_LEN = 64u;
uint8_t rx_buf[BUF_LEN]; /* rejected at file scope: not a constant expression */static uint32_t copy = BUF_LEN; /* rejected: static initializer must be constant */
void handle(uint32_t code){ uint8_t scratch[BUF_LEN]; /* accepted, but it is a variable-length array */
switch (code) { case BUF_LEN: /* rejected: case label must be constant */ break; default: break; } (void)scratch;}Standard C rejects the file-scope array, the static initializer and the case label. GCC accepts some of these as an extension when it can fold the value, and -Wpedantic then warns; other compilers refuse outright, so do not rely on it. The local array is the sneakiest: it compiles silently as a variable-length array (VLA), an array whose size is computed at run time and carved out of the stack. VLAs are banned in most firmware coding standards because their stack cost is not known at compile time. -Wvla warns about every one.
#define, enum or const?
Section titled “#define, enum or const?”C offers three ways to name a constant:
#define BUF_LEN 64u | enum { BUF_LEN = 64 }; | static const uint32_t BUF_LEN = 64u; | |
|---|---|---|---|
Usable as array size, case label, static initializer | yes | yes | no (in standard C) |
| Has a C type | the literal’s type | int | exactly what you wrote |
| Obeys block and file scope | no, it is text replacement until #undef | yes | yes |
Usable in #if | yes | no | no |
| Visible in the debugger | usually not | yes | yes |
| Value range | anything (64-bit, float, strings) | must fit in int | anything |
| Takes storage | no | no | possibly, if its address is taken |
The rules most firmware teams follow:
- Use
enumfor sets of related integer constants (states, command codes, error codes) and for array sizes that fit in anint. - Use
#definefor things that must be visible to the preprocessor (#if), for register addresses and bit masks (always with ausuffix:#define LED_PIN_MASK (1u << 12)), and for values that are notint-sized. - Use
static constfor typed values whose address you need and for tables.
C23 adds constexpr objects, which are true constant expressions with a type. Until your toolchain and coding standard allow C23, the table above is the state of the art. Macro pitfalls are covered in The preprocessor, used well.
volatile: the hardware can change this
Section titled “volatile: the hardware can change this”Normally the compiler assumes that a variable changes only when your code changes it. That assumption is what lets it keep values in registers and remove redundant loads. It is false in two situations that are everyday firmware:
- Memory-mapped hardware registers. The value of
GPIOA->IDR(input data register, address0x40020010on the STM32F407) changes when someone presses the button on PA0, with no code running. - Variables shared with interrupt handlers. An interrupt can fire between any two instructions of
mainand modify a global.
Here is the classic bug:
#include <stdbool.h>
static bool tick_flag; /* bug: should be volatile */
void SysTick_Handler(void) /* called by hardware every millisecond */{ tick_flag = true;}
void wait_for_tick(void){ while (!tick_flag) { /* spin */ } tick_flag = false;}Nothing inside the loop modifies tick_flag and the loop calls no function, so at -O2 the compiler may load the flag once, before the loop, and spin forever on the register copy. On the Cortex-M4 the loop often compiles to a test followed by a branch to itself. The interrupt does fire and does set the variable in RAM, but the loop never looks at RAM again. It works at -O0 and hangs at -O2.
Declaring it static volatile bool tick_flag; tells the compiler that every read and write of this object is an observable event that must actually be performed, in program order relative to other volatile accesses. The loop then re-reads the flag from memory on every iteration. The register case uses the same qualifier through a pointer:
#include <stdbool.h>#include <stdint.h>
#define GPIOA_IDR (*(volatile uint32_t *)0x40020010u) /* GPIOA base + 0x10 */
bool button_pressed(void){ return (GPIOA_IDR & 1u) != 0u; /* PA0, user button, active high */}The cast and the dereference are explained in Memory-mapped registers in C. For now, notice where volatile sits: on the object being pointed to, so every read of GPIOA_IDR becomes a real load instruction.
volatile is necessary but far from sufficient. It does not make an access atomic, it does not stop the CPU or compiler from reordering non-volatile accesses around it, and it does not protect a read-modify-write such as counter++ from being interrupted halfway. The precise rules are in Undefined behavior, volatile, and what the optimizer assumes, and safe data sharing with interrupts is in Interrupts done right.
register and auto: historical
Section titled “register and auto: historical”Two more storage-class keywords exist, and you will see them in old code:
autoexplicitly requests automatic storage duration, which is already the default for locals, so it is never needed in C11. In C23autogains a new meaning (type inference, as in C++), another reason not to use it in its old sense.registerasks the compiler to keep a variable in a register. Modern compilers ignore the hint for allocation decisions; they allocate registers far better than people do. Its only remaining effect is that taking the address of aregistervariable is an error. Do not use it. (GCC’s extensionregister uint32_t sp_val __asm__("sp");binds a variable to a specific register; that is a different, non-portable feature you may meet in startup code.)
Shadowing
Section titled “Shadowing”A declaration in an inner scope shadows a declaration of the same name in an outer scope: inside the inner scope, the name refers to the inner object only. It is legal, and it is a reliable source of bugs:
#include <stdint.h>
static uint32_t total; /* module-wide running total */
uint32_t accumulate(uint32_t sample){ uint32_t total = 0; /* pasted in from another function: shadows the module total */ total += sample; return total; /* returns sample; the module total never changes */}This compiles silently with -Wall -Wextra. Add -Wshadow:
warning: declaration of 'total' shadows a global declaration [-Wshadow]note: shadowed declaration is hereEnable -Wshadow in every firmware build. It also catches a nested loop that reuses i, and a parameter named the same as a global. The usual fix is renaming, and the conventions that prevent it in the first place (module prefixes for file-scope names, g_ for the rare true global) appear in Organizing a real C codebase.
Global variable discipline in firmware
Section titled “Global variable discipline in firmware”Firmware uses more global state than application code: there is no heap to speak of, hardware is global by nature, and interrupt handlers take no arguments, so the only way to communicate with them is through variables with static storage. That makes discipline essential:
- Default to
static. A file-scope variable should bestaticunless another module truly must touch it. Expose it through functions instead. - One definition, one header declaration. If a variable must be global, define it in one
.cfile, declare itexternin that module’s header, and include the header in the defining file. - One writer. Document which module (or which interrupt handler) writes each shared variable. Many readers with one writer is manageable; many writers is a design bug.
- Mark ISR-shared variables
volatile, and remember thatvolatiledoes not make multi-step updates safe. - Initialize in code, not by assumption. Static initializers must be constants, so anything computed (a calibration value, a clock frequency) needs an explicit
module_init()called frommain. Do not read a global from an interrupt that might fire before that init has run. - Count the RAM. Every static object occupies RAM for the entire run, whether or not it is in use. The map file tells you exactly how much each one costs; you will learn to read it in Linker scripts.
Where each variable lives
Section titled “Where each variable lives”Pulling it together, here is a file that touches every kind of storage, annotated with where each object ends up on the STM32F407:
#include <stdint.h>
uint32_t boot_count = 1; /* .data RAM, initial value copied from flash */uint32_t error_count; /* .bss RAM, zeroed by startup code */static uint8_t rx_buffer[256]; /* .bss RAM, private to this file */const uint16_t crc_table[4] = { 0x0000, 0x1021, 0x2042, 0x3063 }; /* .rodata flash */
uint32_t process(uint32_t input) /* .text flash */{ static uint32_t calls; /* .bss RAM, despite block scope */ const uint32_t scale = 3u; /* register or stack: automatic, not flash */ uint32_t result = input * scale; /* register or stack */
calls++; rx_buffer[calls % sizeof rx_buffer] = (uint8_t)result; return result + crc_table[input & 3u] + calls;} FLASH (0x08000000) SRAM (0x20000000, 128 KB) +--------------------------------+ +--------------------------------+ | vector table | | .data boot_count | <- copied from flash | .text process, ... | | .bss error_count, rx_buffer, | <- zeroed | .rodata crc_table, strings | | calls | | .data initial values (1) -----+----> | heap (if any) | +--------------------------------+ | ... | | stack: result, scale, ... | <- grows down from +--------------------------------+ 0x20020000| Declaration | Storage duration | Linkage | Section | Memory |
|---|---|---|---|---|
| file-scope, initialized nonzero | static | external | .data | RAM (plus its initial value in flash) |
| file-scope, uninitialized or zero | static | external | .bss | RAM |
file-scope static | static | internal | .data or .bss | RAM |
file-scope const | static | external | .rodata | flash |
local static | static | none | .data or .bss | RAM |
| local (automatic) | automatic | none | none | registers or stack |
| function | n/a | external or internal | .text | flash |
| string literal | static | none | .rodata | flash |
malloc result | allocated | none | none | heap |
Compile placement.c with arm-none-eabi-gcc -c and run arm-none-eabi-nm on it to see the letters D, B, b, R and T line up with this table. The complete memory map, how startup code fills .data and .bss, and how to measure stack and heap usage are the subject of Memory: where everything lives.
Common mistakes
Section titled “Common mistakes”- Defining a variable in a header: every includer gets a definition and the link fails with “multiple definition”. Put
externin the header and the definition in one.cfile. - Two files with the same uninitialized global: links with old compilers, fails with GCC 10 and later. Define it once and declare it
externelsewhere. - Reading an uninitialized local: the value is whatever was on the stack. Initialize at the declaration and keep
-Wallon. - Using a
constvariable as an array size: at file scope it is rejected; in a function it silently creates a VLA. Use anenumor#define. - Forgetting
volatileon a variable written by an interrupt: the main loop spins forever at-O2. Mark itvolatile, then handle atomicity separately. - Expecting a
staticlocal to be reinitialized each call: it is initialized once. Use an ordinary local if you need a fresh value. - Calling a function with
staticlocal state from bothmainand an ISR: they share one copy of the state. Move the state out and pass it in, or protect it. - Shadowing a global with a local of the same name: writes go to the local. Enable
-Wshadow. - Large arrays as automatic locals: they consume stack on every call and can overflow it. Make them
static(accepting non-reentrancy) or file-scope, and check the map file.
Exercises
Section titled “Exercises”- Build
next_id.c, then remove thestaticfromidand predict the new output before running it. Then moveidto file scope asstatic uint32_t id = 100;and confirm the output returns to 100, 101, 102. Explain what changed in scope and what did not change in storage duration. - Create
a.ccontainingint counter;andint get_a(void) { return counter; }, andb.ccontainingint counter;and amainthat printsget_a(). Link with your compiler’s default, then with-fcommon, then with-fno-common. Explain each result, then fix the code properly withextern. - Compile
placement.cwitharm-none-eabi-gcc -std=c11 -O2 -mcpu=cortex-m4 -mthumb -c, runarm-none-eabi-nm placement.oandarm-none-eabi-size placement.o. Match every symbol to its row in the table above. Then remove theconstfromcrc_tableand explain the change in thesizecolumns: which grew, which shrank, and what it would cost on the chip. - Compile
wait_for_tickfrom thevolatilesection witharm-none-eabi-gcc -O2 -mcpu=cortex-m4 -mthumb -S, once without and once withvolatile. In the non-volatile version, find the loop and show that it contains no load from memory. In the volatile version, find theldrbinside the loop. - Write
uint32_t moving_sum(uint32_t sample)that returns the sum of the last four samples it was given, keeping its history instaticlocals. Test it on the host. Then explain in two sentences why this design fails if two sensors call it, and rewrite the function so the caller passes in the history storage instead.
Check yourself
Section titled “Check yourself”Question 1:What does this print? void f(void) { static int n = 0; n++; printf("%d ", n); } called three times.
A static local is initialized once and keeps its value between calls, so each call increments the same object.
Question 2:Which of these is a declaration but not a definition?
extern without an initializer only announces the name and type. The others all create storage.
Question 3:What does static do to a function defined at file scope?
At file scope, static changes linkage, not storage duration. The function becomes private to its .c file, which also lets the compiler inline or remove it freely.
Question 4:Which can legally be used as the size of a file-scope array in standard C?
An enumeration constant is an integer constant expression. A const variable is a read-only object, which C does not treat as a constant expression.
Question 5:A SysTick handler sets bool done = true; (not volatile) and main runs while (!done) {}. At -O2 the program hangs. Why?
Without volatile the compiler may assume the variable changes only through code it can see. The loop body changes nothing, so it hoists the load out of the loop.
Question 6:Inside a function you write static uint8_t frame[2048];. Where does it live on the STM32F407?
A static local has static storage duration. Without an initializer it goes to .bss in RAM, exactly like an uninitialized file-scope array.
Key takeaways
Section titled “Key takeaways”- Scope (where a name is visible), linkage (whether names in different files match) and storage duration (when the object exists) are separate properties. Learn to name all three for any declaration.
- Every externally linked object or function is defined exactly once; headers contain
externdeclarations,.cfiles contain definitions. staticin a function means “persist between calls”;staticat file scope means “private to this file”. Make everythingstaticthat does not need to be public.- Static-duration objects are zero-initialized if not explicitly initialized and must have constant initializers; automatic objects start with garbage.
constmeans read-only, not compile-time constant. File-scopeconstdata lives in flash and saves RAM. Useenumor#definefor constant expressions.volatileforces every access to happen, which hardware registers and ISR-shared flags require. It does not provide atomicity or ordering of other accesses.- Initialized statics go to
.data(RAM plus a flash copy), zeroed statics to.bss(RAM),constdata to.rodata(flash), locals to registers or the stack.
Next: Operators, expressions, and implicit conversions, where you learn what C really does when it evaluates a + b, and why -1 < 1u is false.