Embedded Debugging

Continuing the saga on embedded development, this post goes over common ways to debug bare metal ARM, illustrating the technique on blue pill (aka stm32f103c8t6) and most common debugger probes.

The debugger itself is a physical piece of hardware that lives inside the processor and can take control of the cpu and memories. When it comes to ARM, there are two protocols available to interface the on chip debugger – JTAG and Serial Wire Debug or swd (more about the differences here). In this tutorial we will focus on swd, which our blue pill has a pinout for. Apart from the pin count the two interfaces are very similar.

1. Probes

To use the available swd (or jtag) interface you would need a debugger probe and a driver that understands how to talk both to the probe and to the gnu debugger instance (further referred as gdb). Let’s call this driver connector program a gdbserver. This tutorial shines some light on using Open On Chip Debugger or openocd as a gdbserver and its alternatives.

To be clear gdbserver and gdb are different programs:

  • gdbserver – the driver program that employs the debugger probe, communicates to the outer world via tcp.
  • gdb – the client program that loads the executable and interacts with the user (i.e. set breakpoints).

1.1 Raspberry Pi

Any raspberry pi on its own can serve as a debugger probe thanks to the spi driver for openocd developed by lupyuen. This is amazing because there is no need for external debugger probe, making embedded development more accessible to broke students like myself. Chances are you have a raspberry pi collecting dust and you don’t have a dedicated debugger hardware (like jlink or stlink). Important to notice, because gdbserver talks to the gdb instance via tcp, you can sit back and enjoy the development process (both building and debugging) from an external computer on the network, that is you don’t have to develop code on the raspberry pi itself.

An increasingly common probe. stlinks are being included with almost every st’s official evaluation board, where besides being a debugger they are also used as a usb to serial converter. As you may have noticed, blue pill does not include an stlink, the on board micro usb receptacle is connected directly to the stm32f103’s on chip usb peripheral.

An external stlink-v3 mini is a tiny, inexpensive and yet powerful debugger. In addition to swd and jtag, it is capable of usb to spi, i2c, can, and gpio interface via a public c++ api stlink-v3-bridge.

The main disadvantage of stlink is that there is no straight forward way to run a compatible gdbserver. For some reason STMicroelectronics does not distribute a gdbserver for stlink as an executable binary as of today, even though gdbserver binaries are included with the STMCube IDE and with little effort can be scrapped from the IDE package (see 2.2.4). From a conversation with an ST developer, turns out their gdbserver is a modified version of openocd publicly developed here and in future might be merged with the original openocd branch.

A royal plug and play probe with official gdbserver binaries available for a wide variety of architectures including raspberry pi. The best choice if you do not have time to grasp openocd and have funds to get the probe.

2. gdbserver

Pick your probe and see what are the options of getting a gdbserver running.

2.1. openocd + Raspberry Pi

Openocd must be built from source to enable swd over spi capability on any raspberry pi. A glance at history; openocd capabilities to make use of the raspberry pi’s spi port were developed in this fork, which to the day of writing this post had not been merged into the original opencod repo. The author of the spi tweak published this article on how to get the hack working with Rust and nrf52, beware this only works on a 32 bit raspbian. With appearance of 64 bit Ubuntu for raspberry pi, a pull request added aarch64 support. The latter is the particular version of openocd that I tested on raspberry pi, it works both on aarch32 raspbian and aarch64 ubuntu.

sudo apt install autoconf libtool libusb-1.0-0 libusb-1.0-0-dev
git clone --recursive
cd openocd-spi
./configure --enable-bcm2835spi

Feel free to grab some coffee, the build process can take a while. Upon success you will find an openocd binary in the src/ folder.

Raspbery pi’s spi is disabled by default. Enable spi by writing dtparam=spi=on to /boot/config.txt in Raspbian or /boot/firmware/usercfg.txt in Ubuntu. Reboot. Then, enable read write access to spi port.

sudo chmod 606 /dev/spidev0.0

Create an openocd configuration file.

# file: swd-pi.ocd
# OpenOCD script for using Raspberry Pi as SWD Programmer for stm32f1x

# Select the Broadcom SPI interface for Raspberry Pi (SWD transport)
interface bcm2835spi

# Set the SPI speed in kHz
bcm2835spi_speed 31200  # 31.2 MHz

# Select stm32f1xx as target
source [find target/stm32f1x.cfg]

# Open gdbserver to the network.

Connect the blue pill.

blue pill raspi
swdio pin 19
swdclk pin 23
gnd gnd

Power up the blue pill. Despite being very convenient, I would not recommend powering the pill from pi’s 3.3V line as if you short something on the pill, raspberry will die. If you supply the power through micro usb or 5V line you will likely to kill the pill’s linear voltage regulator in case something gets shorted.

Run openocd. Argument -s ../tcl/ is a path to the tlc folder where openocd looks for configuration files; argument -f swd-pi.ocd points to the session specific configuration we just created.

