summaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-07-27-openocd-unified-fork.md
blob: e0f1f967869d9f9b67770a0539e36afe575359bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# 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**

```bash
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)

```markdown
# 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**

```bash
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)

```bash
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)**

```bash
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)**

```bash
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**

```bash
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**

```bash
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)

```bash
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)

```bash
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)

```bash
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**

```bash
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`**

```bash
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**

```bash
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`)

```bash
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)**

```bash
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 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)`, 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**

```bash
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`)

```bash
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)

```bash
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), 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)

```tcl
# 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)

```tcl
set _FLASHNAME ch32.flash
flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 ch32.cpu
```
```bash
~/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 wlinke` … `target create $_TARGETNAME.0 wch_riscv …`) must switch to the Task 7 cfg shape. Do this on a TinyUSB branch:
```bash
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):
```bash
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)

```bash
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**

```bash
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)

```bash
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)**

```bash
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**

```bash
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**

```bash
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 flasher `openocd_adi` → `openocd` with 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-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**

```bash
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)**

```bash
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.