Turns an ordinary ESP32-S3 dev board into a CMSIS-DAP debug probe: OpenOCD, pyOCD and PlatformIO talk to it as a standard adapter, and it drives SWD on two GPIOs of your choosing.
It is Seeed's Seeed_Arduino_DAPLink
for the CMSIS-DAP protocol — the same code the XIAO Debug Mate ships — on the
ESP32 Arduino core's own USB stack rather than Adafruit_TinyUSB. That choice
is the whole point of this repository; see Why not Adafruit_TinyUSB below.
Verified against an STM32G071:
CMSIS-DAP: FW Version = 2.0.0
CMSIS-DAP: Serial# = DAPLINK-S3-1
SWD DPIDR 0x0bc11477
[stm32g0x.cpu] Cortex-M0+ r0p1 processor detected — Examination succeed
ESP32-S2 and ESP32-S3 only. The chip has to enumerate as an arbitrary USB device, which needs the native USB-OTG controller. Classic ESP32 has no USB peripheral at all; the C3, C6 and H2 have only the fixed-function USB Serial/JTAG controller, which cannot present a CMSIS-DAP HID interface — no firmware works around that.
| signal | default GPIO | note |
|---|---|---|
| SWDIO | 3 | PIN_SWDIO in platformio.ini |
| SWCLK | 1 | PIN_SWCLK |
| GND | — | required |
| nRESET | 38 | optional, for connect-under-reset |
| TDO / TDI | 42 / 41 | JTAG only, unused for SWD |
log UART (Serial) |
43 / 44 | UART0 — the USB port belongs to CMSIS-DAP |
SWD needs only SWDIO, SWCLK and GND. VTref is not used: the target runs from its own supply. Any free GPIOs will do — but keep them off the S3's strapping pins (0, 3, 45, 46) where you can. The defaults deliberately move nRESET and the status LEDs off GPIO45/46, which the library drives as outputs and which select VDD_SPI voltage and ROM-log enable at reset.
pio run -e corehid
pio run -e corehid -t upload --upload-port /dev/cu.usbmodemXXXXTwo ESP32-S3 flashing details that cost real time if you meet them cold:
- The board manifest hard-codes
-DARDUINO_USB_MODE=1. A-D...=0inbuild_flagsonly shadows it, and which one wins depends on command-line order — the build ends up with both.board_build.extra_flagsis replaced outright here so OTG mode actually takes effect. - USB persist. After esptool writes over the ROM's USB-Serial/JTAG, the S3
latches that peripheral across a soft reset, so correct OTG firmware still
cannot take the port.
esptool --after watchdog_reset, or a power cycle, clears it; a plain--after hard_resetdoes not.
Because the firmware takes the native USB port, the board stops being a serial
port once it runs. To avoid needing the BOOT button on every iteration it waits
USB_TAKEOVER_DELAY_MS (10 s) before claiming USB, during which the ROM's
USB-Serial/JTAG is still enumerated. PlatformIO's own start-up eats most of that
window, so latch the chip with esptool first and write at leisure:
esptool --port $P --before default_reset --after no_reset flash_id
esptool --port $P --chip esp32s3 --before no_reset --after no_reset write_flash -z \
0x0 .pio/build/corehid/bootloader.bin \
0x8000 .pio/build/corehid/partitions.bin \
0x10000 .pio/build/corehid/firmware.binEvery CMSIS-DAP adapter shares VID:PID 0x0d28:0x0204, so OpenOCD takes
whichever it enumerates first. This firmware sets a distinct USB serial and
product string (DAPLINK_SERIAL, DAPLINK_PRODUCT), so a specific probe can be
selected:
adapter driver cmsis-dap
adapter serial DAPLINK-S3-1 ;# cmsis_dap_serial on OpenOCD < 0.12OpenOCD identifies an adapter by finding the substring CMSIS-DAP inside the
product string, so a name like CMSIS-DAP S3 probe 2 still works.
On STM32 parts where SWCLK doubles as BOOT0 (the G0 family's PA14-BOOT0), boot
mode is sampled from that pin at every reset. A probe that idles SWCLK at
INPUT_PULLUP — the usual CMSIS-DAP convention, and the library's default — puts
the ESP32's ~45 kΩ pull-up against the board's typical 100 kΩ pull-down. The
pull-up wins, and the target boots system memory instead of your image, every
time, silently: programming succeeds, verify passes, and the firmware never runs.
The tell is pc reading 0x1fff_xxxx after a halt.
-DDAPLINK_SWCLK_NO_PULL (on by default here) idles SWCLK floating and lets the
target's own pull-down define BOOT0. SWD does not need an idle pull on the clock.
To rescue a target already stuck there, set the vector table as well as the PC —
jumping to the reset handler alone leaves VTOR pointing at ROM, so polled code
runs for a frame or two and then every interrupt lands in the bootloader:
halt
mww 0xE000ED08 0x08000000
reg msp <word at 0x08000000>
reg pc <word at 0x08000004, thumb bit cleared>
resumeThe obvious build — Seeed's library with Adafruit_TinyUSB, as the Debug Mate
does under the Arduino IDE — enumerates, but as the wrong device: VID:PID
0x303a:0x1001, the board name as product string, and zero interfaces.
On arduino-esp32 the core owns the tud_descriptor_*_cb callbacks and builds the
configuration descriptor from interfaces registered through
tinyusb_enable_interface(). Adafruit's ESP32 path never calls that — it only
fills its own descriptor structs, and the hardware is brought up by the core's
USB.begin(). The two libraries also define the same tud_hid_* callbacks, so
only one of them can be linked at all.
src/main_corehid.cpp therefore keeps the DAPLink protocol core and takes USB
from the core's USBHID. deadend/main_adafruit_tinyusb.cpp is the Adafruit
version, kept as the record of that route; it is not built.
env:diag is the bisection tool that settled it: plain core USB CDC, no TinyUSB
library, no DAPLink. If that enumerates, the OTG controller is fine and the fault
is above it — and it also hands you a console on the native port, which the probe
firmware cannot.
lib/Seeed_Arduino_DAPLink is Seeed's library (MIT) with two documented edits:
DAP_config.h: the ESP32 pin defines are wrapped in#ifndefsoplatformio.inican override them, andDAPLINK_NO_ADAFRUIT_TINYUSBdrops the hard#include "Adafruit_TinyUSB.h".DAP_config.cpp: SWCLK idles floating underDAPLINK_SWCLK_NO_PULL.DAP.cpp:DAP_ID_SER_NUManswers fromDAPLINK_SERIALinstead of Adafruit'sTinyUSBDevice.
MIT — see LICENSE. The vendored library is MIT, Copyright (c) Seeed
Technology Co., Ltd.; its own licence text is kept in
lib/Seeed_Arduino_DAPLink/LICENSE.