Module 8 Exercises — Talking to Hardware

Back to the Course 2 syllabus. Read first: Module 8 lessons (MC 39–44, the Embedded Rust Book’s Peripherals and Static Guarantees chapters, and the embedded-hal and embassy-stm32 docs remain available as optional deep-dives).

Work in the labs repo’s course2/ folder — c/mcu, c/linux, rust/mcu, rust/qemu, rust/host, rust/linux as each exercise names — and record everything in m8/notes.md. Everything through Exercise 8.7 runs on the Mac: Cortex-M C is cross-compiled with clang --target=thumbv7em-none-eabihf and read as disassembly, Rust is built for thumbv7em-none-eabihf and inspected with cargo objdump/cargo nm/cargo size, and anything that has to execute bare-metal runs in QEMU from rust/qemu. When a register block appears in a Mac-side exercise it is a fake one at a made-up address — the subject is what the compiler does, not what the peripheral does. Flashing the NUCLEO (probe-rs run, or Course 3’s OpenOCD flow for the C builds) is the optional last step of Exercises 8.4 and 8.7; Exercise 8.8 is the one Jetson exercise, and its observed cells stay “…” until the board is on the bench. Predicted cells are filled in before building; observed cells at the machine.

Exercises

Exercise 8.1 — What volatile buys, proven twice. In c/mcu/src/ex-8-1/, write a flag-polling loop twice: once with a plain uint32_t flag at a fixed fake address, once through a volatile uint32_t *. Cross-compile both at -O0 and -O2 and annotate the four Thumb-2 listings: where the optimizer hoisted the load and turned the plain version into a single test or an infinite loop, and how the volatile version forces an LDR per iteration. In rust/mcu/src/bin/ex-8-1.rs, write the same pair — a plain dereference of a *const u32 versus core::ptr::read_volatile — build for thumbv7em-none-eabihf in debug and release, and annotate the four cargo objdump listings the same way. Then complete the guarantees table from evidence, one row per property, and note in each cell whether the answer differed between the languages:

Property C volatile Rust read_volatile Evidence (listing / reading)
Load/store actually emitted
Atomicity of flag += 1
Ordering vs. surrounding non-volatile accesses
Hardware barrier emitted
Lost events for a level flag

Deliverable: the eight annotated listings and the table, plus one notes.md paragraph on why Rust puts volatility on the access rather than the object and what that changes about where volatile can be forgotten.

Exercise 8.2 — MMIO forensics on a fake register block, C and Rust. Declare a GPIO_TypeDef-style struct — MODER, OTYPER, IDR (volatile const), ODR, BSRR — pinned at a made-up address in c/mcu/src/ex-8-2/, and the same block as a #[repr(C)] struct behind a raw pointer in rust/mcu/src/bin/ex-8-2.rs using read_volatile/write_volatile and &raw const/&raw mut field addresses. Cross-compile both at -O2/release and annotate the generated code for: (a) a BSRR one-shot set versus an ODR |= pin read-modify-write — count the memory operations and name the hazard window; (b) uint8_t/uint16_t/uint32_t (and *const u8/u16/u32) accesses to the same address — confirm LDRB/LDRH/LDR selection by type in both toolchains; (c) in C, the same field update as a bit-field member store versus explicit mask-and-shift, and in Rust the write-versus-modify distinction on a real PAC register (stm32l4 GPIOA bsrr().write(…) versus moder().modify(…), compile-only). Predict, then observe:

Operation Predicted memory ops (C) Observed (C) Predicted (Rust) Observed (Rust)
BSRR = pin / bsrr().write
ODR \|= pin / modify on odr
Bit-field store / moder().modify
write to a write-1-to-clear register, one flag

Deliverable: annotated listings plus a notes.md paragraph on why a write-1-to-clear status register makes |= a bug even though the C looks innocent, and what in the PAC’s write semantics prevents the same bug.

