The Windows 11
Toolchain
Before a single byte of Rust reaches your RP2350, it passes through a pipeline of eight distinct components. Each component in this chain has a job. When the chain breaks — and it will break during setup — knowing each component's job tells you which one failed and why.
YOUR RUST SOURCE CODE │ ▼ rustc ─── Rust compiler (type-checks, borrow-checks, generates IR) │ ▼ LLVM ─── IR → ARM Thumb2 machine code for Cortex-M33 (RP2350) │ ▼ MSVC link ── Windows linker (joins .o files, resolves symbols) │ ▼ flip-link ── Reorders RAM sections to detect stack overflow │ ▼ firmware.elf ── ELF binary with debug symbols │ ▼ probe-rs ─── Speaks SWD protocol over USB to the Debug Probe │ ▼ Debug Probe ─── USB → SWD bridge (your Raspberry Pi Debug Probe) │ ▼ RP2350 FLASH ── firmware executing on silicon
Rust on Windows uses the MSVC linker to produce the final binary. This surprises people — you are writing Rust, not C, so why do you need C build tools? The answer is that Rust does not ship its own linker on Windows (unlike on Linux/macOS). It delegates linking to the platform's native linker. On Windows that is MSVC's link.exe.
Step 1 — MSVC Build Tools (install FIRST — rustup detects it) URL: https://visualstudio.microsoft.com/visual-cpp-build-tools/ Choose: "Desktop development with C++" Size: ~4GB. This installs link.exe, Windows SDK, CRT headers. Step 2 — rustup (the Rust toolchain manager) URL: https://rustup.rs During install: confirm it found MSVC (it will ask) Default profile is correct. Step 3 — Add the ARM target rustup target add thumbv8m.main-none-eabihf ↑ This is the Cortex-M33 (RP2350) target triple: thumb = Thumb instruction set (subset of ARM) v8m = ARMv8-M architecture (RP2350 is v8-M.main) main = Main Extension (includes FPU, DSP) none = no operating system eabihf = Embedded ABI, Hard Float Step 4 — Install flip-link (stack overflow detector) cargo install flip-link Step 5 — Install probe-rs (flashing and debugging) cargo install probe-rs-tools --locked Step 6 — VS Code + rust-analyzer extension Install VS Code, then install the rust-analyzer extension. rust-analyzer gives you: type inference hints, error underlining, completion, go-to-definition, refactoring — all without running cargo.
Your Debug Probe connects over USB, but Windows will initially assign it the wrong driver. Without the correct driver, probe-rs cannot talk to it. You must install the WinUSB driver manually using Zadig:
1. Download Zadig from zadig.akeo.ie. 2. Plug in your Debug Probe. 3. In Zadig, select the Debug Probe from the device list (it may appear as "CMSIS-DAP" or "Raspberry Pi Debug Probe"). 4. Select WinUSB as the driver. 5. Click Replace Driver. After this, probe-rs can enumerate and communicate with the probe. Do this once — the driver persists across reboots.
The linker must know the memory map of your target — where flash starts, how much there is, where RAM starts, how much there is. For embedded targets, this is not known automatically. You provide it in a memory.x file at the root of your project.
MEMORY {
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 /* 256B boot2 loader */
FLASH : ORIGIN = 0x10000100, LENGTH = 4096K - 0x100 /* 4MB flash */
RAM : ORIGIN = 0x20000000, LENGTH = 520K /* 520KB SRAM on RP2350 */
}
EXTERN(BOOT2_FIRMWARE)
SECTIONS {
.boot2 ORIGIN(BOOT2) : {
KEEP(*(.boot2));
} > BOOT2
}
/* flip-link will rearrange RAM sections to detect stack overflow */[package] name = "pico2-noc-sensor" version = "0.1.0" edition = "2021" [dependencies] embassy-executor = { version = "0.6", features = ["arch-cortex-m", "executor-thread"] } embassy-rp = { version = "0.2", features = ["rp2350", "time-driver"] } embassy-time = { version = "0.3" } embassy-sync = { version = "0.6" } embassy-futures = { version = "0.1" } defmt = "0.3" defmt-rtt = "0.4" # RTT transport for defmt panic-probe = { version = "0.3", features = ["print-defmt"] } cortex-m = { version = "0.7", features = ["critical-section-single-core"] } cortex-m-rt = "0.7" heapless = "0.8" # no-alloc data structures [profile.dev] opt-level = 1 # at least O1 for embedded — O0 is too slow [profile.release] opt-level = "z" # optimize for size — flash is precious lto = true # link-time optimization — eliminates dead code debug = 2 # keep debug info even in release for probe-rs
[build] target = "thumbv8m.main-none-eabihf" # default target for all builds [target.thumbv8m.main-none-eabihf] runner = "probe-rs run --chip RP2350A" # what `cargo run` invokes rustflags = [ "-C", "linker=flip-link", # stack overflow detector "-C", "link-arg=-Tlink.x", # Embassy's linker script "-C", "link-arg=--nmagic", # required for cortex-m-rt ]
Standard Rust has println!. In embedded no_std, there is no stdout, no file descriptor, no OS to write to. defmt (deferred formatting) is the solution: a logging framework that encodes log messages as compact binary sequences, sends them over RTT (Real-Time Transfer — a Segger protocol that uses a small ring buffer in the MCU's SRAM accessible via the debug probe), and decodes them on your PC. The result is structured, timestamped logs with minimal overhead — formatting happens on your PC, not on the MCU.
#![no_std] #![no_main] use defmt_rtt as _; // link the RTT transport use panic_probe as _; // print panic messages via defmt before halting // Logging macros mirror std's tracing/log macros defmt::info!("boot complete — firmware v{}", env!("CARGO_PKG_VERSION")); defmt::debug!("i2c write: reg={:#x} val={:#x}", reg, val); defmt::warn!("sensor read retry #{}", attempt); defmt::error!("I2C NAK — is the device powered?"); // cargo run shows defmt output in the terminal via probe-rs // probe-rs run --chip RP2350A — runs and streams defmt output // No UART wiring needed. No terminal emulator. Just `cargo run`.
Error: "error: linker `link.exe` not found" Fix: MSVC Build Tools not installed. Run the Visual Studio installer, select "Desktop development with C++", install. Error: "error[E0463]: can't find crate for `std`" Fix: You are building for the wrong target. Ensure .cargo/config.toml sets target = "thumbv8m.main-none-eabihf". Error: "probe-rs: Error: No connected probes found" Fix: Zadig WinUSB driver not installed. Run Zadig, select the probe, install WinUSB. If still failing: check USB cable is data-capable, try a different USB port, try a different cable. Error: "flip-link: no such file or directory" Fix: flip-link not installed. Run: cargo install flip-link Error: "DEFMT_LOG level not set" Fix: Set env var: $env:DEFMT_LOG="debug" (PowerShell) Or add to .cargo/config.toml: [env] DEFMT_LOG = "debug" Error: "could not compile ... process didn't exit successfully (signal: SIGKILL)" Fix: Usually OOM during compilation. Close other applications. Or: cargo build -j1 (single job, less memory) Error: Hard fault at runtime, no panic message Fix: Stack overflow. flip-link should catch this — verify it is in .cargo/config.toml. Increase EMBASSY_EXECUTOR_TASK_ARENA_SIZE if needed. Error: "cannot borrow as mutable" on embassy peripherals Fix: You tried to use a peripheral in two places. Embassy peripherals implement the singleton pattern — each can be owned by only one task. Pass ownership into the task that needs it via spawner.spawn(). Error: RTT output not appearing after cargo run Fix: defmt-rtt not linked. Ensure your main.rs has: use defmt_rtt as _; The underscore import is required even though it looks unused. Error: "error: `async fn` in trait" compile error Fix: Ensure Rust edition = "2021" in Cargo.toml. Or add #![feature(async_fn_in_trait)] for older toolchains.
Get firmware on the Pico 2 and verify the chain
Create a new project with cargo new pico2-hello. Configure it with the Cargo.toml and .cargo/config.toml from this chapter. Write a main that blinks the onboard LED (GPIO25) at 1Hz using Timer::after_millis(500).await and logs each state change with defmt. Run it with cargo run. You should see defmt output in the terminal and the LED blinking. When this works, the full chain is verified.
Understand what cargo produces
After building, find the ELF binary in target/thumbv8m.main-none-eabihf/release/. Run probe-rs info --chip RP2350A to display chip information. Run cargo size --release -- -A to see section sizes: .text (code in flash), .data (initialized statics in RAM), .bss (zeroed statics in RAM). Note the total size — a basic Embassy blink firmware is approximately 30-50KB. This overhead is the Embassy runtime, the defmt infrastructure, and the panic handler. It does not grow much as you add application code.