Module 8 Exercises — Embedded C in Practice
Back to the Course 3 syllabus. Read first: Module 8 lessons (MC 39–49 and Seacord 6, 9–11 remain available as optional deep-dives).
Work in course3/m8/ of the labs repo. As in Modules 6 and 7, everything runs on the Mac: clang plus sanitizers for the host-side experiments, arm-none-eabi-gcc cross-compilation, objdump, and the linker map for everything STM32-flavored. The NUCLEO stays in its drawer — when a register block appears here it is a fake one at a made-up address, because the subject is what the compiler does, not what the peripheral does.
Exercises
Exercise 8.1 — What volatile buys, proven in disassembly. Write a flag-polling loop twice: once with a plain uint32_t flag, once volatile-qualified. Compile both at -O0 and -O2 on both toolchains and annotate the four disassemblies: where the optimizer hoisted the load and turned the plain version into an infinite loop or a single test, and how the volatile version forces a load per iteration (MC 39). Then complete the guarantees table from evidence, not folklore:
| Property | volatile provides it? |
Evidence (which listing / which reading) |
|---|---|---|
| Load/store actually emitted | … | … |
Atomicity of flag++ |
… | … |
| Ordering vs. surrounding non-volatile accesses | … | … |
| Memory barrier at the hardware level | … | … |
| Event counting (no lost events) | … | … |
Exercise 8.2 — MMIO forensics on a fake register block. Declare a GPIO_TypeDef-style struct with volatile members (and one volatile const input register, MC 40) pinned at a made-up address; cross-compile only. Annotate the generated code for: (a) a BSRR-style one-shot set vs. an ODR |= pin read-modify-write — count the memory operations and name the hazard window (MC 41); (b) uint8_t/uint16_t/uint32_t pointer accesses to the same address — confirm LDRB/LDRH/LDR selection by type; (c) the same field update written as a bit-field member store vs. explicit mask-and-shift — compare the emitted RMW sequences (MC 42). Deliverable: annotated disassembly plus a notes.md paragraph on why a write-1-to-clear status register makes |= a bug even though the C looks innocent.
Exercise 8.3 — Layout: padding, packing, and the wire. Design a mixed-width struct (uint8_t, uint32_t, uint16_t, in that unhelpful order). Predict sizeof and every offsetof on the Cortex-M4, then verify with cross-side _Static_assert probes. Add __attribute__((packed)) and re-verify; take the address of a packed uint32_t member and record the compiler’s reaction (MC 43). Reorder the members by descending alignment and show the padding disappear without packing. Finish by serializing the struct for “the wire” the robust way — explicit memcpy/byte-wise encoding (MC 48) — and _Static_asserting the wire length. Deliverable: the predicted-vs-observed layout table for all three variants plus the serialization routine’s cross-compiled listing.
Exercise 8.4 — UB safari, hunted host-side. Four small programs, each built on a hazard from MC 47–48 that firmware actually hits: a signed int32_t timer delta that overflows; a shift by ≥ the promoted width; a strict-aliasing pun (float read through a uint32_t*); and an out-of-bounds ring-buffer index. For each: predict, then record behavior at clang -O0 and -O2, then run under -fsanitize=undefined,address, then write the correct version (unsigned wraparound arithmetic for the timer, memcpy for the pun, masked index for the ring). Complete the coverage table:
| Hazard | Behavior at -O0 |
Behavior at -O2 |
Caught by sanitizer? | Correct idiom |
|---|---|---|---|---|
| Signed overflow | … | … | … | … |
| Oversized shift | … | … | … | … |
| Aliasing pun | … | … | … | … |
| OOB index | … | … | … | … |
In notes.md: the Seacord 11 discipline in one paragraph — which of these classes warnings catch, which need sanitizers, which need static analysis, and why the host build is where all three run even for firmware code.
Exercise 8.5 — Sections, weak symbols, and the map file. Cross-toolchain throughout (MC 44): place a buffer in a named section with __attribute__((section(".dma_buffer"), aligned(32))); define a weak default handler and then override it with a strong definition in another translation unit. Verify every claim in two places — arm-none-eabi-objdump -h/-t and the linker map: the section exists, the buffer’s address is 32-byte aligned, the strong symbol won. Then connect it back to Exercise 6.4: find the weak default handlers in the Course 2 STM32 startup file and the KEEP directive that protects the vector table, and explain in notes.md what would happen without each. Deliverable: annotated map-file excerpts.
Exercise 8.6 — Sharing with an ISR, three ways. Implement the MC 45 patterns as three cross-compiled variants of “handler produces, main consumes”: (1) a volatile bool level flag; (2) a PRIMASK save/disable/restore critical section around a multi-word update (the MC 15 irq_save idiom); (3) an atomic event counter from Exercise 7.8. Annotate all three -O2 disassemblies. For each, name the failure mode it prevents and the one it doesn’t — lost events for the flag, latency cost and the blind-re-enable bug for the critical section, single-word-only scope for the atomic. Deliverable: the three annotated listings plus a decision table (“shared state shape → pattern”) you’d actually consult while writing Course 2’s ADC/DMA callbacks.
Exercise 8.7 — Capstone: migrate a real firmware module. Apply the MC 49 checklist to one real module of the Course 2 labs repo firmware (the Lab 5.3 ADC + DMA module is the richest target). Pin the dialect and warning set in the toolchain file (-std=gnu17, -Wall -Wextra -Wconversion -Wshadow -Wundef -Wdouble-promotion -Wformat=2 -Wstrict-prototypes), rebuild, and triage every new diagnostic: fix, or justify in a comment. Audit the module against this course: every volatile object (8.1), every register access idiom (8.2), every struct that crosses a DMA or protocol boundary (8.3), every ISR-shared object (8.6), and the map file (8.5). Deliverable: the course’s closing staff-level note — “My modern-C firmware subset”: one page recording the pinned dialect, the always-on warning set, the features in the working subset, the features banned and why (VLAs, heap after init, packed-member pointers, bit-field MMIO, blind |= on status registers), and what the migration actually caught in code that “already worked.”