The plan doc records why the fork exists and how each vendor source was ported; the interim handoff it superseded is dropped. CLAUDE.md: a new worktree should symlink the dependency dirs to the primary checkout rather than re-fetching them, replacing a single symlink only when the branch needs a different dep revision. Also allow 'linke' in codespell - WCH-LinkE is a product name.
36 KiB
Unified OpenOCD Fork (hathach/openocd) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: One OpenOCD fork at hathach/openocd (default branch tinyusb) that flashes, debugs and RTT-captures every TinyUSB rig target — RP2040, RP2350 (arm + riscv), all WCH CH32/CH5xx, Analog Devices MAX32, and Espressif — replacing the four separate OpenOCD trees on ci.
Architecture: Fork openocd-org/openocd master (mainline is 1610 commits ahead of the RPi fork base and now the sole home of RISC-V support). Layer on top: 4 RP2350 TCL configs from the RPi fork, 1 ported max32665 TCL config from the ADI fork, the wlinke adapter + sdi transport + WCH flash drivers from hathach/riscv-openocd-wch (driving CH32 with mainline's riscv target if the DTM hypothesis holds), and ESP32-P4 TCL configs adapted from espressif/openocd-esp32 onto mainline's generic-riscv ESP pattern. ESP32/S2/S3/C3/C6/H2 debug is already in mainline; ESP flash stays with esptool.
Tech Stack: OpenOCD (autotools, C), TCL configs, GitHub CLI, TinyUSB HIL rig (hil_test.py, board_lock.py).
Global Constraints
- Everything runs on ci (this machine is the rig — hostname
ci); no SSH hop needed. - Repo:
hathach/openocd, default branchtinyusb, source clone at~/app/openocd, install prefix$HOME/app/openocd_tinyusb. - One commit per downstream fork on the
tinyusbbranch: one for raspberrypi/openocd, one for analogdevicesinc/openocd, one for riscv-openocd-wch, one for espressif/openocd-esp32 (plus the initial README commit). Iterate withgit commit --amend/ squash before declaring a task done. - No
Co-Authored-By: Claude/Claude-Session:trailers in any commit. ~/.local/bin/openocd_wch(symlink) and~/app/openocd_wch_newstay untouched until Task 8's 4/4 WCH boards pass — it is the rig's only CH32 flasher. Backup exists at~/.local/bin/openocd_wch.bak-20260727.- Hold a board lock for every hardware step:
python3 test/hil/board_lock.py hold <board> --reason "openocd-unified verify"; release after. Never stop the actions-runner. - WCH RTT: always
rtt polling_interval 1; neverreset runinside an SDI session (target does not come back). pkill -x openocd— neverpkill -f(pattern matches your own shell).libjim-devis required to configure mainline; all build deps are already installed on ci (mainline was built here 2026-07-27).- Back up before replacing
/usr/local/bin/openocd; the current binary is the RPi-fork build (byte-identical to~/app/openocd_rpi/src/openocd). - Do not modify the TinyUSB checkout at
~/code/tinyusbexcept where a task explicitly says so (hil_test.py WCH cfg template, on aclaude/-prefixed branch). Nevergit stash -uin a TinyUSB worktree. - OpenOCD resolves its scripts dir relative to the realpath of the binary — repoint via symlink into an installed prefix, never a bare copy of the binary.
Reference: current state (measured 2026-07-27, in OPENOCD_UNIFIED_FORK_HANDOFF.md)
| Tree on ci | Repo @ commit | Role |
|---|---|---|
~/app/openocd_rpi |
raspberrypi/openocd @ ebec9504d (sdk-2.0.0) |
rig default (/usr/local/bin/openocd) |
~/app/openocd_adi |
analogdevicesinc/openocd @ 5fc33af |
max32666fthr (~/app/openocd_adi/src/openocd) |
~/app/riscv-openocd-wch |
hathach/riscv-openocd-wch @ ccb04d7 |
CH32 flash+RTT (~/.local/bin/openocd_wch) |
~/app/openocd-mainline |
openocd-org/openocd @ 43441cd83 |
candidate build, verified on pico/pico2/max32666fthr |
Rig flasher entries (test/hil/tinyusb.json): openocd (pico ×3, fruit_jam, stm32h743nucleo, stm32g0b1nucleo), openocd_adi (max32666fthr), openocd_wch (nanoch32v203, ch32v103r_r1_1v0, ch32v307v_r1_1v0, ch582m_evt), esptool (espressif_s3_devkitm, espressif_p4_function_ev).
Task 1: Create hathach/openocd, tinyusb branch, README
Files:
- Create:
~/app/openocd/(clone),~/app/openocd/README.md
Interfaces:
-
Produces: GitHub repo
hathach/openocdwith default branchtinyusb; local clone~/app/openocdwith remotesorigin(hathach) andupstream(openocd-org). All later tasks commit to this clone'stinyusbbranch. -
Step 1: Fork and clone
gh repo fork openocd-org/openocd --clone=false
git clone --recursive https://github.com/hathach/openocd.git ~/app/openocd
cd ~/app/openocd
git remote add upstream https://github.com/openocd-org/openocd.git
git checkout -b tinyusb origin/master
- Step 2: Verify the clone is at mainline HEAD
Run: cd ~/app/openocd && git log --oneline -1
Expected: 43441cd83 server: add 'services' command to list service information or newer.
- Step 3: Write
README.md(new file — GitHub renders it instead of mainline's plain-textREADME, and leavingREADMEuntouched keeps future rebases conflict-free)
# OpenOCD for the TinyUSB test rig
One OpenOCD build that flashes, debugs and RTT-captures every board family on
the [TinyUSB](https://github.com/hathach/tinyusb) hardware-in-the-loop rig, so
the rig does not need four different OpenOCD trees.
This is the `tinyusb` branch, tracking
[openocd-org/openocd](https://github.com/openocd-org/openocd) `master`.
Everything not listed below is unmodified mainline.
## Cherry-picked / ported from
| Source repo | What we took |
| --- | --- |
| [raspberrypi/openocd](https://github.com/raspberrypi/openocd) (`sdk-2.0.0`) | `tcl/target/rp2350-riscv.cfg`, `rp2350-rescue.cfg`, `rp2350-dbgkey-secure.cfg`, `rp2350-dbgkey-nonsecure.cfg`. The RP2040/RP2350 C flash driver is already better in mainline (`rp2xxx.c`). |
| [analogdevicesinc/openocd](https://github.com/analogdevicesinc/openocd) (`release`) | `tcl/target/max32665.cfg` (MAX32665/MAX32666), re-ported onto mainline's `max32xxx_common.cfg`. The fork's QSPI block is dropped — it is guarded by `QSPI_ENABLE`, which this part sets to 0. |
| [hathach/riscv-openocd-wch](https://github.com/hathach/riscv-openocd-wch) (originally [dragonlock2/miscboards](https://github.com/dragonlock2/miscboards) WCH SDK) | `wlinke` adapter driver, `sdi` single-wire transport, and the WCH flash drivers (`wch_riscv`, `wch_arm`) for CH32V/CH32F/CH5xx over WCH-Link/LinkE. |
| [espressif/openocd-esp32](https://github.com/espressif/openocd-esp32) | `tcl/target/esp32p4.cfg` + `tcl/board/esp32p4-builtin.cfg`, adapted to mainline's generic RISC-V ESP pattern. ESP32/S2/S3/C3/C6/H2 debug is already in mainline; ESP flash programming stays with `esptool`. |
## Build
./bootstrap
./configure --enable-jlink --enable-cmsis-dap --enable-stlink \
--enable-wlinke --disable-werror
make -j$(nproc)
`libjim-dev` is required — mainline no longer builds the bundled jimtcl by
default and configure hard-fails without it.
- Step 4: Commit, push, set default branch
cd ~/app/openocd
git add README.md
git commit -m "README: purpose of the tinyusb branch and its downstream sources"
git push -u origin tinyusb
gh repo edit hathach/openocd --default-branch tinyusb \
--description "OpenOCD for the TinyUSB test rig - one build for RP2040/RP2350, WCH CH32, MAX32 and ESP32 targets"
- Step 5: Verify default branch
Run: gh repo view hathach/openocd --json defaultBranchRef -q .defaultBranchRef.name
Expected: tinyusb
Task 2: Build the fork on ci
Files:
- Create:
~/app/openocd_tinyusb/(install prefix)
Interfaces:
-
Consumes:
~/app/openocdclone from Task 1. -
Produces:
~/app/openocd_tinyusb/bin/openocd(installed binary + scripts at~/app/openocd_tinyusb/share/openocd/scripts/). Every later flash/verify step uses this path. -
Step 1: Configure and build (same recipe that already worked for mainline on this box)
cd ~/app/openocd
./bootstrap
./configure --prefix=$HOME/app/openocd_tinyusb \
--enable-jlink --enable-cmsis-dap --enable-stlink --disable-werror
make -j$(nproc) && make install
- Step 2: Verify version and adapters
Run: ~/app/openocd_tinyusb/bin/openocd --version 2>&1 | head -1
Expected: Open On-Chip Debugger 0.12.0+dev-... with a -g<sha> matching git -C ~/app/openocd rev-parse --short HEAD.
Run: ~/app/openocd_tinyusb/bin/openocd -c 'adapter list; shutdown' 2>&1 | grep -E 'cmsis-dap|jlink|stlink'
Expected: all three listed.
(No commit — build products only.)
Task 3: Import the 5 TCL configs — one commit per downstream fork
Files:
- Create:
~/app/openocd/tcl/target/rp2350-riscv.cfg,rp2350-rescue.cfg,rp2350-dbgkey-secure.cfg,rp2350-dbgkey-nonsecure.cfg,max32665.cfg - Source of truth:
~/code/tinyusb/openocd-unified-configs/(the copies already hardware-verified this week; the max32665 port is already written there)
Interfaces:
-
Consumes:
~/app/openocd+ install prefix from Task 2. -
Produces:
target/rp2350-riscv.cfgandtarget/max32665.cfgresolvable viafindin the installed scripts dir — Task 4 flashes with them. -
Step 1: Copy the RPi configs and commit (downstream commit #1)
cd ~/app/openocd
cp ~/code/tinyusb/openocd-unified-configs/rp2350-riscv.cfg \
~/code/tinyusb/openocd-unified-configs/rp2350-rescue.cfg \
~/code/tinyusb/openocd-unified-configs/rp2350-dbgkey-secure.cfg \
~/code/tinyusb/openocd-unified-configs/rp2350-dbgkey-nonsecure.cfg \
tcl/target/
git add tcl/target/rp2350-*.cfg
git commit -m "tcl/target: add RP2350 riscv/rescue/dbgkey configs from raspberrypi/openocd
Taken from raspberrypi/openocd branch sdk-2.0.0 @ ebec9504d. These four
configs are the only things that fork has which mainline lacks - the
rp2040/rp2350 C driver was consolidated upstream as rp2xxx.c. All four
use only mainline-present commands (swj_newdap, dap create -adiv6,
target create riscv -ap-num, riscv set_enable_virt2phys).
rp2350-riscv.cfg is what hw/bsp/rp2040/family.cmake requests when
PICO_PLATFORM=rp2350-riscv."
- Step 2: Copy the ADI config and commit (downstream commit #2)
cd ~/app/openocd
cp ~/code/tinyusb/openocd-unified-configs/max32665.cfg tcl/target/
git add tcl/target/max32665.cfg
git commit -m "tcl/target: add max32665 config ported from analogdevicesinc/openocd
Ported from analogdevicesinc/openocd @ 5fc33af onto mainline's
max32xxx_common.cfg (the ADI fork calls the same file max32xxx.cfg).
The fork's QSPI block is dropped: it is guarded by QSPI_ENABLE, which
this part sets to 0, and it needs the ADI-only max32xxx_qspi driver.
Covers MAX32665/MAX32666 (both flash banks). Hardware-verified on
max32666fthr 2026-07-27."
- Step 3: Install and verify the configs resolve
cd ~/app/openocd && make install
~/app/openocd_tinyusb/bin/openocd -c 'puts [find target/max32665.cfg]; puts [find target/rp2350-riscv.cfg]; shutdown'
Expected: both paths under ~/app/openocd_tinyusb/share/openocd/scripts/target/ printed; exit without "Can't find".
- Step 4: Push
cd ~/app/openocd && git push
Task 4: Hardware-verify every current-openocd board with the fork binary
Files:
- No source changes. Uses
~/code/tinyusbbuilds +test/hil/hil_test.py.
Interfaces:
- Consumes:
~/app/openocd_tinyusb/bin/openocdwith Task 3 configs installed. - Produces: evidence that the fork can replace
/usr/local/bin/openocd(Task 5's gate). PATH shim dir~/app/openocd_tinyusb/shim/reused by later tasks.
Boards (every openocd/openocd_adi flasher entry in tinyusb.json):
raspberry_pi_pico, raspberry_pi_pico_w, raspberry_pi_pico2, adafruit_fruit_jam, stm32h743nucleo, stm32g0b1nucleo, max32666fthr.
Already verified on plain mainline 2026-07-27: pico, pico2, max32666fthr (re-run anyway — the binary changed).
- Step 1: Build any missing firmware sets (repeat per board without
examples/cmake-build-<board>;cmake-build-raspberry_pi_pico,-stm32g0b1nucleo,-max32666fthralready exist)
cd ~/code/tinyusb/examples
cmake -B cmake-build-raspberry_pi_pico2 -DBOARD=raspberry_pi_pico2 -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel . \
&& cmake --build cmake-build-raspberry_pi_pico2
(Same pattern for raspberry_pi_pico_w, adafruit_fruit_jam, stm32h743nucleo. If a board fails get_deps, run python3 tools/get_deps.py -b <board> first.)
- Step 2: Create the PATH shim (lets
hil_test.py's hardcodedopenocdresolve to the fork; symlink keeps scripts-dir resolution working because OpenOCD follows the realpath)
mkdir -p ~/app/openocd_tinyusb/shim
ln -sf ~/app/openocd_tinyusb/bin/openocd ~/app/openocd_tinyusb/shim/openocd
- Step 3: Smoke-flash one board directly (fast signal before the full suite)
python3 ~/code/tinyusb/test/hil/board_lock.py hold raspberry_pi_pico --reason "openocd-unified verify"
~/app/openocd_tinyusb/bin/openocd -c "adapter serial E6614103E72C1D2F" \
-f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" \
-c "program /home/hathach/code/tinyusb/examples/cmake-build-raspberry_pi_pico/device/cdc_msc/cdc_msc.elf verify reset exit"
Expected: ** Verified OK ** then ** Resetting Target **. Release the lock after (board_lock.py release raspberry_pi_pico).
- Step 4: Run the HIL suite for all 7 boards through the shim
cd ~/code/tinyusb
PATH=~/app/openocd_tinyusb/shim:$PATH \
python3 test/hil/hil_test.py test/hil/tinyusb.json \
-b raspberry_pi_pico -b raspberry_pi_pico_w -b raspberry_pi_pico2 \
-b adafruit_fruit_jam -b stm32h743nucleo -b stm32g0b1nucleo
Notes for the executor:
-
hil_test.pytakes the config as a positional arg and-bper board; it holds board locks itself (that is the board-lock protocol in CI — do not also hold manual locks aroundhil_test.pyruns). -
max32666fthr is not in this run: its
flash_openocd_adi()path uses the hardcodedOPENCOD_ADI_PATH = ~/app/openocd_adi(hil_test.py:408), which the shim can't intercept. Handle it in Step 4b instead. Do not edithil_test.pyfor this — the adi path disappears at cutover (Task 10 flipstinyusb.json's flasher entry to plainopenocdwith-f interface/cmsis-dap.cfg -f target/max32665.cfg). -
Expected: every board PASS in the report. Any failure: stop, diagnose (consult the
hilskill), do not proceed to Task 5. -
Step 4b: max32666fthr — manual flash with the fork, then tests with
--skip-flash
python3 ~/code/tinyusb/test/hil/board_lock.py hold max32666fthr --reason "openocd-unified verify"
~/app/openocd_tinyusb/bin/openocd -c "adapter serial E6614C311B597D32" \
-f interface/cmsis-dap.cfg -f target/max32665.cfg \
-c "program /home/hathach/code/tinyusb/examples/cmake-build-max32666fthr/device/cdc_msc/cdc_msc.elf verify reset exit"
python3 ~/code/tinyusb/test/hil/board_lock.py release max32666fthr
cd ~/code/tinyusb && python3 test/hil/hil_test.py test/hil/tinyusb.json -b max32666fthr -sf
Expected: ** Verified OK ** on the flash, then PASS with -sf (tests run against the firmware just flashed).
- Step 5: RTT smoke on the pico (mainline RTT was verified 2026-07-27; re-confirm on the fork build —
target-debugskill has the full flow)
Expected: RTT control block found, events stream, overflow 0.
Task 5: Repoint the rig default openocd
Files:
- Modify:
/usr/local/bin/openocd(→ symlink), remove Debianopenocdpackage
Interfaces:
-
Consumes: Task 4 all-green.
-
Produces:
which openocd→ fork for every rig user (hil_test.py, skills, CI). Rollback: restore/usr/local/bin/openocd.rpi-backup-20260727. -
Step 1: Back up and repoint
sudo cp -a /usr/local/bin/openocd /usr/local/bin/openocd.rpi-backup-20260727
sudo ln -sf $HOME/app/openocd_tinyusb/bin/openocd /usr/local/bin/openocd
openocd --version 2>&1 | head -1
Expected: fork version string (matches Task 2 Step 2).
- Step 2: Drop the Debian openocd (installed 2026-07-27 only to get a jlink-capable OpenOCD; the fork has
--enable-jlink)
sudo apt-get remove -y openocd
which -a openocd
Expected: only /usr/local/bin/openocd remains.
- Step 3: Re-verify through the default path (no shim)
cd ~/code/tinyusb
python3 test/hil/hil_test.py test/hil/tinyusb.json -b raspberry_pi_pico -b stm32g0b1nucleo -b raspberry_pi_pico2
Expected: 3/3 PASS. If CI kicks a workflow mid-way, board locks arbitrate — just wait.
Task 6: WCH part 1 — port the wlinke adapter + sdi transport (compiles, detects probe)
Files (all in ~/app/openocd, sources from ~/app/riscv-openocd-wch @ ccb04d7 — this copy already carries the GCC-14 fixes):
- Create:
src/jtag/drivers/wlinke.c(2041 lines, copy),src/jtag/sdi.c(~130 lines, port),src/jtag/sdi.h(if the fork has one — checkls ~/app/riscv-openocd-wch/src/jtag/sdi*) - Modify:
src/transport/transport.h(new transport id),src/jtag/interface.h(addsdi_opstostruct adapter_driver+struct sdi_driverdecl),src/jtag/interfaces.c(register driver),src/jtag/drivers/Makefile.am,src/jtag/Makefile.am,configure.ac(--enable-wlinke)
Interfaces:
- Consumes: fork clone + build tree.
- Produces:
openocd -c "adapter driver wlinke"works;wlink_*C exports (wlink_erase,wlink_write,wlink_getromram,wlink_reset,wlink_chip_reset,wlink_clean,wlink_flash_protect, …) available for Task 8's flash driver;sditransport selectable. Commit stays amend-in-progress — Tasks 6–8 squash into downstream commit #3.
Port notes gathered up front (verified against both trees 2026-07-27):
-
Fork wiring to replicate:
configure.ac:117(adapter list entry[[wlinke],[WLINKE Programmer],[WLINKE]]),:284-286(AC_ARG_ENABLE),:537,:737(AM_CONDITIONAL);src/jtag/drivers/Makefile.am:189(DRIVERFILES += %D%/wlinke.c);src/jtag/interfaces.c:154,274(extern + table entry). -
Mainline transports are now a fixed bitmask enum (
src/transport/transport.h:19-25:TRANSPORT_JTAG BIT(0)…TRANSPORT_SWIM BIT(6), plusTRANSPORT_VALID_MASK), andstruct transportselects byunsigned int id, not name. Add#define TRANSPORT_SDI BIT(7), extendTRANSPORT_VALID_MASK, and portsdi.c'stransport_registerto the id-based struct. -
SWIM is the exact precedent — ST's proprietary single-wire transport, wired upstream the same way this needs:
swim_opsfield atsrc/jtag/interface.h:363, its own transport bit, own command namespace. Mirror howgrep -rn swim src/transport/ src/jtag/interface.h src/jtag/swim.cis structured wherever the fork's 0.11-era pattern no longer matches mainline. -
The fork's
sdiop is a raw RISC-V DMI transfer:adapter_driver->sdi_ops->transfer(iIndex, iAddr, iData, iOP, oAddr, oData, oOP)(src/jtag/sdi.c:20-22) — keep that signature; Task 7 builds on it. -
wlinke.cincludes"cmsis_dap.h","hidapi.h","libusb_helper.h"and (spuriously)<windows.h>— drop/guard the windows include; hidapi + libusb helpers exist in mainline's drivers dir. -
Step 1: Copy
wlinke.candsdi.cin; make the wiring edits above -
Step 2: Reconfigure with wlinke and build
cd ~/app/openocd
./configure --prefix=$HOME/app/openocd_tinyusb \
--enable-jlink --enable-cmsis-dap --enable-stlink --enable-wlinke --disable-werror
make -j$(nproc) && make install
Expected: clean build (--disable-werror tolerates the fork's warning-dirty code; do fix outright errors).
- Step 3: Probe-detection test against real hardware (nanoch32v203's WCH-LinkE, serial
EBCA8F0670AF)
python3 ~/code/tinyusb/test/hil/board_lock.py hold nanoch32v203 --reason "wlinke port bring-up"
~/app/openocd_tinyusb/bin/openocd -c "adapter driver wlinke" \
-c "adapter serial EBCA8F0670AF" -c "transport select sdi" \
-c "init" -c "shutdown"
Expected: log lines identifying the WCH-Link probe (firmware version print from wlink_init), no crash. init may complain about missing target — probe identification is the pass signal. Keep the lock held into Task 7 (same board).
- Step 4: Snapshot as work-in-progress commit (will be amended/squashed through Task 8)
cd ~/app/openocd && git add -A && git commit -m "WIP: wch port (squash into single downstream commit before push)"
Do not push until Task 8 squashes.
Task 7: WCH part 2 — target spike: mainline riscv over wlink DMI
The hypothesis (from the handoff, sharpened by code reading): WCH-LinkE's sdi op is a raw DMI transfer, and mainline's riscv-013 target is just a DMI client. If mainline's riscv target can be fed by wlink DMI transfers, we skip porting wch_riscv.c/wch_riscv-013.c (~3.5k lines that #include <target/riscv/...> 0.11-era internals — the worst possible port surface).
Files:
- Modify:
src/jtag/drivers/wlinke.c(add the DTM bridge), possiblysrc/target/riscv/riscv-013.cshim hooks — decided by Step 1's reading.
Interfaces:
-
Consumes: Task 6's working adapter (lock on nanoch32v203 still held).
-
Produces: a
target create ... riscv(or, on fallback,wch_riscv) config shape that Task 8's flash/RTT/HIL work builds on. Records the decision in the WIP commit message. -
Step 1: Read mainline's DMI plumbing before writing anything
Read src/target/riscv/riscv-013.c (the dmi_op/riscv_batch layer) and src/target/riscv/riscv.c's riscv dmi_read/dmi_write command handlers (they exist — mainline's tcl/target/esp32c6.cfg calls them). Determine the narrowest insertion point, in order of preference:
- an existing DTM/DMI abstraction the adapter can implement directly (best);
- a jtag-DTM emulation inside
wlinke.c: exposejtag_opswhose queue executor decodes IR=DTMCS/DMI DR scans intosditransfers (the esp_usb_jtag-style approach, one level up); - nothing viable → fallback (Step 4).
- Step 2: Implement the chosen bridge; build
Same build command as Task 6 Step 2.
- Step 3: Hypothesis test on nanoch32v203 (write the test cfg to the scratchpad, not the repo)
# wch-mainline-riscv-test.cfg
adapter driver wlinke
adapter speed 6000
transport select sdi ;# or jtag, if Step 1 chose the jtag-DTM emulation
wlink_set_address 0x00000000
sdi newtap ch32 cpu -irlen 5 -expected-id 0x00001
target create ch32.cpu riscv -chain-position ch32.cpu
ch32.cpu configure -work-area-phys 0x20000000 -work-area-size 0x2800 -work-area-backup 1
init
Evidence criteria — all four must hold to call the hypothesis confirmed:
halt → "Target halted" with a sane pc
riscv dmi_read 0x11 → plausible dmstatus (nonzero, version field = 2 or 3)
mdw 0x20000000 4 → reads SRAM without error
resume → target runs again (LED blink / CDC re-enumerates)
- Step 4: Decision checkpoint — STOP if the hypothesis fails
If any criterion fails for reasons that look architectural (wlink protocol can't express raw DMI reads, QingKe deviates from the RISC-V debug spec in ways mainline won't tolerate), stop and report to the user with the evidence. The two fallback options, costed:
- (a) Port the fork's full WCH target stack:
src/target/wch_riscv.c(3033 ln) +wch_riscv-013.c+wch_riscv.h, plus the fork's core patches (all findable viagrep -rn 'riscvchip\|wlink_' src/in the fork:src/flash/nor/tcl.c5 hits,src/target/target.c5,src/server/gdb_server.c2). Hard: these files include 0.11-eratarget/riscv/*headers that clash with mainline's current riscv internals. - (b) Ship the unified fork without WCH C support and keep
openocd_wchas the rig's CH32 flasher indefinitely. Do not silently pick (a).
Task 8: WCH part 3 — flash drivers, RTT, 4-board HIL green, squash to downstream commit #3
Files:
- Create:
src/flash/nor/wchriscv.c(324 ln, copy),src/flash/nor/wcharm.c(897 ln, copy — CH32F ARM parts; self-contained memory-mapped driver, zero wlink deps),src/jtag/drivers/wlinke.h(new — prototypes for thewlink_*exports; the fork relied on implicit declarations) - Modify:
src/flash/nor/drivers.c(extern + table entries, fork pattern at its lines 93-94/170-171),src/flash/nor/Makefile.am(fork pattern at lines 78-79) - Modify (TinyUSB repo, separate branch):
test/hil/hil_test.pyWCH cfg template (~line 381) — only if Task 7 landed on the mainline-riscv target shape
Interfaces:
-
Consumes: Task 7's confirmed target shape +
wlink_*exports from Task 6. -
Produces: downstream commit #3 (single squashed commit, pushed);
~/.local/bin/openocd_wchrepointed at the fork; hil_test.py template branchclaude/hil-openocd-unifiedin the TinyUSB repo (unpushed — user pushes; "hold pushes" applies to the TinyUSB repo). -
Step 1: Copy the flash drivers, add
wlinke.h, wiredrivers.c/Makefile.am; build
Keep the flash driver's registered name wch_riscv — the rig's generated per-probe cfg does flash bank ... wch_riscv ... and Task 8 Step 4's template keeps working.
Fork quirk to not copy: the fork patched src/flash/nor/tcl.c (handle_flash_protect_check_command, its line ~414) to call wlink_softreset()/wlnik_protect_check() for WCH banks. Implement that inside wchriscv.c's own protect_check op instead — no core-file patch.
Check the fork's src/server/gdb_server.c 2 wlink_ hits (grep -n 'riscvchip\|wlink_' ~/app/riscv-openocd-wch/src/server/gdb_server.c) — port the behavior into the driver/target layer if it matters for our flow (flash + RTT, no gdb needed on the rig for WCH), else document-and-skip in the commit message.
- Step 2: Flash test on nanoch32v203 (lock held; cfg = Task 7's test cfg + flash bank line)
set _FLASHNAME ch32.flash
flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 ch32.cpu
~/app/openocd_tinyusb/bin/openocd -c "adapter serial EBCA8F0670AF" \
-f wch-mainline-riscv-test.cfg \
-c "program /home/hathach/code/tinyusb/examples/cmake-build-nanoch32v203-usbfs/device/cdc_msc/cdc_msc.elf verify reset exit"
Expected: ** Verified OK **; board re-enumerates as CDC (lsusb | grep -i cafe or dmesg).
- Step 3: RTT test on nanoch32v203 (rig rule:
rtt polling_interval 1, neverreset run)
RTT server start → capture a few seconds → nonzero events. The target-debug skill documents the WCH RTT route.
- Step 4: Update the rig's WCH flow
If Task 7 confirmed the mainline-riscv shape, the generated cfg template in test/hil/hil_test.py (~line 381: adapter driver wlinke … target create $_TARGETNAME.0 wch_riscv …) must switch to the Task 7 cfg shape. Do this on a TinyUSB branch:
cd ~/code/tinyusb && git worktree add .worktrees/claude/hil-openocd-unified -b claude/hil-openocd-unified
# edit test/hil/hil_test.py template in the worktree; commit there; DO NOT push
Then repoint the rig's WCH binary (symlink, so scripts resolve):
ln -sf ~/app/openocd_tinyusb/bin/openocd ~/.local/bin/openocd_wch
(Old target ~/app/openocd_wch_new/bin/… and ~/.local/bin/openocd_wch.bak-20260727 stay as rollback.)
- Step 5: HIL green on all four WCH boards (run from the worktree so the new template is used)
cd ~/code/tinyusb/.worktrees/claude/hil-openocd-unified
python3 test/hil/hil_test.py test/hil/tinyusb.json \
-b nanoch32v203 -b ch32v103r_r1_1v0 -b ch32v307v_r1_1v0 -b ch582m_evt
Expected: 4/4 PASS. Firmware for missing cmake-build-<board> sets: build first (nanoch32v203 sets exist; ch32v103/307/ch582m may need tools/get_deps.py -b <board> + the examples build). Known flake: ch32v103r throughput is ~40% flaky historically — retry before blaming the port. If ch582m misbehaves specifically, note it and check wlinke.c's riscvchip dispatch for CH58x.
- Step 6: Squash Tasks 6–8 into downstream commit #3 and push
cd ~/app/openocd
git reset --soft $(git log --grep='WIP: wch port' --format=%H | tail -1)^
git commit -m "jtag, flash: add WCH-LinkE adapter, sdi transport and CH32 flash drivers
Ported from hathach/riscv-openocd-wch @ ccb04d7 (originally
dragonlock2/miscboards WCH SDK, base openocd 0.11.0):
- src/jtag/drivers/wlinke.c: WCH-Link/LinkE USB adapter (GCC-14 fixes included)
- src/jtag/sdi.c: WCH single-wire debug transport, re-worked onto
mainline's id-based transport API (TRANSPORT_SDI)
- src/flash/nor/wchriscv.c, wcharm.c: CH32V/CH5xx (wlink protocol) and
CH32F (memory-mapped) flash drivers
CH32 cores are driven by mainline's riscv target over wlink DMI
transfers; the fork's wch_riscv target stack is not needed.
The fork's core patches (flash/nor/tcl.c protect-check hack) moved into
the wch_riscv flash driver's protect_check op.
Verified on ci rig: nanoch32v203, ch32v103r_r1_1v0, ch32v307v_r1_1v0,
ch582m_evt - flash + verify + HIL suite + RTT (nanoch32v203)."
git push
(Amend the target-stack paragraph if the fallback path was taken instead.) Release the nanoch32v203 lock if still held.
Task 9: Espressif — ESP32-P4 configs, S3 attach verification, downstream commit #4
Mainline already has: src/target/espressif/ (esp32/s2/s3 xtensa targets + apptrace/semihosting), the esp_usb_jtag adapter driver, and builtin cfgs for c2/c3/c6/h2/s3. Missing vs the rig: anything ESP32-P4. Flash stays esptool (rig flashes ESP via idf.py/esptool; the espressif fork's flash-stub stack is explicitly out of scope).
Files:
- Create:
~/app/openocd/tcl/target/esp32p4.cfg,~/app/openocd/tcl/board/esp32p4-builtin.cfg
Interfaces:
-
Consumes: install prefix; espressif fork cfgs fetched from GitHub.
-
Produces: downstream commit #4; P4 + S3 debug-attach evidence.
-
Step 1: Verify S3 attach with pure mainline inheritance (no new files; proves the "espressif support" baseline)
python3 ~/code/tinyusb/test/hil/board_lock.py hold espressif_s3_devkitm --reason "openocd-unified esp verify"
~/app/openocd_tinyusb/bin/openocd -f board/esp32s3-builtin.cfg -c "init; halt"
Expected: both xtensa cores detected over USB-Serial-JTAG (303a:1001), Target halted. Then resume; shutdown, release lock. Gotchas live in the esp-target-debug skill (S3's debug port can be occupied when TinyUSB firmware owns the USB peripheral — use the same recovery steps as that skill).
- Step 2: Fetch and adapt the P4 configs (write both files)
curl -fsSL https://raw.githubusercontent.com/espressif/openocd-esp32/master/tcl/target/esp32p4.cfg -o /tmp/claude-1000/-home-hathach-code-tinyusb/7dee5f9e-874b-4680-bb09-01a5d13fbd37/scratchpad/esp32p4-espressif.cfg
curl -fsSL https://raw.githubusercontent.com/espressif/openocd-esp32/master/tcl/board/esp32p4-builtin.cfg -o /tmp/claude-1000/-home-hathach-code-tinyusb/7dee5f9e-874b-4680-bb09-01a5d13fbd37/scratchpad/esp32p4-builtin-espressif.cfg
Espressif's cfg creates an esp32p4-type target (their esp_riscv C stack — not in mainline). Rewrite tcl/target/esp32p4.cfg following mainline's own ESP RISC-V pattern — tcl/target/esp32c6.cfg + esp_common.cfg (generic riscv target create, chip quirks via riscv dmi_write with the _RISCV_* register constants from esp_common.cfg) — carrying over from Espressif's file: _CPUTAPID, memory map/workarea, the dual-core SMP topology (P4 is 2× RV32 — model on how mainline handles SMP, and on Espressif's _ESP_SMP_TARGET), and the _ESP_EFUSE_MAC_ADDR_REG value. tcl/board/esp32p4-builtin.cfg = esp_usb_jtag adapter + transport select jtag + source the target cfg (mirror board/esp32c6-builtin.cfg, adjusting ESP_USB_JTAG_* ids to Espressif's P4 values).
Also check src/jtag/drivers/esp_usb_jtag.c accepts the P4 (VID/PID 303a:1001 is shared; verify any chip-id gating).
- Step 3: P4 attach test
python3 ~/code/tinyusb/test/hil/board_lock.py hold espressif_p4_function_ev --reason "openocd-unified esp verify"
cd ~/app/openocd && make install
~/app/openocd_tinyusb/bin/openocd -f board/esp32p4-builtin.cfg -c "init; halt"
Evidence criteria: both HP cores halt, mdw 0x4ff00000 4 (P4 HP TCM/SRAM — cross-check the address against Espressif's cfg memory map before running) reads, resume works. Known nuance from prior sessions: P4 attach can need the reset-into-attach dance — the esp-target-debug skill documents it; an attach that only works with that dance still counts as pass (note it in the commit).
Decision checkpoint: if the generic-riscv shape cannot attach P4 for architectural reasons (needs Espressif's C-level esp_riscv assist), stop and report — options are cherry-picking their esp_riscv stack (large) vs shipping P4 as esptool-flash-only with debug via ESP-IDF's openocd as today. Do not silently pick either.
- Step 4: Commit (downstream commit #4) and push
cd ~/app/openocd
git add tcl/target/esp32p4.cfg tcl/board/esp32p4-builtin.cfg
git commit -m "tcl: add ESP32-P4 target/board configs adapted from espressif/openocd-esp32
Adapted from espressif/openocd-esp32 master onto mainline's generic
RISC-V ESP pattern (tcl/target/esp32c6.cfg + esp_common.cfg): generic
riscv targets over esp_usb_jtag instead of the fork's esp_riscv C
stack. Flash programming stays with esptool, matching how the rig
flashes all Espressif boards. ESP32/S2/S3/C3/C6/H2 were already
supported by mainline.
Verified on ci rig: espressif_p4_function_ev and espressif_s3_devkitm
attach/halt/resume over built-in USB-Serial-JTAG."
git push
Release both ESP board locks.
Task 10: Final sweep, README truth-up, rig config flip
Files:
- Modify:
~/app/openocd/README.md(only if scope shifted in Tasks 7–9) - Modify (TinyUSB worktree from Task 8):
test/hil/tinyusb.json— max32666fthr flasheropenocd_adi→openocdwith args-f interface/cmsis-dap.cfg -f target/max32665.cfg(plain openocd now serves it)
Interfaces:
-
Consumes: everything green from Tasks 4–9.
-
Produces: the finished fork; TinyUSB branch
claude/hil-openocd-unifiedwith hil_test.py + tinyusb.json changes, committed, unpushed (user pushes per standing instruction). -
Step 1: Full HIL regression across every openocd-family board
cd ~/code/tinyusb/.worktrees/claude/hil-openocd-unified
python3 test/hil/hil_test.py test/hil/tinyusb.json \
-b raspberry_pi_pico -b raspberry_pi_pico_w -b raspberry_pi_pico2 \
-b adafruit_fruit_jam -b stm32h743nucleo -b stm32g0b1nucleo -b max32666fthr \
-b nanoch32v203 -b ch32v103r_r1_1v0 -b ch32v307v_r1_1v0 -b ch582m_evt
Expected: 11/11 PASS (ch32v103r throughput may need its usual retries).
- Step 2: README truth-up
Re-read README.md against what actually landed (WCH target route, P4 outcome). Fix any row that no longer matches; amend into the README commit or add
git commit -m "README: reflect verified scope". Push.
- Step 3: Verify the one-commit-per-fork shape
Run: git -C ~/app/openocd log --oneline upstream/master..tinyusb
Expected: exactly 5 commits (or 6 with a README truth-up): README, RPi configs, ADI config, WCH port, ESP32-P4 configs. If not, interactive-free cleanup: git rebase --onto / reset --soft re-squash, then git push --force-with-lease (fork branch, ours alone — safe).
- Step 4: Commit the TinyUSB-side changes in the worktree (do not push)
cd ~/code/tinyusb/.worktrees/claude/hil-openocd-unified
git add test/hil/hil_test.py test/hil/tinyusb.json
git commit -m "test(hil): drive WCH boards and max32666fthr through the unified openocd"
Leave for the user to push/PR.
- Step 5: Leftovers report (no deletions now)
Write a short status into OPENOCD_UNIFIED_FORK_HANDOFF.md (append a "2026-07-XX outcome" section): what was repointed, rollback paths (/usr/local/bin/openocd.rpi-backup-20260727, ~/.local/bin/openocd_wch.bak-20260727), and that ~/app/openocd_rpi, ~/app/openocd_adi, ~/app/openocd-mainline, ~/app/openocd_mainline, ~/app/openocd_wch_new, ~/app/riscv-openocd-wch can be retired after a week of green CI — not now.