Loading U-Boot into a Pi with JTAG
Making development/iteration faster
I should preface this with a note that JTAG isn't required to get U-Boot running on any of the Raspberry Pi boards. Also, this article won't cover the configuration of a U-Boot build itself, since there is already a defconfig for the Raspberry Pis.
I found myself needing to do so in order to more quickly iterate on changes to driver code I am developing. Traditionally, U-Boot can be run on a Raspberry Pi by loading a suitable build of it to the SD card and adjusting the kernel
parameter in config.txt
. This is fine for users of stable builds, but when it comes time to start customising configurations, or worse yet, developing drivers for the bootloader, it becomes tedious to repeat the process of:
- Powering the board down
- Pulling the SD card
- Re-flashing the SD card with a fresh build of U-Boot
- Reinserting the SD card into the board
- Powering the board back up to test
If only there was a way of squirting a fresh build of U-Boot straight into the board's RAM and booting from there, removing the need for SD cards to be stressed by flash writes and insertion/removal cycles.
The code and OpenOCD configuration used in this article are kept in this git repo should you want to follow along.
Enter JTAG
It turns out JTAG is the perfect solution to this problem. Using JTAG from an external workstation targeting the embedded board, it is entirely possible to:
- Halt the CPU
- Load a U-Boot image into the target's RAM
- Fiddle the program counter register to enforce a jump instruction to the newly-loaded U-Boot image
- Let the CPU run again
Earlier this year, I thought I'd try the Bus Blaster v3 from Dangerous Prototypes, a simple USB-based JTAG debugger. I hadn't really had an excuse to break it out until now, so it is what I am using in the rest of the article, but any JTAG debugger should work. In addition, I'll be using OpenOCD and GDB in order to drive the JTAG debugger from a Linux workstation.
Configuring the Pi
Exposing JTAG Lines
Before we begin wiring, let's first set the Raspberry Pi up to expose the CPU's JTAG lines on the 40-pin expansion header. Edit your config.txt
and add the following line:
enable_jtag_gpio=1
This changes some of the pins exposed on the GPIO header to their "Alt4" modes.
Installing a dead-end program
Since the JTAG lines aren't exposed until the Pi's firmware does its thing, there's too narrow a window to catch and halt the board using JTAG before it boots the kernel configured in config.txt
. The simplest solution to this is to just create a flat binary that will keep the CPU spinning in an endless loop. You can either clone my repo and build the code yourself, e.g.
CROSS_COMPILE=arm-linux-gnueabihf- make -C loop
...or you can take the "punching in machine code with a hex editor" route and punch in some machine code with a hex editor. In the case of the BCM2835, the machine code for the endless loop is:
$ hexdump -C loop.bin
00000000 fe ff ff ea |....|
00000004
Use your favourite hex editor to punch in this machine code, and you should be golden. If not using the proper toolchain, my preferred hack would be:
xxd -r > loop.bin <<< "0xfe 0xff 0xff 0xea"
With loop.bin
ready, copy it to the Pi's SD card, and also enable firmware UART debug per the documentation while we're in there:
# sed -i -e "s/BOOT_UART=0/BOOT_UART=1/" bootcode.bin
Down to business, comment out any existing kernel=
lines in config.txt
, and let's set the kernel to our endless loop binary in config.txt
:
kernel=loop.bin
Booting the board at this point should result in some decent output from the long-mysterious bootcode.bin
:
Raspberry Pi Bootcode
Found SD card, config.txt = 1, start.elf = 1, recovery.elf = 0, timeout = 0
Read File: config.txt, 539 (bytes)
Raspberry Pi Bootcode
Read File: config.txt, 539
Read File: start_x.elf, 4036548 (bytes)
MESS:00:00:01.171857:0: brfs: File read: /mfs/sd/config.txt
MESS:00:00:01.176245:0: brfs: File read: 539 bytes
MESS:00:00:01.189779:0: HDMI:EDID error reading EDID block 0 attempt 0
MESS:00:00:01.195856:0: HDMI:EDID error reading EDID block 0 attempt 1
...
The bits we're concerned with are:
...
MESS:00:00:01.748405:0: brfs: File read: /mfs/sd/loop.bin
MESS:00:00:01.752230:0: Loading 'loop.bin' to 0x8000 size 0x4
...
This confirms what the U-Boot configuration for this board declares SYS_TEXT_BASE
as being: 0x8000
. Since the U-Boot image for this board is also a flat image, this is where it expects us to load it in memory before starting it. Be it bootcode.bin
that does it or ourselves through JTAG.
Once you get to:
...
MESS:00:00:03.227782:0: uart: Baud rate change done...
MESS:00:00:03.231214:0: uart: Baud rate change done...
MESS:00:00:03.236711:0: gpioman: gpioman_get_pin_num: pin SDCARD_CONTROL_POWER not defined
...you know that the board is booted. I have seen other people's printouts lacking the last line. Perhaps it's a quirk of Pi Zeros, but don't be too concerned about it. The UART baud rate messages are the last material ones to be printed before bootcode.bin
jumps to 0x8000
.
Wiring
The wiring follows the many guides on the internet, paying careful attention to use the "Alt4" and not "Alt5" functionality. It can be double-checked against the BCM2835 ARM Peripherals manual:
JTAG | BCM | Header |
---|---|---|
VTG | N/A | Pin 1 or 17* |
GND | N/A | Pin 14 or 20** |
TRST | GPIO22 | Pin 15 |
RTCK | GPIO23 | Pin 16 |
TDO | GPIO24 | Pin 18 |
TCK | GPIO25 | Pin 22 |
TDI | GPIO26 | Pin 37 |
TMS | GPIO27 | Pin 13 |
* Make sure NOT to connect VTG to 5 V. Use 3.3 V only!
** You can use any ground pin, but these are close by.
OpenOCD Configuration
The steps here will vary slightly depending on which board you are targeting and which JTAG debug adapter you are using. However, what follows is the setup I ended up putting together based on some scattered resources on the internet.
Interface Configuration
Luckily for me, OpenOCD already has an interface configuration shipping with it for the Bus Blaster. For me, it's installed to /usr/share/openocd/scripts/interface/ftdi/dp_busblaster.cfg
.
Target Configuration
OpenOCD doesn't seem to currently ship with a target configuration for the BCM2835, which means a small custom one needs to be scraped together:
transport select jtag
# These may depend on your adapter
adapter_khz 2000
adapter_nsrst_delay 300
# RPi doesn't expose SRST, use TRST alone
reset_config trst_only
set _CHIPNAME rpi
set _CPU_TAPID 0x07b7617f
jtag newtap $_CHIPNAME arm -irlen 5 -expected-id $_CPU_TAPID
set _TARGETNAME $_CHIPNAME.arm
target create $_TARGETNAME arm11 -chain-position $_TARGETNAME
# Needed for verify_image
rpi.arm configure -work-area-phys 0x2F0000 -work-area-size 8096
I saved mine as pi-zero.cfg
.
Performing a download
Check CPU halts
Let's perform a quick sanity check before going all-guns-blazing. By this point, we should be able to start OpenOCD, attach a GDB client to it and halt and resume the CPU. Let's start it:
$ openocd -f /usr/share/openocd/scripts/interface/ftdi/dp_busblaster.cfg -f pi-zero.cfg
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : If you need SWD support, flash KT-Link buffer from https://github.com/bharrisau/busblaster
and use dp_busblaster_kt-link.cfg instead
adapter speed: 2000 kHz
trst_only separate trst_push_pull
8096
Info : clock speed 2000 kHz
Info : JTAG tap: rpi.arm tap/device found: 0x07b7617f (mfg: 0x0bf (Broadcom), part: 0x7b76, ver: 0x0)
Info : found ARM1176
Info : rpi.arm: hardware has 6 breakpoints, 2 watchpoints
Now, let's halt the board. Your GDB's name may vary, I'm just running a build I already had lying around, technically the wrong version for this CPU, but it doesn't matter too much since we're mostly using GDB to speak to OpenOCD rather than the CPU itself. Mostly.
$ armv8l-linux-gnueabihf-gdb
...
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) target remote :3333
...
0x00000000 in ?? ()
(gdb) mon halt
target halted in ARM state due to debug-request, current mode: Supervisor
cpsr: 0x000001d3 pc: 0x00008000
(gdb) mon reg pc
pc (/32): 0x00008000
(gdb) mon resume
If you get to this point, it's good news.
Downloading a new image
Let's say we have the new U-Boot image stored at /tmp/u-boot.bin
. For the Raspberry Pi, it's a flat binary rather than an ELF, which makes executing it a little easier; no need to calculate the jump offset, just jump into the start of where we loaded the binary.
Let's load it:
(gdb) mon halt
(gdb) mon load_image /tmp/u-boot.bin 0x8000 bin
546236 bytes written at address 0x00008000
downloaded 546236 bytes in 3.301985s (161.549 KiB/s)
Cool. Let's do a sanity check and verify that everything was sent correctly (load_image should have done this, but it's easy to check):
(gdb) mon verify_image /tmp/u-boot.bin 0x8000
verified 546236 bytes in 0.122922s (4339.610 KiB/s)
It's that easy. Now let's jump the CPU to address 0x8000
and resume it so that it can execute our new code. Luckily, the resume command takes an optional parameter for this:
(gdb) mon resume 0x8000
Looking over at your UART console, you should see U-Boot running with its normal printout.
Wrapping things up: a script
Sure, I'll admit that compared to swapping SD cards out, what we've achieved so far is worlds better. However we can squeeze it to be just a little bit simpler. You'll notice that all of the commands I performed in GDB weren't actually GDB commands (excepting target remote :3333
). They were mon
commands, which basically lets GDB delegate to the GDB server—OpenOCD in this instance. It's entirely possible to have sent the halt
, load_image
etc commands directly to the OpenOCD server on its telnet port, 4444. However, GDB is more readily scripted, so I wrote a small shell script that runs these commands in GDB in order to automatically halt, load, and resume the board. You can find it in the same repo as the infinite loop source.