Debugging on Raspberry Pi 4 and 5

By Macoy Madson. Published on .

This article details how to debug the Raspberry Pi 4 and 5 CPUs using hardware interfaces. This is useful for bare metal programming.

While there seems to be a great many steps and complicated configuration, remember that this will allow you to step debug the hardware, which will doubtless save you countless hours of frustration when your bare metal project doesn't work. Seriously, if you think you can do bare metal with serial/print debugging only, you are going to waste huge amounts of time. Get a real debugger.

You can also use these interfaces to upload new programs to the CPUs, reducing the amount of SD card wear-and-tear from having to swap it out each time you make a change.

Raspberry Pi 4

I got most of my information from this article (Archive.org) and also referenced the following articles, some of which do not work, but I felt I should include in case they have some other useful information:

I detail the entire setup here in case A) these articles disappear and B) the configurations in the articles don't actually work. You shouldn't need to read any of them, but in case you have troubles with my setup you may want to look at theirs to compare.

You will need a hardware JTAG interface. I bought mine from Adafruit. It is called the "Adafruit FT232H Breakout - General Purpose USB to GPIO, SPI, I2C - USB C & Stemma QT". This guide is for this model exactly. Other JTAG interfaces are available, but you will need to do different things to get them to work.

Wiring

Solder the headers to the board so you can connect jumpers like these ("Premium Female/Female Jumper Wires") from the interface to the Raspberry Pi GPIO pins.

You then need to make the following connections:

Function FT232h Pin Pi Pin Pi GPIO
TCK D0 22 25
TDI D1 37 26
TDO D2 18 24
TMS D3 13 27
TRST D4 15 22
SRST D5 Not connected Not connected
RTCK D7 16 23
GND GND 39 GND

You can ignore the Pi GPIO column and rely on the Pi Pin column since the Pin column is spatial. The GPIOs are not in order on the header so they are confusing to wire against, but do act as a value you can double-check, and are how the CPU refers to them so are still useful to include here.

These match the interface configuration below. See Pinout (unofficial) (Archive.org) for where the pins are on the headers.

For posterity, here is the complete Pi 4 GPIO table, copied from Pinout:

Left side, closest to board edge:

Pin number (1-indexed) Function
1 3v3 Power
3 GPIO 2 (I2C1 SDA)
5 GPIO 3 (I2C1 SCL)
7 GPIO 4 (TDI (Alt5))
9 Ground
11 GPIO 17
13 GPIO 27 (TMS (Alt4))
15 GPIO 22 (TRST (Alt4))
17 3v3 Power
19 GPIO 10 (SPI0 MOSI)
21 GPIO 9 (SPI0 MISO)
23 GPIO 11 (SPI0 SCLK)
25 Ground
27 GPIO 0 (EEPROM SDA)
29 GPIO 5 (TDO (Alt5))
31 GPIO 6 (RTCK (Alt5))
33 GPIO 13 (TCK (Alt5))
35 GPIO 19 (PCM FS)
37 GPIO 26 (TDI (Alt4))
39 Ground

Right side, closest to board inside:

Pin number (1-indexed) Function
2 5v Power
4 5v Power
6 Ground
8 GPIO 14 (UART TX)
10 GPIO 15 (UART RX)
12 GPIO 18 (PCM CLK)
14 Ground
16 GPIO 23 (RTCK (Alt4))
18 GPIO 24 (TDO (Alt4))
20 Ground
22 GPIO 25 (TCK (Alt4))
24 GPIO 8 (SPI0 CE0)
26 GPIO 7 (SPI0 CE1)
28 GPIO 1 (EEPROM SCL)
30 Ground
32 GPIO 12 (TMS (Alt5))
34 Ground
36 GPIO 16
38 GPIO 20 (PCM DIN)
40 GPIO 21 (PCM DOUT)

Legend

Orientate your Pi with the GPIO on the right and the HDMI port(s) on the left.

  • GPIO (General Purpose IO)
  • SPI (Serial Peripheral Interface)
  • I2C (Inter-integrated Circuit)
  • UART (Universal Asynchronous Receiver/Transmitter)
  • PCM (Pulse Code Modulation)
  • Ground
  • 5v (Power)
  • 3.3v (Power)

JTAG - Joint Test Action Group

