- C 91.7%
- Assembly 7.6%
- CMake 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| 01_blinky | ||
| debugprobe-src | ||
| .gitignore | ||
| debugprobe_custom.uf2 | ||
| README.md | ||
| rp2350.cfg | ||
Pico 2 GDB Debugging Guide
This repository contains:
- A C SDK blinky example for the Raspberry Pi Pico 2 (
01_blinky/). - Custom compiled debugprobe firmware (
debugprobe_custom.uf2) for using a Waveshare RP2040-Zero as an SWD programmer/UART bridge. - OpenOCD target configuration files (
rp2350.cfg). - A step-by-step walkthrough explaining the Linux USB permissions and driver binding mechanisms.
1. Hardware Setup & Wiring
The Waveshare RP2040-Zero acts as the CMSIS-DAP Debug Probe, while the Raspberry Pi Pico 2 (RP2350) is the Target.
We use the standard canonical Raspberry Pi Debug Probe pin layout on the RP2040-Zero:
| RP2040-Zero (Probe Pin) | Target Pico 2 (Pin) | Purpose |
|---|---|---|
| GP2 | SWCLK (C pin) | SWD Clock |
| GP3 | SWDIO (D pin) | SWD Data I/O |
| GND | GND (G pin) | Ground reference |
| GP4 (UART TX) | GP1 (RX pin) | UART Target RX |
| GP5 (UART RX) | GP0 (TX pin) | UART Target TX |
2. Official Debugprobe Firmware
We use the official precompiled debugprobe_on_pico.uf2 firmware. Because we are using the standard hardware-mapped pins, the following features work out-of-the-box:
- Hardware UART: Exposes a stable serial bridge utilizing the RP2040 hardware
uart1peripheral on GP4/GP5, which includes a hardware 32-element FIFO and native interrupts to prevent data loss. - 1200-Baud Bootloader Reset: Automatic rebooting to BOOTSEL mode via
picotool reboot -f -u(or serial connection at 1200 baud) is fully supported, allowing the probe to be reprogrammed or reset over USB without pressing the physical BOOT/RST buttons.
3. The Linux USB & OpenOCD "Magic" Explained
why ST-Link v2 Fails with Pico 2
Standard ST-Link v2 debuggers are High-Level Adapters (HLA). They wrap JTAG/SWD commands inside a closed USB protocol and do not expose raw Access Port (AP) or Debug Access Port (DAP) registers. The dual-core start sequence and security architecture of the RP2040/RP2350 require raw, low-level register manipulation, which HLA programmers cannot perform.
Instead, we use a CMSIS-DAP probe (the RP2040-Zero), which translates raw CMSIS USB packets directly into low-level SWD bit-bang signals.
CMSIS-DAP v1 (HID) vs v2 (Bulk)
- CMSIS-DAP v1: Uses the USB Human Interface Device (HID) class. It creates a
/dev/hidrawXnode. Under Linux, these nodes default to ownerroot:rootand permission mode0600, preventing normal users from connecting. - CMSIS-DAP v2: Uses high-speed USB Bulk endpoints. It does not use the HID subsystem, meaning it communicates via raw USB buffers rather than
/dev/hidrawX.
Resolving Permissions (udev)
For your user to access the raw USB device nodes without running OpenOCD as sudo, we installed a udev rule in /etc/udev/rules.d/99-pico-debug.rules:
# USB rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000c", TAG+="uaccess", MODE="660", GROUP="plugdev"
SUBSYSTEM=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000d", TAG+="uaccess", MODE="660", GROUP="plugdev"
# HID rules (fallback for v1)
KERNEL=="hidraw*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000c", TAG+="uaccess", MODE="660", GROUP="plugdev"
KERNEL=="hidraw*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000d", TAG+="uaccess", MODE="660", GROUP="plugdev"
The systemd ACL manager (TAG+="uaccess") dynamically grants the active desktop session user read/write access to the device node, while fallback group permissions (GROUP="plugdev") grant access to any members of the plugdev group.
The Kernel Driver Conflict (can't set config #1)
The debug probe exposes a composite USB device:
- Interface 0: CMSIS-DAP v2 Bulk
- Interface 1 & 2: CDC Serial UART (claimed by the kernel
cdc_acmdriver to expose/dev/ttyACM0) - Interface 3: CMSIS-DAP v1 HID (unbound)
When you plug in the board, the Linux kernel immediately claims Interfaces 1 & 2. When OpenOCD starts and calls libusb_set_configuration(1) to initialize the device, the request fails with can't set config #1, error -32 (EPIPE/Stall) because the CDC interface is already claimed by the kernel.
To fix this:
- We corrected the descriptor parsing offsets in the firmware, ensuring TinyUSB never gets misaligned when parsing the configuration descriptor.
- In OpenOCD, raw bulk communication handles data directly, leaving the kernel serial driver undisturbed.
4. Running OpenOCD
To launch the GDB server bridge using our custom-compiled OpenOCD (which has the rp2040_flash driver built-in), run the following command from the root of this repository:
./openocd-src/src/openocd -s ./openocd-src/tcl -f interface/cmsis-dap.cfg -c "adapter speed 1000" -f ./rp2350.cfg
Once running, OpenOCD will bind GDB server ports:
- Port 3333: Target Core 0 (
rp2350.cm0) - Port 3334: Target Core 1 (
rp2350.cm1)
5. Running the Debug Session (GDB) & SWD Flashing
Because this OpenOCD version is compiled with the rp2040_flash driver, the target board (Pico 2) only needs power (which can come from a USB charger or raw power pins). It does not need to be plugged into the PC via USB.
-
Compile your blinky example with debug symbols:
mkdir -p 01_blinky/build && cd 01_blinky/build cmake -DPICO_BOARD=pico2 -DPICO_PLATFORM=rp2350 .. make -j$(nproc) -
Ensure OpenOCD is running in another terminal.
-
Launch
gdb-multiarch:gdb-multiarch blinky.elf -
Inside the GDB prompt, connect to the GDB server:
(gdb) target remote localhost:3333 -
Load the program onto the Pico 2 flash over the SWD wires and run:
(gdb) monitor reset halt (gdb) load (gdb) break main (gdb) continue
6. Multi-Core Debugging (Cortex-M33 Core 0 & Core 1)
The RP2350 has a dual-core architecture. OpenOCD allows you to debug both cores in parallel using one of two methods:
Method A: SMP Mode (Default)
In SMP (Symmetric Multiprocessing) mode, OpenOCD runs a single GDB server on port 3333 and presents both cores as threads of a single process.
- View active cores:
This lists(gdb) info threadsThread 1(Core 0) andThread 2(Core 1). - Switch focus to a core:
To switch GDB's focus to Core 1:
(gdb) thread 2 - Synchronized control: resumptions, halts, and breakpoints are synchronized. If one core hits a breakpoint, OpenOCD automatically halts both cores to prevent race conditions or state desynchronization.
Method B: Isolated Mode
If the cores are running decoupled tasks and you want to debug one without interrupting the execution of the other, you can run them as completely independent GDB servers.
- To disable SMP, start OpenOCD with the
USE_SMPflag overridden to0:./openocd-src/src/openocd -s ./openocd-src/tcl -f interface/cmsis-dap.cfg -c "adapter speed 1000" -c "set USE_SMP 0" -f ./rp2350.cfg - Now OpenOCD will bind two separate GDB ports:
- Port 3333: Dedicated exclusively to Core 0 (
rp2350.dap.core0) - Port 3334: Dedicated exclusively to Core 1 (
rp2350.dap.core1)
- Port 3333: Dedicated exclusively to Core 0 (
- You can open two separate terminals and launch two separate GDB sessions (
target remote :3333andtarget remote :3334) to control the cores independently.