MODULE 8 · LESSON 8.3 · EXAMPLES
Scope and Linkage Examples in Embedded C: Private State, Shared Flags, Link Errors and Safe Recursion
Eight worked examples with real output: a blinker with private state, debouncers, an ISR flag shared with extern volatile, one-time table building, static name clashes, multiple-definition link errors, depth-limited recursion and an explicit stack. Study this one of the scope and linkage examples in embedded C closely.
Real output
Compiled and run
Scope and Linkage Examples in Embedded C: What This Page Covers
These scope and linkage examples in embedded C apply Lesson 8.3: private module state with static, a debouncer that works for one button and fails for two, a flag shared between main code and an interrupt handler, one-time initialisation, name clashes between files, and recursion with a safe depth limit. Every program was compiled with GCC and run; the output is real. These scope and linkage examples in embedded C build on each other.
| # | Example | Skill |
|---|
| 1 | A blinker with private module state | Internal linkage |
| 2 | Static-local debouncer versus per-button state | Reentrancy |
| 3 | A flag shared with an interrupt handler | extern and volatile |
| 4 | Build a lookup table once | Static duration |
| 5 | Two files with the same static name | Linkage |
| 6 | The same name defined twice: the link error | One definition |
| 7 | Recursion with a depth limit | Bounded recursion |
| 8 | An explicit stack instead of recursion | Constant stack |
| 9 | Mini exercises | Self-test |
Example 1: A Blinker with Private Module State
Problem: Toggle an LED every three ticks without letting other code corrupt the counter. The three state variables are file-scope but static, so they have internal linkage: main.c can only use the three public functions. Changing the implementation later (a different timer scheme) cannot break any caller. Among the scope and linkage examples in embedded C this pattern is very common.
blink.h
#ifndef BLINK_H
#define BLINK_H
#include <stdint.h>
void blink_init(uint8_t half_period_ticks);
void blink_tick(void); /* call every 1 ms */
uint8_t blink_led_is_on(void);
#endif
blink.c
#include “blink.h”
static uint8_t s_half; /* private: nobody else can set these directly */
static uint8_t s_count;
static uint8_t s_on;
void blink_init(uint8_t half_period_ticks)
{
s_half = half_period_ticks;
s_count = 0;
s_on = 0;
}
void blink_tick(void)
{
if (++s_count >= s_half)
{
s_count = 0;
s_on ^= 1u;
}
}
uint8_t blink_led_is_on(void)
{
return s_on;
}
main.c
#include <stdio.h>
#include “blink.h”
int main(void)
{
blink_init(3);
for (int t = 1; t <= 12; t++)
{
blink_tick();
printf(“t=%2d led=%u\n”, t, blink_led_is_on());
}
return 0;
}
Output (verified by compiling and running)
t= 1 led=0
t= 2 led=0
t= 3 led=1
t= 4 led=1
t= 5 led=1
t= 6 led=0
t= 7 led=0
t= 8 led=0
t= 9 led=1
t=10 led=1
t=11 led=1
t=12 led=0
Pattern: File-scope static variables plus a small header make a module with private data: the C equivalent of a class with private members.
Example 2: Static-Local Debouncer versus Per-Button State
Problem: Debounce two buttons with the same function. In version 1 both buttons share the single pair of static variables, so each call disturbs the other (A reads 1, B reads 0, they fight over last and stable and neither is ever reported as stable). In version 2 the return value 1 means “this input has been stable for two samples”: A is stable at 1 and B is stable at 0. Version 2 stores the state in a struct passed by pointer, so every button has its own copy. This is why functions with static locals are not reentrant. Keep these scope and linkage examples in embedded C for revision.
deb_two.c
#include <stdio.h>
/* Version 1: state hidden in static locals – works for ONE input only */
static int debounce_static(int raw)
{
static int last = 0, stable = 0;
if (raw == last) { if (stable < 2) stable++; }
else { last = raw; stable = 0; }
return stable >= 2;
}
/* Version 2: state in a struct owned by the caller – works for any number of inputs */
typedef struct { int last; int stable; } deb_t;
static int debounce(deb_t *d, int raw)
{
if (raw == d->last) { if (d->stable < 2) d->stable++; }
else { d->last = raw; d->stable = 0; }
return d->stable >= 2;
}
int main(void)
{
int btnA[] = {1, 1, 1, 1, 1, 1}; /* steady press */
int btnB[] = {0, 0, 0, 0, 0, 0}; /* steady release */
printf(“static locals, two buttons interleaved:\n”);
for (int i = 0; i < 6; i++)
printf(” A=%d B=%d\n”, debounce_static(btnA[i]), debounce_static(btnB[i]));
printf(“struct state, one deb_t per button:\n”);
deb_t a = {0, 0}, b = {0, 0};
for (int i = 0; i < 6; i++)
printf(” A=%d B=%d\n”, debounce(&a, btnA[i]), debounce(&b, btnB[i]));
return 0;
}
Output (verified by compiling and running)
static locals, two buttons interleaved:
A=0 B=0
A=0 B=0
A=0 B=0
A=0 B=0
A=0 B=0
A=0 B=0
struct state, one deb_t per button:
A=0 B=0
A=0 B=1
A=1 B=1
A=1 B=1
A=1 B=1
A=1 B=1
Rule: If a function must serve several instances, its state belongs in a structure owned by the caller, not in the function.
Example 3: A Flag Shared with an Interrupt Handler
Problem: The handler and main live in different files and must see the same variable. The header declares it extern volatile; exactly one file (isr.c) defines it, and it includes the header so the compiler checks that both declarations agree. On the board the two handlers are called by the hardware; here they are called by hand so that the program runs on a PC.
flag.h
#ifndef FLAG_H
#define FLAG_H
#include <stdint.h>
extern volatile uint8_t g_button_event; /* set by the ISR, cleared by main */
extern volatile uint32_t g_ticks;
void EXTI0_IRQHandler(void); /* the real STM32F407 vector name */
void SysTick_Handler(void);
#endif
isr.c
#include “flag.h”
volatile uint8_t g_button_event; /* the one definition (zero at start-up) */
volatile uint32_t g_ticks;
void EXTI0_IRQHandler(void)
{
g_button_event = 1; /* real code would also clear EXTI_PR bit 0 */
}
void SysTick_Handler(void)
{
g_ticks++;
}
main.c
#include <stdio.h>
#include “flag.h”
int main(void)
{
for (int step = 1; step <= 6; step++)
{
SysTick_Handler(); /* pretend the timer fired */
if (step == 4) EXTI0_IRQHandler(); /* pretend PA0 was pressed */
if (g_button_event)
{
g_button_event = 0;
printf(“step %d: button event handled at tick %lu\n”, step, (unsigned long)g_ticks);
}
}
printf(“total ticks = %lu\n”, (unsigned long)g_ticks);
return 0;
}
Output (verified by compiling and running)
step 4: button event handled at tick 4
total ticks = 6
Why volatile: Without volatile the compiler may read g_button_event once and keep it in a register, never noticing the handler’s change. Module 14 explains this fully.
Example 4: Build a Lookup Table Once
Problem: Compute a waveform table the first time it is needed and reuse it afterwards. A static array keeps the data between calls and a static flag remembers whether it is built. The message appears only once, in the first call. The table lives in .bss (16 bytes) rather than on the stack, so there is no cost on each call. The table holds a triangle wave so that no maths library is needed.
table_once.c
#include <stdio.h>
#include <stdint.h>
#define N 8
static uint16_t wave_q15(unsigned i) /* builds the table on first use */
{
static uint16_t table[N];
static int ready = 0;
static int builds = 0;
if (!ready)
{
for (unsigned k = 0; k < N; k++) /* triangular wave: simple, no <math.h> needed */
table[k] = (uint16_t)((k < N / 2) ? k * 8192u : (N – k) * 8192u);
ready = 1;
builds++;
printf(” (table built, builds=%d)\n”, builds);
}
return table[i % N];
}
int main(void)
{
for (unsigned i = 0; i < 6; i++)
printf(“i=%u -> %u\n”, i, wave_q15(i));
return 0;
}
Output (verified by compiling and running)
(table built, builds=1)
i=0 -> 0
i=1 -> 8192
i=2 -> 16384
i=3 -> 24576
i=4 -> 32768
i=5 -> 24576
Caution: Lazy initialisation is not thread- or interrupt-safe. Two contexts could both see ready == 0. In firmware, build tables in an explicit init() before enabling interrupts, or place them in Flash as const arrays.
Example 5: Two Files with the Same Static Name
Problem: Two drivers each need a counter called s_count. Because both are static (internal linkage), the linker sees two unrelated objects. Neither driver can accidentally read or change the other’s counter. In a large project with hundreds of files this is what keeps names from colliding.
uart.c
#include <stdio.h>
static int s_count; /* private to uart.c */
void uart_event(void) { s_count++; }
void uart_report(void) { printf(“uart events: %d\n”, s_count); }
spi.c
#include <stdio.h>
static int s_count; /* same name, different object */
void spi_event(void) { s_count += 10; }
void spi_report(void) { printf(“spi events: %d\n”, s_count); }
main.c
void uart_event(void); void uart_report(void);
void spi_event(void); void spi_report(void);
int main(void)
{
uart_event(); uart_event();
spi_event();
uart_report();
spi_report();
return 0;
}
Output (verified by compiling and running)
uart events: 2
spi events: 10
Example 6: The Same Name Defined Twice (Link Error)
Problem: Remove static from the two counters of Example 5. Now both have external linkage and the linker finds two definitions of one name. The GCC option -fno-common (the default since GCC 10) makes this an error instead of silently merging the two variables.
uart.c
int s_count;
void uart_event(void) { s_count++; }
spi.c
int s_count;
void spi_event(void) { s_count += 10; }
main.c
void uart_event(void);
void spi_event(void);
int main(void) { uart_event(); spi_event(); return 0; }
Linker output for two external definitions of s_count
/usr/bin/ld: file1.o:(.bss+0x0): multiple definition of `s_count’; file2.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
Fix: Make the variable static if it is private, or give each a unique prefix (uart_count, spi_count) if it must be global.
Example 7: Recursion with a Depth Limit
Problem: If recursion cannot be avoided, bound it. sum_to carries a depth counter and returns an error instead of diving deeper than MAX_DEPTH, so the worst-case stack use is known (16 frames). Euclid’s gcd is a safe recursive function because each call shrinks the numbers quickly: the depth grows only with the logarithm of the inputs. A tail-call version can be turned into a loop by the compiler.
depth_limit.c
#include <stdio.h>
#define MAX_DEPTH 16
/* returns the sum of 1..n, or -1 if the recursion would be deeper than MAX_DEPTH */
static int sum_to(int n, int depth)
{
if (depth > MAX_DEPTH) return –1; /* guard: refuse to dive too deep */
if (n <= 0) return 0; /* base case */
int rest = sum_to(n – 1, depth + 1);
return (rest < 0) ? –1 : n + rest;
}
static int gcd(int a, int b) /* Euclid: depth is small by construction */
{
return b == 0 ? a : gcd(b, a % b);
}
int main(void)
{
printf(“sum_to(10) = %d\n”, sum_to(10, 0));
printf(“sum_to(16) = %d\n”, sum_to(16, 0));
printf(“sum_to(100) = %d (refused)\n”, sum_to(100, 0));
printf(“gcd(1071, 462) = %d\n”, gcd(1071, 462));
printf(“gcd(48, 18) = %d\n”, gcd(48, 18));
return 0;
}
Output (verified by compiling and running)
sum_to(10) = 55
sum_to(16) = 136
sum_to(100) = -1 (refused)
gcd(1071, 462) = 21
gcd(48, 18) = 6
Worst case: Stack bound = (maximum depth) × (frame size). Measure the frame size with -fstack-usage and keep a margin for interrupts: each nested exception adds at least 32 bytes on the Cortex-M4.
Example 8: An Explicit Stack Instead of Recursion
Problem: Visit the nodes of a binary tree in sorted order. The recursive version is short but its stack use depends on the tree height. The iterative version keeps its own array stack of 8 entries, checks for overflow and returns an error instead of crashing, and reports the peak use (3 entries for this 7-node tree). Both print the same sorted sequence.
tree_walk.c
#include <stdio.h>
/* A binary tree stored in arrays: node i has children left[i], right[i]; -1 = none */
#define NODES 7
static const int value[NODES] = { 50, 30, 70, 20, 40, 60, 80 };
static const int left [NODES] = { 1, 3, 5, –1, –1, –1, –1 };
static const int right[NODES] = { 2, 4, 6, –1, –1, –1, –1 };
static void inorder_rec(int n) /* recursive: stack grows with tree height */
{
if (n < 0) return;
inorder_rec(left[n]);
printf(” %d”, value[n]);
inorder_rec(right[n]);
}
#define STACK_MAX 8
static int inorder_iter(void) /* explicit stack: size known at compile time */
{
int stack[STACK_MAX], sp = 0, n = 0, high = 0;
while (n >= 0 || sp > 0)
{
while (n >= 0)
{
if (sp == STACK_MAX) return –1; /* overflow check instead of a crash */
stack[sp++] = n;
n = left[n];
}
if (sp > high) high = sp;
n = stack[–sp];
printf(” %d”, value[n]);
n = right[n];
}
return high;
}
int main(void)
{
printf(“recursive :”); inorder_rec(0); printf(“\n”);
printf(“iterative :”);
int high = inorder_iter();
printf(“\nmaximum stack entries used = %d\n”, high);
return 0;
}
Output (verified by compiling and running)
recursive : 20 30 40 50 60 70 80
iterative : 20 30 40 50 60 70 80
maximum stack entries used = 3
Design choice: An explicit stack turns an unpredictable run-time failure into a visible, testable limit, which is what safety-critical code needs.
Example 9: Mini Exercises
Exercise 1: What happens if blink.c in Example 1 declares s_count without static?
It gets external linkage. Another file defining or declaring s_count would share it or clash with it at link time.
Exercise 2: Why does the static-local debouncer fail for two buttons?
Both buttons update the same last and stable; each call overwrites the other’s history.
Exercise 3: Which file should define g_ticks in Example 3?
Exactly one .c file (isr.c), which also includes the header. All others only include the header.
Exercise 4: Where does the static table of Example 4 live on an STM32F407?
In SRAM, in .bss (zero-initialised before main). A static const table would be in Flash (.rodata).
Exercise 5: What does the linker say if two files define the same non-static global?
multiple definition of the name; the build stops.
Exercise 6: What bounds the stack in Example 8?
The array stack[STACK_MAX], 8 ints = 32 bytes, with an explicit overflow check.
Exercise 7: Can gcd overflow the stack?
Not for 32-bit inputs: the depth is at most about 45 calls (Fibonacci-sized worst case), but the bound must still be checked against the real frame size.
Key Takeaways from the Scope and Linkage Examples in Embedded C
- These scope and linkage examples in embedded C show that file-scope
static variables make private module state. - State shared by several instances belongs in a struct, not in static locals.
- Shared variables are declared
extern in a header and defined in one .c file. - Static names in different files never clash; external names must be unique.
- Bound every recursion or replace it with an explicit stack of known size.
Ready for the quiz?
Next: predict static-variable output, find the linkage errors and answer the scope and linkage interview questions.
2 Comments