JTAG is a standardised interface for debugging integrated circuits which you can use to debug your Raspberry Pi.

There are two separate JTAG interfaces available on the Pi:

  • Alt5 on GPIOs 4, 5, 6, 12 and 13
  • Alt4 on GPIOs 22, 23, 24, 25, 26 and 27

I use the "Alt4" JTAG connections.

Preparing the SD card

Prepare a FAT32-formatted microSD card with Raspberry Pi firmware.

Official firmware

It is important to have the latest version of the firmware, which can be downloaded here. Unfortunately, these are largely proprietary binary blobs; it is not yet possible to run the Pi 4 on open firmware like the earlier Pis.

See my shell script for how to automatically download only these files, though note from my experience that Github tends to change how their raw links are formatted, so this may break in a few years. Do NOT try to clone the entire raspberrypi/firmware repository, it is enormous and will take too much space on your drive.

I use a more minimal set of firmware for bare metal, which may break e.g. a regular Linux distribution. Here are the files I require for my bare metal projects (some may not be necessary), in the root of the SD card:

File Purpose
bcm2711-rpi-4-b.dtb Device tree for Pi 4
bcm2712-rpi-5-b.dtb Device tree for Pi 5
LICENCE.broadcom Legal license for Broadcom firmware
bootcode.bin Bootloader (not used on Raspberry Pi 4)
start.elf Firmware executable for Raspberry Pi 1-3
fixup.dat Relocation information for start.elf
start4.elf Firmware executable for Raspberry Pi 4
fixup4.dat Relocation information for start4.elf
start_cd.elf Cut-down firmware for Raspberry Pi 1-3 (optional)
fixup_cd.dat Relocation information for start_cd.elf (optional)
start4cd.elf Cut-down firmware for Raspberry Pi 4 (optional)
fixup4cd.dat Relocation information for start4cd.elf (optional)
armstub8-rpi4.bin The Arm cores start up with this. See armstub8.S
overlays/vc4-fkms-v3d.dtbo Device tree overlay for GPU
overlays/vc4-fkms-v3d-pi4.dtbo Device tree overlay for GPU
overlays/vc4-kms-v3d-pi4.dtbo Device tree overlay for GPU
overlays/vc4-kms-v3d.dtbo Device tree overlay for GPU
overlays/vc4-kms-v3d-pi5.dtbo Device tree overlay for GPU

Your kernel

Copy a valid kernel8.img to the SD card and insert it in the Pi. See any bare metal tutorial or build Circle or my RPI System samples to get one with debugging symbols.

You'll want a kernel8.elf for the host computer to have debug symbols, and a stripped kernel8.img for the target Pi to actually run. Because the kernel8.img is stripped of all ELF headers and e.g. debug sections, the host computer must have the elf file the img was derived from in order to make sense of the img addresses for debugging.

Later, you can use GDB to upload a new kernel8.elf while the Pi is running so you don't need to swap the SD each time.

The config.txt

Create a config.txt in the SD card root with at least the following contents:

# Disable pull downs
gpio=22-27=np
# Enable jtag pins (i.e. GPIO22-GPIO27)
enable_jtag_gpio=1

Adding these seems to change some behavior; for example, I can no long turn off my Pi 4 by holding my Argon One case power button, I need to unplug it. I'm unsure whether this is related, but if you notice things like that, it may be because the JTAG puts the power into a special mode.

Building OpenOCD on Linux

You can find instructions for other platforms by looking around online. I don't really build OpenOCD in a special way, I only include it here for completeness and easy copy-pasting. Make sure to replace stuff in <angles> before running.

git clone git://git.code.sf.net/p/openocd/code openocd
cd openocd
# Not sure if libcapstone is required, but libusb definitely is
sudo apt install libusb-1.0-0 libusb-1.0-0-dev libcapstone-dev
./bootstrap
./configure --enable-ftdi --prefix=</full/path/to/an/install/directory>
make -j
# This will install to the directory specified by --prefix
make install

OpenOCD will then be in your prefix directory's bin subdirectory.

As far as I know you do not need to use the Raspberry Pi OpenOCD fork now. I have f76c8de910e1e12f4b180956d0189c9483e949a5 from the master branch of OpenOCD's official repository and can debug both Raspberry 4 and 5 with it.

Configuring OpenOCD

