Module 0 Lessons — Toolchains, Targets, and the Availability Matrix
Back to the Course 2 syllabus · Practice: Module 0 exercises
This page is the module’s teaching text. It establishes the vocabulary the rest of the course uses — the three runtime tiers, what “freestanding” and no_std actually remove, and how the Mac toolchain reaches every tier — and ends with the availability matrix, the table of which language facilities exist on which tier, which the exercises then fill in by experiment. As with every lessons page in this course, it is AI-drafted teaching text reviewed by me; see the syllabus’s note on AI use. The Embedded Rust Book, the Embedonomicon, and Seacord’s first chapter remain available as optional deep-dives; nothing below requires them.
1 · Three tiers, three C libraries, three Rust crate sets
Embedded code is not one thing. The same C source can run against three very different runtimes, and the same Rust crate can be compiled with three very different amounts of the standard library. Everything in this course is placed on one of these three tiers, and the tier decides what the code may use.
| Tier | This site’s hardware | What runs underneath the program | C library | Rust library |
|---|---|---|---|---|
| Bare metal | STM32 NUCLEO-L476RG (Cortex-M4F, 80 MHz) | Nothing: reset vector → your startup code → main |
newlib-nano (a freestanding-plus subset: no files, no processes, printf only if you provide _write) |
core only (#![no_std]) — optionally alloc with a heap you supply |
| RTOS | the same STM32 | A kernel library linked into the binary: FreeRTOS, or in Rust the RTIC or Embassy framework | newlib-nano + the kernel’s API | core + the framework’s crates; alloc optional |
| Embedded Linux | Jetson Orin Nano, Raspberry Pi 5 | A full Linux kernel with processes, virtual memory, and drivers | glibc + POSIX | full std |
The Cortex-M4F is not an ARM64 core. It executes the 32-bit Thumb-2 instruction set of ARMv7E-M, has no MMU, no privilege separation beyond thread/handler mode, and a single-precision-only FPU. The Jetson and the Pi are AArch64 A-profile machines with an MMU, caches, and a kernel between the program and the hardware. The two ends of the table are different computers, and the middle tier is a library choice on the first one.
1.1 Hosted vs. freestanding C
The C standard defines two kinds of implementation. A hosted implementation provides the whole standard library and starts the program at main. A freestanding implementation is only required to provide the headers that contain no functions — <float.h>, <iso646.h>, <limits.h>, <stdalign.h>, <stdarg.h>, <stdbool.h>, <stddef.h>, <stdint.h>, <stdnoreturn.h> — and the program’s entry point is implementation-defined. A bare-metal STM32 is a freestanding environment onto which a partial hosted library, newlib-nano, is bolted. That is why printf exists but writes nowhere until you implement _write, why malloc exists but has no idea how big the heap is until _sbrk tells it, and why <threads.h> is missing entirely.
// Freestanding-safe: compiles for thumbv7em with -ffreestanding and no libc.
#include <stdint.h>
#include <stddef.h>
int32_t dot_q15(const int16_t *a, const int16_t *b, size_t n) {
int32_t acc = 0;
for (size_t i = 0; i < n; ++i) {
acc += (int32_t)a[i] * b[i];
}
return acc;
}// Hosted-only: <stdio.h> exists on the Mac and on the Jetson; on the STM32 it
// exists in name but its output goes nowhere until _write() is retargeted.
#include <stdio.h>
int main(void) { puts("hello"); return 0; }Apple’s clang compiles the first file for the Cortex-M directly (--target=thumbv7em-none-eabihf -ffreestanding -c) with no ARM toolchain installed, and refuses the second for that target because there is no <stdio.h> in a freestanding sysroot. Keeping the kernel and the driver in separate files — one freestanding-safe, one hosted — is the habit this distinction produces, and the course’s C workspace is built around it.
1.2 core, alloc, std
Rust’s standard library is three crates stacked on each other:
| Crate | Needs | Provides | Available on |
|---|---|---|---|
core |
Nothing — no OS, no allocator | Primitive types and their methods, Option, Result, slices, iterators, fmt::Write, core::sync::atomic, core::ptr, core::mem, MaybeUninit, Cell/RefCell |
Every tier |
alloc |
A global allocator (#[global_allocator]) |
Box, Vec, String, Rc, Arc, BTreeMap, format! |
Bare metal if you supply a heap (embedded-alloc); Linux |
std |
An operating system | Threads, files, sockets, println!, HashMap, Instant, environment, process |
Linux only |
#![no_std] at the top of a crate means “link core instead of std”. It removes println!, Vec, String, Box, threads, files, and the default panic machinery. #![no_main] additionally removes the standard main — the entry point is then whatever the runtime crate (cortex-m-rt) names. Crucially, core is not a reduced language: ownership, traits, generics, iterators, match, and Result all live there, so the Rust you write on the Cortex-M is the same language, minus the parts that need an OS or a heap.
#![no_std]
// Everything below is `core`: no allocator, no OS, and the same language.
pub fn dot_q15(a: &[i16], b: &[i16]) -> i32 {
a.iter().zip(b).map(|(&x, &y)| i32::from(x) * i32::from(y)).sum()
}The slice version carries its length and is bounds-checked by construction — the (pointer, n) pair of the C version has become one type. That one change is the beginning of most of the Rust half of this course.
2 · The portability taxonomy
Seacord opens Effective C with four categories of behavior the C standard deliberately leaves open. Every embedded C bug of the “works on the Mac, fails on the board” kind is one of them, and Rust’s design can be read as a set of decisions about each.
| Category | Definition | Embedded example | What Rust does |
|---|---|---|---|
| Implementation-defined | The implementation must choose and document | sizeof(long) (8 on LP64 macOS/Linux, 4 on the Cortex-M4); right shift of a negative value; char signedness |
Fixed by the language: i64 is 64 bits everywhere, >> on signed is arithmetic, char is a Unicode scalar; only pointer width varies (usize) |
| Unspecified | Any of several behaviors, need not be documented, may vary per evaluation | Order of evaluation of function arguments; padding bytes’ contents | Evaluation order is specified (left to right); padding is unobservable in safe code |
| Undefined | No requirements at all — the compiler may assume it never happens | Signed overflow, oversized shift, out-of-bounds access, data race, strict-aliasing violation | Impossible in safe code; each becomes a contract you take on explicitly with unsafe (Module 7) |
| Locale-specific | Depends on the locale | isalpha, strtod decimal point, printf thousands grouping |
core has no locale; text is UTF-8 |
The consequence for this course: C code is written against the standard’s guarantees, not the compiler’s habits, and every implementation-defined choice that matters (widths, alignment, endianness) is pinned by _Static_assert at compile time. Rust code gets most of that for free and pays for it elsewhere — at the unsafe boundary, where the C-style obligations return in writing.
Anything the standard calls implementation-defined is a build-time assertion, not a comment. Anything it calls undefined is a bug the optimizer is allowed to weaponize.
3 · The Mac toolchain, tier by tier
Everything below installs on an Apple Silicon Mac; the boards are optional at every step.
3.1 C
| Tool | Reaches | Install |
|---|---|---|
clang (Apple, from the Xcode command-line tools) |
Host builds with sanitizers; cross-compiles C to thumbv7em-none-eabihf for compile-and-disassemble forensics — no ARM toolchain needed |
xcode-select --install |
arm-none-eabi-gcc (optional) |
Linking real STM32 firmware, newlib-nano, the linker scripts CubeMX generates | brew install --cask gcc-arm-embedded — the same toolchain Course 3’s firmware uses |
cmake + ninja |
The c/ workspace’s build driver |
brew install cmake ninja |
| A Linux compiler | Linux-tier C is compiled where it runs — on the Jetson or the Pi, over SSH or CLion’s remote toolchain | JetPack’s gcc; see Course 3’s Jetson setup essentials |
The Cortex-M flag set, used identically by clang --target=thumbv7em-none-eabihf and by arm-none-eabi-gcc, names the core and its FPU exactly:
-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -std=gnu17fpv4-sp-d16 says single-precision, sixteen double-word registers. There is no hardware double on this core; -Wdouble-promotion exists to catch the float expression that silently became a software-emulated double because of one unsuffixed literal.
3.2 Rust
| Tool | Reaches | Install |
|---|---|---|
rustup + stable toolchain |
Host builds, tests, Clippy, rustfmt |
curl https://sh.rustup.rs -sSf \| sh |
Target thumbv7em-none-eabihf |
Bare-metal and RTOS tiers on the Cortex-M4F | rustup target add thumbv7em-none-eabihf |
Target thumbv7m-none-eabi |
The QEMU simulation board (a Cortex-M3) | rustup target add thumbv7m-none-eabi |
Target aarch64-unknown-linux-gnu |
cargo check of Linux-tier code on the Mac; the actual build happens on the board |
rustup target add aarch64-unknown-linux-gnu |
llvm-tools + cargo-binutils |
cargo size, cargo objdump, cargo nm, cargo readobj on any target |
rustup component add llvm-tools · cargo install cargo-binutils --locked |
probe-rs |
Flash and run on the NUCLEO through its ST-LINK; defmt log output over RTT |
brew tap probe-rs/probe-rs && brew install probe-rs (or the install script on probe.rs) |
| QEMU | Run thumbv7m binaries with semihosting, no board |
brew install qemu |
| Miri | Detect undefined behavior in unsafe code under an interpreter |
rustup component add miri (Module 7) |
Rust targets are named <arch><sub>-<vendor>-<os>-<abi>: thumbv7em-none-eabihf is Thumb-2 on ARMv7E-M, no vendor, no OS, hard-float EABI. The four Cortex-M targets differ only in ISA level and FPU; picking the wrong one produces a binary that runs but does floating point in software, which is a Module 12 size-ladder experiment rather than a build error.
Miri, the interpreter Module 7 uses to check unsafe code, ships only on the nightly channel: rustup toolchain install nightly && rustup +nightly component add miri, then cargo +nightly miri test. Nothing else in the course needs nightly.
3.3 Python
The Python track runs on the host and, optionally, on the Linux boards; there is no Python on the microcontroller tiers, which is the point of the two compiled tracks.
| Tool | Reaches | Install |
|---|---|---|
uv |
The labs repo’s single root project (Python 3.13, uv.lock tracked): NumPy, SciPy, Matplotlib, pandas, scikit-learn, pytest, Jupyter, pyserial, soundfile/sounddevice/librosa, OpenCV, Pillow |
brew install uv, then uv sync at the repo root |
The ml dependency group |
PyTorch (+ torchaudio, torchvision), ONNX, ONNX Runtime | uv sync --group ml |
| Jupyter | Notebooks for the exploratory half of every Python exercise | uv run jupyter lab |
pytest |
The scripted half — every reference implementation has a test that pins its output | uv run pytest course2/python/tests |
| The boards | CuPy, onnxruntime, TensorRT on the Jetson; onnxruntime on the Pi — installed per Course 3’s edge setup |
on the board, not in uv.lock |
Two conventions from Course 3 carry over unchanged: uv run is the only way any Python is invoked (no global interpreter, no activated shells), and the accelerator is selected, never assumed — torch.backends.mps.is_available() on the Mac, torch.cuda.is_available() on the RTX 4090 desktop and the Jetson, CPU otherwise. Module 1 opens with the environment; Module 3 with the device probe.
3.4 One editor
CLion opens all three trees: the c/ CMake projects natively (with the same remote-toolchain workflow Course 3 uses for the Jetson), the rust/ workspace through its Rust plugin, and — as PyCharm or through CLion’s Python plugin — the uv project with its virtual environment as the interpreter. rust-analyzer needs one hint for cross targets — the mcu/ and qemu/ crates carry their own .cargo/config.toml with a [build] target, so they are analyzed against their Cortex-M target and not the Mac.
4 · The workspace
The labs repo’s course2/ folder is organized by tier, not by module, because a tier is a build configuration and a module is not:
course2/
README.md
m0/ … m12/ # notes.md per module: predicted vs. observed write-ups
python/
src/ex-M-N.py # scripts — uv run python course2/python/src/ex-1-3.py (from the repo root)
notebooks/ex-M-N.ipynb # notebooks — uv run jupyter lab
tests/test_ex_M_N.py # pytest — uv run pytest course2/python/tests
c/
cmake/ # shared warning set, the per-directory exercise rule, the thumbv7em toolchain file
host/ # CMake · clang · ASan+UBSan · one executable per src/ex-M-N/
mcu/ # CMake · clang --target=thumbv7em · freestanding objects + build/m4/<ex>/*.lst disassembly
# (real-ELF linking lives in Course 3's firmware projects)
linux/ # CMake · POSIX · built on the Jetson/Pi; the macOS-common subset builds here too
rust/
Cargo.toml # one workspace, four crates; default-members = host, linux
rust-toolchain.toml # pins the channel; installs the three targets, llvm-tools, rust-src on first build
.cargo/config.toml # per-target runners (QEMU, probe-rs) and link args
host/ # std · unit tests · Miri · Clippy
mcu/ # no_std · thumbv7em-none-eabihf · embassy-stm32 for the L476 · probe-rs runner
qemu/ # no_std · thumbv7m-none-eabi · lm3s6965evb · semihosting runner
linux/ # std + nix + gpiod + linux-embedded-hal · aarch64 check on the Mac
An exercise is a per-tier file: python/src/ex-1-3.py, c/host/src/ex-4-2/ (every .c in the directory links into build/ex-4-2), rust/qemu/src/bin/ex-9-3.rs (cargo run --bin ex-9-3 from inside qemu/ boots it in QEMU). A cross-tier exercise has one entry in each tier it touches, and the module’s notes.md holds the comparison. Each crate ships one smoke-<tier> binary — the toolchain check Exercise 0.1 runs — and a bare cargo build at the workspace root builds only the two host-runnable crates (host, linux); the Cortex-M crates are built with -p mcu --target thumbv7em-none-eabihf / -p qemu --target thumbv7m-none-eabi, or from inside their directories, where a crate-local .cargo/config.toml sets the default target.
The Rust side’s runner configuration is what makes cargo run mean “boot in QEMU” for one crate and “flash the NUCLEO” for another:
# rust/.cargo/config.toml
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
rustflags = ["-C", "link-arg=-Tlink.x"]
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32L476RGTx"
rustflags = ["-C", "link-arg=-Tlink.x", "-C", "link-arg=-Tdefmt.x"]link.x is the linker script cortex-m-rt generates from your memory.x; defmt.x adds the log-string table defmt needs. The Linux crate needs no runner: on the Mac it is only ever cargo check --target aarch64-unknown-linux-gnu; on the board it is plain cargo build --release.
5 · The first bare-metal binary, end to end
The point of running one no_std program in QEMU before anything else is that it makes visible everything a hosted main hides: who sets up the stack, who zeroes .bss, who copies .data out of flash, what happens on panic, and where “output” goes when there is no terminal.
5.1 The Rust version
Four files make a bootable Cortex-M program.
# Cargo.toml (the qemu crate)
[package]
name = "qemu"
edition = "2024"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
cortex-m-semihosting = "0.5"
panic-halt = "1"
[profile.release]
debug = 2 # keep symbols in release builds: probe-rs and objdump want them
lto = true
opt-level = "s"/* memory.x — the LM3S6965 that QEMU emulates */
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
// src/bin/ex-0-2.rs
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};
use panic_halt as _; // the panic handler: link it, never call it
#[entry]
fn main() -> ! {
hprintln!("hello from thumbv7m in QEMU");
debug::exit(debug::EXIT_SUCCESS); // tell QEMU to quit
loop {}
}cargo run --bin ex-0-2 --target thumbv7m-none-eabi # builds, then the runner boots QEMUWhat each piece does:
cortex-m-rtprovides the vector table (initial stack pointer first, then theResethandler), theResetroutine that zeroes.bssand copies.datafrom flash to RAM, default handlers for every exception, and the#[entry]attribute that names your function as the oneResetjumps to.fn main() -> !is required: there is nothing to return to.memory.xis the only board-specific input to the linker script. ChangeORIGIN/LENGTHto the STM32L476’s flash (1 MB at0x0800_0000) and RAM (96 KB at0x2000_0000, plus 32 KB of SRAM2) and the same crate targets the NUCLEO;embassy-stm32’smemory-xfeature writes the file for you from the chip name.panic-haltis the#[panic_handler]: on panic, disable interrupts and spin. The bare-metal tier has no unwinding and no default handler, so ano_stdbinary without exactly one panic handler does not link — the error message is the first thing Exercise 0.5 asks you to read.- Semihosting is a debugger-mediated system call:
hprintln!traps into the host (QEMU or a probe) which prints on its behalf. It is slow and stops the core while it runs, which is why real boards usedefmtover RTT instead; in QEMU it is the right tool.
5.2 The C version, for comparison
The same four responsibilities exist in every CubeMX project, spread over files you did not write:
| Responsibility | Rust (cortex-m-rt) |
C (CubeMX / newlib) |
|---|---|---|
| Vector table and reset | generated by the crate; #[entry] |
startup_stm32l476xx.s: g_pfnVectors, Reset_Handler |
.data copy, .bss zero |
Reset in the crate |
the loop in Reset_Handler before main |
| Memory layout | memory.x → link.x |
STM32L476RGTX_FLASH.ld |
| Panic / fault | #[panic_handler]; HardFault via #[exception] |
HardFault_Handler (weak default: infinite loop) |
| Output | semihosting / RTT + defmt |
retarget _write to a UART, or SWO, or semihosting |
Reading the Course 3 startup file next to cortex-m-rt’s source is Exercise 0.3. The crate is short; the startup file is shorter; the point is that both are doing the same six things.
6 · Reading what you built
Sizes and disassembly are the course’s primary instruments — every “the compiler will…” claim on these pages is settled by looking. No numbers are given here; the exercises produce them.
# Rust, any target
cargo size --bin ex-0-2 --release -- -A # .text/.rodata/.data/.bss by section
cargo objdump --bin ex-0-2 --release -- -d --no-show-raw-insn | less
cargo nm --bin ex-0-2 --release -- --size-sort | tail
# C on the Mac
clang -O2 -c kernel.c -o kernel.o && $(xcrun --find llvm-objdump) -d kernel.o
clang --target=thumbv7em-none-eabihf -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
-O2 -ffreestanding -c kernel.c -o kernel-m4.o && $(xcrun --find llvm-objdump) -d kernel-m4.o
# C on the board toolchain
arm-none-eabi-size -A firmware.elf
arm-none-eabi-objdump -d firmware.elf | lessThree things to know before reading a Cortex-M disassembly. Addresses in flash are odd in the vector table because bit 0 of a Thumb function pointer is the Thumb-state flag — 0x0800_0139 is a function at 0x0800_0138. .data appears twice in a size report — its load address in flash and its run address in RAM — and only the flash copy costs storage. And .bss costs RAM but no flash at all, which is why a large static buffer that is never initialized is cheaper than one initialized to zeros in the source only if the compiler notices the zeros — it does, in both languages.
7 · Warnings and lints as a contract
Compiler defaults drift between releases, so the project pins its dialect and diagnostics in the build system and treats new diagnostics like failing tests: fix, or justify in a comment.
7.1 C
# Applied to every target in c/host, c/mcu, c/linux
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_EXTENSIONS ON) # gnu17: CMSIS and newlib headers assume GNU extensions
add_compile_options(-Wall -Wextra -Wpedantic -Wconversion -Wshadow
-Wstrict-prototypes -Wundef -Wdouble-promotion -Wformat=2)| Flag | Catches |
|---|---|
-Wall -Wextra |
The broad, low-noise defect set — always on |
-Wpedantic |
Non-ISO constructs; the portability build’s flag |
-Wconversion |
Implicit narrowing and sign changes — the big one for register and DSP math |
-Wshadow |
An inner declaration silently hiding an outer one |
-Wstrict-prototypes |
void f() (unspecified arguments) vs. void f(void) |
-Wundef |
An undefined macro silently evaluating to 0 inside #if |
-Wdouble-promotion |
float arithmetic quietly promoted to software double on the M4F |
-Wformat=2 |
printf-family format/argument mismatches |
-Werror=vla |
Variable-length arrays — banned outright in firmware (Module 6) |
-fsanitize=address,undefined |
Host builds only: the runtime half of the contract (Module 7) |
7.2 Rust
The compiler’s defaults are already most of the C list; what the project adds is policy:
// At the top of every crate in rust/
#![deny(unsafe_op_in_unsafe_fn)] // an unsafe fn body still needs unsafe blocks
#![warn(missing_docs)]
// and in the no_std crates:
#![no_std]cargo clippy --all-targets -- -D warnings # host crates: lints are errors
cargo clippy -p mcu --target thumbv7em-none-eabihf --lib --bins -- -D warnings # no_std crates have no test target
cargo fmt --all --check # formatting is not negotiable
cargo check -p mcu --target thumbv7em-none-eabihf
cargo check -p linux --target aarch64-unknown-linux-gnuClippy’s unwrap_used/expect_used lints, denied in the mcu and qemu crates, are how the course enforces the firmware panic policy of Module 5: a Result is handled, never unwrapped, in code that runs on the board.
Pin the dialect, the edition, and the warning set in the build files. A warning you cannot fix is a comment explaining why, next to the code — never a flag that turns it off globally.
8 · The availability matrix
This is the module’s deliverable, stated as claims for the exercises to confirm. Each cell answers “can this program use it on this tier?” for the site’s actual hardware and the pinned toolchains. Where the answer is “yes, but…”, the “but” is the lesson.
| Facility | Bare metal, C | Bare metal, Rust | RTOS, C (FreeRTOS) | RTOS, Rust (RTIC / Embassy) | Linux, C | Linux, Rust |
|---|---|---|---|---|---|---|
Heap (malloc / Box) |
Exists via newlib; banned after init by project rule | Only with alloc + embedded-alloc; banned in this course |
FreeRTOS heap schemes exist; static allocation preferred | Same as bare metal | Yes | Yes |
| Threads | No (__STDC_NO_THREADS__) |
No std::thread |
Tasks, from the kernel | Tasks (RTIC) / async tasks (Embassy) | pthread, C11 threads on glibc |
std::thread |
| Atomics, ≤ 4 bytes | Lock-free (LDREX/STREX) |
Lock-free (core::sync::atomic) |
same | same | Lock-free | Lock-free |
| Atomics, 8 bytes | Not lock-free (library call) | AtomicU64 absent on thumbv7em |
same | same | Lock-free | Lock-free |
float hardware |
Yes (FPv4-SP) | Yes | Yes | Yes | Yes | Yes |
double hardware |
No — software emulation | No — f64 is software |
No | No | Yes | Yes |
| Formatted output | printf after retargeting _write; size cost |
core::fmt (large) or defmt (small, needs a host) |
same | same | printf |
println! |
| Files | No | No | No (unless FatFS-style libraries) | No | Yes | Yes |
| Monotonic time | A hardware timer / DWT cycle counter you configure | embassy-time or a monotonic you supply |
Tick count (xTaskGetTickCount) |
RTIC monotonic / embassy-time::Instant |
CLOCK_MONOTONIC |
std::time::Instant |
| Wall-clock time | Only with an RTC | Only with an RTC | same | same | CLOCK_REALTIME |
SystemTime |
| Signals | No | No | No | No | POSIX signals | nix::sys::signal |
| Unwinding / exceptions | No | No — panic = "abort" |
No | No | glibc unwinding exists; C has no exceptions | panic = "unwind" by default |
| Dynamic loading | No | No | No | No | dlopen |
libloading (crate) |
Three cells deserve a comment now. The 8-byte atomic row is why a 64-bit timestamp shared with an ISR is a Module 9 problem and not a volatile uint64_t. The double row is why -Wdouble-promotion is in the C warning set and why Rust code on the M4F uses f32 and micromath/libm rather than f64. And the formatted output row is why defmt exists: it compiles format strings out of the binary and ships only an index over RTT, which is the difference between a logging firmware that fits and one that does not.
9 · Lesson → exercise map
| Section | Exercise it feeds |
|---|---|
§1 tiers, freestanding vs. hosted, core/alloc/std |
0.1 (bring-up on every tier), 0.4 (the matrix by experiment) |
| §2 portability taxonomy | 0.4, 0.5 (deliberate failures) |
| §3 toolchain | 0.1, 0.7 (Python environment and device probe) |
| §4 workspace, runners | 0.1, 0.2 |
§5 first no_std binary |
0.2 (hello in QEMU), 0.3 (startup, side by side) |
| §6 sizes and disassembly | 0.3, 0.6 |
| §7 warnings and lints | 0.5, 0.6 |
| §8 availability matrix | 0.4 |