# From openocd-spi/src
./openocd -s ~/projects/openocd-spi/tcl/ -f swd-pi.ocd

Upon success you shall see an output containing

Info : BCM2835 SPI SWD driver
Info : SWD only mode enabled
Info : clock speed 31200 kHz
Info : SWD DPIDR 0x1ba01477
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : Listening on port 3333 for gdb connections

In case you see

Info : SWD DPIDR 0x0000ffff

openocd failed to connect to the microcontroller, check connection.

In case you see the following being endlessly printed

spi_transmit failed: Bad file descriptor

Then either spi is disabled or openocd does not have permissions to /dev/spidev0.0. Odds are you can’t ctrl+c yourself out of the error, then you need to find and kill the process from a different window.

pgrep openocd # Lookup process id.
kill -9 <process id> # Kill openocd process.

Getting stlink to cooperate is pretty much about getting openocd to work on your system one way or the other. Let’s start with a configuration file for stlink and stm32fxx as it will be the same for any host.

# file: stlink-swd.ocd

# Select stlink probe
source [find interface/stlink.cfg]

# Select stm32f1xx as target
source [find target/stm32f1x.cfg]

# Select swd 
transport select hla_swd

# Open gdbserver to the network.

You may notice that openocd is available in a package manager apt or brew, but the version you will get is very outdated (0.1 as of today) and will not support stlink-v3 even if you import a proper interface configuration. Also, depending on your system, 0.1 may crash if the probe is plugged into a usb3 port. Therefore I highly recommend building openocd from source.

2.2.1. Debian aarch32/64

Follow the openocd installation guide for the raspberry pi above, you may skip the spi details if you are not using a raspberry pi or you don’t want swd over spi support. Then start openocd with the configuration file for stlink.

2.2.2. Ubuntu

Ubuntu is the most straightforward with building openocd.

sudo apt install autoconf libtool libusb-1.0-0 libusb-1.0-0-dev # Dependencies.
git clone --recursive # Original openocd repo.
cd openocd
make -j4

Upon success you will find an openocd binary in the src/ folder.

Grab the configuration script stlink-swd.ocd presented above, connect your target to stlink and stlink to the host computer.

sudo ./openocd -s ../tcl -f stlink-swd.ocd

Argument -s ../tcl is a path to the tlc folder where openocd looks for configuration files; argument -f stlink-swd.ocd points to the session specific configuration we just created.

Upon success you shall see a similar output:

Info : clock speed 1000 kHz
Info : STLINK V3J7M2 (API v3)
Info : Target voltage: 3.3
Info : Listening on port 3333 for gdb connections

2.2.3. MacOS

Unfortunately, openocd throws compilation errors on MacOS that I did not have motivation to resolve. The older version is available to install through brew install openocd, but it would not work with the newer stlink-v3. You are encouraged to troubleshoot the build and inform me on your findings.

brew install autoconf libtool libusb
git clone --recursive # Original openocd repo.
cd openocd
# Here you need to specify the paths to libusb.
./configure LIBUSB1_CFLAGS=-I/usr/local/Cellar/libusb/1.0.23/include/libusb-1.0 LIBUSB1_LIBS=-L/usr/local/Cellar/libusb/1.0.23/lib
make -j4

Yet it is possible to debug with the latest stlink hardware on MacOS, proceed to 2.2.4 Workaround.

2.2.4. Workaround

As a workaround you could scrape a pre built binary from STMCubeIDE. Tested on MacOS, should work on x86 Ubuntu and Windows. You’d have to download the STMCubeIDE package, but do not install it. Instead, extract and browse the package content, look for the following files.

  • ST-LINK_gdbserver
  • libSTLinkUSBDriver.dylib # .so on linux
  • STM32_Programmer_CLI
  • STLinkUpgrade.jar

Copy your findings to a separate folder. The gdbserver executable looks for its libSTLinkUSBDriver library in a certain subfolder. Make sure your files are organized as in the following example for macos.

├── ST-LINK_gdbserver
├── STLinkUpgrade.jar
├── STM32_Programmer_CLI
└── native
    └── mac_x64
        └── libSTLinkUSBDriver.dylib # .so on linux

Otherwise you will get the following error.

dyld: Library not loaded: @rpath/libSTLinkUSBDriver.dylib

Connect the hardware. Now you can run the gdbserver as

./ST-LINK_gdbserver -e -d -cp .

Uppon success you shall see

ST-LINK device initialization OK
Waiting for debugger connection...
Waiting for connection on port 61234...

Here -cp . is the path to STM32_Programmer_CLI, -d is for swd. If you look closer into the content of the STMCubeIDE package, you will find a config.txt in the folder with ST-LINK_gdbserver, which goes over all command line arguments the gdbserver can take. Once running, you might experience complaints about libusb, which can be installed with brew install libusb and outdated stlink firmware version, which cab be updated by running STLinkUpgrade.jar (has a gui).

I find ST-LINK_gdbserver easy to use as you do not have to provide any configuration files that openocd requires, it is rather hypocritical of STMicroelectronics not to freely distribute ST-LINK_gdbserver.

