01 The debug chain — why OpenOCD + GDB
OpenOCD is a translator: it turns the Arm SWD/JTAG wire protocol that an ST-Link speaks into a plain TCP GDB remote server, so any arm-none-eabi-gdb can flash and single-step your STM32 — from a terminal, a Makefile, or CI.
The STM32L4R5 (Arm Cortex-M4F, RM0432, DS12023) exposes a 2-pin SWD debug port. You cannot talk to that port directly from a PC; you need a debug probe (an ST-Link/V2 or V3, which is soldered onto every Nucleo/Discovery board) plus host software that drives the probe. That host software is OpenOCD.
Host (your PC) Debug probe Target MCU
┌───────────────────┐ TCP :3333 (GDB remote) ┌───────────┐ SWD ┌──────────────┐
│ arm-none-eabi-gdb │◄─────────────────────────►│ OpenOCD │◄──USB──►│ ST-Link │◄─┤ STM32L4R5 │
│ (ELF + symbols) │ TCP :4444 (telnet CLI) │ (server) │ │ V2 / V3 │ │ Cortex-M4F │
└───────────────────┘ TCP :6666 (Tcl RPC) └───────────┘ └───────────┘ └──────────────┘
"the client" "the adapter" SWDIO=PA13 / SWCLK=PA14
OpenOCD and GDB are two separate processes that talk over a socket. You run OpenOCD once and leave it running; GDB attaches and detaches freely. Because the interface is just TCP port 3333, the exact same setup drives VS Code (cortex-debug), Eclipse, or a headless CI runner.
| OpenOCD service | Default port | Used for |
|---|---|---|
| GDB remote | 3333 | target extended-remote :3333 from arm-none-eabi-gdb |
| Telnet monitor | 4444 | Interactive OpenOCD command shell (reset halt, flash …) |
| Tcl RPC | 6666 | Scripting / automation (binary command protocol) |
02 Install, invoke & the port model
OpenOCD is configured by layering config files: first the interface (which probe), then the target (which chip). The two-file invocation -f interface/stlink.cfg -f target/stm32l4x.cfg is the canonical STM32 command line.
# Debian / Ubuntu (distro build; fine for STM32L4)
sudo apt install openocd gdb-multiarch gcc-arm-none-eabi
# macOS
brew install open-ocd arm-none-eabi-gdb arm-none-eabi-gcc
# verify — need OpenOCD 0.11+ for ST-Link V3 and modern L4+ flash
openocd --version
arm-none-eabi-gdb --version
Config-file layering. OpenOCD ships hundreds of .cfg scripts under its data dir (/usr/share/openocd/scripts). -f means "find & source this file". Order matters: the interface must be declared before the target, because the target script calls transport select against the already-selected adapter.
# interface first (the probe), then target (the chip)
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg
# stm32l4x.cfg covers the WHOLE L4 / L4+ family incl. the STM32L4R5.
# The flash driver auto-probes size + bank layout over SWD.
#
# Expected startup log:
# Info : STLINK V3J13M4 (API v3) VID:PID 0483:374E
# Info : Target voltage: 3.285000
# Info : stm32l4x.cpu: Cortex-M4 r0p1 processor detected
# Info : starting gdb server for stm32l4x.cpu on 3333
# Info : Listening on port 3333 for gdb connections
# Leave this running — it is now your GDB server. Ctrl-C to stop.
Since OpenOCD 0.11 the ST-Link script simply runs adapter driver stlink and defaults the transport to hla_swd (ST-Link's high-level adapter layer). Both ST-Link/V2 and V3 are auto-detected by USB VID:PID (0483:3748 / 0483:374E/374F). The older interface stlink command still works but prints a deprecation warning; prefer adapter driver stlink in hand-written configs.
Changing / binding the ports
# -c runs an OpenOCD command; use it to set ports before init
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg \
-c "gdb_port 3333" \
-c "telnet_port 4444" \
-c "tcl_port 6666"
# expose the gdb server to other machines (default binds to localhost only)
-c "bindto 0.0.0.0"
# headless flashing only — no gdb server, no telnet
-c "gdb_port disabled" -c "telnet_port disabled"
USB permissions (Linux)
# Symptom without this: "Error: libusb_open() failed ... LIBUSB_ERROR_ACCESS"
sudo cp /usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
# add yourself to plugdev, then LOG OUT / IN (or replug the board)
sudo usermod -aG plugdev $USER
03 The STM32L4R5 target — memory, flash & SWD pins
To flash and debug correctly you need three facts about the STM32L4R5: its memory map (where flash and RAM live), its flash bank layout (single vs dual bank via the DBANK option byte), and its SWD pins (which you must not repurpose as GPIO).
Memory map (RM0432 / DS12023)
| Region | Base address | Size | Notes |
|---|---|---|---|
| Main flash | 0x0800 0000 | 2 MB | Code + const. Reset vector fetched here (after boot remap). |
| System memory | 0x1FFF 0000 | ~28 KB | ST factory ROM bootloader (DFU/UART). |
| OTP | 0x1FFF 7000 | 512 B | One-time-programmable. |
| Option bytes | 0x1FFF 7800 | — | RDP, DBANK, BOR level, watchdog, WRP. |
| SRAM1 | 0x2000 0000 | 192 KB | Start of the 640 KB contiguous SRAM. |
| SRAM2 | 0x2003 0000 | 64 KB | Contiguous after SRAM1; also aliased at 0x1000 0000. |
| SRAM3 | 0x2004 0000 | 384 KB | Ends at 0x2009 FFFF → top of RAM = 0x200A 0000. |
| Peripherals | 0x4000 0000 | — | APB1/APB2/AHB1/AHB2 register blocks. |
Flash bank layout — the DBANK option byte
The 2 MB flash is organised by the DBANK option byte (in FLASH_OPTR, bit 22 on L4+). You rarely change it, but it decides page size — which matters when you erase individual pages. OpenOCD's stm32l4x driver reads the option byte and the FLASH_SIZE register and configures the banks automatically.
| DBANK | Layout | Page (erase) size | Read width |
|---|---|---|---|
0 | Single bank — 2 MB contiguous, 256 pages | 8 KB | 128-bit |
1 | Dual bank — 2 × 1 MB (bank 1 @ 0x0800 0000, bank 2 @ 0x0810 0000) | 4 KB | 64-bit |
The stock target/stm32l4x.cfg declares the flash bank with size 0 (flash bank $_FLASHNAME stm32l4x 0x08000000 0 0 0 $_TARGETNAME). Size 0 means "probe it": on init OpenOCD reads the die's flash size and DBANK and sizes the bank itself. Run monitor flash probe 0 to see what it detected. Do not hard-code a bank size for the L4R5.
SWD / trace pins — do not reuse these
| Signal | Pin | Alternate function | Role |
|---|---|---|---|
| SWDIO | PA13 | SYS_JTMS-SWDIO (default at reset) | Bidirectional debug data |
| SWCLK | PA14 | SYS_JTCK-SWCLK (default at reset) | Debug clock |
| SWO | PB3 | SYS_JTDO-SWO / TRACESWO (AF0) | Single-wire ITM/SWO trace (optional) |
| NRST | NRST | — | Hardware reset (SRST) |
PA13/PA14 boot as SWD. If your firmware reconfigures them as GPIO or alternate function, you lose debug access after that code runs (see Gotchas). The Cortex-M4 core also fixes your breakpoint budget: 6 hardware breakpoints (FPB) and 4 watchpoints (DWT) — OpenOCD prints exactly this at connect.
04 A complete flashable firmware
You need something to flash and step through. Here is a full, self-contained, register-level blinky for the NUCLEO-L4R5ZI (LD1 green = PC7) — four files, no CMSIS, no HAL — that compiles with arm-none-eabi-gcc and lands at 0x08000000.
#include <stdint.h>
/* NUCLEO-L4R5ZI: LD1 (green) = PC7 */
#define RCC_AHB2ENR (*(volatile uint32_t *)0x4002104C) /* GPIO port clocks */
#define GPIOC_MODER (*(volatile uint32_t *)0x48000800) /* PC pin modes */
#define GPIOC_BSRR (*(volatile uint32_t *)0x48000818) /* PC atomic set/rst*/
#define LED 7u
static void delay(volatile uint32_t n) { while (n--) __asm__ volatile ("nop"); }
int main(void)
{
RCC_AHB2ENR |= (1u << 2); /* GPIOCEN: enable GPIOC clock */
GPIOC_MODER &= ~(3u << (LED * 2)); /* clear the 2 mode bits */
GPIOC_MODER |= (1u << (LED * 2)); /* 01 = general-purpose output */
for (;;) {
GPIOC_BSRR = (1u << LED); /* BS7: drive PC7 high → LED on */
delay(400000);
GPIOC_BSRR = (1u << (LED + 16)); /* BR7: drive PC7 low → LED off */
delay(400000);
}
}
#include <stdint.h>
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss, _estack;
int main(void);
void Reset_Handler(void)
{
uint32_t *s = &_sidata, *d = &_sdata;
while (d < &_edata) *d++ = *s++; /* copy .data flash → RAM */
for (d = &_sbss; d < &_ebss;) *d++ = 0; /* zero .bss */
main();
for (;;) { } /* main() never returns */
}
void Default_Handler(void) { for (;;) { } }
typedef void (*vector_t)(void);
/* First 16 core vectors are enough to boot and to debug. */
__attribute__((section(".isr_vector"), used))
const vector_t g_vectors[] = {
(vector_t)(&_estack), /* 0x00 initial MSP */
Reset_Handler, /* 0x04 reset */
Default_Handler, /* NMI */
Default_Handler, /* HardFault */
Default_Handler, /* MemManage */
Default_Handler, /* BusFault */
Default_Handler, /* UsageFault */
0, 0, 0, 0, /* reserved */
Default_Handler, /* SVCall */
Default_Handler, /* DebugMonitor */
0, /* reserved */
Default_Handler, /* PendSV */
Default_Handler, /* SysTick */
};
ENTRY(Reset_Handler)
_estack = 0x200A0000; /* top of 640 KB SRAM (0x20000000 + 640K) */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 640K
}
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : {
*(.text*)
*(.rodata*)
} > FLASH
_sidata = LOADADDR(.data);
.data : {
_sdata = .;
*(.data*)
_edata = .;
} > RAM AT > FLASH
.bss : {
_sbss = .;
*(.bss*)
*(COMMON)
_ebss = .;
} > RAM
}
# STM32L4R5 = Cortex-M4 with single-precision FPU → fpv4-sp-d16 / hard ABI
# -O0 -g3 gives 1:1 line stepping and full macro info for debugging
arm-none-eabi-gcc \
-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
-O0 -g3 -Wall -ffreestanding -nostdlib \
-T stm32l4r5.ld startup.c main.c -o blink.elf
# derive raw + Intel-HEX images from the ELF
arm-none-eabi-objcopy -O binary blink.elf blink.bin
arm-none-eabi-objcopy -O ihex blink.elf blink.hex
arm-none-eabi-size blink.elf
# text data bss dec hex filename
# ~90 0 0 90 5a blink.elf
05 Flashing from the CLI
To only program the chip (no debugging), let OpenOCD do everything in one shot with the program helper, then exit. This is the command you put in a Makefile flash: target.
# program <file> [verify] [reset] [exit] [offset]
# ELF/HEX carry their own load address → no offset needed
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg \
-c "program blink.elf verify reset exit"
# A raw .bin has NO address — you MUST pass the flash base 0x08000000
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg \
-c "program blink.bin verify reset exit 0x08000000"
The program keywords: verify reads flash back and compares, reset resets and runs the new image, exit shuts OpenOCD down when finished. It internally does init → reset halt → erase → write → verify → reset run → shutdown.
The explicit long form (what program expands to)
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg \
-c "init" \
-c "reset halt" \
-c "flash write_image erase blink.hex" \
-c "verify_image blink.hex" \
-c "reset run" \
-c "shutdown"
# flash write_image [erase] [unlock] <file> [offset] [type]
# erase → erase the sectors the image covers first (required!)
# unlock → also clear write protection if set
Interactive control over telnet (port 4444)
With an OpenOCD server already running (Section 02), open its command shell. Everything here is also reachable from GDB by prefixing with monitor.
telnet localhost 4444
# Open On-Chip Debugger
> reset halt # stop the core at the reset vector
> flash probe 0 # report detected flash: size, banks, page size
> flash write_image erase blink.hex # erase + program
> verify_image blink.hex # read back & compare
> mdw 0x08000000 4 # dump 4 words at the vector table
> reg # dump core registers (r0..pc, xPSR)
> reset run # run the freshly-flashed firmware
> exit # close the telnet session (server stays up)
reset halt = reset then stop at the very first instruction. reset init = reset, then run the target's reset-init hook (clock/PLL setup) and stop — use this before flashing at high speed. reset run = reset and let it run free. From GDB these are monitor reset halt, etc.
06 The GDB debug session
With OpenOCD running as the server, drive a full flash-and-step session from arm-none-eabi-gdb. The five load-bearing commands are target extended-remote :3333, monitor reset halt, load, break, and continue.
# Terminal A already runs: openocd -f interface/stlink.cfg -f target/stm32l4x.cfg
arm-none-eabi-gdb -q blink.elf
# connect to OpenOCD's gdb server. Use EXTENDED-remote so you can
# reset / rerun and stay attached after the program "exits".
(gdb) target extended-remote :3333
(gdb) monitor reset halt # halt CPU before touching flash
(gdb) load # write ELF to flash via OpenOCD's memory map
# Loading section .isr_vector, size 0x40 lma 0x8000000
# Loading section .text, size 0x5c lma 0x8000040
# Start address 0x08000041, load size 156
(gdb) monitor reset halt # reset so PC = the new reset vector
(gdb) break main # hardware breakpoint (flash) — 6 available
(gdb) continue # run to main()
(gdb) info registers # r0..r12, sp, lr, pc, xPSR
(gdb) next # step over one source line
(gdb) step # step into
(gdb) stepi # one machine instruction
(gdb) x/4xw 0x48000800 # examine GPIOC MODER..BSRR (4 words, hex)
(gdb) print/x $pc # current program counter
(gdb) watch *(uint32_t *)0x48000818 # DWT watchpoint on GPIOC_BSRR
(gdb) continue # stops when BSRR is written (LED toggles)
(gdb) monitor reset run # let it run free on the board
(gdb) detach # release the target, keep it running
(gdb) quit
load PROGRAMS FLASHOpenOCD advertises a memory map to GDB (gdb_memory_map enable, on by default). When load writes to an address inside a declared flash bank, GDB routes it through OpenOCD's flash driver, which erases the needed pages and programs them — you get real flashing, not just a RAM write. If you linked the program for RAM instead, load would simply drop it into RAM.
Non-interactive / scripted GDB
# one-shot: connect, flash, break at main, hand you the prompt
arm-none-eabi-gdb -q blink.elf \
-ex "target extended-remote :3333" \
-ex "monitor reset halt" \
-ex "load" \
-ex "monitor reset halt" \
-ex "break main" \
-ex "continue"
# or keep the recipe in a file and run: arm-none-eabi-gdb -x flash.gdb blink.elf
target extended-remote :3333
monitor reset halt
load
monitor reset halt
break main
continue
| GDB command | Effect |
|---|---|
target extended-remote :3333 | Attach to OpenOCD's gdb server (allows reset + rerun) |
monitor <cmd> | Send <cmd> straight to OpenOCD (e.g. monitor reset halt) |
load | Program the ELF's loadable sections to flash/RAM |
break / tbreak | Breakpoint / one-shot breakpoint (uses FPB hardware on flash) |
watch / rwatch | Data write / read watchpoint (uses DWT — max 4) |
continue (c) | Resume execution |
next (n) / step (s) | Step over / step into one source line |
stepi (si) / finish | One instruction / run until current function returns |
info registers | Dump core registers |
x/<n><f><u> addr | Examine memory, e.g. x/8xw 0x40021000 (RCC block) |
backtrace (bt) | Call stack |
07 Full flash-and-debug workflow
The daily loop is two terminals: OpenOCD in one (the server, left running), GDB in the other (edit → build → load → debug, repeat). A project-local openocd.cfg makes the server a bare openocd.
# ── Terminal A: the OpenOCD server (start once, leave running) ──
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg
# ... Listening on port 3333 for gdb connections
# ── Terminal B: edit → build → reflash → debug, over and over ──
make # rebuild blink.elf
arm-none-eabi-gdb -q blink.elf -ex "target extended-remote :3333"
(gdb) monitor reset halt
(gdb) load # reflash without restarting the server
(gdb) monitor reset halt
(gdb) break main
(gdb) continue
A project-local openocd.cfg
Drop this file next to your firmware. Running openocd with no arguments auto-loads ./openocd.cfg, so the server command becomes a single word.
# run simply as: openocd
source [find interface/stlink.cfg]
transport select hla_swd # ST-Link high-level SWD (default)
source [find target/stm32l4x.cfg]
adapter speed 1800 # kHz; conservative for the on-board ST-Link
reset_config srst_only srst_nogate # use NRST, keep it released between resets
# Optional: flash + halt at main automatically when this cfg is sourced.
# init
# reset halt
# program blink.elf verify
Semihosting — printf() to the GDB console
(gdb) monitor arm semihosting enable
# Now newlib's write()/printf() in firmware appears in this GDB console.
# Requires linking with a semihosting stub (e.g. --specs=rdimon.specs).
For non-halting printf-style tracing, the STM32L4R5 streams ITM data out the SWO pin (PB3). Configure OpenOCD's TPIU with the tpiu command object, matching -traceclk to your real HCLK (up to 120 MHz on the L4R5). Firmware writes bytes to ITM->PORT[0]; OpenOCD decodes them to a file or a TCP port. This needs no CPU halt, unlike breakpoints.
08 Gotchas, common mistakes & quick reference
The failures below account for the vast majority of "OpenOCD won't connect / won't flash" reports. Most are configuration-order, permission, or reset issues — not hardware faults.
-f interface/… BEFORE -f target/…. The target script selects the transport against an already-declared adapter; reversed order errors with "session transport was not selected".target remote localhost:4444 attaches to the telnet shell and hangs. Telnet is a human CLI; 3333 is the GDB protocol.target remote drops the connection when the program "exits" and forbids run/restart. Use target extended-remote :3333 so you can monitor reset and reflash without relaunching GDB.monitor reset halt before load, and again after (so PC points at the new reset vector).60-openocd.rules, add yourself to plugdev, replug (Section 02). Running as root is a smell, not a fix.break on flash fails: "Cannot insert breakpoint / hardware breakpoints used exceeds limit". Delete some or move code to RAM (soft breakpoints).program blink.bin … without a trailing 0x08000000 writes to address 0 and silently mis-flashes. ELF/HEX carry their address; raw BIN never does.adapter speed (try 480, then 1000, 1800). The stm32l4x cfg starts at 500 kHz and boosts to 4 MHz only after reset-init.printf hit a BKPT and hang the MCU. Only enable it under an active GDB/OpenOCD session.Recovering a locked / bricked L4R5 (connect under reset)
# Board runs code that kills SWD, or is RDP level 1 → hold in reset, unlock, wipe.
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg \
-c "reset_config connect_assert_srst" \
-c "init" \
-c "reset halt" \
-c "stm32l4x unlock 0" \
-c "stm32l4x mass_erase 0" \
-c "reset run" \
-c "shutdown"
# stm32l4x unlock 0 sets RDP to 0xAA (level 0); the option-byte change
# triggers a full mass erase, clearing whatever bricked the part.
Quick reference — monitor / OpenOCD commands
Command (telnet, or monitor … in GDB) | Effect |
|---|---|
reset halt / reset init / reset run | Reset + stop / reset + run reset-init hook / reset + run free |
halt / resume | Stop the core / continue the core |
program <file> verify reset exit [addr] | One-shot: erase, program, verify, reset, quit |
flash write_image erase <file> [addr] | Erase covered sectors, then program the image |
flash probe 0 | Detect flash size, bank layout, page size |
flash erase_sector 0 0 last | Erase every sector of bank 0 |
mdw / mdh / mdb addr [count] | Memory display: word / half / byte |
mww addr value | Memory write word |
reg | Dump core registers |
arm semihosting enable | Route firmware stdout to the GDB console |
stm32l4x unlock 0 / stm32l4x mass_erase 0 | Clear RDP / wipe all flash (recovery) |
shutdown | Stop the OpenOCD server |
The whole flow in six lines
- Server:
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg - Flash only: append
-c "program blink.elf verify reset exit" - Debug:
arm-none-eabi-gdb -q blink.elf target extended-remote :3333→monitor reset halt→loadmonitor reset halt→break main→continue- Unbrick: connect-under-reset +
stm32l4x unlock 0+stm32l4x mass_erase 0