As part of my work on the Ashen project, I'm currently doing some Linux kernel work. Since the AI bubble made normal
Raspberry Pis unaffordable, I was shopping around for another, less expensive dev board. After some looking around, I
settled on the Arduino Uno Q, a devboard in the venerable Arduino Uno form factor that comes with a chonky
STM32U585 microcontroller instead of the ATMega328 of yore, and that brings a full-blown Linux SoC instead of the
ATMega32U4 with its USB/UART firmware that the original Arduino Unos used.
The debug setup in question. What you see is a glasgow_ connected to the Arduino Uno Q target.
The "Q" suffix in the name stands for Qualcomm, since this board clearly is a result of Arduino being acquired by them.
Of course, the Linux SoC at the heart of the board is a Qualcomm chip: A QRB2210, which is a fairly barebones chip
trimmed for IoT applications. It still packs a punch however, bringing four 64-bit ARM cores. What's really nice about
the whole package is that the board comes with 4 GB of RAM, 32 GB of onboard eMMC flash and onboard Wifi for only about
60€! A Raspberry Pi with a similar spec costs about twice that.
While working on the linux kernel on this board I hit some troubles with kernels getting hung up in odd places (during
kdump/kexec), which is hard to debug when all you have is a serial port. Being a seasoned embedded engineer, my
immediate instinct was to reach for JTAG to directly access the four CPU cores' state and registers from a debugger.
However, to my slight disappointment, I was unable to find any documentation on how to get that up and running on this
particular target. After some messing around, and pointing an LLM at the problem of identifying the unknown JTAG base
addresses, here's what I found out.
JTAG Test Points
First off, note that the board has JTAG ports in two places. The STM32 microcontroller, which is the actual thing
you're supposed to program on this board, has its JTAG hooked up to GPIOs on the QRB2210, where software bitbangs it to
program the microcontroller. The STM32 JTAG is not what this article is about!
We want to access the QRB2210 host SoC running linux. The physical JTAG port of this chip is brought out on a row of
test points on the bottom side of the board along the right edge, near the 6-pin SPI pin header. To better access it, I
attached a debug pin header breakout board I made a while ago (gerbers), to which I connected a glasgow. glasgow is an
embedded multitool that can simultaneously do UART, JTAG, and others and for this sort of project brings everying I need
on one board. Notably, it has adjustable IO voltages that can be configured for the 1.8V the QRB2210 requires.
The testpoints on the bottom layer, at the right edge of the board shown in Kicad.
The pinout of the JTAG testpoints is most easily seen by opening the Allegro .brd file provided in the "CAD files"
download on the Arduino Uno Q documentation page in Kicad. When looking at the board from the back with the 6-pin SPI
pin header on the left side, the testpoints are (from top to bottom): PS_HOLD, TRST, TMS, TDO, TDI,
TCK, and SRST. PS_HOLD is a logic input separate from JTAG that can be used to tell the board's power
management IC (PMIC) to power cycle the board. The other pins are all hooked up to the QRB2210's JTAG port. SRST is
optional, and doesn't have to be connected for JTAG to work. The other pins are needed. The SPI connector's pin that is
directly next to the PS_HOLD pin is GND and can be used to provide an easy ground for the JTAG probe.
The wiring between the testpoints and the breakout board (gerbers_).
Power supply interface
You could theoretically use JTAG to talk to the SoC's system control periphery to issue a reset. However, this is
fragile because the SoC's JTAG machinery is rather complex, and it is very much possible to get its state machines so
hung up that openocd can't do anything anymore. Thus, a more reliable way to reset the target is needed. In practice,
the most reliable method is also the most basic: Just re-apply power. Luckily, the board's designers thought of this and
put a little logic-controlled power switch right on the board's USB 5V input. Conveniently, this switch can be accessed
without any soldering through the VBUS_POWER_SWITCH_DISABLE_N signal on pin 10 of the 10-pin JCTL1 pin header near
the USB port that also carries the SoC's UART signals. Simply shorting that pin to ground for a few microseconds is
enough to cleanly reboot the entire board, including both the SoC and the STM32.
Emergency Download Mode
On the same JCTL1 header we find another signal that is very useful for low-level kernel debugging: When the board is
power-cycled while the FORCED_USB_BOOT_N signal on pin 2 of JCTL1 is shorted to ground, instead of booting into
eMMC, the SoC's internal ROM bootloader will enter a USB "emergency download" (EDL) mode. In that mode, the open-source
QDL tool can be used to directly flash (part of) the eMMC. Arduino's own recovery utility uses this to restore a
bricked board. Since EDL mode is handled entirely by ROM code inside the SoC, it's independent of what you do to the
eMMC and as long as you don't physically destroy the SoC (and stay away from its fuses), it should be able to recover
any bricked board.
Glasgow setup
glasgow can be used to connect to both JTAG and UART at the required 1.8V logic level. Using a little patch that I
wrote, it can also be used to control the power supply and emergency download mode pins on the JCTL1 header through
some leftover GPIOs while also doing JTAG and UART. After applying the patch, glasgow can be invoked like this to start
all three interfaces:
$ glasgow multi \
jtag-openocd -V 1.8 --tck B0 --tms B3 --tdi B1 --tdo B2 --trst B4 -f 2500 unix:/tmp/jtag.sock ++ \
uart -V 1.8 --rx A1 --tx A0 -b 115200 tty ++ \
control-gpio -V 1.8 --pins A4,A5,A6,A7 --socket unix:/tmp/gpio
This invocation lets glasgow open a unix socket that OpenOCD's remote bitbang driver can connect to. Simultaneously, it
will relay UART input and output to the TTY, and open another unix socket for GPIO control. GPIOs can be controlled like
this:
$ echo -e 'A5=0\nA5=Z' | socat - UNIX-CONNECT:/tmp/gpio
OpenOCD Configuration
Since I wasn't able to find a ready-made openocd configuration for this chip, or any other documentation on its debug
interface for that matter, I had a LLM figure out the debug access information by brute force. This approach was pretty
effective, and resulted in success within a day. It was also one of the weirder working experiences of my life.
The LLM evidently evaluated the task described by my prompt to be tedious, and tried really hard to convince me it was
unnecessary. I ended up having to reset the LLM's session a few times when it started hallucinating or ran of in an
unproductive direction, but after some attempts despite its whining, the LLM generated a working configuration. Based on
that config, I created the (tested, working) congfiguration below. Feel free to copy it for your enjoyment.
adapter driver remote_bitbang
transport select jtag
remote_bitbang host /tmp/jtag.sock
reset_config none
jtag newtap auto0 tap -irlen 4 -expected-id 0x5ba00477
jtag newtap auto1 tap -irlen 11 -expected-id 0x001c80e1
dap create auto0.dap -chain-position auto0.tap -ignore-syspwrupack
cti create auto0.cti0 -dap auto0.dap -ap-num 1 -baseaddr 0x87020000
cti create auto0.cti1 -dap auto0.dap -ap-num 1 -baseaddr 0x87120000
cti create auto0.cti2 -dap auto0.dap -ap-num 1 -baseaddr 0x87220000
cti create auto0.cti3 -dap auto0.dap -ap-num 1 -baseaddr 0x87320000
target create auto0.a53.0 aarch64 -dap auto0.dap -ap-num 1 -coreid 0 \
-dbgbase 0x87010000 -cti auto0.cti0 -gdb-port 3333
target create auto0.a53.1 aarch64 -dap auto0.dap -ap-num 1 -coreid 1 \
-dbgbase 0x87110000 -cti auto0.cti1 -gdb-port 3334
target create auto0.a53.2 aarch64 -dap auto0.dap -ap-num 1 -coreid 2 \
-dbgbase 0x87210000 -cti auto0.cti2 -gdb-port 3335
target create auto0.a53.3 aarch64 -dap auto0.dap -ap-num 1 -coreid 3 \
-dbgbase 0x87310000 -cti auto0.cti3 -gdb-port 3336
# Comment out if you want to debug issues during shutdown/suspend when all four cores may not be active
target smp auto0.a53.0 auto0.a53.1 auto0.a53.2 auto0.a53.3
# AXI Memory AP, accesses past the CPU caches
target create auto0.axi mem_ap -dap auto0.dap -ap-num 0
gdb breakpoint_override hard