All guides
TECHNICAL GUIDESTM32 DEBUGOPENOCD2026

OpenOCD + GDB
Flash & Debug STM32 from the CLI

Program and step-debug an STM32L4R5 over SWD with nothing but openocd, arm-none-eabi-gdb, and an ST-Link — no IDE required.

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
OpenOCDDaemon. Opens the USB probe, drives SWD, exposes a GDB server (3333), a telnet command shell (4444) and a Tcl RPC port (6666). Owns the flash-programming algorithms.
arm-none-eabi-gdbThe debugger front-end. Holds your ELF and its symbols, connects to OpenOCD over TCP, sets breakpoints, and reads/writes registers and memory.
ST-LinkThe probe. On a Nucleo/Discovery it is the second chip; a stand-alone dongle for custom boards. Speaks USB on one side, SWD on the other.
SWDSerial Wire Debug — 2 wires (SWDIO + SWCLK) + NRST + GND. The on-chip debug access port (DAP) reaches the Cortex-M4 core and the whole memory bus.
KEY IDEA

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 serviceDefault portUsed for
GDB remote3333target extended-remote :3333 from arm-none-eabi-gdb
Telnet monitor4444Interactive OpenOCD command shell (reset halt, flash …)
Tcl RPC6666Scripting / 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.

bash — install the toolchain + OpenOCD
# 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.

bash — the canonical STM32 invocation
# 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.
NOTE — interface/stlink.cfg

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

bash — override the default 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)

bash — udev rules so the ST-Link works without root
# 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)

RegionBase addressSizeNotes
Main flash0x0800 00002 MBCode + const. Reset vector fetched here (after boot remap).
System memory0x1FFF 0000~28 KBST factory ROM bootloader (DFU/UART).
OTP0x1FFF 7000512 BOne-time-programmable.
Option bytes0x1FFF 7800RDP, DBANK, BOR level, watchdog, WRP.
SRAM10x2000 0000192 KBStart of the 640 KB contiguous SRAM.
SRAM20x2003 000064 KBContiguous after SRAM1; also aliased at 0x1000 0000.
SRAM30x2004 0000384 KBEnds at 0x2009 FFFF → top of RAM = 0x200A 0000.
Peripherals0x4000 0000APB1/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.

DBANKLayoutPage (erase) sizeRead width
0Single bank — 2 MB contiguous, 256 pages8 KB128-bit
1Dual bank — 2 × 1 MB (bank 1 @ 0x0800 0000, bank 2 @ 0x0810 0000)4 KB64-bit
TIP — let OpenOCD auto-probe

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

SignalPinAlternate functionRole
SWDIOPA13SYS_JTMS-SWDIO (default at reset)Bidirectional debug data
SWCLKPA14SYS_JTCK-SWCLK (default at reset)Debug clock
SWOPB3SYS_JTDO-SWO / TRACESWO (AF0)Single-wire ITM/SWO trace (optional)
NRSTNRSTHardware 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.

main.c — register-level GPIO blink (PC7)
#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);
    }
}
startup.c — minimal vector table + reset handler
#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           */
};
stm32l4r5.ld — linker script (2 MB flash, 640 KB RAM)
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
}
bash — build for Cortex-M4F, keep debug symbols
# 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.

bash — one-liner flash (the everyday command)
# 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)

bash — flash write_image, step by step
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.

bash — telnet monitor shell
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 FLAVORS

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.

gdb — an interactive session, start to finish
# 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
WHY load PROGRAMS FLASH

OpenOCD 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

bash + gdb — batch flash-and-break with -ex, or a command file
# 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
flash.gdb — reusable GDB command file
target extended-remote :3333
monitor reset halt
load
monitor reset halt
break main
continue
GDB commandEffect
target extended-remote :3333Attach to OpenOCD's gdb server (allows reset + rerun)
monitor <cmd>Send <cmd> straight to OpenOCD (e.g. monitor reset halt)
loadProgram the ELF's loadable sections to flash/RAM
break / tbreakBreakpoint / one-shot breakpoint (uses FPB hardware on flash)
watch / rwatchData write / read watchpoint (uses DWT — max 4)
continue (c)Resume execution
next (n) / step (s)Step over / step into one source line
stepi (si) / finishOne instruction / run until current function returns
info registersDump core registers
x/<n><f><u> addrExamine 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.

bash — two-terminal workflow
# ── 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.

openocd.cfg — project config for NUCLEO-L4R5ZI
# 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 — route firmware stdout into GDB, no UART needed
(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).
OPTIONAL — SWO/ITM trace

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.

Wrong -f orderAlways -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".
Port confusionGDB connects to 3333, not 4444. target remote localhost:4444 attaches to the telnet shell and hangs. Telnet is a human CLI; 3333 is the GDB protocol.
remote vs extended-remotePlain 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.
load without haltFlashing while the core runs can corrupt the write. Always monitor reset halt before load, and again after (so PC points at the new reset vector).
LIBUSB_ERROR_ACCESSMissing udev rules → "libusb_open() failed". Install 60-openocd.rules, add yourself to plugdev, replug (Section 02). Running as root is a smell, not a fix.
More than 6 breakpointsCortex-M4 FPB gives 6 hardware breakpoints on flash and 4 DWT watchpoints. The 7th break on flash fails: "Cannot insert breakpoint / hardware breakpoints used exceeds limit". Delete some or move code to RAM (soft breakpoints).
.bin with no addressprogram blink.bin … without a trailing 0x08000000 writes to address 0 and silently mis-flashes. ELF/HEX carry their address; raw BIN never does.
Killed SWD pinsIf firmware reconfigures PA13/PA14 (or calls a "disable JTAG/SWD" remap), the next connect fails. Recover with connect-under-reset: hold the core in reset while attaching so your code never runs.
Read-out protection (RDP)RDP level 1 blocks debug + flash read. Recover by dropping RDP to level 0, which forces a mass erase (see below). At RDP level 2 the debug port is permanently disabled — no recovery.
Adapter speed too high"Error: init mode failed (unable to connect to the target)" on long wires / noisy boards → lower adapter speed (try 480, then 1000, 1800). The stm32l4x cfg starts at 500 kHz and boosts to 4 MHz only after reset-init.
Semihosting hangEnabling semihosting in firmware but running with NO debugger attached makes the first printf hit a BKPT and hang the MCU. Only enable it under an active GDB/OpenOCD session.
Stale ST-Link firmwareVery old ST-Link firmware isn't recognized ("unknown adapter"). Update it with ST's STLinkUpgrade tool, then OpenOCD 0.11+ detects V2/V3 fine.

Recovering a locked / bricked L4R5 (connect under reset)

bash — force RDP→0 + mass erase to unbrick
# 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 runReset + stop / reset + run reset-init hook / reset + run free
halt / resumeStop 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 0Detect flash size, bank layout, page size
flash erase_sector 0 0 lastErase every sector of bank 0
mdw / mdh / mdb addr [count]Memory display: word / half / byte
mww addr valueMemory write word
regDump core registers
arm semihosting enableRoute firmware stdout to the GDB console
stm32l4x unlock 0 / stm32l4x mass_erase 0Clear RDP / wipe all flash (recovery)
shutdownStop 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 :3333monitor reset haltload
  • monitor reset haltbreak maincontinue
  • Unbrick: connect-under-reset + stm32l4x unlock 0 + stm32l4x mass_erase 0