Debugging firmware
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)
- standard serial port for testing and debugging (JTAG - Wikipedia).
Since 1990, an industry standard (IEEE 1149.1) for testing electronics.
Can use 4 or 5 lines:
TDI
(test data input )TDO
(test data output)TCK
(test clock)TMS
(test mode select )TRST
(test reset) line optional With these lines, you can write a program image to memory, receive debug messages, read the internal state, stop and step through the code.
Provides access to the internal registers and memory of the chip for debugging.
SWD (Serial Wire Debug)
- alternative 2-pin ARM debug interface (Serial Wire Debug (SWD) Issue #785 riscv/riscv-debug-spec GitHub).
Similar to JTAG, but uses onlySWDIO
andSWCLK
lines, saving MCU pins.
βββ βοΈ Debugging tools
π Hardware
- Connect to the device via JTAG (4-5 lines) or SWD (2 lines). | Tool | Protocol | Target platforms | |----------------------|--------------|------------------------------| | Black Magic Probe| SWD/JTAG | General-purpose (ARM, RISC-V) | | ST-Link | SWD | STM32 | | J-Link | SWD/JTAG | Multi-platform | | Rusty Probe | SWD | Specialized for Rust |
π½ Software
π₯οΈ Servers
OpenOCD (Open On Chip Debugger)
OpenOCD
: An open source tool for debugging, testing and programming embedded systems, providing an interface between the host and the hardware, often used withGDB
.- Since 2005, Open Source supports almost every architecture.
- Until recently,
OpenOCD
paired with theGDB
debugger was the way you developed your embedded project code.
probe-rs
probe-rs
: Modern framework for flashing and debugging microcontrollers. Works as an alternative to OpenOCD (supports SWD/JTAG).probe-rs-cli
β allows flashing a binary and reading memory via a USB debugger.cargo-embed
β Combines assembly, flashing viaprobe-rs
, logging viaRTT
and connecting toGDB
. (cargo embed --chip STM32F103
will upload the program to the board and start outputting in the console.)probe-rs-tools
π¬ Debuggers
GDB
: General-purpose debugger for examining program state, setting breakpoints/checkpoints, and inspecting memory and registers.
Can connect to built-in targets viaOpenOCD
orprobe-rs
.
Supports Rust-specific debugging (pretty printing, IDE integration).
βββ π Logging
defmt
-
[defmt]
(https://crates.io/crates/defmt) (deferred formatting): an efficient logging framework for embedded systems:- Formats logs on the host, passing only indexes and values ββfrom the device.
- Integrates with GDB, suitable for real-time debugging.
- Part of the Knurling-rs project.
-
[defmt-rtt]
(https://crates.io/crates/defmt-rtt) β transport layer fordefmt
:- Uses RTT (Real-Time Transfer) for fast log transfer.
- Uses macros (
defmt::info!
,defmt::error!
) with lazy formatting. - Minimizes the load on MCU, saving useful data for analysis on the host side.
- This approach provides compact and fast debug messages without performance losses.
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.
- no built-in formatting on the host,
- higher load on the MCU,
- strings are transmitted as is - takes up more memory and traffic.
[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
- Memory inspection via
GDB
: access to variables, registers and memory areas (print,x/10x 0x...
). Memory is defined in memory.x. - Linker errors: check that
memory.x
matches the chip configuration. - Reading registers: include .svd to correctly display peripherals.
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
- Write code β add
rprintln!()
for logs. cargo build
βcargo flash --chip ...
- Run the server
probe-rs run --gdb
in the background. - Connect VS Code/GDB β set breakpoints β run.
- Analyze variables/logs β fix error β repeat.
- 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. |
- Automate builds in VS through
tasks.json
.
// .vscode/launch.json
{
"type": "cortex-debug",
"servertype": "probe-rs",
"executable": "target/.../app",
"request": "launch",
"device": "STM32F411CEUx"
}
Key Points
- RTT instead of UART: No extra pins needed, works at any speed.
- probe-rs > OpenOCD: Easier installation, better integration with Rust.
- defmt for complex projects: Compressed logs, formatting structures.
- SVD files: Automate register viewing in the IDE.