Jlink comes with all software ready to go, get it from here.

Look for JLinkGUIServerExe (warning gui), or JLinkGDBServerCLExe (command line). To avoid the gui prompts you need to provide enough arguments, described here.

JLinkGDBServerCLExe -if swd -device stm32f103c8

Warning: jlink edu is painful to run on a remote server as it requires you to confirm the license agreement in a gui prompt at least once a day. In case you need to work with a remote jlink edu, use openocd instead.

3. gdb

The GNU Debugger or gdb is a standard tool for debugging and hacking software in various languages written for various architectures; gdb allows tracing program execution by setting breakpoints, watching memories and processor states.

The example presented in this tutorial is just a tiny glimpse of what gdb is capable of. You can debugg along with this empty initialization code for blue pill.

Start with loading the firmware containing debug symbols. Most flavors of linux including Ubuntu have gdb installed by default.

gdb blue-pill.elf

In case you are on macos, the default system debugger is lldb. I use arm-none-eabi-gdb from the embedded arm toolchain package available here.

Running gdb will welcome you into the debugger console, your command line output should be looking similar to the following.

Reading symbols from blue-pill.elf...

Next, make sure your gdbserver (openocd or jlink) is running and listening for connections. If you are familiar with gdb, you may know that you can attach to a process running on the same machine. Since in bare metal scenario, the program being debugged is running on a physically different machine and and likely a different architecture, the gdb connection will always be “remote” even if the gdbserver is running on the same computer.

The following command establishes connection with the gdbserver. Make sure your ip address and port matches the configuration. In case gdbserver is running on the same computer, you can put localhost instead of the ip address. The port depends on the gdbserver you are running, 3333 is default for openocd.

(gdb) target extended-remote

Issuing load will flash the firmware!

(gdb) load

Loading firmware does not reset the mcu, that is the program counter has not been set to the reset handler address. Issue a reset and halt.

(gdb) monitor reset halt

monitor <expression> command sends the expression to gdbserver, gdb (client) itself does not know what reset halt does.

step will progress the program counter to the next location.

(gdb) step

Look, we are exiting reset handler! Your output should be similar to the following.

Reset_Handler ()
    at /some_path/startup_stm32f103xb.s:66

Also, see how gdb knows Reset_Handler() is declared in startup_stm32f103xb.s line 66.

Let’s set a breakpoint at the main function.

(gdb) break main

Referencing functions by name in gdb will only work with “global” functions. Let’s set a breakpoint in the infinite loop by referring to the corresponding line in file main.c.

break main.c:96

To list all breakpoints type info break.
Continue program execution.

(gdb) continue

This should take us to Breakpoint 1.

Breakpoint 1, main () at /some_path/blue-pill/sources/src/main.c:74

list will display the proceeding lines of code following our current position. Here you could play with step and next, step will take you to the next program counter location, next – to the next function call. If you are familiar with debugging in any IDE, step is equivalent of “step in”, and next is “step out”. continue to get to our second breakpoint. You can delete all breakpoints by issuing delete or delete a specific breakpoint delete 1, or you can also disable breakpoints with disable. If there is no active breakpoints, issuing continue will continue executions until you manually stop it with ctrl+c – this halts execution of the debugee and does not exit gdb. There is so much to go on about gdb, we haven’t started with watchpoints, processor states and memory.

Also, there is a python api that can help automate testing and hacking with gdb!

3.1. gdb + gui

Many, if not all IDEs use gdb behind their visual interface. Now you can configure embedded debugging support in your favorite text editor. For instance if you use vscode, add a configuration file .vscode/launch.json in the project root. Make sure you have ms-vscode.cpptools extension. Here is my vscode debug configuration.

// file: .vscode/launch.json
    // For more information, visit:
    "configurations": [
            "name": "debug",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceRoot}/build/blue-pill.elf",
            "miDebuggerPath": "/Applications/ARM/bin/arm-none-eabi-gdb",
            // "debugServerPath" and "debugServerArgs" are commands to start the gdbserver
            //"debugServerPath": "/Applications/SEGGER/JLink_V662/JLinkGDBServerCLExe",
            //"debugServerArgs": "-device stm32l412cb -if swd",
            "cwd": "${workspaceRoot}",
            "MIMode": "gdb",
            "setupCommands": [
                    "description": "Connect to gdbserver",
                    "text": "target extended-remote"
                    "description": "load executable",
                    "text": "file ${workspaceRoot}/build/blue-pill.elf"
                    "description": "Flash Firmware",
                    "text": "load"
                    "description": "Reset target",
                    "text": "monitor reset halt"


In case your favorite openocd repository is missing the latest stm configurations, you can check st’s openocd fork (configs are in tcl/ folder).

Help Me Improve
I am learning to write meaningful documentation. I hope you enjoyed this post, please help me back by emailing some feedback!

  • Is information clear, correct and up to date?
  • How would you improve this post?