Bare-Metal Debugging with JTAG and RPI 4
Introduction
When developing software, a debugger is often needed to find and fix errors. A common debugger is GDB. As long as the program can be started from an environment like Linux or Windows, this debugger can be used directly. However, in bare-metal development, things are somewhat more challenging because the usual operating system environment is absent. In such cases, emulators or special hardware solutions are necessary.
Since emulators usually cannot perfectly reflect the hardware, we use the host system, which communicates directly with the Raspberry Pi 4 (RPI4). The program is executed directly on the Raspberry Pi. However, through the host system, we can directly manipulate the code and see the result immediately on the hardware.
The Raspberry Pi supports the JTAG protocol for such communication. Unfortunately, host systems seldom provide this protocol directly, which is why we rely on additional hardware. An affordable option is the "CJMCU FT232H module" (https://amzn.eu/d/hb8tKuA), which I used for this guide.
Preparing the hardware
The FT232H module came with pin headers, but they first had to be soldered onto the module. Afterwards, the connection to the Raspberry Pi 4 could be established using jumper wires.
Wiring
The wiring between the FT232H module and the Raspberry Pi 4 is as follows:

| FT232H | Raspi 4 | |
|---|---|---|
| Name | GPIO | PIN |
| AD0 | GPIO25 | 22 |
| AD1 | GPIO26 | 37 |
| AD2 | GPIO24 | 18 |
| AD3 | GPIO27 | 13 |
| GND | GND | 6 (9,14,20,25,30,34,39) |
Using Windows
Under Windows, we use the MSYS2 environment. This provides a Unix-like environment under Windows and is useful for many development tasks.
Installation and Setup If not already done, we install MSYS2 following this instruction to create a programming environment (console) and set up the programming environment by executing the following commands:
- Start MSYS2 MSYS
- Enter the following commands to install OpenOCD and set the environment variables:
pacman -S mingw64/mingw-w64-x86_64-openocd
echo 'export PATH="/mingw64/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Creating Configuration File
Unfortunately, we lack a configuration file that describes JTAG and FT232H. Therefore, we create a file named ft232h-jtag.cfg with the following content:
adapter driver ftdi
ftdi vid_pid 0x0403 0x6014
# Set the layout for JTAG signals (TDI, TDO, TMS, TCK, nTRST, nSRST)
# These pins are required for JTAG
ftdi layout_init 0x0000 0x000b
ftdi layout_signal nTRST -data 0x0020 -oe 0x0020
ftdi layout_signal nSRST -data 0x0010 -oe 0x0010
ftdi layout_signal TDI -data 0x0011 -oe 0x0011
ftdi layout_signal TDO -data 0x0012 -oe 0x0012
ftdi layout_signal TMS -data 0x0013 -oe 0x0013
ftdi layout_signal TCK -data 0x0014 -oe 0x0014
# Select the JTAG mode
transport select jtag
We place this file in the directory C:\msys64\mingw64\share\openocd\scripts\interface\ftdi if MSYS2 is installed as instructed.
Driver Installation
OpenOCD wants to access the device via libusb. Typically, Windows doesn't provide this driver, so we need to install a driver here. For this, we use "Zadig," which can be downloaded from https://zadig.akeo.ie/.
- Install Zadig.
- Start Zadig and enable the "List All Devices" option in the "Options" menu.
- Select "Single RS232-HS" from the device list.
- Choose "libusbK" as the driver and click "Replace Driver".
With this, Windows is ready to communicate with the module via OpenOCD.
Using Linux
Installation and Setup
First, we need OpenOCD:
sudo apt update
sudo apt install openocd
Creating Configuration File
Unfortunately, we lack a configuration file that describes JTAG and FT232H. Therefore, we create a file named ft232h-jtag.cfg with the following content:
adapter driver ftdi
ftdi vid_pid 0x0403 0x6014
# Set the layout for JTAG signals (TDI, TDO, TMS, TCK, nTRST, nSRST)
# These pins are required for JTAG
ftdi layout_init 0x0000 0x000b
ftdi layout_signal nTRST -data 0x0020 -oe 0x0020
ftdi layout_signal nSRST -data 0x0010 -oe 0x0010
ftdi layout_signal TDI -data 0x0011 -oe 0x0011
ftdi layout_signal TDO -data 0x0012 -oe 0x0012
ftdi layout_signal TMS -data 0x0013 -oe 0x0013
ftdi layout_signal TCK -data 0x0014 -oe 0x0014
# Select the JTAG mode
transport select jtag
We place this file in the directory "/usr/share/openocd/scripts/interface/ftdi".
Preparing the Raspberry Pi
After setting up our operating system on the Raspberry Pi, we need to prepare the Raspberry Pi 4 for bare-metal programming. For this, we write a small program that merely creates an endless loop but starts the Raspberry Pi:
//
// The boot program for RPI4
// 07.03.2025 www.satyria.de
//
.section .init // Ensure the linker places this at the beginning of the kernel image
.globl _start // Generates a global label
_start: // The label _start (entry address)
b _start // endless loop
We use a Makefile to compile and link the program (see "Unser erstes Programm in C (PI4)#Kompilieren des Programms mit Make"). We copy the generated kernel8.img onto the SD card. Note that the following files must also be in the root directory of the SD card:
- bcm2711-rpi-4-b.dtb
- bootcode.bin
- fixup4.dat
- start4.elf
Next, we need to inform the Raspberry Pi to respond to JTAG. For this, we create a "config.txt" with the following content:
gpio=22-27=np
enable_jtag_gpio=1
We also place this file in the root directory of the SD card.
Starting OpenOCD and Using GDB
Establishing Connection
First, connect the FT232H module via USB to the host system. An LED on the module should light up, indicating that the device is operational. Start the Raspberry Pi. If it is connected to a monitor, the screen should display the rainbow image, indicating that the system has started.
Now start "MSYS2 MSYS" or open a terminal on Linux:

openocd -f interface/ftdi/ft232h-jtag.cfg -f target/bcm2711.cfg
If everything is set up correctly, OpenOCD should display all four CPUs of the Raspberry Pi. It also shows that ports 3333, 3334, 3335, and 3336 are provided for GDB communication.
Kernel Upload and Debugging To be sure, open another "MSYS2 MSYS" window or a second terminal on Linux. Change to the directory containing the code to be tested. If the code is not yet compiled, this should be done now.
For these tests, use the kernel8.elf:
aarch64-none-elf-gdb
target remote localhost:3333
This establishes a connection to CPU0.
Now upload the kernel to the Raspberry Pi:
load kernel8.elf
To start the kernel, use the following commands:
set $pc = 0x80000
continue