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.
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:
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"
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
Booting the board at this point should result in some decent output from the long-mysterious
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
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:
|VTG||N/A||Pin 1 or 17*|
|GND||N/A||Pin 14 or 20**|
* 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.
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.
Luckily for me, OpenOCD already has an interface configuration shipping with it for the Bus Blaster. For me, it's installed to
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
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
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.