OpenOCD can use multiple different interfaces to debug multiple different targets. In our case the interface is the Adafruit FT232H and the target is the Raspberry Pi 4.

These configurations might now be in the official repository, but I include them here for completeness.

Interface

Here is the configuration for the Adafruit FT232H:

# config file for generic FT232H based USB-serial adaptor
# TCK:  D0
# TDI:  D1
# TDO:  D2
# TMS:  D3
# TRST: D4
# SRST: D5
# RTCK: D7

adapter driver ftdi
ftdi vid_pid 0x0403 0x6014
ftdi layout_init 0x0078 0x017b
adapter speed 1000

# Set sampling to allow higher clock speed
ftdi tdo_sample_edge falling

ftdi layout_signal nTRST -ndata 0x0010 -noe 0x0040
ftdi layout_signal nSRST -ndata 0x0020 -noe 0x0040
# change this to 'transport select swd' if required
transport select jtag

# references
# http://sourceforge.net/p/openocd/mailman/message/31617382/
# http://www.baremetaldesign.com/index.php?section=hardware&hw=jtag

Target

And here is the Pi 4 configuration:

# SPDX-License-Identifier: GPL-2.0-or-later
# The Broadcom BCM2711 used in Raspberry Pi 4
# No documentation was found on Broadcom website
# Partial information is available in raspberry pi website:
# https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/

if { [info exists CHIPNAME] } {
    set  _CHIPNAME $CHIPNAME
} else {
    set  _CHIPNAME bcm2711
}
if { [info exists CHIPCORES] } {
    set _cores $CHIPCORES
} else {
    set _cores 4
}
if { [info exists USE_SMP] } {
    set _USE_SMP $USE_SMP
} else {
    set _USE_SMP 0
}
if { [info exists DAP_TAPID] } {
    set _DAP_TAPID $DAP_TAPID
} else {
    set _DAP_TAPID 0x4ba00477
}

jtag newtap $_CHIPNAME cpu -expected-id $_DAP_TAPID -irlen 4
adapter speed 3000

dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu

# MEM-AP for direct access
target create $_CHIPNAME.ap mem_ap -dap $_CHIPNAME.dap -ap-num 0

# these addresses are obtained from the ROM table via 'dap info 0' command
set _DBGBASE {0x80410000 0x80510000 0x80610000 0x80710000}
set _CTIBASE {0x80420000 0x80520000 0x80620000 0x80720000}

set _smp_command "target smp"

for { set _core 0 } { $_core < $_cores } { incr _core } {
    set _CTINAME $_CHIPNAME.cti$_core
    set _TARGETNAME $_CHIPNAME.cpu$_core
    cti create $_CTINAME -dap $_CHIPNAME.dap -ap-num 0 -baseaddr [lindex $_CTIBASE $_core]
    target create $_TARGETNAME aarch64 -dap $_CHIPNAME.dap -ap-num 0 -dbgbase [lindex $_DBGBASE $_core] -cti $_CTINAME
    set _smp_command "$_smp_command $_TARGETNAME"
}

if {$_USE_SMP} {
    eval $_smp_command
}

# default target is cpu0
targets $_CHIPNAME.cpu0

Connecting with OpenOCD

Save the configurations listed previously to files. You then have OpenOCD reference them with the --file argument, e.g.:

sudo my-openocd/install/bin/openocd --file openocd_adafruit-ft232h.cfg --file openocd_raspi4.cfg

You can then do the following to test the complete configuration:

OpenOCD should output something like the following:

$ sudo ./openocd/install/bin/openocd --file openocd_adafruit-ft232h.cfg --file openocd_raspi4.cfg
[sudo] password for macoy:
Open On-Chip Debugger 0.12.0+dev-01348-gf76c8de91 (2023-10-06-09:50)
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
jtag
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 3000 kHz
Info : JTAG tap: bcm2711.cpu tap/device found: 0x4ba00477 (mfg: 0x23b (ARM Ltd), part: 0xba00, ver: 0x4)
Info : bcm2711.cpu0: hardware has 6 breakpoints, 4 watchpoints
Info : bcm2711.cpu1: hardware has 6 breakpoints, 4 watchpoints
Info : bcm2711.cpu2: hardware has 6 breakpoints, 4 watchpoints
Info : bcm2711.cpu3: hardware has 6 breakpoints, 4 watchpoints
Info : gdb port disabled
Info : starting gdb server for bcm2711.cpu0 on 3333
Info : Listening on port 3333 for gdb connections
Info : starting gdb server for bcm2711.cpu1 on 3334
Info : Listening on port 3334 for gdb connections
Info : starting gdb server for bcm2711.cpu2 on 3335
Info : Listening on port 3335 for gdb connections
Info : starting gdb server for bcm2711.cpu3 on 3336
Info : Listening on port 3336 for gdb connections

