Module 10 Exercises — RTOS and Async

Back to the Course 2 syllabus. Read first: Module 10 lessons (the FreeRTOS book, the RTIC book, the Embassy documentation, and Rust Book 17 remain available as optional deep-dives).

Work in the labs repo’s course2/ folder and record everything in m10/notes.md. The FreeRTOS exercises are designed and compiled on the Mac — the task/queue/mutex code lives in c/mcu/src/ex-7-N/ against the FreeRTOS kernel headers and is cross-compiled with clang --target=thumbv7em-none-eabihf; running it means dropping the same files into the Course 3 m7-rtos CubeMX project and flashing the NUCLEO, which is Lab 7.2’s territory and optional here. The RTIC and Embassy exercises are rust/mcu/src/bin/ex-7-N.rs (built for thumbv7em-none-eabihf, probe-rs optional) and, where the exercise says so, rust/qemu/src/bin/ex-7-N.rs — both frameworks have lm3s6965 examples, so the pipeline logic runs in QEMU with a simulated timer interrupt and semihosting output. Predicted cells are filled in before building; observed cells at the machine, and every cell that needs the bench stays “…” until Course 3’s Saleae is out.

Exercises

Exercise 10.1 — The pipeline in FreeRTOS-C. Design Lab 7.2’s three-stage pipeline — acquire (timer/DMA-driven, fixed cadence) → process (a block filter) → output (DAC or UART) — as three tasks and two queues, in native FreeRTOS calls (not the CMSIS-RTOS2 wrapper), using the static creation API for every object. Before writing code, fill the design table: for each task its priority (higher number = higher priority), stack size in words with a one-line justification, period or trigger, the queue it reads, the queue it writes, and the item type and depth of each queue. Then fill the configuration table for FreeRTOSConfig.h and CubeMX — configTICK_RATE_HZ, configUSE_PREEMPTION, configUSE_TIME_SLICING, configSUPPORT_STATIC_ALLOCATION, configSUPPORT_DYNAMIC_ALLOCATION, configCHECK_FOR_STACK_OVERFLOW, configUSE_MUTEXES, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY, the HAL timebase timer — with, for each, the value you chose and the lesson section that justifies it. Write the acquire ISR hand-off with xQueueSendFromISR + portYIELD_FROM_ISR and the acquire task with vTaskDelayUntil. Deliverable: the two tables, the source in c/mcu/src/ex-10-1/, a clean cross-compile, and a paragraph in notes.md on which kernel calls would be illegal from the ISR and which NVIC priority the ADC/DMA interrupt must therefore have.

Exercise 10.2 — The pipeline in RTIC. Rebuild Exercise 10.1 as an RTIC 2 application in rust/mcu/src/bin/ex-10-2.rs (and, for the logic only, rust/qemu/src/bin/ex-10-2.rs against lm3s6965 with a SysTick-driven acquire): acquire as a hardware task bound to a timer interrupt, process and output as software tasks on dispatchers, blocks carried by heapless::spsc or an rtic-sync channel held in #[local] resources, and one deliberately #[shared] resource (a “UART busy” flag or a statistics struct) touched by two tasks. Before building, predict for every resource its ceiling and, for every lock call, whether RTIC emits a BASEPRI write or elides it:

Resource Tasks that use it (priority) Predicted ceiling lock in task … emits BASEPRI? (predicted) Observed (disassembly)

Then build with --release, read the disassembly of each task, and fill the observed column. Deliverable: the table, the source, and a notes.md paragraph explaining from the SRP why the application cannot deadlock even though two tasks share a resource and one of them also spawns the other.

Exercise 10.3 — The pipeline in Embassy. Rebuild the pipeline a third time as Embassy tasks in rust/mcu/src/bin/ex-10-3.rs: acquire on a Ticker, blocks through a static Channel, output on the DAC or a defmt line, main as the heartbeat. Choose and justify the raw mutex kind for every embassy-sync object, then add a second version in which acquire runs on an interrupt executor at a higher priority and note every object whose mutex kind had to change. Predict, from the state-machine argument in lessons §4.6, the relative .bss cost of each task’s future — which task’s future is largest and why — and confirm with cargo size and cargo nm --size-sort:

Task Largest local held across an .await (predicted) Predicted relative future size Observed size
acquire
process
output

Deliverable: both versions, the table, and the closing note of the three-way comparison in notes.md: one paragraph per runtime on what the pipeline’s source made visible that the other two hid (the FreeRTOS priority numbers, RTIC’s ceilings, Embassy’s yield points).

