summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZixun LI <[email protected]>2026-07-29 00:41:26 +0200
committerZixun LI <[email protected]>2026-07-29 00:41:26 +0200
commita7d778c68e46c6013a39d8520c176a3592fc0cd7 (patch)
tree66a45d15db61ccd6ed146f336143e09b7a74a8c2
parent192e0bd872608b4a39b36047e0d5c1d18c2a8f02 (diff)
parent1eb216ed024002119a16ac15004e0b0124a7da6a (diff)
Merge tinyusb/master into fix/lpc43-hfp-reliability
-rw-r--r--.claude/skills/usb-kernel-debug/SKILL.md6
-rw-r--r--.claude/skills/usb-kernel-recover/SKILL.md73
-rwxr-xr-x.claude/skills/usb-kernel-recover/scripts/usb_recover.sh89
-rwxr-xr-x.github/workflows/ci_set_matrix.py1
-rw-r--r--.gitignore1
-rw-r--r--.idea/debugServers/wch_riscv.xml2
-rw-r--r--CLAUDE.md2
-rw-r--r--docs/superpowers/plans/2026-07-27-openocd-unified-fork.md602
-rw-r--r--examples/device/net_lwip_webserver/skip.txt1
-rw-r--r--hw/bsp/BoardPresets.json44
-rw-r--r--hw/bsp/apm32f0xx/FreeRTOSConfig/FreeRTOSConfig.h108
-rw-r--r--hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.cmake7
-rw-r--r--hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.mk5
-rw-r--r--hw/bsp/apm32f0xx/family.c25
-rw-r--r--hw/bsp/apm32f0xx/family.cmake3
-rw-r--r--hw/bsp/apm32f0xx/family.mk3
-rw-r--r--hw/bsp/ch32v10x/family.cmake2
-rw-r--r--hw/bsp/ch32v10x/family.mk2
-rw-r--r--hw/bsp/ch32v20x/family.cmake2
-rw-r--r--hw/bsp/ch32v20x/family.mk2
-rw-r--r--hw/bsp/ch32v30x/family.cmake2
-rw-r--r--hw/bsp/ch32v30x/family.mk2
-rw-r--r--hw/bsp/ch583/family.cmake2
-rw-r--r--hw/bsp/ch583/family.mk2
-rw-r--r--hw/bsp/ch583/wch-riscv.cfg2
-rw-r--r--hw/bsp/family_rules.mk16
-rw-r--r--hw/bsp/family_support.cmake44
-rw-r--r--hw/bsp/rp2040/family.cmake2
-rw-r--r--src/common/tusb_types.h2
-rw-r--r--src/device/usbd.h2
-rwxr-xr-xtest/hil/hil_test.py47
-rw-r--r--test/hil/tinyusb.json10
-rwxr-xr-xtest/hil/usbtest.py124
-rw-r--r--tools/codespell/ignore-words.txt1
-rwxr-xr-xtools/get_deps.py3
35 files changed, 1073 insertions, 168 deletions
diff --git a/.claude/skills/usb-kernel-debug/SKILL.md b/.claude/skills/usb-kernel-debug/SKILL.md
index e4169b049..fd291c0ac 100644
--- a/.claude/skills/usb-kernel-debug/SKILL.md
+++ b/.claude/skills/usb-kernel-debug/SKILL.md
@@ -12,11 +12,11 @@ sits in the link — the rig PC when it is the host, or a Linux gadget peer
(dwc2/UDC + gadget modules) when TinyUSB is the host. It cannot see inside
the TinyUSB MCU — that is the `target-debug` skill.
-Run this skill's `scripts/usb_dyndbg.sh` with `sudo` (abbreviated to
-`usb_dyndbg.sh` in the examples below). It flips the dynamic-debug print flag
-for an allowlisted set of USB modules only:
+Run this skill's `scripts/usb_dyndbg.sh` with `sudo`. It flips the dynamic-debug
+print flag for an allowlisted set of USB modules only:
```bash
+# all examples below abbreviate: sudo .claude/skills/usb-kernel-debug/scripts/usb_dyndbg.sh
sudo usb_dyndbg.sh on usbcore xhci_hcd # enable +p; pick modules from `lsusb -t` Driver=
sudo usb_dyndbg.sh status [module] # list enabled print sites
sudo usb_dyndbg.sh off usbcore xhci_hcd # ALWAYS turn off when done — very noisy
diff --git a/.claude/skills/usb-kernel-recover/SKILL.md b/.claude/skills/usb-kernel-recover/SKILL.md
index 3f03722fe..9e456417f 100644
--- a/.claude/skills/usb-kernel-recover/SKILL.md
+++ b/.claude/skills/usb-kernel-recover/SKILL.md
@@ -5,18 +5,20 @@ description: Use when a USB device or fixture attached to the ci HIL rig's Linux
# USB Recovery on the HIL Rig (Linux kernel side)
-Run this skill's `scripts/usb_recover.sh` with `sudo` (abbreviated to
-`usb_recover.sh` in the examples below). It wraps the sysfs reset actions, a
-uhubctl power-cycle escalator, and a resolver:
+Run this skill's `scripts/usb_recover.sh` with `sudo`. It wraps the sysfs reset
+actions, a uhubctl power-cycle escalator, and a resolver:
```bash
+# all examples below abbreviate: sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh
sudo usb_recover.sh resolve /dev/ttyACM3 # /dev node -> busport (e.g. 3-4.7); also ttyUSB*, sg*
sudo usb_recover.sh authorized <busport> # deauthorize+reauthorize: re-enumerate, no VBUS cut
sudo usb_recover.sh rebind <busport> # usb driver unbind+bind: re-probe
sudo usb_recover.sh hub-cycle <busport> # uhubctl VBUS cycle of the feeding port, walking parent hub
# -> root port until the device re-enumerates
+sudo usb_recover.sh root-cycle <busport> [serial] # uhubctl VBUS cut straight at the ROOT port (real ppps), no
+ # leaf walk, no device-lock touch: the D-state cure.
+ # [serial] is checked and a mismatch refused.
sudo usb_recover.sh pci-rebind <pciaddr> # whole HCD controller unbind+bind, e.g. 0000:02:00.0
-sudo usb_recover.sh pci-reset <pciaddr> # PCI function-level reset: kills URBs at HW level, no device lock
sudo usb_recover.sh pci-bind <pciaddr> [drv] # re-bind a DRIVERLESS controller (auto-tries xHCI drivers)
```
@@ -34,21 +36,45 @@ ps -eo pid,stat,wchan:30,cmd | awk '$2 ~ /D/'
```
**If yes** (uninterruptible sleep, typically a usbfs ioctl — e.g. testusb inside
-`usb_sg_wait`): run `pci-reset` and NOTHING ELSE first:
+`usb_sg_wait`): cut VBUS at the root port, and nothing else.
```bash
-sudo usb_recover.sh pci-reset <pciaddr>
+sudo usb_recover.sh root-cycle <busport> # e.g. 11-3.7 -> cycles bus 11 root port 3
```
-FLR kills the URBs at the hardware level without taking the per-device lock;
-the ioctl then returns and the convoy unwinds on its own.
+This drops power to the wedged device, so its in-flight URB fails and the ioctl
+returns. It targets the *root hub* — a different USB device from the wedged one —
+and never *writes* the wedged device's sysfs. It reads a few attributes from it —
+`idVendor`/`idProduct`/`serial`/`product` to report and check the target, and the
+directory inode plus `devnum` afterwards — none of which take the device lock, so
+it does not join the convoy the way `authorized`/`rebind`/`pci-rebind` do.
+Recovery is proven by that inode changing — a real disconnect destroys the
+kobject and reconnecting creates a new one, whereas a disconnect blocked on the
+device lock leaves it untouched. It exits non-zero if the device does not come
+back; a **zero exit only means it re-enumerated**, so still confirm the D-state
+process actually let go. Pass the expected serial as a third argument and it
+refuses a busport that now names a different device.
-**Not every controller supports FLR.** The Renesas uPD720201 (`0000:01:00.0`)
-has no reset method — `pci-reset` fails with `Inappropriate ioctl for device`
-(ENOTTY). On those, there is no clean software D-state cure — a VM reboot is NOT
+It bounces **every fixture under that root port** — on ci that is up to 25
+devices. Hold the affected boards' locks first if you can, but note
+`board_lock.py` uses `LOCK_EX | LOCK_NB` and so fails immediately when CI already
+holds them; there is no wait-for-lock. When CI is mid-run you are choosing
+between bouncing its fixtures and leaving the bus wedged for everything. The
+automated path in `usbtest.py` takes no locks at all and accepts that collateral
+deliberately: by the time a D-state wedge exists the convoy will take the bus
+down anyway.
+
+(The VBUS mechanism is verified on the ci rig — the leaf hubs report
+`bmAttributes=e0`, "self-powered", but are physically bus-powered with no adapter,
+so a root-port cut really does kill downstream power. Do not re-derive this from
+the descriptor; it lies. Not yet confirmed against a live D-state wedge. If
+`uhubctl` itself hangs, the convoy has already spread — escalate.)
+
+If `root-cycle` does not free the D-state process, there is no software cure
+left: ask the operator for a full PVE **host** power cycle. A VM reboot is NOT
reliable (downstream hubs can latch up across the PCIe reset and need a physical
-replug); ask the operator for a full PVE host power cycle instead. Do NOT
-fall through to `pci-rebind` (see next).
+replug), and a graceful reboot stalls on the D-state process anyway. Do NOT fall
+through to `pci-rebind` (see next).
**`pci-rebind` can strand the controller driverless.** Its unbind succeeds but,
with a D-state process still holding a URB, the *re-bind* hangs — leaving the
@@ -62,10 +88,9 @@ power cycle (operator action) recovers. The Renesas binds via `xhci-pci-renesas`
**Ordering is critical.** `authorized`/`rebind`/`pci-rebind` all take the
per-device lock the stuck ioctl holds — they block and join the convoy, and
soon every libusb tool (uhubctl, JLinkExe) hangs too. Worse, a blocked
-`pci-rebind` grabs the PCI device lock on its way in, which `pci-reset` also
-needs: once a rebind has been attempted and is stuck, even FLR deadlocks and
-**only a full PVE host power cycle recovers**. pci-reset first (if supported), and never
-`pci-rebind` a D-state wedge.
+`pci-rebind` grabs the PCI device lock on its way in and can wedge the whole
+function, after which **only a full PVE host power cycle recovers**. `root-cycle`
+first, and never `pci-rebind` a D-state wedge.
**If no** (device merely dead or silent), escalate gently:
@@ -93,15 +118,19 @@ hubs themselves claim "ganged" switching but do not actually cut power.
## Common mistakes
- `resolve` takes a **/dev node**, not a busport or serial ("no such device node").
-- `authorized`/`rebind` take a **busport** (`3-4.7`); `pci-rebind`/`pci-reset`
- take a **PCI addr**.
+- `authorized`/`rebind`/`hub-cycle`/`root-cycle` take a **busport** (`3-4.7`);
+ `pci-rebind`/`pci-bind` take a **PCI addr**.
- Command produces no output and doesn't return → it is blocked on the device
lock: a D-state holder exists; see above.
- Trying `pci-rebind` on a D-state hang — its re-bind hangs and strands the
controller **driverless**; recover with `pci-bind <addr>`, or a PVE host power
- cycle if the D-state URB is unkillable. Use `pci-reset` (if supported) for D-state, never
+ cycle if the D-state URB is unkillable. Use `root-cycle` for D-state, never
`pci-rebind`.
-- Running `pci-reset` on a controller without FLR support (Renesas) → ENOTTY;
- no software recovery — needs a PVE host power cycle.
+- Writing `/sys/bus/pci/devices/<addr>/reset` because the attribute is there. No
+ rig controller has FLR, so it becomes a PCIe bus reset that resets the xHCI
+ behind its live driver — the write succeeds, the card is halted for good, and
+ only a PVE host power cycle brings it back. Use `root-cycle`.
+- `root-cycle` bounces **every** fixture under that root port, not just the target
+ — hold the sibling boards' locks first.
- A J-Link reset (`r; go`) does not disconnect a wedged DUT from the host: the
DWC2 soft-connect pullup stays up through a core halt, so stuck URBs stay stuck.
diff --git a/.claude/skills/usb-kernel-recover/scripts/usb_recover.sh b/.claude/skills/usb-kernel-recover/scripts/usb_recover.sh
index 7652253fa..2230602b9 100755
--- a/.claude/skills/usb-kernel-recover/scripts/usb_recover.sh
+++ b/.claude/skills/usb-kernel-recover/scripts/usb_recover.sh
@@ -6,9 +6,6 @@
# sudo usb_recover.sh authorized <busport> # e.g. 3-2 -> deauthorize+reauthorize (re-enumerate, NO VBUS cut)
# sudo usb_recover.sh rebind <busport> # e.g. 3-2 -> usb driver unbind+bind (re-probe)
# sudo usb_recover.sh pci-rebind <pciaddr> # e.g. 0000:01:00.0 -> HCD unbind+bind (WHOLE controller)
-# sudo usb_recover.sh pci-reset <pciaddr> # e.g. 0000:01:00.0 -> PCI function-level reset: kills URBs at
-# # HW level WITHOUT the device lock; the only cure when a process
-# # is stuck in D state (usbfs ioctl) and unbind paths would convoy
# sudo usb_recover.sh pci-bind <pciaddr> [driver] # bind a DRIVERLESS controller (e.g. after a pci-rebind
# # whose re-bind hung and left it unbound). Auto-tries the xHCI
# # drivers (xhci-pci-renesas, xhci_hcd) unless one is named.
@@ -17,6 +14,11 @@
# # re-enumerates. Ganged/fake-switching hubs may bounce ALL
# # siblings; self-powered hubs only reset their uplink, which
# # is why the walk ends at the root port (real xHCI ppps).
+# sudo usb_recover.sh root-cycle <busport> [serial] # e.g. 13-1.6 -> uhubctl VBUS cut at the ROOT port feeding
+# # it; [serial] is verified against the device and refused on mismatch,
+# # skipping the leaf hubs (which fake ganged switching and do not
+# # actually cut power). Bounces every sibling under that root port.
+# # The D-state escape: no device lock, so it cannot convoy.
# sudo usb_recover.sh resolve <devnode> # e.g. /dev/ttyACM3 -> print its <busport> (no privilege needed)
set -euo pipefail
@@ -25,6 +27,29 @@ PCI_RE='^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9]$'
DRIVER_RE='^[A-Za-z0-9_-]+$'
die() { echo "usb_recover: $*" >&2; exit 1; }
+
+# Generation marker for "did this device actually re-enumerate". A real disconnect destroys the
+# usb_device and its sysfs kobject; reconnecting creates a new one, and kernfs hands out inode
+# numbers monotonically, so the directory inode changes. Verified on the rig: ports re-enumerated
+# minutes ago carry inodes in the millions while ports untouched since boot are still in the tens
+# of thousands, ranking identically to their mtimes.
+#
+# This beats comparing devnum, which Linux reuses once the per-bus map wraps (observed live: a
+# single cycle moved one device 123 -> 113). It also beats watching for the node to vanish, since
+# `uhubctl -a cycle` holds the whole power-off window inside itself and a poll afterwards can
+# never witness the gap. The inode survives the gap, so no observation window is needed.
+#
+# Crucially, if the disconnect is blocked on the wedged device's lock the kobject is never
+# recreated -- same inode -- which is exactly the case that must be reported as a failure. Verified
+# against kernfs: __kernfs_new_node() allocates via idr_alloc_cyclic() but kernfs_id_ino() exposes
+# the full 64-bit (id_highbits<<32 | lowbits) as st_ino on 64-bit ino_t, so a repeat needs ~2^64
+# node creations. authorized-toggle, set_configuration and suspend/resume all leave the parent
+# device kobject alone, so none of them can move the marker and fake a success.
+#
+# The trailing slash is load-bearing: /sys/bus/usb/devices/<busport> is a SYMLINK with its own
+# separate inode, so without it stat reports the link rather than the device it points at, and the
+# value would never change. Do not "tidy" it away.
+sysfs_gen() { stat -c %i "/sys/bus/usb/devices/$1/" 2>/dev/null || echo none; }
usage() { grep -E '^# sudo usb_recover' "$0" >&2; exit 2; }
# Refuse to touch a PCI function that is not a USB controller (class 0x0c03xx), so a stray or
@@ -107,6 +132,11 @@ case "$action" in
[[ "$target" =~ $USBPATH_RE ]] || die "bad usb path: $target"
UHUBCTL=$(command -v uhubctl || echo /sbin/uhubctl)
[ -x "$UHUBCTL" ] || die "uhubctl not installed"
+ # sysfs generation, not node existence: a disconnect blocked on the device lock leaves the
+ # old node (and its idVendor) in place, so an existence check reports success without anything
+ # having happened -- and the walk to the root port, which is the part that actually cuts power
+ # on these fake-ganged leaf hubs, would never run.
+ gen=$(sysfs_gen "$target")
dev="$target"
while :; do
if [[ "$dev" =~ ^([0-9]+)-([0-9]+)$ ]]; then # parent is the root hub
@@ -118,8 +148,9 @@ case "$action" in
"$UHUBCTL" -l "$loc" -p "$port" -a cycle -d 5 -f || echo " (uhubctl failed at $loc; walking up)"
for _ in $(seq 1 10); do
sleep 1
- if [ -e "/sys/bus/usb/devices/$target/idVendor" ]; then
- echo "recovered: $target re-enumerated"; exit 0
+ now=$(sysfs_gen "$target")
+ if [ "$now" != none ] && [ "$now" != "$gen" ]; then
+ echo "recovered: $target re-enumerated (gen $gen -> $now)"; exit 0
fi
done
[ -n "$up" ] || break
@@ -127,12 +158,48 @@ case "$action" in
done
die "hub-cycle: $target still not enumerated after cycling up to the root port"
;;
- pci-reset)
- [[ "$target" =~ $PCI_RE ]] || die "bad pci addr: $target"
- require_usb_controller "$target"
- [ -e "/sys/bus/pci/devices/$target/reset" ] || die "no reset support on $target"
- echo 1 > "/sys/bus/pci/devices/$target/reset"
- echo "flr-reset pci $target"
+ root-cycle)
+ # VBUS cut at the ROOT port, where xHCI ppps is real. Unlike hub-cycle this does not walk up
+ # from the leaf (the 1a40:0201 hubs claim ganged switching but never cut power) and never
+ # writes the wedged device's sysfs or takes its lock, so it cannot join a D-state convoy.
+ # uhubctl exits 0 even when it does nothing ("No compatible devices detected" still returns
+ # 0), so its status proves nothing -- the sysfs_gen check below is the only real verdict.
+ [[ "$target" =~ $USBPATH_RE ]] || die "bad usb path: $target"
+ UHUBCTL=$(command -v uhubctl || echo /sbin/uhubctl)
+ [ -x "$UHUBCTL" ] || die "uhubctl not installed"
+ # Existence alone only proves *something* occupies that path -- bus numbers renumber every
+ # boot, so a stale busport can name a different device entirely and we would cut power to its
+ # whole subtree (up to 25 fixtures on this rig). Callers that know what they expect pass the
+ # serial as a third argument and we refuse on mismatch; otherwise print the identity so a
+ # wrong target is at least visible.
+ [ -e "/sys/bus/usb/devices/$target" ] || die "no such usb device: $target"
+ idf="/sys/bus/usb/devices/$target"
+ serial=$(cat "$idf/serial" 2>/dev/null || echo -)
+ expect=${3:-}
+ [ -z "$expect" ] || [ "$expect" = "$serial" ] || \
+ die "root-cycle: $target has serial '$serial', expected '$expect' — stale busport, refusing"
+ echo "root-cycle: target $target is $(cat "$idf/idVendor" 2>/dev/null || echo -):$(cat "$idf/idProduct" 2>/dev/null || echo -)" \
+ "serial=$serial product=$(cat "$idf/product" 2>/dev/null || echo -)"
+ bus=${target%%-*}; rest=${target#*-}; rootport=${rest%%.*}
+ gen=$(sysfs_gen "$target")
+ echo "root-cycle: cutting VBUS on bus $bus root port $rootport (feeds $target, bounces its siblings)"
+ # -S is load-bearing. By default uhubctl writes /sys/.../usb<bus>-port<n>/disable (verified:
+ # two O_WRONLY opens per cycle), and the kernel's disable_store() takes the ROOT HUB's lock and
+ # synchronously usb_disconnect()s the child BEFORE cutting power -- against a wedged device that
+ # blocks on the lock we are trying to free, so power would never drop and uhubctl would D-state
+ # holding the root hub's lock, poisoning the whole bus. -S forces the libusb path, which sends
+ # the power-off control transfer straight to the root hub with no child-disconnect in front.
+ "$UHUBCTL" -S -l "$bus" -p "$rootport" -a cycle -d 5 \
+ || die "uhubctl failed to cycle bus $bus port $rootport"
+ for _ in $(seq 1 10); do
+ sleep 1
+ now=$(sysfs_gen "$target")
+ if [ "$now" != none ] && [ "$now" != "$gen" ]; then
+ echo "root-cycled $bus port $rootport: $target re-enumerated"\
+ "(devnum $(cat "/sys/bus/usb/devices/$target/devnum" 2>/dev/null || echo ?), gen $gen -> $now)"; exit 0
+ fi
+ done
+ die "root-cycle: $target did not re-enumerate after cycling bus $bus port $rootport (sysfs generation still $gen: no disconnect happened)"
;;
*)
usage
diff --git a/.github/workflows/ci_set_matrix.py b/.github/workflows/ci_set_matrix.py
index dc0d3871f..50ada5964 100755
--- a/.github/workflows/ci_set_matrix.py
+++ b/.github/workflows/ci_set_matrix.py
@@ -16,6 +16,7 @@ toolchain_list = [
# family: [supported toolchain]
family_list = {
+ "apm32f0xx": ["arm-gcc"],
"at32f402_405": ["arm-gcc"],
"at32f403a_407": ["arm-gcc"],
"at32f413": ["arm-gcc"],
diff --git a/.gitignore b/.gitignore
index ca745ee19..8773322e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,6 +78,7 @@ hw/mcu/artery/
hw/mcu/broadcom/
hw/mcu/bridgetek/ft9xx/ft90x-sdk/
hw/mcu/gd/
+hw/mcu/geehy/
hw/mcu/hpmicro/
hw/mcu/infineon/
hw/mcu/microchip/
diff --git a/.idea/debugServers/wch_riscv.xml b/.idea/debugServers/wch_riscv.xml
index 2e147f1b6..0b2b83b2e 100644
--- a/.idea/debugServers/wch_riscv.xml
+++ b/.idea/debugServers/wch_riscv.xml
@@ -4,7 +4,7 @@
<debugger kind="GDB" isBundled="true" />
<env />
</debugger>
- <gdbserver dir="$CMakeProjectDir$" exe="$USER_HOME$/app/riscv-openocd-wch/src/openocd" args="-f hw/bsp/ch32v20x/wch-riscv.cfg">
+ <gdbserver dir="$CMakeProjectDir$" exe="openocd" args="-f target/wch-riscv.cfg">
<env />
</gdbserver>
<console enabled="true" port="4444" />
diff --git a/CLAUDE.md b/CLAUDE.md
index 77dab4565..94b8192b7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -12,7 +12,7 @@ Bias toward caution over speed. For trivial tasks, use judgment.
- **Simplicity** — no features, abstractions, flexibility, or error handling beyond what was asked. If 200 lines could be 50, rewrite.
- **Surgical changes** — touch only what the task requires; match existing style; don't refactor working code; mention unrelated dead code rather than deleting it. Remove only orphans *your* changes created.
- **Goal-driven** — turn tasks into verifiable goals ("write failing test, make it pass"). For multi-step work, state a brief `step → verify` plan.
-- **Worktrees** — default to a git worktree for any branch or multi-step work; never switch the shared primary checkout's branch. Sessions run concurrently: switching the primary checkout mid-flight disrupts other sessions and can silently point a review, build, or commit at the wrong diff. Only trivial one-shot fixes may skip this. Standard location: `.worktrees/<branch-name>` at the repo root (gitignored), e.g. `git worktree add .worktrees/my-branch -b my-branch`.
+- **Worktrees** — default to a git worktree for any branch or multi-step work; never switch the shared primary checkout's branch. Sessions run concurrently: switching the primary checkout mid-flight disrupts other sessions and can silently point a review, build, or commit at the wrong diff. Only trivial one-shot fixes may skip this. Standard location: `.worktrees/<branch-name>` at the repo root (gitignored), e.g. `git worktree add .worktrees/my-branch -b my-branch`. In a new worktree, symlink the dependency dirs (`lib/*`, `hw/mcu/*`, `tools/linkermap` — the keys of `deps_all` in `tools/get_deps.py`) to the primary checkout instead of re-cloning them; only if the branch needs a different dep revision, replace that one symlink with a real dir and run `get_deps.py` for it.
## Ground Rules
diff --git a/docs/superpowers/plans/2026-07-27-openocd-unified-fork.md b/docs/superpowers/plans/2026-07-27-openocd-unified-fork.md
new file mode 100644
index 000000000..e0f1f9678
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-27-openocd-unified-fork.md
@@ -0,0 +1,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.
diff --git a/examples/device/net_lwip_webserver/skip.txt b/examples/device/net_lwip_webserver/skip.txt
index c3df1ee4b..53836b581 100644
--- a/examples/device/net_lwip_webserver/skip.txt
+++ b/examples/device/net_lwip_webserver/skip.txt
@@ -1,3 +1,4 @@
+mcu:APM32F0XX
mcu:CH32V103
mcu:CH32V20X
mcu:LPC11UXX
diff --git a/hw/bsp/BoardPresets.json b/hw/bsp/BoardPresets.json
index 93a8f2c32..a480efc3e 100644
--- a/hw/bsp/BoardPresets.json
+++ b/hw/bsp/BoardPresets.json
@@ -43,6 +43,10 @@
"inherits": "default"
},
{
+ "name": "apm32f072_dev_board",
+ "inherits": "default"
+ },
+ {
"name": "arduino_nano33_ble",
"inherits": "default"
},
@@ -511,6 +515,10 @@
"inherits": "default"
},
{
+ "name": "py32f071_dev_board",
+ "inherits": "default"
+ },
+ {
"name": "pybadge",
"inherits": "default"
},
@@ -1008,6 +1016,11 @@
"configurePreset": "apard32690"
},
{
+ "name": "apm32f072_dev_board",
+ "description": "Build preset for the apm32f072_dev_board board",
+ "configurePreset": "apm32f072_dev_board"
+ },
+ {
"name": "arduino_nano33_ble",
"description": "Build preset for the arduino_nano33_ble board",
"configurePreset": "arduino_nano33_ble"
@@ -1638,6 +1651,11 @@
"configurePreset": "portenta_c33"
},
{
+ "name": "py32f071_dev_board",
+ "description": "Build preset for the py32f071_dev_board board",
+ "configurePreset": "py32f071_dev_board"
+ },
+ {
"name": "pybadge",
"description": "Build preset for the pybadge board",
"configurePreset": "pybadge"
@@ -2258,6 +2276,19 @@
]
},
{
+ "name": "apm32f072_dev_board",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "apm32f072_dev_board"
+ },
+ {
+ "type": "build",
+ "name": "apm32f072_dev_board"
+ }
+ ]
+ },
+ {
"name": "arduino_nano33_ble",
"steps": [
{
@@ -3896,6 +3927,19 @@
]
},
{
+ "name": "py32f071_dev_board",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "py32f071_dev_board"
+ },
+ {
+ "type": "build",
+ "name": "py32f071_dev_board"
+ }
+ ]
+ },
+ {
"name": "pybadge",
"steps": [
{
diff --git a/hw/bsp/apm32f0xx/FreeRTOSConfig/FreeRTOSConfig.h b/hw/bsp/apm32f0xx/FreeRTOSConfig/FreeRTOSConfig.h
new file mode 100644
index 000000000..b8d555ebe
--- /dev/null
+++ b/hw/bsp/apm32f0xx/FreeRTOSConfig/FreeRTOSConfig.h
@@ -0,0 +1,108 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2026, Ha Thach (tinyusb.org)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * This file is part of the TinyUSB stack.
+ */
+
+#ifndef FREERTOS_CONFIG_H_
+#define FREERTOS_CONFIG_H_
+
+#ifndef __IASMARM__
+ #include "apm32f0xx.h"
+#endif
+
+#define configENABLE_MPU 0
+#define configENABLE_FPU 0
+#define configENABLE_TRUSTZONE 0
+#define configMINIMAL_SECURE_STACK_SIZE 1024
+
+#define configUSE_PREEMPTION 1
+#define configUSE_PORT_OPTIMISED_TASK_SELECTION 0
+#define configCPU_CLOCK_HZ SystemCoreClock
+#define configTICK_RATE_HZ 1000
+#define configMAX_PRIORITIES 5
+#define configMINIMAL_STACK_SIZE 128
+#define configTOTAL_HEAP_SIZE ( configSUPPORT_DYNAMIC_ALLOCATION * 4 * 1024 )
+#define configMAX_TASK_NAME_LEN 16
+#define configUSE_16_BIT_TICKS 0
+#define configIDLE_SHOULD_YIELD 1
+#define configUSE_MUTEXES 1
+#define configUSE_RECURSIVE_MUTEXES 1
+#define configUSE_COUNTING_SEMAPHORES 1
+#define configQUEUE_REGISTRY_SIZE 4
+#define configUSE_QUEUE_SETS 0
+#define configUSE_TIME_SLICING 0
+#define configUSE_NEWLIB_REENTRANT 0
+#define configENABLE_BACKWARD_COMPATIBILITY 1
+#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 0
+
+#define configSUPPORT_STATIC_ALLOCATION 1
+#define configSUPPORT_DYNAMIC_ALLOCATION 0
+
+#define configUSE_IDLE_HOOK 0
+#define configUSE_TICK_HOOK 0
+#define configUSE_MALLOC_FAILED_HOOK 0
+#define configCHECK_FOR_STACK_OVERFLOW 2
+#define configCHECK_HANDLER_INSTALLATION 0
+
+#define configGENERATE_RUN_TIME_STATS 0
+#define configRECORD_STACK_HIGH_ADDRESS 1
+#define configUSE_TRACE_FACILITY 1
+#define configUSE_STATS_FORMATTING_FUNCTIONS 0
+
+#define configUSE_CO_ROUTINES 0
+#define configMAX_CO_ROUTINE_PRIORITIES 2
+
+#define configUSE_TIMERS 1
+#define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 2 )
+#define configTIMER_QUEUE_LENGTH 32
+#define configTIMER_TASK_STACK_DEPTH configMINIMAL_STACK_SIZE
+
+#define INCLUDE_vTaskPrioritySet 0
+#define INCLUDE_uxTaskPriorityGet 0
+#define INCLUDE_vTaskDelete 0
+#define INCLUDE_vTaskSuspend 1
+#define INCLUDE_xResumeFromISR 0
+#define INCLUDE_vTaskDelayUntil 1
+#define INCLUDE_vTaskDelay 1
+#define INCLUDE_xTaskGetSchedulerState 0
+#define INCLUDE_xTaskGetCurrentTaskHandle 1
+#define INCLUDE_uxTaskGetStackHighWaterMark 0
+#define INCLUDE_xTaskGetIdleTaskHandle 0
+#define INCLUDE_xTimerGetTimerDaemonTaskHandle 0
+#define INCLUDE_pcTaskGetTaskName 0
+#define INCLUDE_eTaskGetState 0
+#define INCLUDE_xEventGroupSetBitFromISR 0
+#define INCLUDE_xTimerPendFunctionCall 0
+
+#define xPortPendSVHandler PendSV_Handler
+#define xPortSysTickHandler SysTick_Handler
+#define vPortSVCHandler SVC_Handler
+
+#define configPRIO_BITS __NVIC_PRIO_BITS
+#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY ( ( 1 << configPRIO_BITS ) - 1 )
+#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 2
+#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << ( 8 - configPRIO_BITS ) )
+#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << ( 8 - configPRIO_BITS ) )
+
+#endif
diff --git a/hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.cmake b/hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.cmake
index 33148dbd4..d25f675be 100644
--- a/hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.cmake
+++ b/hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.cmake
@@ -4,5 +4,10 @@ set(MCU_LINKER_NAME APM32F07xxB)
set(JLINK_DEVICE APM32F072RB)
function(update_board TARGET)
- target_compile_definitions(${TARGET} PUBLIC ${MCU_VARIANT})
+ target_compile_definitions(${TARGET} PUBLIC
+ ${MCU_VARIANT}
+ CFG_EXAMPLE_MSC_READONLY
+ CFG_EXAMPLE_MSC_DUAL_READONLY
+ CFG_EXAMPLE_VIDEO_READONLY
+ )
endfunction()
diff --git a/hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.mk b/hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.mk
index 2e5df9947..f78d2091f 100644
--- a/hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.mk
+++ b/hw/bsp/apm32f0xx/boards/apm32f072_dev_board/board.mk
@@ -4,4 +4,7 @@ MCU_LINKER_NAME = APM32F07xxB
JLINK_DEVICE = APM32F072RB
CFLAGS += \
- -D${MCU_VARIANT}
+ -D${MCU_VARIANT} \
+ -DCFG_EXAMPLE_MSC_READONLY \
+ -DCFG_EXAMPLE_MSC_DUAL_READONLY \
+ -DCFG_EXAMPLE_VIDEO_READONLY
diff --git a/hw/bsp/apm32f0xx/family.c b/hw/bsp/apm32f0xx/family.c
index cbc427f8c..643490797 100644
--- a/hw/bsp/apm32f0xx/family.c
+++ b/hw/bsp/apm32f0xx/family.c
@@ -32,10 +32,18 @@
#include "apm32f0xx_rcm.h"
#include "apm32f0xx_gpio.h"
#include "apm32f0xx_misc.h"
-#include "apm32f0xx_crs.h"
#include "bsp/board_api.h"
#include "board.h"
+void USBD_IRQHandler(void);
+#if CFG_TUSB_OS == OPT_OS_NONE
+void SysTick_Handler(void);
+void SVC_Handler(void);
+void PendSV_Handler(void);
+#endif
+void HardFault_Handler(void);
+void _init(void);
+
//--------------------------------------------------------------------+
// Forward USB interrupt events to TinyUSB IRQ Handler
//--------------------------------------------------------------------+
@@ -47,18 +55,11 @@ void USBD_IRQHandler(void) {
// Board Init
//--------------------------------------------------------------------+
void board_init(void) {
- // Enable HSI48 for USB clock
- RCM_EnableHSI48();
- while (RCM_ReadStatusFlag(RCM_FLAG_HSI48RDY) == RESET) {}
-
- // Select HSI48 as USB clock source
- RCM_ConfigUSBCLK(RCM_USBCLK_HSI48);
+ // Configure HSE and PLL for a 48 MHz system clock
+ SystemClockConfig();
- // Enable CRS for automatic HSI48 calibration from USB SOF
- RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_CRS);
- CRS_ConfigSynchronizationSource(CRS_SYNC_SOURCE_USB);
- CRS_EnableAutomaticCalibration();
- CRS_EnableFrequencyErrorCounter();
+ // Route the 48 MHz PLL clock to USB
+ RCM_ConfigUSBCLK(RCM_USBCLK_PLLCLK);
// Enable USB peripheral clock
RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_USB);
diff --git a/hw/bsp/apm32f0xx/family.cmake b/hw/bsp/apm32f0xx/family.cmake
index 99a94a7a8..199ba7cb5 100644
--- a/hw/bsp/apm32f0xx/family.cmake
+++ b/hw/bsp/apm32f0xx/family.cmake
@@ -1,7 +1,7 @@
include_guard()
set(APM32_FAMILY apm32f0xx)
-set(APM32_SDK ${TOP}/hw/mcu/geehy/APM32F0xx_SDK_V1.8.6/Libraries)
+set(APM32_SDK ${TOP}/hw/mcu/geehy/APM32F0xx_SDK/Libraries)
# include board specific
include(${CMAKE_CURRENT_LIST_DIR}/boards/${BOARD}/board.cmake)
@@ -31,7 +31,6 @@ function(family_add_board BOARD_TARGET)
${APM32_SDK}/APM32F0xx_StdPeriphDriver/src/apm32f0xx_gpio.c
${APM32_SDK}/APM32F0xx_StdPeriphDriver/src/apm32f0xx_misc.c
${APM32_SDK}/APM32F0xx_StdPeriphDriver/src/apm32f0xx_rcm.c
- ${APM32_SDK}/APM32F0xx_StdPeriphDriver/src/apm32f0xx_crs.c
)
target_include_directories(${BOARD_TARGET} PUBLIC
${CMAKE_CURRENT_FUNCTION_LIST_DIR}
diff --git a/hw/bsp/apm32f0xx/family.mk b/hw/bsp/apm32f0xx/family.mk
index 73a762d69..7e26918c2 100644
--- a/hw/bsp/apm32f0xx/family.mk
+++ b/hw/bsp/apm32f0xx/family.mk
@@ -1,5 +1,5 @@
APM32_FAMILY = apm32f0xx
-APM32_SDK = hw/mcu/geehy/APM32F0xx_SDK_V1.8.6/Libraries
+APM32_SDK = hw/mcu/geehy/APM32F0xx_SDK/Libraries
include $(TOP)/$(BOARD_PATH)/board.mk
@@ -20,7 +20,6 @@ SRC_C += \
$(APM32_SDK)/APM32F0xx_StdPeriphDriver/src/apm32f0xx_gpio.c \
$(APM32_SDK)/APM32F0xx_StdPeriphDriver/src/apm32f0xx_misc.c \
$(APM32_SDK)/APM32F0xx_StdPeriphDriver/src/apm32f0xx_rcm.c \
- $(APM32_SDK)/APM32F0xx_StdPeriphDriver/src/apm32f0xx_crs.c \
$(APM32_SDK)/Device/Geehy/APM32F0xx/Source/system_apm32f0xx.c
INC += \
diff --git a/hw/bsp/ch32v10x/family.cmake b/hw/bsp/ch32v10x/family.cmake
index fb9ccb3a3..287b8c2ff 100644
--- a/hw/bsp/ch32v10x/family.cmake
+++ b/hw/bsp/ch32v10x/family.cmake
@@ -93,6 +93,6 @@ function(family_configure_example TARGET RTOS)
# Flashing
family_add_bin_hex(${TARGET})
- family_flash_openocd_wch(${TARGET})
+ family_flash_openocd(${TARGET})
#family_flash_uf2(${TARGET} ${UF2_FAMILY_ID})
endfunction()
diff --git a/hw/bsp/ch32v10x/family.mk b/hw/bsp/ch32v10x/family.mk
index fb699b0bb..443509699 100644
--- a/hw/bsp/ch32v10x/family.mk
+++ b/hw/bsp/ch32v10x/family.mk
@@ -49,5 +49,5 @@ INC += \
FREERTOS_PORTABLE_SRC = $(FREERTOS_PORTABLE_PATH)/RISC-V
-OPENOCD_WCH_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg
+OPENOCD_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg
flash: flash-openocd-wch
diff --git a/hw/bsp/ch32v20x/family.cmake b/hw/bsp/ch32v20x/family.cmake
index 785f5ee35..a27ff021e 100644
--- a/hw/bsp/ch32v20x/family.cmake
+++ b/hw/bsp/ch32v20x/family.cmake
@@ -125,7 +125,7 @@ function(family_configure_example TARGET RTOS)
# Flashing
family_add_bin_hex(${TARGET})
- family_flash_openocd_wch(${TARGET})
+ family_flash_openocd(${TARGET})
family_flash_wlink_rs(${TARGET})
#family_flash_uf2(${TARGET} ${UF2_FAMILY_ID})
endfunction()
diff --git a/hw/bsp/ch32v20x/family.mk b/hw/bsp/ch32v20x/family.mk
index 1d059bcba..1889c4e26 100644
--- a/hw/bsp/ch32v20x/family.mk
+++ b/hw/bsp/ch32v20x/family.mk
@@ -63,6 +63,6 @@ INC += \
FREERTOS_PORTABLE_SRC = $(FREERTOS_PORTABLE_PATH)/RISC-V
-OPENOCD_WCH_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg
+OPENOCD_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg
flash: flash-wlink-rs
#flash: flash-openocd-wch
diff --git a/hw/bsp/ch32v30x/family.cmake b/hw/bsp/ch32v30x/family.cmake
index b974bd5e7..e33e4b85d 100644
--- a/hw/bsp/ch32v30x/family.cmake
+++ b/hw/bsp/ch32v30x/family.cmake
@@ -115,6 +115,6 @@ function(family_configure_example TARGET RTOS)
# Flashing
family_add_bin_hex(${TARGET})
- family_flash_openocd_wch(${TARGET})
+ family_flash_openocd(${TARGET})
family_flash_wlink_rs(${TARGET})
endfunction()
diff --git a/hw/bsp/ch32v30x/family.mk b/hw/bsp/ch32v30x/family.mk
index 5ccdea8ae..59778ec54 100644
--- a/hw/bsp/ch32v30x/family.mk
+++ b/hw/bsp/ch32v30x/family.mk
@@ -62,5 +62,5 @@ LD_FILE ?= $(FAMILY_PATH)/linker/ch32v30x.ld
# For freeRTOS port source
FREERTOS_PORTABLE_SRC = $(FREERTOS_PORTABLE_PATH)/RISC-V
-OPENOCD_WCH_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg
+OPENOCD_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg
flash: flash-openocd-wch
diff --git a/hw/bsp/ch583/family.cmake b/hw/bsp/ch583/family.cmake
index a379298e5..f4b874e4e 100644
--- a/hw/bsp/ch583/family.cmake
+++ b/hw/bsp/ch583/family.cmake
@@ -104,5 +104,5 @@ function(family_configure_example TARGET RTOS)
# Flashing
family_add_bin_hex(${TARGET})
- family_flash_openocd_wch(${TARGET})
+ family_flash_openocd(${TARGET})
endfunction()
diff --git a/hw/bsp/ch583/family.mk b/hw/bsp/ch583/family.mk
index 98d0f9337..3444c4811 100644
--- a/hw/bsp/ch583/family.mk
+++ b/hw/bsp/ch583/family.mk
@@ -52,7 +52,7 @@ INC += \
LD_FILE ?= $(FAMILY_PATH)/linker/ch582.ld
-OPENOCD_WCH_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg
+OPENOCD_OPTION=-f $(TOP)/$(FAMILY_PATH)/wch-riscv.cfg
flash: flash-openocd-wch
# For freeRTOS port source
diff --git a/hw/bsp/ch583/wch-riscv.cfg b/hw/bsp/ch583/wch-riscv.cfg
index 64d595d8e..aa35aa9c5 100644
--- a/hw/bsp/ch583/wch-riscv.cfg
+++ b/hw/bsp/ch583/wch-riscv.cfg
@@ -9,7 +9,7 @@ sdi newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x00001
set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME.0 wch_riscv -chain-position $_TARGETNAME
-$_TARGETNAME.0 configure -work-area-phys 0x20000000 -work-area-size 0x8000 -work-area-backup 1
+$_TARGETNAME.0 configure -work-area-phys 0x20000000 -work-area-size 10000 -work-area-backup 1
set _FLASHNAME $_CHIPNAME.flash
flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 $_TARGETNAME.0
diff --git a/hw/bsp/family_rules.mk b/hw/bsp/family_rules.mk
index ccf49dd0e..011572888 100644
--- a/hw/bsp/family_rules.mk
+++ b/hw/bsp/family_rules.mk
@@ -130,20 +130,18 @@ flash-pyocd: $(BUILD)/$(PROJECT).hex
#pyocd reset -t $(PYOCD_TARGET)
# --------------- openocd -----------------
+# OPENOCD can name another build, e.g. one of the vendor forks, though
+# https://github.com/hathach/openocd branch tinyusb covers every board here
+OPENOCD ?= openocd
OPENOCD_OPTION ?=
flash-openocd: $(BUILD)/$(PROJECT).elf
- openocd $(OPENOCD_OPTION) -c "program $< verify reset exit"
+ $(OPENOCD) $(OPENOCD_OPTION) -c "program $< verify reset exit"
# --------------- openocd-wch -----------------
-# wch-linke is not supported yet in official openOCD yet. We need to either use
-# 1. download openocd as part of mounriver studio http://www.mounriver.com/download or
-# 2. compiled from https://github.com/hathach/riscv-openocd-wch or
-# https://github.com/dragonlock2/miscboards/blob/main/wch/SDK/riscv-openocd.tar.xz
-# with ./configure --disable-werror --enable-wlinke --enable-ch347=no
-OPENOCD_WCH ?= /home/${USER}/app/riscv-openocd-wch/src/openocd
-OPENOCD_WCH_OPTION ?=
+# WCH parts need an openocd built with the wlinke adapter. The image is written
+# without verify: WCH code flash is not readable back over the debug bus.
flash-openocd-wch: $(BUILD)/$(PROJECT).elf
- $(OPENOCD_WCH) $(OPENOCD_WCH_OPTION) -c init -c halt -c "flash write_image $<" -c reset -c exit
+ $(OPENOCD) $(OPENOCD_OPTION) -c init -c halt -c "flash write_image $<" -c reset -c exit
# --------------- wlink-rs -----------------
# flash with https://github.com/ch32-rs/wlink
diff --git a/hw/bsp/family_support.cmake b/hw/bsp/family_support.cmake
index 1f3952205..33ceb49c2 100644
--- a/hw/bsp/family_support.cmake
+++ b/hw/bsp/family_support.cmake
@@ -688,7 +688,9 @@ function(family_flash_stflash TARGET)
endfunction()
-# Add flash openocd target
+# Add flash openocd target.
+# The default 'openocd' should be https://github.com/hathach/openocd (branch tinyusb): which is mainline plus
+# every config the rig needs (RP2350, MAX32/MAX78, WCH) and a drop-in superset of the vendor (downstream) forks
function(family_flash_openocd TARGET)
if (NOT DEFINED OPENOCD)
set(OPENOCD openocd)
@@ -715,38 +717,20 @@ function(family_flash_openocd TARGET)
#set_property(TARGET ${TARGET}-openocd PROPERTY FOLDER ${TARGET}-group)
endfunction()
-
-# Add flash openocd-wch target
-# compiled from https://github.com/hathach/riscv-openocd-wch or https://github.com/dragonlock2/miscboards/blob/main/wch/SDK/riscv-openocd.tar.xz
-function(family_flash_openocd_wch TARGET)
- if (NOT DEFINED OPENOCD)
- set(OPENOCD $ENV{HOME}/app/riscv-openocd-wch/src/openocd)
+# Add flash openocd adi (Analog Devices) target using the openocd included
+# with msdk (MAXIM_PATH), otherwise the default openocd
+function(family_flash_openocd_adi TARGET)
+ # use openocd from msdk if MAXIM_PATH is set, as cmake variable or in the
+ # environment. Normalize the latter since msdk can be Windows (MinGW) or Linux
+ if (NOT DEFINED MAXIM_PATH AND DEFINED ENV{MAXIM_PATH})
+ file(TO_CMAKE_PATH "$ENV{MAXIM_PATH}" MAXIM_PATH)
endif ()
- family_flash_openocd(${TARGET})
-endfunction()
-
-
-# Add flash openocd adi (Analog Devices) target
-# included with msdk or compiled from release branch of https://github.com/analogdevicesinc/openocd
-function(family_flash_openocd_adi TARGET)
- if (DEFINED MAXIM_PATH)
- # use openocd from msdk with MAXIM_PATH cmake variable first if the user specified it
- set(OPENOCD ${MAXIM_PATH}/Tools/OpenOCD/openocd)
- set(OPENOCD_OPTION2 "-s ${MAXIM_PATH}/Tools/OpenOCD/scripts")
- elseif (DEFINED ENV{MAXIM_PATH})
- # use openocd from msdk with MAXIM_PATH environment variable. Normalize
- # since msdk can be Windows (MinGW) or Linux
- file(TO_CMAKE_PATH "$ENV{MAXIM_PATH}" MAXIM_PATH_NORM)
- set(OPENOCD ${MAXIM_PATH_NORM}/Tools/OpenOCD/openocd)
- set(OPENOCD_OPTION2 "-s ${MAXIM_PATH_NORM}/Tools/OpenOCD/scripts")
- else()
- # compiled from source
- if (NOT DEFINED OPENOCD_ADI_PATH)
- set(OPENOCD_ADI_PATH $ENV{HOME}/app/openocd_adi)
+ if (MAXIM_PATH)
+ if (NOT DEFINED OPENOCD)
+ set(OPENOCD ${MAXIM_PATH}/Tools/OpenOCD/openocd)
endif ()
- set(OPENOCD ${OPENOCD_ADI_PATH}/src/openocd)
- set(OPENOCD_OPTION2 "-s ${OPENOCD_ADI_PATH}/tcl")
+ set(OPENOCD_OPTION2 "-s ${MAXIM_PATH}/Tools/OpenOCD/scripts")
endif ()
family_flash_openocd(${TARGET})
diff --git a/hw/bsp/rp2040/family.cmake b/hw/bsp/rp2040/family.cmake
index aab9a4fae..43b1dc234 100644
--- a/hw/bsp/rp2040/family.cmake
+++ b/hw/bsp/rp2040/family.cmake
@@ -28,6 +28,8 @@ elseif (PICO_PLATFORM STREQUAL "rp2350-arm-s" OR PICO_PLATFORM STREQUAL "rp2350"
set(OPENOCD_TARGET rp2350)
elseif (PICO_PLATFORM STREQUAL "rp2350-riscv")
set(JLINK_DEVICE rp2350_riscv_0)
+ # rp2350-riscv.cfg needs the raspberrypi/openocd fork: mainline's riscv
+ # target does not take the -dap/-ap-num the config uses
set(OPENOCD_TARGET rp2350-riscv)
endif()
diff --git a/src/common/tusb_types.h b/src/common/tusb_types.h
index d49d277ed..fa4d67df1 100644
--- a/src/common/tusb_types.h
+++ b/src/common/tusb_types.h
@@ -244,7 +244,7 @@ enum {
TUSB_DESC_CONFIG_ATT_SELF_POWERED = 1 << 6,
};
-#define TUSB_DESC_CONFIG_POWER_MA(x) ((x)/2)
+#define TUSB_DESC_CONFIG_POWER_MA(x) ((uint8_t)TU_MIN((x)/2, UINT8_MAX))
// USB 2.0 Spec Table 9-7: Test Mode Selectors
typedef enum {
diff --git a/src/device/usbd.h b/src/device/usbd.h
index 9be12ed0a..2015d1869 100644
--- a/src/device/usbd.h
+++ b/src/device/usbd.h
@@ -229,7 +229,7 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ
// Config number, interface count, string index, total length, attribute, power in mA
#define TUD_CONFIG_DESCRIPTOR(config_num, _itfcount, _stridx, _total_len, _attribute, _power_ma) \
- 9, TUSB_DESC_CONFIGURATION, U16_TO_U8S_LE(_total_len), _itfcount, config_num, _stridx, TU_BIT(7) | _attribute, (_power_ma)/2
+ 9, TUSB_DESC_CONFIGURATION, U16_TO_U8S_LE(_total_len), _itfcount, config_num, _stridx, TU_BIT(7) | _attribute, (uint8_t)TU_MIN((_power_ma)/2, UINT8_MAX)
//--------------------------------------------------------------------+
// CDC Descriptor Templates
diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py
index e3073faba..f7f053492 100755
--- a/test/hil/hil_test.py
+++ b/test/hil/hil_test.py
@@ -24,11 +24,12 @@
# Host setup (required: a missing tool fails its test rather than skipping it):
# - System packages: sudo apt install mtools libmtp9 libmtp-runtime alsa-utils iperf
-# mtools - read_disk_file (device/cdc_msc, device/msc_dual_lun)
-# libmtp9 - pymtp ctypes load (device/mtp); Debian 13 uses libmtp9t64
-# libmtp-runtime - mtp-probe and the completed-device /dev/libmtp-* marker
-# alsa-utils - arecord (device/audio_test_freertos)
-# iperf - throughput tests (device/net_lwip_*)
+# mtools read_disk_file (device/cdc_msc, device/msc_dual_lun)
+# libmtp9 pymtp ctypes load (device/mtp); Debian 13 uses libmtp9t64
+# libmtp-runtime mtp-probe and the completed-device /dev/libmtp-* marker
+# alsa-utils arecord (device/audio_test_freertos)
+# iperf throughput tests (device/net_lwip_*)
+# openocd unified openocd from https://github.com/hathach/openocd (branch tinyusb) for wch, rp2040/rp2350, analog max32
# - device/usbtest: usbtest kernel module + testusb binary (kernel tools/usb/testusb.c) on PATH,
# plus sudo for modprobe / sysfs writes
# - Python packages: pip install -r requirements.txt
@@ -378,25 +379,6 @@ def cmd_stdout_text(out: Any) -> str:
return out.decode('utf-8', errors='ignore')
return str(out)
-WCH_RISCV_CONTENT = """
-adapter driver wlinke
-adapter speed 6000
-transport select sdi
-
-wlink_set_address 0x00000000
-set _CHIPNAME wch_riscv
-sdi newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x00001
-
-set _TARGETNAME $_CHIPNAME.cpu
-
-target create $_TARGETNAME.0 wch_riscv -chain-position $_TARGETNAME
-$_TARGETNAME.0 configure -work-area-phys 0x20000000 -work-area-size 10000 -work-area-backup 1
-set _FLASHNAME $_CHIPNAME.flash
-
-flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 $_TARGETNAME.0
-
-echo "Ready for Remote Connections"
-"""
MSC_README_TXT = \
b"This is tinyusb's MassStorage Class demo.\r\n\r\n\
@@ -726,24 +708,15 @@ def reset_openocd(board):
def flash_openocd_wch(board, firmware):
flasher = board['flasher']
- f_wch = f"wch-riscv_{board['uid']}.cfg"
- if not os.path.exists(f_wch):
- with open(f_wch, 'w') as file:
- file.write(WCH_RISCV_CONTENT)
-
- ret = run_cmd(f'openocd_wch -c "adapter serial {flasher["uid"]}" -f {f_wch} '
- f'-c "program {firmware}.elf reset exit"')
+ ret = run_cmd(f'openocd -c "tcl_port disabled" -c "gdb_port disabled" -c "telnet_port disabled" '
+ f'-c "adapter serial {flasher["uid"]}" {flasher.get("args", "")} -c "program {firmware}.elf reset exit"')
return ret
def reset_openocd_wch(board):
flasher = board['flasher']
- f_wch = f"wch-riscv_{board['uid']}.cfg"
- if not os.path.exists(f_wch):
- with open(f_wch, 'w') as file:
- file.write(WCH_RISCV_CONTENT)
-
- ret = run_cmd(f'openocd_wch -c "adapter serial {flasher["uid"]}" -f {f_wch} -c "program reset exit"')
+ ret = run_cmd(f'openocd -c "tcl_port disabled" -c "gdb_port disabled" -c "telnet_port disabled" '
+ f'-c "adapter serial {flasher["uid"]}" {flasher.get("args", "")} -c "init; reset run; exit"')
return ret
diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json
index 812eb7571..8316dbc33 100644
--- a/test/hil/tinyusb.json
+++ b/test/hil/tinyusb.json
@@ -147,7 +147,7 @@
"dual": false
},
"flasher": {
- "name": "openocd_adi",
+ "name": "openocd",
"uid": "E6614C311B597D32",
"args": "-f interface/cmsis-dap.cfg -f target/max32665.cfg"
}
@@ -465,7 +465,7 @@
"flasher": {
"name": "openocd_wch",
"uid": "EBCA8F0670AF",
- "args": ""
+ "args": "-f target/wch-riscv.cfg"
}
},
{
@@ -480,7 +480,7 @@
"flasher": {
"name": "openocd_wch",
"uid": "BC4954081051",
- "args": ""
+ "args": "-f target/wch-riscv.cfg"
}
},
{
@@ -499,7 +499,7 @@
"flasher": {
"name": "openocd_wch",
"uid": "BC5DA47360D0",
- "args": ""
+ "args": "-f target/wch-riscv.cfg"
}
},
{
@@ -514,7 +514,7 @@
"flasher": {
"name": "openocd_wch",
"uid": "7FD88F0604B5",
- "args": ""
+ "args": "-f target/wch-riscv.cfg"
}
},
{
diff --git a/test/hil/usbtest.py b/test/hil/usbtest.py
index e17705a48..83ea3e24c 100755
--- a/test/hil/usbtest.py
+++ b/test/hil/usbtest.py
@@ -254,11 +254,46 @@ def dmesg_tail():
return '\n'.join(lines[-8:])
-def pci_addr_of_bus(busnum):
- """Return the PCI B:D.F backing a USB bus, or None for a non-PCI (SoC/platform) controller."""
- m = re.search(r'([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9])/usb\d+$',
- os.path.realpath(f'/sys/bus/usb/devices/usb{int(busnum)}'))
- return m.group(1) if m else None
+def wedged_pids(devnode):
+ """Return (pids, complete): PIDs in uninterruptible sleep whose cmdline names devnode, i.e.
+ still holding its usbfs device lock, and whether every /proc entry could actually be read.
+
+ Matched by device node rather than by our child's pid because run_case() may wrap testusb in
+ sudo, in which case the Popen pid is the wrapper and the blocked process is its child --
+ killing the wrapper would make a pid-based check look clean while the real holder is stuck.
+
+ complete is False when a PermissionError hid an entry (a hidepid/ProtectProc mount, or the
+ root-owned child of that same sudo). An entry we could not read might be the holder, so the
+ caller must treat that as unrecovered rather than as an all-clear."""
+ stuck, complete = [], True
+ # hidepid=2 and systemd's ProtectProc=invisible omit other users' processes from iterdir()
+ # entirely -- no entry at all, so no PermissionError to catch -- and testusb runs under sudo
+ # whenever the device node is not writable. The scan would then look clean while hiding the
+ # very holder it exists to find. pid 1 is always root-owned, so being unable to read it means
+ # enumeration is restricted and no result from this scan can be trusted as complete.
+ if os.geteuid() != 0 and not os.access('/proc/1/cmdline', os.R_OK):
+ complete = False
+ for entry in Path('/proc').iterdir():
+ if not entry.name.isdigit():
+ continue
+ try:
+ cmdline = (entry / 'cmdline').read_bytes()
+ except PermissionError:
+ complete = False # cannot rule this pid out
+ continue
+ except OSError:
+ continue # raced with process exit: genuinely gone, not hidden
+ if devnode.encode() not in cmdline:
+ continue
+ try:
+ stat = (entry / 'stat').read_text()
+ if stat[stat.rindex(')') + 2] == 'D': # comm may contain ')', so scan from the right
+ stuck.append(int(entry.name))
+ except PermissionError:
+ complete = False
+ except (OSError, ValueError, IndexError):
+ continue
+ return stuck, complete
def run_case(num, dev, testusb, quick, timeout):
@@ -387,20 +422,68 @@ def main():
extra += f" {r['mbps']} MB/s" if 'mbps' in r else ''
print(f"test {num:2d} {r['name']:22s} {r['status']:6s}{extra}")
if r['status'] == 'HUNG':
- pci = pci_addr_of_bus(dev['node'].split('/')[-2])
- if pci:
- print(f'aborting battery: kernel-side hang, device wedged mid-transfer.\n'
- f'auto-recovering: sudo {USB_RECOVER} pci-reset {pci} '
- f'(see .claude/skills/usb-kernel-recover)', file=sys.stderr)
- # FLR frees the D-state ioctl without the device lock; must run BEFORE
- # any unbind/remove_id, which would deadlock the bus otherwise
- if sudo([str(USB_RECOVER), 'pci-reset', pci]).returncode != 0:
- unrecovered_hang = True
- time.sleep(5) # let the bus re-enumerate before cleanup touches sysfs
- else:
- unrecovered_hang = True
- print('aborting battery: kernel-side hang, and the controller has no PCI address '
- 'for FLR recovery — manual intervention (reboot) required', file=sys.stderr)
+ print(f'aborting battery: kernel-side hang, device wedged mid-transfer.\n'
+ f'auto-recovering: {USB_RECOVER.name} root-cycle {dev["sysname"]} '
+ f'(see .claude/skills/usb-kernel-recover)', file=sys.stderr)
+ # Cutting VBUS at the root port fails the in-flight URB so the usbfs ioctl returns.
+ # Must run BEFORE any unbind/remove_id, which would take the device lock the stuck
+ # ioctl holds and deadlock the bus.
+ #
+ # Assume unrecovered until proven otherwise, so that any early exit from this block
+ # -- an OSError spawning the helper, a KeyboardInterrupt, a sudo prompt killing the
+ # run -- still reaches the finally cleanup with the flag set, instead of running
+ # the remove_id/unbind the comments there forbid while a device lock is held.
+ unrecovered_hang = True
+ # Pass the serial so the helper refuses a stale busport rather than cutting power
+ # to whatever else now occupies that path. Popen rather than sudo()/subprocess.run:
+ # run() would kill() then wait() unbounded on timeout, which never returns if
+ # uhubctl is itself in D state -- the case the timeout exists for. Merge stderr
+ # into stdout so the helper's target-identity and action lines are not lost.
+ # Only pass the serial when we actually have one: an empty third argument reads as
+ # "no expectation" and would silently disable the helper's stale-busport guard.
+ cmd = [str(USB_RECOVER), 'root-cycle', dev['sysname']]
+ if dev['serial']:
+ cmd.append(dev['serial'])
+ if os.geteuid() != 0:
+ cmd = ['sudo', '-n'] + cmd
+ try:
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ text=True)
+ except OSError as e:
+ # helper missing or not executable, or sudo unavailable. unrecovered_hang is
+ # already True so the finally block still skips the unsafe cleanup -- this only
+ # replaces a traceback with a message that says what to fix.
+ print(f'cannot run {USB_RECOVER}: {e}', file=sys.stderr)
+ break
+ rc = None
+ try:
+ out, _ = p.communicate(timeout=60) # normal run is ~8s
+ rc = p.returncode
+ except subprocess.TimeoutExpired:
+ p.kill()
+ try:
+ out, _ = p.communicate(timeout=5)
+ rc = p.returncode
+ except subprocess.TimeoutExpired:
+ out = ('root-cycle abandoned after 60s: uhubctl did not die to SIGKILL, so '
+ 'it is wedged too and the convoy has spread beyond this device')
+ if out:
+ print(out.strip(), file=sys.stderr)
+ if rc is not None:
+ time.sleep(5) # let the bus settle and the freed ioctl unwind
+ # Authoritative either way. A non-zero exit only means the device did not come
+ # back within the poll (a slow bootloader will do that) -- if nothing still
+ # holds the lock, the bus is usable and cleanup is safe. Conversely a zero exit
+ # only proves re-enumeration, not that the D-state holder let go.
+ stuck, complete = wedged_pids(dev['node'])
+ if stuck:
+ print(f'{dev["sysname"]}: pid(s) {stuck} still in D state on '
+ f'{dev["node"]} — the device lock was never released', file=sys.stderr)
+ elif not complete:
+ print('cannot confirm recovery: /proc is only partly readable, so a '
+ 'hidden D-state holder cannot be ruled out', file=sys.stderr)
+ else:
+ unrecovered_hang = False
break
# re-resolve: after a mid-battery re-enumeration the devnum (and thus the node
# path) changes; keep testing the live node instead of the stale one. Match on the
@@ -419,7 +502,8 @@ def main():
if unrecovered_hang:
# testusb is still stuck in a usbfs ioctl holding the device lock; remove_id/unbind
# would join the convoy and deadlock the bus (see usb-kernel-recover skill) — leave it be
- print('skipping cleanup after unrecovered hang: reboot required to release the bus',
+ print('skipping cleanup after unrecovered hang: ask the operator for a full PVE host '
+ 'power cycle (a VM reboot is not reliable — hubs latch up across the PCIe reset)',
file=sys.stderr)
elif not args.keep_binding:
sysfs_write(DRIVER / 'remove_id', f'{VID} {PID}', check=False)
diff --git a/tools/codespell/ignore-words.txt b/tools/codespell/ignore-words.txt
index 0b1aa284a..6b301d27b 100644
--- a/tools/codespell/ignore-words.txt
+++ b/tools/codespell/ignore-words.txt
@@ -5,6 +5,7 @@ endianess
fro
hsi
inout
+linke
mot
ore
pris
diff --git a/tools/get_deps.py b/tools/get_deps.py
index e26810cac..baaf3761f 100755
--- a/tools/get_deps.py
+++ b/tools/get_deps.py
@@ -46,6 +46,9 @@ deps_optional = {
'hw/mcu/gd/nuclei-sdk': ['https://github.com/Nuclei-Software/nuclei-sdk.git',
'7eb7bfa9ea4fbeacfafe1d5f77d5a0e6ed3922e7',
'gd32vf103'],
+ 'hw/mcu/geehy/APM32F0xx_SDK': ['https://github.com/GeehySemi/APM32F0xx_SDK.git',
+ 'cfc1fe826e1869133de86d4c6b298fc153e6bc32',
+ 'apm32f0xx'],
'hw/mcu/infineon/mtb-xmclib-cat3': ['https://github.com/Infineon/mtb-xmclib-cat3.git',
'daf5500d03cba23e68c2f241c30af79cd9d63880',
'xmc4000'],