All guides
TECHNICAL GUIDESTM32 DEBUGPROBE-RS2026

probe-rs
Modern CLI Flash & Debug for STM32L4R5

One host-side toolchain that replaces OpenOCD + GDB + ST-LINK Utility: flash an ELF, stream RTT/defmt logs, and catch panics on a Cortex-M4F — over ST-Link, J-Link, or CMSIS-DAP.

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).

probe-rsThe main CLI: list, info, download, run, attach, erase, reset, verify, gdb, dap-server, chip.
cargo-flashCargo subcommand: build the crate, then flash the resulting ELF. cargo flash --chip STM32L4R5ZITx --release.
cargo-embedCargo subcommand: build + flash + open an RTT terminal (and optionally a GDB server), all driven by an Embed.toml file.
Library (crate)The same engine as a Rust crate for building your own tooling, plus 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 familyVersions / examplesNotes
ST-LinkV2, 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-LinkEDU, BASE, PLUS, on-board OBFull speed SWD/JTAG; excellent for custom boards.
CMSIS-DAPDAPLink, Picoprobe (RP2040), MCU-LinkUSB-HID (v1) and USB-bulk (v2) both supported; no vendor driver needed.
FTDIFT2232H / FT4232H basedGeneric JTAG/SWD adapters.
WHY IT MATTERS

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.

bash — install
# 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
bash — Linux udev rules (non-root USB access)
# 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

bash — probe-rs list & info
# 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.

bash — resolve the exact target
# 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
FactValue (STM32L4R5ZI)
probe-rs targetSTM32L4R5ZITx (short: STM32L4R5ZI)
CoreArm Cortex-M4F (single-precision FPU), up to 120 MHz
Rust target triplethumbv7em-none-eabihf (hard-float)
Flash2 MiB @ 0x0800_0000 (dual bank)
SRAM640 KiB contiguous @ 0x2000_0000 (SRAM1+SRAM2+SRAM3)
Reference manual / datasheetRM0432 / 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.

SignalMCU pinRole
SWDIOPA13Serial-wire data (bidirectional)
SWCLKPA14Serial-wire clock
NRSTNRSTHardware reset — needed for --connect-under-reset
SWOPB3Trace/ITM out (AF0, optional — not used by RTT)
GND / VREFGND / 3V3Common 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.

bash — download / flash
# 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
bash — verify, erase, reset
# 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
WATCH OUT

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.

bash — probe-rs run
# 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.

.cargo/config.toml
[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.x — cortex-m-rt linker regions (STM32L4R5ZI)
MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 2048K
  RAM   : ORIGIN = 0x20000000, LENGTH = 640K
}
Cargo.toml — firmware dependencies
[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"] }
src/main.rs — minimal defmt firmware
#![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
        }
    }
}
terminal — what probe-rs run prints
# $ 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
   ...
NOTE

--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:

Up channelsTarget → host. Channel 0 is the conventional log/defmt stream. You can add more (e.g. a raw binary telemetry channel).
Down channelsHost → target. Channel 0 is the conventional input/console for sending bytes back to the firmware.

Overflow modes (how the target behaves when a buffer is full)

ModeBehaviourUse when
NoBlockSkipDrop the whole message if it does not fitNever stall the CPU; lossy logging is acceptable
NoBlockTrimWrite as much as fits, discard the restBest-effort logging, keep partial lines
BlockIfFullSpin until the host drains the bufferYou must not lose a byte — but the app can freeze if no host is attached

Host-side channel formats

FormatMeaning
StringShow the target bytes as UTF-8 text directly (plain rtt-target logging).
DefmtDecode compact defmt frames on the host using symbols from the ELF (used by defmt-rtt).
BinaryLEDisplay 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.

src/main.rs — rtt-target String channel
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);
    }
}
NOTE

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.

bash — cargo embed
# 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.tomlEmbed.toml.embed.toml → built-in defaults. Commit Embed.toml; put machine-specific overrides in Embed.local.toml and .gitignore it.

Embed.toml — annotated (full default schema)
[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"
TIP — profiles

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.

bash — attach without flashing
# 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
bash — GDB server + arm-none-eabi-gdb
# 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.

.vscode/launch.json
{
  "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

SubcommandPurpose
listEnumerate connected debug probes (index, name, VID:PID:Serial).
infoProbe the target: DAP, cores, ROM tables, architecture.
downloadFlash ELF/BIN/HEX/IDF/UF2 without running.
runFlash + reset + stream RTT/defmt; catch panics/faults.
attachConnect to a running target and stream RTT (no flash/reset).
verifyCompare flash contents against a file.
eraseFull-chip or sector erase (needs --allow-erase-all).
resetReset the target and (optionally) halt.
read / writeDirect memory access at 8/16/32/64-bit widths.
chip list / chip infoQuery the built-in target registry.
gdbStart a GDB server.
dap-serverStart a Debug Adapter Protocol server (VS Code).
benchmark / profileMeasure probe throughput / PC-sample the running core.

Global flags (target selection)

FlagEffect
--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-resetAssert NRST while attaching (recover a locked-out SWD).

Key per-command flags

CommandFlagEffect
run--catch-panicHalt + backtrace when the firmware panics.
run--catch-hardfaultHalt + backtrace on a HardFault.
run--rtt-scan-memoryScan RAM for the RTT control block (release/LTO builds).
run--always-print-stacktracePrint a backtrace even on a clean exit.
run--no-locationSuppress 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-eraseFull erase before writing.
download--disable-progressbarsClean output for CI logs.
erase--allow-erase-allRequired 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.

1 — Wrong or ambiguous chip name

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.

2 — "No RTT control block found"

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.

3 — Forgetting the defmt linker arg

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.

4 — SWD locked by your own firmware

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.

5 — Flashing a raw .bin to the wrong address

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.

6 — Old ST-Link firmware

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".

7 — Mismatched ELF on attach

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.

8 — Right Rust target

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.