Skip to content

Debugging STM32 firmware — bare-metal, Zephyr and FreeRTOS

gdbforge is a Vim-inspired GDB terminal UI for STM32 bare-metal, Zephyr, and FreeRTOS development. Lua scripts under lua/stm32/ spawn ST-Link + OpenOCD or J-Link GDB Server over SWD, attach GDB, and stop at main — the ST-Link scripts with monitor reset halt, the J-Link script after load.

Demo — Nucleo F429ZI (Zephyr)

Nucleo-F429ZI / ST-Link + OpenOCD — first, bare-metal debug of a Zephyr app on the Nucleo board (:lua nucleo_f429zi baremetal); then switch to Zephyr-aware debug (:lua nucleo_f429zi zephyr) with info threads and on-board displays. Watch on YouTube.

STM32 Nucleo F429ZI debug demo — bare metal then Zephyr-aware

Prerequisites

Need Why Check
gdbforge installed — gdbforge -version (install)
arm-none-eabi-gdb (or the Zephyr SDK's arm-zephyr-eabi-gdb) Cortex-M is 32-bit ARM; the scripts run set architecture arm. The host gdb cannot debug it arm-none-eabi-gdb --version
OpenOCD ≥ 0.12 (ST-Link boards) or J-Link software Serves GDB over SWD openocd --version
ST-Link / J-Link probe, plus udev rules on Linux Probe access without sudo lsusb shows the probe
Firmware ELF built with -g Source-level debugging needs DWARF file build/zephyr/zephyr.elf
ZEPHYR_BASE exported (Zephyr only) The scripts dir the Zephyr and app sources so stepping shows code echo $ZEPHYR_BASE
(optional) a clone of the gdbforge repo The lua/stm32/ board scripts already ship inside the binary — clone only to edit them locally (precedence) —

Point gdbforge at the cross GDB with -d:

gdbforge -d arm-none-eabi-gdb ./build/zephyr/zephyr.elf

Getting started (order of operations)

Follow these steps once, then repeat from step 5 for each debug session.

  1. Host tools — install OpenOCD (ST-Link boards) and/or J-Link software (board 2 J-Link path). See OpenOCD version and install below.
  2. USB / udev — plug in the board; ensure ST-Link is visible (lsusb). On Linux, install OpenOCD udev rules if permission denied.
  3. gdbforge — build or install gdbforge; verify openocd --version on PATH.
  4. Board scripts — nothing to do: the board scripts ship inside the binary. Copy a folder into your project only if you want to edit it, since project-local scripts win over the embedded catalog (see Lua API):
mkdir -p .gdbforge/lua
cp -r /path/to/gdbforge/lua/stm32/nucleo_f429zi .gdbforge/lua/   # board 1
cp -r /path/to/gdbforge/lua/stm32/stm32f405 .gdbforge/lua/       # board 2
  1. Build firmware — Zephyr west build, Makefile, CMake, etc.; note path to zephyr.elf / firmware.elf.
  2. Environment — export board-specific vars (Zephyr ZEPHYR_BASE, OpenOCD cfg paths — see board sections below).
  3. Launch gdbforge with your ELF from the build directory, e.g. gdbforge ./zephyr/zephyr.elf
  4. Connect probe — :lua nucleo_f429zi (or stm32f405_stlink / stm32f405_jlink).
  5. Debug — you are stopped at main; see To your first breakpoint below.

OpenOCD is started detached (like kdmx): the :lua job finishes and Ctrl-C no longer tears down OpenOCD. It is stopped when you run :lua nucleo_f429zi again or when gdbforge exits.

To your first breakpoint

Unlike the MPSoC scripts, the STM32 scripts do continue for you: the ST-Link flow ends in break main + continue, so when :lua nucleo_f429zi returns, the core is already stopped at main and ━━▶ marks the program counter in the Code pane.

  1. Move the cursor to any line in the Code pane and press Space to toggle a breakpoint there.
  2. Press c to continue, n to step over, s to step into, f to finish the frame. The Call Stack, Threads and Breakpoints panes refresh at every stop; for variables use print / info locals in :b gdb.
  3. For Zephyr thread awareness, use the zephyr profile (:lua nucleo_f429zi zephyr) and run info threads in :b gdb.
  4. :b gdb is the real GDB console (info registers, x/16x $sp, monitor reset halt, …); the OpenOCD log is at /tmp/gdbforge-openocd.log (or GDBFORGE_OPENOCD_LOG).

If it did not stop at main, the usual causes are a probe that never enumerated (check lsusb and the OpenOCD log), a stale OpenOCD holding port 3333, or an ELF that does not match what is flashed. Each script also defines help() listing its environment variables.


OpenOCD version and install

ST-Link scripts (nucleo_f429zi, stm32f405_stlink) run openocd on the host. gdbforge does not bundle OpenOCD.

Version

Tested with OpenOCD 0.12.0 (current stable; works with Nucleo F429ZI + Zephyr -rtos Zephyr)
Minimum 0.12.0 recommended; Zephyr thread awareness (info threads) needs > 0.11.0
Check openocd --version → should print Open On-Chip Debugger 0.12.x

Scripts path (for [find board/…] in cfg): usually /usr/share/openocd/scripts on Linux.

Download & install

Official project

Linux package managers (fastest if version ≥ 0.12)

# Debian / Ubuntu
sudo apt update && sudo apt install openocd

# Fedora
sudo dnf install openocd

# Gentoo
sudo emerge -av openocd

Zephyr SDK (includes OpenOCD + scripts + udev rules)

sudo cp $ZEPHYR_SDK_INSTALL_DIR/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d/
sudo udevadm control --reload

Point gdbforge at SDK OpenOCD if not on PATH:

export GDBFORGE_OPENOCD=$ZEPHYR_SDK_INSTALL_DIR/sysroots/x86_64-pokysdk-linux/usr/bin/openocd

Build from source (latest git)

git clone https://github.com/openocd-org/openocd.git
cd openocd
./bootstrap && ./configure --enable-stlink && make -j$(nproc)
sudo make install

Override binary (any install location):

export GDBFORGE_OPENOCD=/usr/bin/openocd    # default: openocd on PATH

Board catalog (extensible)

Scripts are grouped by board folder: lua/stm32/<board>/. The list below is the first two entries; new boards are added as new folders (same layout). Board number is documentation order only — not a version or priority flag.

# Board folder MCU / kit :lua scripts Probe
1 nucleo_f429zi/ STM32F429ZI (Nucleo-F429ZI) nucleo_f429zi, nucleo_f429zi_stlink On-board ST-Link + OpenOCD
2 stm32f405/ STM32F405 stm32f405_stlink, stm32f405_jlink ST-Link + OpenOCD or J-Link SWD
3+ <board>/ (future) <board>_stlink, … per board

Adding board #3: create lua/stm32/<board>/ with at least one *.lua (:lua command = basename) and optional *_openocd.cfg. Copy nucleo_f429zi/ for ST-Link + Zephyr/OpenOCD, or stm32f405/ for a generic F4 + J-Link variant. Update this table and lua/stm32/README.md.

Install scripts into the project (no gdbforge rebuild):

mkdir -p .gdbforge/lua
cp -r lua/stm32/nucleo_f429zi .gdbforge/lua/   # board 1
cp -r lua/stm32/stm32f405 .gdbforge/lua/       # board 2

Typical Zephyr app on Nucleo-F429ZI.

Command line

Example (west build dir = project folder; ELF at zephyr/zephyr.elf):

cd ~/alyn/alyn/game-controller/nucleo_f429zi
mkdir -p .gdbforge/lua
cp -r /path/to/gdbforge/lua/stm32/nucleo_f429zi .gdbforge/lua/

export ZEPHYR_BASE=~/alyn/zephyr-3.4.99/zephyr

gdbforge ./zephyr/zephyr.elf

Inside gdbforge:

:lua nucleo_f429zi zephyr

Only ZEPHYR_BASE is required for Zephyr. OpenOCD board cfg and GDB app dir are derived from ZEPHYR_BASE and your $PWD. Profile zephyr enables OpenOCD -rtos Zephyr (needs CONFIG_DEBUG_THREAD_INFO=y). Stops at main. Check threads: info threads in :b gdb.

For bare metal (no thread awareness): :lua nucleo_f429zi baremetal — ignores ZEPHYR_BASE for RTOS.

OpenOCD (background) → wait port 3333 → dir Zephyr paths → target remote → break main → continue (stops at main).

Optional env: GDBFORGE_OPENOCD, GDBFORGE_OPENOCD_PORT (default 3333).


Same ST-Link / OpenOCD pattern as board 1.

mkdir -p .gdbforge/lua
cp -r lua/stm32/stm32f405 .gdbforge/lua/
export GDBFORGE_OPENOCD_SCRIPTS=/usr/share/openocd/scripts   # if bundled cfg [find …] fails
gdbforge ./build/firmware.elf
:lua stm32f405_stlink

Then :b gdb: continue to run (already loaded and stopped at main if script finished OK).


cp -r lua/stm32/stm32f405 .gdbforge/lua/
export GDBFORGE_JLINK=/opt/JLink_Linux_V914a_x86_64/JLinkGDBServer
gdbforge ./build/firmware.elf
:lua stm32f405_jlink

Spawns J-Link, target remote, load, break main.


Script catalog (all boards)

# Board :lua Probe Purpose
1 Nucleo F429ZI nucleo_f429zi ST-Link + OpenOCD Bare-metal, Zephyr, or FreeRTOS profile
1 nucleo_f429zi_stlink (alias) same
2 STM32F405 stm32f405_stlink ST-Link + OpenOCD Bare-metal, Zephyr, or FreeRTOS profile
2 stm32f405_jlink J-Link SWD J-Link + load + break main

Zephyr awareness (GDB, not gdbforge core)

gdbforge does not embed Zephyr-specific logic. Use standard GDB setup:

Need How
Source paths export ZEPHYR_BASE=~/alyn/zephyr-3.4.99/zephyr then :lua … zephyr — script runs dir $ZEPHYR_BASE and dir $PWD
Cross-GDB gdbforge -d …/arm-zephyr-eabi-gdb -- ./zephyr/zephyr.elf
OpenOCD board cfg derived from $ZEPHYR_BASE/boards/arm/<board>/support/openocd.cfg
info threads (Zephyr RTOS) CONFIG_DEBUG_THREAD_INFO=y and :lua … zephyr and ZEPHYR_BASE set

FreeRTOS awareness

The ST-Link scripts accept a freertos profile alongside baremetal and zephyr. It configures the OpenOCD target with -rtos FreeRTOS and then runs the same attach sequence as baremetal:

:lua nucleo_f429zi freertos
:lua stm32f405_stlink freertos

Unlike zephyr, this profile needs no ZEPHYR_BASE and adds no GDB source directories; OpenOCD scripts are taken from the bundled board cfg plus the system scripts directory (GDBFORGE_OPENOCD_SCRIPTS if it is elsewhere). Task awareness comes from OpenOCD's FreeRTOS support and requires the usual kernel symbols in the ELF — when it works, tasks appear as GDB threads in info threads and in the Threads pane.


Variable Default Notes
GDBFORGE_OPENOCD openocd see OpenOCD version and install
GDBFORGE_OPENOCD_PORT 3333
ZEPHYR_BASE (unset) Required for profile zephyr — kernel tree; OpenOCD cfg derived automatically
GDBFORGE_STM32_BOARD (unset) Default board for :lua stm32_stlink when only profile is passed

J-Link (stm32f405_jlink only): GDBFORGE_JLINK, GDBFORGE_JLINK_DEVICE (STM32F405RG), GDBFORGE_JLINK_PORT (2334).

See also: Lua catalog — STM32 · User Guide — Lua · FAQ — choosing a profile