Exercise 8.3 — A typestate LED driver, and its C shadow. Using the fake register block from Exercise 8.2, write a pin driver in rust/qemu/src/bin/ex-8-3.rs whose states are types — Pin<Disabled>, Pin<Input>, Pin<Output> — with consuming transitions (enable(), into_input(), into_output()) modeled on the Embedded Rust Book’s GPIO state tree, is_high() only on Pin<Input>, and set_high()/set_low() only on Pin<Output>. Point the fake block at a RAM address inside the QEMU board’s memory map so the program can run and print the block’s contents over semihosting after each transition. Then write, in a separate commented-out section, the four misuses — set an input high, read an output’s level, use a pin after it was consumed by a transition, and construct a second driver over the same pin — and record which ones fail to compile and what the diagnostic says. In c/mcu/src/ex-8-3/, write the same driver with a mode field checked at runtime and an error return, and record which of the four misuses it catches, when, and at what cost in the -O2 listing (the checks are instructions the Rust version does not have). Deliverable: the two drivers, the QEMU transcript, and the table:

Misuse Rust: caught at C: caught at Cost in the C listing
Set level on an input
Read level of an output
Use after transition
Second driver on the same pin

Exercise 8.4 — Blink, three ways, on the real chip. Write the NUCLEO-L476RG LD2 (PA5) blink three times, all cross-compiled for the real chip: (1) in c/mcu/src/ex-8-4/, register-level C — RCC->AHB2ENR, GPIOA->MODER, GPIOA->BSRR, a busy-wait delay — against the CMSIS device header; (2) the STM32Cube HAL version (HAL_GPIO_Init, HAL_GPIO_TogglePin, HAL_Delay) as a CubeMX project in the Course 3 firmware tree, if arm-none-eabi-gcc is installed; (3) in rust/mcu/src/bin/ex-8-4.rs, embassy-stm32’s blocking API — embassy_stm32::init, Output::new(p.PA5, …), cortex_m::asm::delay. Before building, list every hardware step that must happen before a write to BSRR has any effect (clock enable, mode, speed, type) and predict which of the three sources performs each step explicitly, implicitly inside a call, or not at all. Compare .text size (arm-none-eabi-size -A, cargo size -- -A) and the instruction count of the loop body in each listing. Optional rung: flash (1) and (2) through Course 3’s OpenOCD flow and (3) with probe-rs run --chip STM32L476RGTx, and confirm the LED.

Register-level C HAL C embassy-stm32 Rust
Clock enable: explicit / implicit / missing
Mode configuration: explicit / implicit
.text size (predicted ordering only)
Loop-body instructions
Blinks (optional rung)

Deliverable: the three sources, the table, and a notes.md paragraph on what embassy_stm32::init did that none of the C blinks made you write.

Exercise 8.5 — One I²C driver, two boards. Write an ADS1115 driver (address 0x48, Course 3 Lab 3.4) as a library in rust/host/src/ads1115.rs, generic over embedded_hal::i2c::I2c, with new, a config-register read, a single-shot conversion start, and a conversion-register read that returns the signed 16-bit code — every method returning the bus’s error type. Compile it three ways: on the Mac with a fake I2c implementation that records transactions and replays canned responses (a unit test asserting the exact byte sequences of each method); for the STM32 from rust/mcu with embassy_stm32::i2c::I2c in blocking mode; and for the Jetson from rust/linux with linux_embedded_hal::I2cdev (cargo check --target aarch64-unknown-linux-gnu). Then write the C version in c/host/src/ex-8-5/: the same driver over an i2c_transport struct of function pointers, with a host fake, a c/mcu transport skeleton over the HAL’s HAL_I2C_Mem_Read-style calls, and a c/linux transport over I2C_RDWR. Deliverable: both drivers, the host tests passing in both languages, the three-target compile record, and a notes.md table of what the trait boundary contains (address width, repeated-start semantics, error type, ownership of the bus) and how each of those is expressed — or left unexpressed — in the C transport struct.

Boundary property embedded_hal::i2c::I2c C i2c_transport
Repeated start between write and read
Error type
Who owns the bus while the driver exists
Checked at compile time?

