Raspberry Pi 4 Model B Bare Metal Debugging with GDB and OpenOCD

Instruction for setting up an efficient workflow for bare metal development for the Raspberry Pi 4 Model B including full system reset from GDB - no more power cycling!

I created this as an offshoot of my AArch64OS project. There are a lot of great resources for bare metal development on the Raspberry Pi 4 Model B (RPi4B from here on), but I found that there were a few gaps that I wanted to fill. In particular this article and the corresponding repository strives to provide a concise but complete set of scripts and code with the following features to get started quickly with GDB, OpenOCD, and a debug probe:

This last feature is something I could not find in any other resources I came across. I found lots of hints and discussions, but no complete solution. Maybe I just missed it, but regardless I hope this helps others where I struggled.

Hardware

Serial Connection

A serial connection is used for monitoring the RPi4B bootloader messages and output from the bare metal program. Only three wires are needed for a basic serial connection: GND, TXD, and RXD. The TXD and RXD pins are used to send and receive data to and from the RPi4B. The GND pin is used as a reference voltage for the serial communication.

There isn’t a standard for the colors of the wires that is universally followed, but for my adapter relevant pins are:

If you are using a different adapter, you will need to look up the pinout.

Connect these to the RPi4B as follows (TXD connects to RXD and RXD connects to TXD):

When looking at the GPIO header (J8) on the RPi4B with the USB/ethernet ports at the bottom, the top left pin is 1 and the bottom right pin is 40.

JTAG Connection

Adafruit FT232H The table below shows the connections between the JTAG debug probe and the RPi4B. The colors are just the colors of the wires I used. The tables shows the appropriate pins for both the J-Link and the FT232H; only one of those two columns are applicable depending on which probe you are using.

Note that for the FT232H I show both the generic FT232H pin name and in parentheses the names as labeled on the Adafruit breakout board.

J-Link pin mappings come from Segger.

The FT232H pin mappings depend on the OpenOCD configuration file you use as multiple mappings are possible depending on the ftdi layout_init and ftdi layout_signal settings in the file. I used the interface/ftdi/um232h.cfg file that comes with OpenOCD so these pin mappings are specific to that config. There are a lot of non-working config files in various forums and blog posts for this debug probe.

Also, I show the RTCK pin connected to the J-Link, but I know it is not needed for the FT232H and it probably is not needed for the J-Link either but it doesn’t hurt.

JTAG Signal J-Link FT232H Raspberry Pi Color
TMS 7 ADBUS3 (D3) J8-13 Orange
nTRST 3 ACBUS0 (C0) J8-15 (TRST) Blue
RTCK 11 N/A J8-16 Brown
VTREF 1 N/A J8-17 (3v3) Red
TDO 13 ADBUS2 (D2) J8-18 Green
TCK 9 ADBUS0 (D0) J8-22 White
TDI 5 ADBUS1 (D1) J8-37 Grey
GND 4 GND J8-39 Black
nRESET/SRST 15 ACBUS1 (C1) J2-RUN Purple

When looking at the J-Link with the notch for the ribbon cable lock on the left, the top left pin is 1 and the bottom right pin is 20. When looking at the RPi4B GPIO header (J8) with the USB/ethernet ports at the bottom, the top left pin is 1 and the bottom right pin is 40.

The Tricky Part: The RPi4B does not expose a nRESET pin for JTAG to allow resetting the entire system. For efficient bare metal development, this is critical otherwise everytime you reload a new kernel to test, you have to physically power cycle the Raspberry Pi. To work around this, I connected the nRESET pin on the debug probe to the RUN pin on the J2 header of the Raspberry Pi. The J2 header does not have any header pins soldered to it by default, so I solder three breakaway header pins.

Software

Arm AAarch64 Toolchain

For basic assembly programming all you really need is a verion of binutils targeting the right architecture, but the most convenient way to get this along with a complete GCC toolchain is to use the Arm GNU Toolchain Downloads. Specifically, I used this download for AArch64 bare-metal target (aarch64-none-elf) because we are targeting a bare metal system with no pre-existing OS like Linux.

