← Writing

Debugging firmware

2025-07-01 rust · embedded · basic


Classic software development is always inside the cycle of write - run - check - fix.
In classic systems, debugging is simplified by built-in logs, breakpoints and step-by-step code execution directly in the IDE.

When developing programs for microcontrollers, debugging is significantly more complicated since the code is written on a PC and executed on a device with a completely different architecture.
Obtaining debug information, setting breakpoints, accessing registers and local variables require a special environment and integration of tools, and the process itself becomes much less obvious.
🟠 In this article I want to describe the standard process when debugging software running on a microcontroller and describe the tools that play a key role in this.


    🏗️ Debug Interfaces

JTAG (Joint Test Action Group)

SWD (Serial Wire Debug)

    ⚒️ Debugging tools

📟 Hardware

💽 Software

🖥️ Servers

OpenOCD (Open On Chip Debugger)

probe-rs

🔬 Debuggers

    📐 Logging

defmt

use panic_probe as _;
use defmt_rtt as _;

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
    defmt::error!("PANIC: {}", defmt::Display2Format(info));
    defmt::info!("Hello from probe-rs!");
    loop {}
}

rtt

Low-level library for direct RTT logging. Allows writing strings directly to the RTT buffer without additional logic.

[dependencies]
rtt-target = "0.6.0"
use rtt_target::{rtt_init_print, rprintln};

rtt_init_print!();
rprintln!("\nREAD from address 0x1B");

In VS Code, you can install the “Debugger for probe-rs” extension.
Then open the project and start debugging by pressing F5 – it uses probe-rs to flash and run the application.
This approach allows you to see the defmt/RTT output directly in the VSCode console.

Debugging memory in Embedded Rust

Despite Rust’s safety, unsafe blocks are often necessary for low-level work. They should be checked manually and supplemented with static analysis (Clippy, Miri) and profiling (high-water marks) for reliability and optimal memory usage.

This approach combines Rust’s compile-time safety with analysis tools, ensuring high stability of embedded software.

    🔄 Typical workflow

  1. Write code → add rprintln!() for logs.
  2. cargo build → cargo flash --chip ...
  3. Run the server probe-rs run --gdb in the background.
  4. Connect VS Code/GDB → set breakpoints → run.
  5. Analyze variables/logs → fix error → repeat.
  6. In case of panic → look at call stack and RTT logs.
Stage Tools Flow Point
1️⃣ Development - rustc + cargo
- embedded-hal
- rtt-target
- panic-halt, etc
#![no_std]
#![no_main]
- no_std disables the standard library.
- panic_handler is required.
- RTT/defmt adds a logging channel.
2️⃣ Compilation - cargo + target toolchain (e.g. thumbv7em-none-eabihf)
- probe-rs / cargo-binutils
cargo build --target thumbv7em-none-eabihf
cargo objcopy --bin app -- -O binary firmware.bin
- Toolchain generates code for specific Cortex-M.
- objcopy creates firmware image.
3️⃣ Flashing - Hardware: ST-Link, J-Link, Black Magic Probe
- Software: probe-rs, openocd, cargo-flash
probe-rs download --chip STM32F411CEUx firmware.bin
cargo flash --chip STM32F411CEUx
- probe-rs works without config files.
- Use --chip to match the target MCU.
4️⃣ Debugging - Server: probe-rs / openocd
- Client: GDB, Cortex-Debug (VS Code)
1. Run the server probe-rs run --gdb
2. Connect GDB gdb target/thumbv7em-none-eabihf/debug/app
(gdb) target extended-remote :1337
(gdb) break main
(gdb) continue
(gdb) print _x
3. Or use VS Code.
- GDB enables stepping, variable inspection.
- VS Code offers GUI for debugging.
5️⃣ Diagnostics - RTT Viewer (J-Link)
- defmt-print
- GDB commands
1. RTT logsprobe-rs rtt --chip STM32F411CEUx
2. GDB memory:
   - x/16x 0x20000000
   - p &_x
3. View registers:
   - In VS Code via .svd
   - In GDB: info registers
- RTT provides real-time logs.
- Memory view helps detect corruption.
- Registers show peripheral states.
6️⃣ Panic Handling - panic-probe + defmt
- GDB backtrace
- Enable backtrace: set debug = 2 in Cargo.toml - Allows debugging panic location with full context.
// .vscode/launch.json
{
  "type": "cortex-debug",
  "servertype": "probe-rs",
  "executable": "target/.../app",
  "request": "launch",
  "device": "STM32F411CEUx"
}

Key Points