Exercise 10.4 — Priority inversion, three ways. Reproduce Lab 7.2 Part E’s scenario in each runtime: output (low) holds a shared resource, a medium-priority hog runs, acquire (high) needs the resource. In FreeRTOS write it twice — the resource guarded by a binary semaphore, then by a mutex — and predict which build has bounded blocking and what bounds it. In RTIC express the same three tasks and the shared resource and predict what the framework does at compile time about the scenario (which task runs at which ceiling inside the resource, and whether a hog task can interpose at all). In Embassy build it on one executor and then with acquire on an interrupt executor, and predict for each whether the hog can starve acquire and what “holding the resource” even means for a cooperative task. Fill the reasoning table before building:

Runtime / variant Can the hog delay acquire while output holds the resource? What bounds acquire’s blocking, if anything Where the bound comes from (runtime, compiler, or convention)
FreeRTOS, binary semaphore
FreeRTOS, mutex
RTIC
Embassy, one executor
Embassy, interrupt executor for acquire

Deliverable: the five sources, the table, and — for the Rust variants — the exact compiler diagnostic, if any, that the “wrong” design produced. The bench measurement of the inversion (Saleae latency distributions) is Lab 7.2’s and is not repeated here.

Exercise 10.5 — Stack sizing, predicted and measured. For the FreeRTOS pipeline of Exercise 10.1, predict each task’s stack requirement by hand: the deepest call chain’s frames (from -fstack-usage .su files, Module 6), plus the interrupt frame the Cortex-M4F pushes on exception entry (with and without lazy FPU stacking), plus the kernel’s own context-save area. Compare against the size you chose and against uxTaskGetStackHighWaterMark on the board (or leave “…”):

Task Deepest chain frames (from .su) + exception frame + context save Predicted minimum (words) Chosen size (words) Observed high-water mark
acquire
process
output
idle + timer service

Then do the equivalent accounting for Exercise 10.2 (one stack: the sum of the worst preemption chain, not of the tasks) and Exercise 10.3 (the executor’s stack plus the static future pool), and write a notes.md paragraph on why the three totals differ in kind, not just in size. Deliverable: the tables and the paragraph.

Exercise 10.6 — A latency-measurement plan. Write, without running it, the measurement plan that would put numbers on lessons §5’s latency reasoning row for all three runtimes: which marker GPIO (Course 3’s convention: PA8/D7 first marker; the Jetson-side pins are not involved here) goes high at which instruction in the ISR, the task entry, and the task exit; what the Saleae capture would show for ISR-to-task latency, per-stage execution time, and end-to-end latency; what the worst case should be per runtime from the lessons’ bounds; and what would make the measurement lie (a marker toggle that is itself preempted, defmt output on the critical path, the tick interrupt landing inside a stage). Complete the prediction table with derivable or qualitative entries only:

Quantity FreeRTOS (predicted bound) RTIC (predicted bound) Embassy, one executor (predicted bound) Observed
ISR → acquire task start
Worst-case acquire jitter with the hog running
End-to-end latency, three stages

Deliverable: the plan and the table in notes.md, and the marker-toggle code added to each of Exercises 10.1–10.3 so that the plan can be executed on the bench later. Observed cells are Course 3 work.

Exercise 10.7 — Timers, tickers, and a bounded wait. Implement a slow periodic job (a heartbeat plus a watchdog kick) and a bounded receive in each runtime. FreeRTOS: a software timer for the heartbeat and xQueueReceive with a finite timeout for the bounded receive; predict at what priority the timer callback runs and what it must therefore never do. RTIC: a monotonic-driven software task with delay_until for the heartbeat; a bounded receive expressed by spawning a timeout task, and a paragraph on why RTIC has no “wait with timeout” primitive. Embassy: a Ticker for the heartbeat, with_timeout and then select against a Signal for the bounded receive; record what select drops when the other branch wins and why that is safe. Run the Embassy version in QEMU (rust/qemu/src/bin/ex-10-7.rs) with the timeout deliberately expiring, and record the output. Complete the comparison:

Concern FreeRTOS software timer RTIC monotonic task Embassy Ticker
Runs in which context / priority
Drift over many periods (predicted: none / accumulates)
Can it block or .await?
Memory cost per timer

Deliverable: the three sources, the table, the QEMU output, and a notes.md line naming the one situation in each runtime where the slow-periodic mechanism is the wrong tool.