Files
tinyusb/docs/superpowers/plans/2026-07-27-openocd-unified-fork.md
hathach 3fdd294b95 docs: add the unified OpenOCD plan, note worktree dep symlinks
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.
2026-07-28 14:43:32 +07:00

36 KiB
Raw Blame History

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 branch tinyusb, source clone at ~/app/openocd, install prefix $HOME/app/openocd_tinyusb.
  • One commit per downstream fork on the tinyusb branch: one for raspberrypi/openocd, one for analogdevicesinc/openocd, one for riscv-openocd-wch, one for espressif/openocd-esp32 (plus the initial README commit). Iterate with git 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_new stay 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; never reset run inside an SDI session (target does not come back).
  • pkill -x openocd — never pkill -f (pattern matches your own shell).
  • libjim-dev is 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/tinyusb except where a task explicitly says so (hil_test.py WCH cfg template, on a claude/-prefixed branch). Never git stash -u in 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/openocd with default branch tinyusb; local clone ~/app/openocd with remotes origin (hathach) and upstream (openocd-org). All later tasks commit to this clone's tinyusb branch.

  • 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-text README, and leaving README untouched 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/openocd clone 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.cfg and target/max32665.cfg resolvable via find in 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/tinyusb builds + test/hil/hil_test.py.

Interfaces:

  • Consumes: ~/app/openocd_tinyusb/bin/openocd with 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, -max32666fthr already 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 hardcoded openocd resolve 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.py takes the config as a positional arg and -b per board; it holds board locks itself (that is the board-lock protocol in CI — do not also hold manual locks around hil_test.py runs).

  • max32666fthr is not in this run: its flash_openocd_adi() path uses the hardcoded OPENCOD_ADI_PATH = ~/app/openocd_adi (hil_test.py:408), which the shim can't intercept. Handle it in Step 4b instead. Do not edit hil_test.py for this — the adi path disappears at cutover (Task 10 flips tinyusb.json's flasher entry to plain openocd with -f interface/cmsis-dap.cfg -f target/max32665.cfg).

  • Expected: every board PASS in the report. Any failure: stop, diagnose (consult the hil skill), 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-debug skill 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 Debian openocd package

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 — check ls ~/app/riscv-openocd-wch/src/jtag/sdi*)
  • Modify: src/transport/transport.h (new transport id), src/jtag/interface.h (add sdi_ops to struct adapter_driver + struct sdi_driver decl), 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; sdi transport selectable. Commit stays amend-in-progress — Tasks 68 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), plus TRANSPORT_VALID_MASK), and struct transport selects by unsigned int id, not name. Add #define TRANSPORT_SDI BIT(7), extend TRANSPORT_VALID_MASK, and port sdi.c's transport_register to the id-based struct.

  • SWIM is the exact precedent — ST's proprietary single-wire transport, wired upstream the same way this needs: swim_ops field at src/jtag/interface.h:363, its own transport bit, own command namespace. Mirror how grep -rn swim src/transport/ src/jtag/interface.h src/jtag/swim.c is structured wherever the fork's 0.11-era pattern no longer matches mainline.

  • The fork's sdi op 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.c includes "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.c and sdi.c in; 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.


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), possibly src/target/riscv/riscv-013.c shim 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:

  1. an existing DTM/DMI abstraction the adapter can implement directly (best);
  2. a jtag-DTM emulation inside wlinke.c: expose jtag_ops whose queue executor decodes IR=DTMCS/DMI DR scans into sdi transfers (the esp_usb_jtag-style approach, one level up);
  3. 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 via grep -rn 'riscvchip\|wlink_' src/ in the fork: src/flash/nor/tcl.c 5 hits, src/target/target.c 5, src/server/gdb_server.c 2). Hard: these files include 0.11-era target/riscv/* headers that clash with mainline's current riscv internals.
  • (b) Ship the unified fork without WCH C support and keep openocd_wch as 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 the wlink_* 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.py WCH 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_wch repointed at the fork; hil_test.py template branch claude/hil-openocd-unified in the TinyUSB repo (unpushed — user pushes; "hold pushes" applies to the TinyUSB repo).

  • Step 1: Copy the flash drivers, add wlinke.h, wire drivers.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, never reset 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 wlinketarget 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 68 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 patterntcl/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 79)
  • Modify (TinyUSB worktree from Task 8): test/hil/tinyusb.json — max32666fthr flasher openocd_adiopenocd with args -f interface/cmsis-dap.cfg -f target/max32665.cfg (plain openocd now serves it)

Interfaces:

  • Consumes: everything green from Tasks 49.

  • Produces: the finished fork; TinyUSB branch claude/hil-openocd-unified with 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.