Remember that your Pi needs to be powered on and past its firmware boot sequence before you can run this command to connect.

Sometimes the interface will mysteriously stop connecting or working. It's rare, but if it does happen, try unplugging and plugging it back in.

I haven't needed to use the OpenOCD interface, but if you do, you can connect via:

telnet 127.0.0.1 4444

Connecting GDB

Now that OpenOCD is connected to the running Pi, you can connect GDB.

Briefly, the entire debug setup works like this:

I use GDB distributed as part of the Arm GNU toolchain, though a "multi-arch" GDB through e.g. sudo apt install gdb-multiarch likely will work.

Each core has a separate connection. For cpu0 you connect through port 3333.

I like to connect GDB the manual way to make sure things are working, then move the commands into a script for repeated use.

Run your multi-arch or Arm GDB (I run arm-gnu-toolchain-12.3.rel1-x86_64-aarch64-none-elf/bin/aarch64-none-elf-gdb), then type the following commands into GDB:

target extended-remote :3333
file path/to/my/kernel8.elf

It will prompt you:

A program is being debugged already. Are you sure you want to change the file? (y or n)

You can answer y. This warning serves as a reminder that the img is already loaded and running, and therefore you need to be careful that the elf you specify with the file command may not exactly match the img (if e.g. you choose the wrong elf file, or have an older img on the SD, etc.). The file command will NOT load the elf file to the Pi; that's what the load command is for.

If all goes well, you should be able to run bt to see a backtrace, or press Ctrl+C and/or run interrupt to see the callstack of the running Pi core.

If you don't have symbols or are debugging assembly or something, you can do this sort of thing with raw addresses and disassembly:

# Break on exact address
b *0x00005555570d249a
# A must when single-stepping instructions
set disassemble-next-line on
# Step instruction
stepi
# Disassemble range
disassemble 0x5555570d1be7, +128

Setting up a stub for early debugging

When working on bare metal you may want to debug your startup assembly code. You need to trap the processor in a state where it is doing nothing until you attach your debugger, then you can proceed to single step, set breakpoints etc. before your startup code runs.

I do this with the following assembly:

// Normal entry point for kernels prepared with the default armstub8.S
.global _start
_start:
    // Wait for GDB to set x0 to 0 (for debug only)
    ldr x0, =1
2:  nop
    cbnz x0, 2b
// Startup code follows...

The processor will then get trapped in this spinloop. You can take your time getting GDB connected, then:

If there is a better way of doing this, please let me know.

Loading new images

While the processor is stuck in this early spinloop, you can load new images. Set the ELF to load via file or simply by passing it in as the file to GDB. When you run load, the new image will be uploaded to the Pi and the instruction pointer will be set to the start of the new image.

Note that any processor configuration which happened before you load remains; there isn't any reset step that happens. This is why it is important to put the spinloop very early when tinkering with startup code.

Here's what my GDB script usually looks like, named e.g. ConnectJTAG.gdb:

target extended-remote :3333
load
# Set breakpoints in exception handlers that otherwise result
# in spin-loops or halts. These functions are specific to my
# kernel, and are only here for example.
b assertion_failed
b ExceptionHandler_ThrowWithFrame

…where I invoke GDB with e.g.:

aarch64-none-elf-gdb --command=ConnectJTAG.gdb path/to/newKernel.elf

When I run this command, it will immediately upload the newKernel.elf to the Pi with the processor halted. I can then set breakpoints, run continue, etc.

By loading images this way I can avoid wear-and-tear, try new kernels easily, and debug from the very start of new kernels.

Raspberry Pi 5

The Raspberry Pi 5 hardware debug interface differs from the Raspberry Pi 4. The 5 uses "serial wire debugging" whereas the 4 uses JTAG over GPIO. You can see the official documentation for debugging the RP2040 with the debug probe here (Archive.org).

