01 What probe-rs is and why use it
probe-rs is a host-side debugging toolset and Rust library for programming and debugging ARM Cortex-M and RISC-V targets over SWD/JTAG. On the STM32L4R5 it is the modern one-command alternative to the classic OpenOCD + arm-none-eabi-gdb + ST-LINK Utility stack.
Everything runs on your development host and talks to the MCU through a debug probe. probe-rs speaks the debug transport directly (no separate GDB server process required), owns a built-in flash algorithm database, and can decode RTT and defmt logs live. The typical workflow collapses flash, reset, and log into a single probe-rs run or cargo embed invocation (the build stays a normal cargo build step).
list, info, download, run, attach, erase, reset, verify, gdb, dap-server, chip.cargo flash --chip STM32L4R5ZITx --release.Embed.toml file.dap-server for editor integration.Supported debug probes
probe-rs is probe-agnostic. On a Nucleo-L4R5ZI the on-board ST-Link is used with zero wiring; on custom boards any of the following work through the same commands.
| Probe family | Versions / examples | Notes |
|---|---|---|
| ST-Link | V2, V2-1, V3 (on-board on every Nucleo) | May prompt for a firmware update; use STM32CubeProgrammer's ST-Link upgrader if probe-rs refuses an old version. |
| SEGGER J-Link | EDU, BASE, PLUS, on-board OB | Full speed SWD/JTAG; excellent for custom boards. |
| CMSIS-DAP | DAPLink, Picoprobe (RP2040), MCU-Link | USB-HID (v1) and USB-bulk (v2) both supported; no vendor driver needed. |
| FTDI | FT2232H / FT4232H based | Generic JTAG/SWD adapters. |
probe-rs is the default runner for embedded Rust (defmt + panic-probe), but it flashes any ELF/BIN/HEX — including C firmware built with the STM32 GCC toolchain. You get one CLI, one chip name, and one log stream regardless of the source language.
02 Install, probe detection & the L4R5 chip name
Install the probe-rs-tools package (it ships all three binaries), set up USB permissions on Linux, then confirm the probe and the exact target name for the STM32L4R5.
# Recommended for Rust users — builds probe-rs, cargo-flash, cargo-embed
cargo install probe-rs-tools --locked
# Or prebuilt binaries (no Rust toolchain needed)
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh
# Bleeding-edge (git) if you need a just-added target
cargo install probe-rs-tools --git https://github.com/probe-rs/probe-rs --locked
# Optional: shell completion
probe-rs complete install
# Install the maintained rules, then reload — no sudo needed afterwards
sudo curl -L \
https://probe.rs/files/69-probe-rs.rules \
-o /etc/udev/rules.d/69-probe-rs.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
# Re-plug the probe (or add yourself to the plugdev group) after this.
Find the probe
# Enumerate connected debug probes
probe-rs list
# The following debug probes were found:
# [0]: STLink V3 -- 0483:374e:002700xxxxxx (ST-LINK)
# Interrogate the target itself (ARM DAP, cores, ROM tables)
probe-rs info --chip STM32L4R5ZITx
# If SWD is stuck (e.g. firmware reconfigures SWD pins), add:
probe-rs info --chip STM32L4R5ZITx --connect-under-reset
The correct chip name for the L4R5
probe-rs identifies chips by their full ST part number (with the package-coded suffix), and it does substring matching. The Nucleo-L4R5ZI carries an STM32L4R5ZIT6, whose probe-rs target is STM32L4R5ZITx. Passing the shorter --chip STM32L4R5ZI also works as long as it resolves to a single candidate.
# List every matching variant probe-rs knows
probe-rs chip list | grep -i STM32L4R5
# STM32L4R5AGxx / STM32L4R5AIxx / STM32L4R5QGxx / STM32L4R5QIxx
# STM32L4R5VGxx / STM32L4R5VIxx / STM32L4R5ZGxx / STM32L4R5ZITx ...
# Detailed memory map / flash algo for one variant
probe-rs chip info STM32L4R5ZITx
| Fact | Value (STM32L4R5ZI) |
|---|---|
| probe-rs target | STM32L4R5ZITx (short: STM32L4R5ZI) |
| Core | Arm Cortex-M4F (single-precision FPU), up to 120 MHz |
| Rust target triple | thumbv7em-none-eabihf (hard-float) |
| Flash | 2 MiB @ 0x0800_0000 (dual bank) |
| SRAM | 640 KiB contiguous @ 0x2000_0000 (SRAM1+SRAM2+SRAM3) |
| Reference manual / datasheet | RM0432 / DS12023 |
SWD debug pins (custom boards)
On a Nucleo these route to the on-board ST-Link automatically. On your own PCB, wire the probe to these pins — RTT works over the SWD memory bus, so SWO is optional.
| Signal | MCU pin | Role |
|---|---|---|
| SWDIO | PA13 | Serial-wire data (bidirectional) |
| SWCLK | PA14 | Serial-wire clock |
| NRST | NRST | Hardware reset — needed for --connect-under-reset |
| SWO | PB3 | Trace/ITM out (AF0, optional — not used by RTT) |
| GND / VREF | GND / 3V3 | Common ground and level reference |
03 Flashing: download, verify, erase, reset
probe-rs download is the non-interactive programmer. It auto-selects a flash algorithm from the chip name and accepts ELF, BIN, HEX, IDF, and UF2 inputs.
# Flash an ELF — format is auto-detected, addresses come from the ELF
probe-rs download --chip STM32L4R5ZITx firmware.elf
# Be explicit about the format when it is ambiguous
probe-rs download --chip STM32L4R5ZITx --binary-format elf firmware.elf
# Raw BIN has no addresses — you MUST give a base address
probe-rs download --chip STM32L4R5ZITx \
--binary-format bin --base-address 0x08000000 firmware.bin
# Full chip erase before writing (vs. default page-by-page erase)
probe-rs download --chip STM32L4R5ZITx --chip-erase firmware.elf
# CI-friendly: no progress bars, and pick a specific probe
probe-rs download --chip STM32L4R5ZITx \
--probe 0483:374e --disable-progressbars firmware.elf
# Compare flash against an ELF without reprogramming
probe-rs verify --chip STM32L4R5ZITx firmware.elf
# Erase the whole chip (guard flag required so you don't wipe by accident)
probe-rs erase --chip STM32L4R5ZITx --allow-erase-all
# Pulse a reset and let the target run
probe-rs reset --chip STM32L4R5ZITx
A .bin carries no load address. If you flash one without --base-address 0x08000000 it lands at address 0 and the MCU will not boot. Prefer flashing the .elf, which already encodes the correct sections.
04 probe-rs run: flash + RTT + catch panic
probe-rs run is the flagship command: it flashes the firmware, resets, streams RTT/defmt logs to your terminal, and — with --catch-panic / --catch-hardfault — halts the core and prints a decoded backtrace when things go wrong.
# Flash, reset, and attach RTT in one shot
probe-rs run --chip STM32L4R5ZITx \
target/thumbv7em-none-eabihf/debug/my-firmware
# Halt + backtrace on panic!() and on a HardFault
probe-rs run --chip STM32L4R5ZITx \
--catch-panic --catch-hardfault \
target/thumbv7em-none-eabihf/debug/my-firmware
# Force a full RAM scan for the RTT control block (release/LTO builds)
probe-rs run --chip STM32L4R5ZITx \
--rtt-scan-memory \
--always-print-stacktrace \
target/thumbv7em-none-eabihf/release/my-firmware
Wire it into Cargo as the runner
Point Cargo's runner at probe-rs run and cargo run becomes flash-and-log. This is the standard embedded-Rust setup.
[target.thumbv7em-none-eabihf]
# cargo run == flash + reset + RTT/defmt for the L4R5
runner = "probe-rs run --chip STM32L4R5ZITx"
rustflags = [
"-C", "link-arg=-Tlink.x", # cortex-m-rt linker script
"-C", "link-arg=-Tdefmt.x", # defmt symbol table
]
[build]
target = "thumbv7em-none-eabihf"
[env]
DEFMT_LOG = "debug" # host-side log-level filter
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 2048K
RAM : ORIGIN = 0x20000000, LENGTH = 640K
}
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
defmt = "0.3"
defmt-rtt = "0.4" # the RTT transport for defmt
panic-probe = { version = "0.3", features = ["print-defmt"] }
#![no_std]
#![no_main]
use defmt_rtt as _; // global defmt logger over RTT
use panic_probe as _; // panic handler: log via defmt, then halt
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
defmt::info!("STM32L4R5ZI booted, RTT is live");
let mut n: u32 = 0;
loop {
defmt::debug!("heartbeat #{}", n);
n = n.wrapping_add(1);
cortex_m::asm::delay(4_000_000);
if n == 5 {
defmt::error!("simulated fault -> panic");
panic!("boom at n={}", n); // --catch-panic halts here with a backtrace
}
}
}
# $ cargo run
Erasing ✔ [00:00:00] 12.00 KiB/12.00 KiB
Programming ✔ [00:00:00] 12.00 KiB/12.00 KiB
Finished in 0.41s
INFO STM32L4R5ZI booted, RTT is live
DEBUG heartbeat #0
DEBUG heartbeat #1
...
ERROR simulated fault -> panic
ERROR panicked at src/main.rs:22:13: boom at n=5
# stack backtrace, with source locations, printed by --catch-panic
0: my_firmware::main
at src/main.rs:22
1: main
...
--catch-panic works by inserting a breakpoint at panic-probe's abort symbol; --catch-hardfault breaks in the fault handler. Both need the ELF (with symbols) — which is exactly what probe-rs run and the Cargo runner already pass in.
05 RTT channels, modes & formats
RTT (Real-Time Transfer) is a ring-buffer protocol living in target RAM. The debug host reads/writes those buffers over SWD while the CPU runs — no UART, no extra pin. The firmware declares a control block (the ASCII marker SEGGER RTT) that probe-rs finds by scanning RAM.
RTT provides multiple independent channels in each direction:
Overflow modes (how the target behaves when a buffer is full)
| Mode | Behaviour | Use when |
|---|---|---|
NoBlockSkip | Drop the whole message if it does not fit | Never stall the CPU; lossy logging is acceptable |
NoBlockTrim | Write as much as fits, discard the rest | Best-effort logging, keep partial lines |
BlockIfFull | Spin until the host drains the buffer | You must not lose a byte — but the app can freeze if no host is attached |
Host-side channel formats
| Format | Meaning |
|---|---|
String | Show the target bytes as UTF-8 text directly (plain rtt-target logging). |
Defmt | Decode compact defmt frames on the host using symbols from the ELF (used by defmt-rtt). |
BinaryLE | Display raw bytes as little-endian hex — for custom binary telemetry channels. |
Plain-string RTT without defmt
If you are not using defmt (e.g. C firmware, or plain Rust), the rtt-target crate gives a String channel that any RTT host — including probe-rs and cargo-embed — can read.
use rtt_target::{rtt_init_print, rprintln};
#[entry]
fn main() -> ! {
rtt_init_print!(); // installs the RTT control block
rprintln!("hello over RTT channel 0");
let mut i = 0;
loop {
rprintln!("counter = {}", i);
i += 1;
cortex_m::asm::delay(8_000_000);
}
}
Pick one logging path per channel. defmt-rtt emits binary defmt frames (Defmt format); rtt-target emits text (String format). Reading a defmt channel as String shows garbage, and vice-versa.
06 cargo-embed and Embed.toml
cargo embed builds your crate, flashes it, and opens a full RTT terminal UI (and optionally a GDB server), all configured by an Embed.toml in the project root. It is the "richer" sibling of probe-rs run.
# Build (release) + flash + open the default profile
cargo embed --release
# Select a named profile defined in Embed.toml (positional arg)
cargo embed --release with_rtt
# cargo-embed is part of probe-rs-tools; also: cargo flash
cargo flash --chip STM32L4R5ZITx --release
Config is layered as <profile>.<section>. The default profile is literally named default. Loading precedence (first found wins): Embed.local.toml → .embed.local.toml → Embed.toml → .embed.toml → built-in defaults. Commit Embed.toml; put machine-specific overrides in Embed.local.toml and .gitignore it.
[default.probe]
# Pin a specific probe when several are attached
# usb_vid = "0483"
# usb_pid = "374e"
# serial = "002700xxxxxx"
protocol = "Swd" # "Swd" or "Jtag"
# speed = 4000 # link speed in kHz
[default.general]
chip = "STM32L4R5ZITx" # the target — this is the key line
chip_descriptions = []
log_level = "WARN"
connect_under_reset = false # true == assert NRST while attaching
[default.flashing]
enabled = true
restore_unwritten_bytes = false
do_chip_erase = false # true == full erase instead of page-by-page
[default.reset]
enabled = true
halt_afterwards = false # true == stay halted after reset (debug from reset)
[default.rtt]
enabled = true # open the RTT UI after flashing
timeout = 3000 # ms to keep retrying the RTT attach
show_timestamps = true
log_enabled = false # save RTT history to disk
log_path = "./logs"
# up_mode = "BlockIfFull" # global default overflow mode
channels = [
# up/down are channel numbers; format is required
{ up = 0, down = 0, name = "logs", up_mode = "NoBlockTrim", format = "Defmt" },
]
[default.gdb]
enabled = false
gdb_connection_string = "127.0.0.1:1337"
Add a second profile by prefixing sections with its name, e.g. [with_gdb.gdb] with enabled = true. Then cargo embed with_gdb flashes and drops you at a GDB server, while plain cargo embed keeps the RTT-only default.
07 attach, gdb server & VS Code (DAP)
Not every session should re-flash. probe-rs attach connects to firmware that is already running; probe-rs gdb and probe-rs dap-server give you classic breakpoint debugging from GDB or an editor.
# Attach to the running target and stream RTT — no reset, no re-flash.
# Pass the same ELF so defmt frames and backtraces can be decoded.
probe-rs attach --chip STM32L4R5ZITx \
target/thumbv7em-none-eabihf/debug/my-firmware
# Reconnect over a flaky SWD line by asserting reset first
probe-rs attach --chip STM32L4R5ZITx --connect-under-reset my-firmware
# Terminal 1: open a GDB server on the default 127.0.0.1:1337
probe-rs gdb --chip STM32L4R5ZITx
# Terminal 2: connect a GDB and load symbols
arm-none-eabi-gdb target/thumbv7em-none-eabihf/debug/my-firmware
# (gdb) target remote :1337
# (gdb) load
# (gdb) break main
# (gdb) continue
VS Code via the Debug Adapter Protocol
The probe-rs-debugger VS Code extension talks to probe-rs dap-server. A launch.json of type probe-rs-debug gives you breakpoints, memory/register views, and an RTT terminal inside the editor.
{
"version": "0.2.0",
"configurations": [
{
"type": "probe-rs-debug",
"request": "launch",
"name": "L4R5 (probe-rs)",
"chip": "STM32L4R5ZITx",
"coreConfigs": [
{
"programBinary": "target/thumbv7em-none-eabihf/debug/my-firmware",
"rttEnabled": true
}
]
}
]
}
Start the adapter with probe-rs dap-server (the extension launches it for you), then press F5.
08 Command & flag reference tables
Everything you need on one screen. Global flags apply to any subcommand that touches a target; per-command flags are the ones you reach for most.
Subcommands
| Subcommand | Purpose |
|---|---|
list | Enumerate connected debug probes (index, name, VID:PID:Serial). |
info | Probe the target: DAP, cores, ROM tables, architecture. |
download | Flash ELF/BIN/HEX/IDF/UF2 without running. |
run | Flash + reset + stream RTT/defmt; catch panics/faults. |
attach | Connect to a running target and stream RTT (no flash/reset). |
verify | Compare flash contents against a file. |
erase | Full-chip or sector erase (needs --allow-erase-all). |
reset | Reset the target and (optionally) halt. |
read / write | Direct memory access at 8/16/32/64-bit widths. |
chip list / chip info | Query the built-in target registry. |
gdb | Start a GDB server. |
dap-server | Start a Debug Adapter Protocol server (VS Code). |
benchmark / profile | Measure probe throughput / PC-sample the running core. |
Global flags (target selection)
| Flag | Effect |
|---|---|
--chip <name> | Target, e.g. STM32L4R5ZITx (substring match allowed). |
--probe <VID:PID[:Serial]> | Select a specific probe, e.g. 0483:374e. |
--protocol <swd|jtag> | Debug transport (default SWD on STM32). |
--speed <kHz> | SWD/JTAG clock, e.g. --speed 4000. |
--connect-under-reset | Assert NRST while attaching (recover a locked-out SWD). |
Key per-command flags
| Command | Flag | Effect |
|---|---|---|
| run | --catch-panic | Halt + backtrace when the firmware panics. |
| run | --catch-hardfault | Halt + backtrace on a HardFault. |
| run | --rtt-scan-memory | Scan RAM for the RTT control block (release/LTO builds). |
| run | --always-print-stacktrace | Print a backtrace even on a clean exit. |
| run | --no-location | Suppress source file:line in log output. |
| download | --binary-format <fmt> | elf / bin / hex / idf / uf2. |
| download | --base-address <addr> | Load address for raw bin input. |
| download | --chip-erase | Full erase before writing. |
| download | --disable-progressbars | Clean output for CI logs. |
| erase | --allow-erase-all | Required confirmation for a full-chip erase. |
09 Gotchas & common mistakes
The failures that eat the most time on an STM32L4R5, and how to avoid each one.
Guessing STM32L4R5 (no package suffix) can match several variants and error out, or select the wrong flash geometry. Use STM32L4R5ZITx for the Nucleo, and confirm with probe-rs chip list | grep STM32L4R5. The --chip value must be identical in .cargo/config.toml, Embed.toml, and launch.json.
RTT needs the firmware to actually initialize an RTT logger (defmt-rtt or rtt-target). If you optimized it out, or the control block sits outside the default scan window (common with release + LTO), add --rtt-scan-memory. Also make sure defmt::info! calls are reachable — dead-code elimination can drop the logger entirely.
defmt requires -Tdefmt.x in rustflags in addition to -Tlink.x. Without it the build fails to link, or logs decode as gibberish. It must come after -Tlink.x.
If your code reconfigures PA13/PA14 as GPIO, enters Stop/Standby immediately, or sets RDP read-protection, probe-rs can no longer attach normally. Recover with --connect-under-reset; for RDP level 1 a full probe-rs erase --allow-erase-all (mass erase) clears protection and unbricks the part.
A .bin has no addresses. Always flash the .elf, or pass --binary-format bin --base-address 0x08000000. Flashing a bin without a base address writes to 0 and the MCU will not boot.
probe-rs may refuse a very old on-board ST-Link. Update it with STM32CubeProgrammer's "Firmware upgrade" tool (STLinkUpgrade). Also close STM32CubeIDE / OpenOCD first — only one host can own the probe, and a lingering session gives "probe already in use".
probe-rs attach decodes defmt and backtraces using the ELF you pass. If that ELF is not the exact build running on the chip, log strings and source locations will be wrong. Re-flash, or pass the matching artifact.
The L4R5 has an FPU, so build for thumbv7em-none-eabihf (hard-float), not thumbv7em-none-eabi. Add it once with rustup target add thumbv7em-none-eabihf.