OpenOCD

OpenOCD is software for talking to various debug adapters and target boards/chips attached to them. I used the version from the Debian repository (v0.12.0). To use OpenOCD you need two config files: one for the adapter and one for the target. I used the interface/ftdi/um232h.cfg (or interface/jlink.cfg) that comes with OpenOCD but I slightly modified the board/rpi4b.cfg file that comes with OpenOCD to support resetting the board using the nRESET pin on the debug probe. Specifically, I changed reset_config trst_only to reset_config trst_and_srst.

OpenOCD once started acts as a server. You can connect to it with GDB (e.g., target remote localhost:3333) or you can connect to it with telnet (e.g., telnet localhost 4444) to issue commands directly.

Raspberry Pi 4 Model B Firmware

We need a few “firmware” files from Raspberry Pi bootloader. I used the latest version from the Raspberry Pi GitHub repository.

Picocom

There are multiple options for serial terminals, but I used picocom because it is simple and easy to use. I installed it from the Debian repository. The main thing you need to know about picocom is ctrl-a ctrl-x is used to exit it. minicom is another popular option that could be used if you are more familiar with it.

The Code

My rpi4b-bare-metal-debugging repository has a set of basic scripts and code to easily get started.

[1841835.047410] sd 6:0:0:3: [sdd] 124735488 512-byte logical blocks: (63.9 GB/59.5 GiB)
[1841835.049479] sdd: detected capacity change from 0 to 124735488
[1841835.049993]  sdd: sdd1

which would indicate the device name is /dev/sdd.

First Time Setup

  1. Clone the repository
git clone git@github.com:dsmcfarl/rpi4b-bare-metal-debugging.git
cd rpi4b-bare-metal-debugging
  1. Install tools
./get-tools.sh
  1. Initial build (required to create the boot directory for sdcard.sh)
./build.sh
  1. Create a bootable SD card (insert an SD card into a card reader first)
sudo dmesg | tail # to see the device name
sudo ./sdcard.sh /dev/sdX # where /dev/sdX is the device name of the sd card
  1. Boot (insert the SD card into the RPi4B and power it on)

The RPi4B is now running the loop.s program as its kernel. It will just loop forever doing nothing.

Development Workflow

  1. Start a serial terminal (plug in the serial adapter first)
./serial.sh /dev/ttyUSB0 # where /dev/ttyUSB0 is the device name of the serial adapter, change as required
  1. Start openocd in another terminal
./openocd.sh
  1. Start gdb in another terminal
./gdb.sh
  1. Reset and load the kernel. In gdb:
resetload
  1. Test the kernel. In gdb:
continue
  1. Make changes to kernel.s (for example, edit the message “hello world”)

  2. Reload the kernel and test. In gdb:

resetload
continue
  1. Press ctrl-c in gdb terminal to interrupt the kernel.

  2. Repeat steps 6-8 as needed.

Its nice if you have multiple monitors or are using a multiplexer like tmux so you can see all three terminals (serial, openocd, and gdb) at the same time.

The nice part about the way this is setup is that you don’t even need physical access to the RPi4B after the initial setup. For example, I have the RPi4B/JTAG debug probe connected to a machine in my basement, but I can ssh into that machine from my laptop and do bare metal development from anywhere without lugging around a debug probe, serial adapter, power supply, and Raspberry Pi.

Conclusion

I hope this helps others get started with bare metal development on the Raspberry Pi 4 Model B. Hopefully the code in the repository in particular, provides a good starting point for you to expand on for your own projects. Please leave me a comment or submit an issue on github if you have any questions or suggestions for improvement.

Postscript

Why not QEMU? Why use real hardware instead of the convenience of an emulator? Emulators work great for rapidly iterating on development, but for bare metal development, an emulator cannot accurately simulate all of the quirks of the hardware. As a result, when you spend too much time developing on an emulator, you can end up with a system that works great in the emulator but not on the real hardware and when you try to migrate it to hardware, you end up backtracking a lot trying to find the source of the bugs. I find it is best to develop on real hardware from the beginning to avoid this problem.