Module 9 Exercises — Interrupts and Shared State

Back to the Course 2 syllabus. Read first: Module 9 lessons (MC 33–35 and 45, Rust Book Ch 16, and the Embedded Rust Book’s Concurrency chapter remain available as optional deep-dives).

Work in the labs repo’s course2/ folder — c/mcu, c/host, rust/qemu, rust/mcu, and for the last exercise c/linux/rust/linux — and record everything in m9/notes.md. The C side of this module is read, not run: every STM32-flavored program is cross-compiled at -O2 with clang --target=thumbv7em-none-eabihf (or arm-none-eabi-gcc if installed) and its disassembly annotated. The Rust side runs: rust/qemu boots the lm3s6965evb machine, whose SysTick and peripheral timers are real enough to interrupt a loop. Flashing the NUCLEO (rust/mcu) is the optional last rung wherever an exercise mentions it. Predicted cells are filled in before building; observed cells at the machine.

Exercises

Exercise 9.1 — A shared counter, three ways in C. In c/mcu/src/ex-9-1/, write “handler increments, main drains” three times: (1) a plain volatile uint32_t with count++ in the handler and n = count; count = 0; in main; (2) the same with a PRIMASK save/disable/restore critical section around the drain (the irq_save/irq_restore pair from lessons §3.2); (3) an atomic_uint with atomic_fetch_add_explicit in the handler and atomic_exchange_explicit(…, 0, …) in main. Cross-compile each at -O2 and annotate the disassembly of the handler and the drain. For each variant name the failure shape from lessons §1 it prevents and the one it does not:

Variant Handler instructions (annotated) Drain instructions (annotated) Prevents Still fails at Latency added to other interrupts?
(1) volatile
(2) critical section
(3) atomic

Then break (2) deliberately: replace the restore with a blind __enable_irq() and write, in notes.md, the call sequence in which the blind version corrupts a caller’s own critical section. Deliverable: the three annotated listings, the table, and the paragraph.

Exercise 9.2 — The versions that refuse to compile. In rust/qemu/src/bin/ex-9-2.rs, write the counter four times, predicting for each whether it compiles and, if it does, whether it is correct: (a) static mut COUNTER: u32 with unsafe increments in main and a SysTick #[exception] that resets it; (b) static COUNTER: Cell<u32>; (c) static COUNTER: critical_section::Mutex<Cell<u32>> with critical_section::with; (d) static COUNTER: AtomicU32. For (a) and (b) record the exact diagnostic (or lint) and the trait or rule it cites; for (c) and (d) run them in QEMU with SysTick firing at a rate you choose and hprintln! reporting the drained count each second. Then compare cargo objdump of (c)’s and (d)’s increment paths against Exercise 9.1’s C listings for variants (2) and (3).

Version Predicted: compiles? correct? Observed diagnostic / behavior Matches which C variant’s code?
(a) static mut
(b) static Cell
(c) Mutex<Cell>
(d) AtomicU32

