The structures that show up directly in production firmware: memory pools, table-driven state machines, and lock-free ISR-to-main communication.
/* DAY 22 · Week 4 */
Linear & Binary Search
Focus: searching fixed lookup/calibration tables efficiently.
Topics
- Linear search, when it’s actually the right choice (small/unsorted n)
- Iterative binary search, off-by-one pitfalls
Practice Questions
- Implement iterative binary search on a sorted ADC calibration table.
- Find the closest value (not exact match) in a sorted array — useful for lookup-table interpolation.
Code
binary_search.c
int16_t binary_search(const int16_t *arr, uint8_t n, int16_t target) {
int16_t lo = 0, hi = n - 1;
while (lo <= hi) {
int16_t mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
/* DAY 23 · Week 4 */
Recursion vs. Iteration & Stack Budget
Focus: quantifying stack usage — a 512-byte–1KB task stack does not forgive deep recursion.
Topics
- Estimating stack frame size, worst-case recursion depth
- Tail recursion and why most embedded compilers won’t reliably optimize it — convert manually
Practice Questions
- Convert recursive Fibonacci to an iterative O(n) version.
- Convert recursive GCD (Euclid’s algorithm) to iterative.
- Given a stack frame of ~32 bytes and a 1KB stack, compute the max safe recursion depth.
Code
fib_iterative.c
uint32_t fib_iterative(uint8_t n) {
uint32_t a = 0, b = 1;
for (uint8_t i = 0; i < n; i++) {
uint32_t next = a + b;
a = b; b = next;
}
return a;
}
/* DAY 24 · Week 4 */
Graph Basics (Iterative BFS/DFS)
Focus: small fixed graphs — sensor networks, dependency/init-order resolution.
Topics
- Adjacency matrix vs adjacency list, memory tradeoffs for small node counts
- Iterative BFS using a static queue array, iterative DFS using a static stack array
Practice Questions
- Represent an 8-node sensor network as an adjacency matrix and run BFS from node 0.
- Use DFS to detect whether the module-init dependency graph has a cycle.
Code
bfs_graph.c
#define N_NODES 8
uint8_t adj[N_NODES][N_NODES]; /* 1 = edge exists */
void bfs(uint8_t start) {
uint8_t visited[N_NODES] = {0};
uint8_t queue[N_NODES], head = 0, tail = 0;
queue[tail++] = start; visited[start] = 1;
while (head != tail) {
uint8_t node = queue[head++];
printf("Visit %d\n", node);
for (uint8_t n = 0; n < N_NODES; n++) {
if (adj[node][n] && !visited[n]) {
visited[n] = 1;
queue[tail++] = n;
}
}
}
}
/* DAY 25 · Week 4 */
Fixed-Block Memory Pool Allocator
Focus: a deterministic, fragmentation-free replacement for malloc/free.
Topics
- Fixed-block pool design, intrusive free list stored inside unused blocks
- O(1) alloc and free, deterministic worst-case timing
Practice Questions
- Implement
pool_alloc() / pool_free() for fixed 32-byte blocks.
- Explain why this design has no fragmentation compared to a general-purpose heap.
Code
mem_pool.c
#define BLOCK_SIZE 32
#define BLOCK_COUNT 16
typedef struct free_block { struct free_block *next; } free_block_t;
static uint8_t pool_mem[BLOCK_SIZE * BLOCK_COUNT];
static free_block_t *free_list = NULL;
void pool_init(void) {
free_list = NULL;
for (int i = BLOCK_COUNT - 1; i >= 0; i--) {
free_block_t *b = (free_block_t *)&pool_mem[i * BLOCK_SIZE];
b->next = free_list;
free_list = b;
}
}
void *pool_alloc(void) {
if (!free_list) return NULL;
void *block = free_list;
free_list = free_list->next;
return block;
}
void pool_free(void *block) {
free_block_t *b = (free_block_t *)block;
b->next = free_list;
free_list = b;
}
/* DAY 26 · Week 4 */
Table-Driven Finite State Machines
Focus: FSMs as a data structure — arrays of structs + function pointers, not nested switch statements.
Topics
- State/event transition tables represented as 2D arrays or structs
- Entry/exit action function pointers per state
Practice Questions
- Design a table-driven FSM for a push-button debouncer (Idle, Debounce, Pressed).
- Add an entry-action callback that fires whenever a state changes.
Code
fsm_debounce.c
typedef enum { ST_IDLE, ST_DEBOUNCE, ST_PRESSED, ST_COUNT } state_t;
typedef enum { EV_BTN_DOWN, EV_TIMEOUT, EV_BTN_UP, EV_COUNT } event_t;
static const state_t transition_table[ST_COUNT][EV_COUNT] = {
/* EV_BTN_DOWN EV_TIMEOUT EV_BTN_UP */
/* ST_IDLE */ {ST_DEBOUNCE, ST_IDLE, ST_IDLE},
/* ST_DEBOUNCE*/{ST_DEBOUNCE, ST_PRESSED, ST_IDLE},
/* ST_PRESSED*/ {ST_PRESSED, ST_PRESSED, ST_IDLE},
};
state_t fsm_step(state_t current, event_t ev) {
return transition_table[current][ev];
}
/* DAY 27 · Week 4 */
Lock-Free SPSC Ring Buffer (ISR-Safe)
Focus: revisiting Day 11’s ring buffer for true ISR-to-main-loop safety.
Topics
- Why a shared
count field is unsafe between an ISR (producer) and main loop (consumer)
- Single-producer/single-consumer buffer using only
volatile head/tail, no shared count, no locks
Practice Questions
- Rewrite the Day 11 ring buffer to use only
volatile head/tail (no count) so it’s safe for one ISR writer and one main-loop reader.
- Explain why this pattern still breaks with two producers (multiple ISRs) without a critical section.
Code
spsc_ring_buffer.c
#define SPSC_CAP 64 /* power of two */
typedef struct {
uint8_t buf[SPSC_CAP];
volatile uint16_t head; /* written only by producer (ISR) */
volatile uint16_t tail; /* written only by consumer (main loop) */
} spsc_rb_t;
/* Called from ISR */
int spsc_push(spsc_rb_t *rb, uint8_t byte) {
uint16_t next = (rb->head + 1) & (SPSC_CAP - 1);
if (next == rb->tail) return 0; /* full */
rb->buf[rb->head] = byte;
rb->head = next; /* single atomic-ish write */
return 1;
}
/* Called from main loop */
int spsc_pop(spsc_rb_t *rb, uint8_t *out) {
if (rb->tail == rb->head) return 0; /* empty */
*out = rb->buf[rb->tail];
rb->tail = (rb->tail + 1) & (SPSC_CAP - 1);
return 1;
}
/* DAY 28 · Week 4 */
Mini Project — UART Command Processor
Focus: wire together the ring buffer, hash dispatcher and FSM into one realistic module.
Practice Questions
- Design a UART command processor: bytes land in the Day 27 ring buffer from an RX ISR; the main loop pops bytes, assembles a line, looks up the command in the Day 20 hash table, and dispatches a handler.
- Add a small FSM (Day 26 style) with states
WAIT_START, READ_LINE, DISPATCH to drive the parser.
- Add a fixed-block pool (Day 25) to hand out small “command result” structs to handlers.
Code
uart_command_processor.c
void uart_rx_isr(void) {
uint8_t byte = UART_READ_DATA_REG(); /* hardware read */
spsc_push(&rx_rb, byte);
}
void main_loop_step(void) {
static char line[64]; static uint8_t idx = 0;
uint8_t byte;
while (spsc_pop(&rx_rb, &byte)) {
if (byte == '\n') {
line[idx] = '\0';
void (*handler)(void) = ht_lookup(line);
if (handler) handler();
idx = 0;
} else if (idx < sizeof(line) - 1) {
line[idx++] = byte;
}
}
}