You will need to buy a single wire debug interface for the Pi 5. I bought mine from Adafruit, but it is available at many Pi stores because it is an official Raspberry Pi product. It is called the "Raspberry Pi Debug Probe Kit for Pico and RP2040". It uses the 3-pin connector detailed here.

Wiring

Connect the 3 pin wire to the D port of the Pi Probe, then connect the other end to the UART port of the target Pi 5 located in between the two HDMI ports. Connect the Probe via USB to the host computer.

I have the official Pi 5 case with fan. I keep the top removed and put the SWD wire through the small hole one of the cover latches goes in. This prevents me from putting the top cover on, but if I really wanted to put it on I could cut a slot in the case side to allow the wire room to exit.

config.txt

On the SD card, create or edit the file config.txt at the root of the file system with the following line included:

enable_jtag_gpio=1

Note that even though the Pi 5 no longer uses the GPIO, this is still the option needed to enable hardware debugging.

OpenOCD configuration

Follow the same steps for building OpenOCD as for the Pi 4.

The Pi 5 needs the following new configuration files.

Interface

This is included by default as tcl/interface/cmsis-dap.cfg in OpenOCD; it is copied here for completeness.

# SPDX-License-Identifier: GPL-2.0-or-later

#
# ARM CMSIS-DAP compliant adapter
#
# http://www.keil.com/support/man/docs/dapdebug/
#

adapter driver cmsis-dap

# Optionally specify the serial number of CMSIS-DAP usb device.
# adapter serial 02200201E6661E601B98E3B9

Target

I got this from the Raspberry Pi forums (Archive.org), which the commentator says isn't thoroughly tested. It seems to work fine for me. You can see it is similar to the Raspberry Pi 4 target configuration.

# SPDX-License-Identifier: GPL-2.0-or-later

# The Broadcom BCM2712 used in Raspberry Pi 5
# No documentation was found on Broadcom website

# Partial information is available in Raspberry Pi website:
# https://www.raspberrypi.com/documentation/computers/processors.html#bcm2712

# v1.0 initial revision - trejan on forums.raspberrypi.com

if { [info exists CHIPNAME] } {
        set  _CHIPNAME $CHIPNAME
} else {
        set  _CHIPNAME bcm2712
}

if { [info exists CHIPCORES] } {
        set _cores $CHIPCORES
} else {
        set _cores 4
}

if { [info exists USE_SMP] } {
        set _USE_SMP $USE_SMP
} else {
        set _USE_SMP 0
}

if { [info exists DAP_TAPID] } {
        set _DAP_TAPID $DAP_TAPID
} else {
        set _DAP_TAPID 0x4ba00477
}

transport select swd

swd newdap $_CHIPNAME cpu -expected-id $_DAP_TAPID -irlen 4
adapter speed 4000

dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu

# MEM-AP for direct access
target create $_CHIPNAME.ap mem_ap -dap $_CHIPNAME.dap -ap-num 0

# these addresses are obtained from the ROM table via 'dap info 0' command
set _DBGBASE {0x80010000 0x80110000 0x80210000 0x80310000}
set _CTIBASE {0x80020000 0x80120000 0x80220000 0x80320000}

set _smp_command "target smp"

for { set _core 0 } { $_core < $_cores } { incr _core } {
        set _CTINAME $_CHIPNAME.cti$_core
        set _TARGETNAME $_CHIPNAME.cpu$_core

        cti create $_CTINAME -dap $_CHIPNAME.dap -ap-num 0 -baseaddr [lindex $_CTIBASE $_core]
        target create $_TARGETNAME aarch64 -dap $_CHIPNAME.dap -ap-num 0 -dbgbase [lindex $_DBGBASE $_core] -cti $_CTINAME

        set _smp_command "$_smp_command $_TARGETNAME"
}

if {$_USE_SMP} {
        eval $_smp_command
}

# default target is cpu0
targets $_CHIPNAME.cpu0

Running OpenOCD

Just like the Pi 4, you need to run OpenOCD with the correct interface and target scripts which you can copy into files and then reference with something like the following command:

sudo my-openocd/install/bin/openocd --file cmsis-dap.cfg --file openocd_raspi5.cfg

Everything else in the Pi 4 section related to GDB works on the Pi 5.