Arduino Uno Q Kernel JTAG Debugging

2026-06-30

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.

A glasgow board connected to an Arduino Uno Q board that has a little pin header breakout PCB attached to some testpoints. Both boards are powered up and have several LEDs lit.
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.

screenshot of kicad showing testpoints
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 back side of the Uno Q shown in the header picture. Thin copper wires are visible that connec the tiny testpoints at the board edge to a green breakout PCB. The breakout PCB has two rows of pin headers with empty white label fields next to them. The pins are numbered.
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