Exercise 8.6 — Sections, weak symbols, and the map file, both toolchains. In C (c/mcu/src/ex-8-6/, linked as a real ELF if arm-none-eabi-gcc is present, otherwise object-level with llvm-objdump -h -t): place a buffer in .dma_buffer with __attribute__((section, aligned(32))), define a weak default handler, override it with a strong definition in a second translation unit, and verify all three claims — the section exists, the buffer’s address is 32-byte aligned, the strong symbol won — in objdump output and the map file. In Rust (rust/mcu/src/bin/ex-8-6.rs): the same buffer with #[unsafe(link_section = ".dma_buffer")] and #[used], a #[unsafe(no_mangle)] extern "C" symbol, an #[interrupt] handler for one L476 vector and a #[exception] DefaultHandler, with -C link-arg=-Map=ex-8-6.map added to rustflags; verify with cargo objdump -- -h, cargo objdump -- -t, cargo nm, and the map. Find the L476 memory.x that embassy-stm32’s memory-x feature generated (in the build directory) and diff it against rust/qemu’s hand-written one. Then connect both sides back to the Course 3 STM32 startup file: find the weak vector aliases and the KEEP directive, and explain in notes.md what cortex-m-rt’s link.x does in each of those two places. Deliverable: annotated map-file and objdump excerpts from both toolchains, and a table:

Claim C evidence Rust evidence
Buffer is in .dma_buffer
Buffer is 32-byte aligned
Strong / named handler replaced the default
Vector table survives --gc-sections

Exercise 8.7 — HardFault forensics, both handlers. In rust/qemu/src/bin/ex-8-7.rs, install #[exception] unsafe fn HardFault(ef: &ExceptionFrame) -> ! that prints the stacked r0r3, r12, lr, pc, and xpsr over semihosting, then trigger a fault deliberately — a volatile read from an address outside the QEMU board’s memory map — from a small function called through one intermediate frame, so the stacked lr and pc are both informative. Predict, before running, which of pc and lr will point where, and which System Control Block fault-status bits (CFSR: BFSR/MMFSR/UFSR) should be set; read CFSR and BFAR inside the handler through the cortex_m::peripheral::SCB register block and check. In c/mcu/src/ex-8-7/, write the C equivalent — the naked HardFault_Handler that selects MSP or PSP from EXC_RETURN and tail-calls a C function taking the frame pointer, the pattern Course 3 Lab 7.1 writes by hand — cross-compile it, and annotate what cortex-m-rt’s generated trampoline does that your naked handler does by hand. Optional rung: run the Rust version on the NUCLEO under probe-rs and compare the frame.

Field Predicted Observed (QEMU) Observed (NUCLEO, optional)
Stacked pc
Stacked lr
CFSR bits set
BFAR valid?

Deliverable: both handlers, the QEMU transcript, the table, and a notes.md paragraph on what the stacked frame can and cannot tell you (the faulting instruction vs. the faulting data address, precise vs. imprecise bus faults).

Exercise 8.8 — GPIO and I²C on the Jetson (optional rung). On the Jetson Orin Nano (Course 3 setup essentials; Raspberry Pi 5 as the variant), first record gpiodetect --version, gpiodetect, and gpioinfo for the chip carrying header pin 7, and the output of i2cdetect -y 7 with the ADS1115 on header pins 3/5 (bus 1 on the Pi). Then blink header pin 7 twice: in c/linux/src/ex-8-8/ with libgpiod’s C API (the v1 calls if the board reports 1.x, the v2 request-object API otherwise — note which), and in rust/linux/src/bin/ex-8-8.rs with the gpiod crate, giving both requests a consumer name and confirming it appears in gpioinfo while the program runs. Finally, read the ADS1115 config register with both Exercise 8.5 drivers on the board — the C one over I2C_RDWR, the Rust one over linux_embedded_hal::I2cdev — and once more with plain write()/read() and I2C_SLAVE in C, observing whether the device tolerates the STOP between the two transactions. Every observed cell stays “…” until the board is on the bench.

Item Predicted Observed (Jetson) Observed (Pi 5)
libgpiod version 1.x on JetPack 6
Chip / line offset for header pin 7 from gpioinfo
Consumer name visible in gpioinfo while running yes
ADS1115 config register (reset value, from the datasheet)
I2C_SLAVE + write/read returns the same value as I2C_RDWR

Deliverable: the four programs, the transcripts, the table, and a notes.md paragraph on what changed for the Rust driver between the STM32 build and the Jetson build — which files, which lines, and what did not change at all.