Deliverable: the file with all four versions (the failing ones behind #[cfg(feature = …)] gates so the crate still builds), the table, and a paragraph on what Sync checked that the C compiler could not.

Exercise 9.3 — An SPSC ring across SysTick, in QEMU. Write rust/qemu/src/bin/ex-9-3.rs: a heapless::spsc::Queue<u16, N> allocated through StaticCell, split, the Producer moved into a Mutex<RefCell<Option<…>>> reachable from a SysTick handler that enqueues a synthetic sample each tick, the Consumer kept in main, which drains in bursts and sleeps with wfi. Predict, then measure with counters printed at exit: the number of enqueue failures when the consumer is deliberately made slower than the producer (insert a busy delay), and confirm that the maximum observed occupancy is N − 1. Write the same ring in C in c/mcu/src/ex-9-3/ using lessons §3.4’s two atomic indices, cross-compile at -O2, and annotate where the release store and acquire load became plain str/ldr (and why that is correct on this core). Optional rung: add a second, higher-priority peripheral-timer interrupt (from the lm3s6965 PAC) that only reads the occupancy, and argue from lessons §5 why it needs no critical section.

Quantity Predicted Observed (QEMU)
Max occupancy
enqueue failures, consumer faster than producer
enqueue failures, consumer slower
Instructions in the C push fast path at -O2

Deliverable: both programs, the table, and the annotated C listing.

Exercise 9.4 — The lock-free table, by prediction. Fill the table from the lessons before touching a compiler, then verify: in C on the host with atomic_is_lock_free on objects of each width, and on the cross side by cross-compiling atomic_fetch_add on each width at -O2 and recording whether the output is an LDREX/STREX loop or a library call (__atomic_fetch_add_8); in Rust with cfg!(target_has_atomic = "8" | "16" | "32" | "64") printed on the host and a cargo check --target thumbv7em-none-eabihf of a file that names AtomicU64. Add the thumbv6m-none-eabi column by cargo check only (rustup target add it for this exercise) to see the load/store-only case.

Width A64 host, C — predicted / observed Cortex-M4, C — predicted / observed A64 host, Rust thumbv7em, Rust thumbv6m, Rust
1 byte … / … … / …
2 bytes … / … … / …
4 bytes … / … … / …
8 bytes … / … … / …

In notes.md: what a 64-bit timestamp shared with an ISR should be on this core, in each language. Deliverable: the table and the two diagnostics (C link error or library call; Rust compile error) for the 8-byte case.

Exercise 9.5 — The double-buffered DMA hand-off, designed twice. Without DMA hardware, design the ownership protocol of lessons §3.5/§4.6 so that it compiles and its invariants are visible. C, in c/mcu/src/ex-9-5/: two _Alignas(32) buffers, an enum owner { CPU, DMA } per buffer, a half-transfer and a complete-transfer handler that flip ownership and publish with atomic_store_explicit(…, memory_order_release), a consumer that acquires, processes, and returns the buffer; cross-compile at -O2 and mark in the listing every point where the compiler could have hoisted a buffer read across the handoff if the ordering were missing. Rust, in rust/qemu/src/bin/ex-9-5.rs: a Transfer type that takes &'static mut [u16; N] from a StaticCell, holds it while “in flight”, and returns it only from a complete() method driven by a SysTick tick; show, with a commented-out line and its diagnostic, that main cannot read the buffer while the transfer holds it. Simulate the DMA’s writes from the handler.

Property C: how enforced Rust: how enforced
Buffer untouched while DMA-owned
Handoff ordered (payload before flag)
Consumer deadline (one buffer-time)
Buffer alignment and placement

Deliverable: both programs, the table, and a cross-reference in notes.md to where each property lives in Course 3 Lab 5.3’s circular-ADC firmware.

Exercise 9.6 — Fence forensics. In c/mcu/src/ex-9-6/, write a flag-plus-payload publish/consume pair four ways: no fence; atomic_signal_fence(memory_order_release) after the payload store and …acquire after the flag load; atomic_thread_fence in the same places; and the release-store/acquire-load form on an atomic_bool with no separate fence. Cross-compile at -O2 and, for each, record which instructions appear between the payload store and the flag store, whether a dmb was emitted, and whether the payload store could legally have been sunk below the flag. Repeat the middle two variants on the host for A64 and compare. In Rust, core::sync::atomic::compiler_fence and fence are the same two tools — write the second and fourth variants in rust/qemu/src/bin/ex-9-6.rs and check cargo objdump agrees with the C.

Variant Instruction between payload and flag stores (M4) dmb emitted? (M4) dmb emitted? (A64 host) Correct on a single core? Correct on the Jetson?
No fence
atomic_signal_fence / compiler_fence
atomic_thread_fence / fence
Release store / acquire load

Deliverable: the table and one paragraph on why the single-core answer and the multi-core answer differ in exactly one column.

Exercise 9.7 — The same problem as a signal (optional rung, Linux tier). In c/linux/src/ex-9-7/ and rust/linux/src/bin/ex-9-7.rs, write a loop that counts iterations and stops cleanly on SIGINT: first with a handler that sets a volatile sig_atomic_t (C) / static AtomicBool (Rust via nix::sys::signal::sigaction), then with the signal blocked and delivered through signalfd read from the loop. Build on the Jetson or Pi (or the macOS-common subset on the Mac for the first variant). For each variant list what the handler is allowed to call and what would deadlock; then map every row of lessons §8’s decision table to its Linux equivalent (signal mask ↔︎ PRIMASK, sig_atomic_t ↔︎ atomic flag, signalfd ↔︎ “demote the interrupt to a message”).

Concern Bare-metal answer (Module 9) Linux answer Where Module 11 takes it
Mask / unmask
One-word flag
Multi-word state
Turning an interrupt into a queue entry

Deliverable: both programs in both languages and the mapping table.