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 # deauthorize+reauthorize: re-enumerate, no VBUS cut sudo usb_recover.sh rebind # usb driver unbind+bind: re-probe sudo usb_recover.sh hub-cycle # uhubctl VBUS cycle of the feeding port, walking parent hub # -> root port until the device re-enumerates +sudo usb_recover.sh root-cycle [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 # whole HCD controller unbind+bind, e.g. 0000:02:00.0 -sudo usb_recover.sh pci-reset # PCI function-level reset: kills URBs at HW level, no device lock sudo usb_recover.sh pci-bind [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 +sudo usb_recover.sh root-cycle # 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 `, 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//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 # e.g. 3-2 -> deauthorize+reauthorize (re-enumerate, NO VBUS cut) # sudo usb_recover.sh rebind # e.g. 3-2 -> usb driver unbind+bind (re-probe) # sudo usb_recover.sh pci-rebind # e.g. 0000:01:00.0 -> HCD unbind+bind (WHOLE controller) -# sudo usb_recover.sh pci-reset # 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 [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 [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 # e.g. /dev/ttyACM3 -> print its (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/ 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-port/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 @@ - + 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/` 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/` 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 --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` 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-`; `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 ` 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) `` — 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 ` 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-` sets: build first (nanoch32v203 sets exist; ch32v103/307/ch582m may need `tools/get_deps.py -b ` + 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 @@ -42,6 +42,10 @@ "name": "apard32690", "inherits": "default" }, + { + "name": "apm32f072_dev_board", + "inherits": "default" + }, { "name": "arduino_nano33_ble", "inherits": "default" @@ -510,6 +514,10 @@ "name": "portenta_c33", "inherits": "default" }, + { + "name": "py32f071_dev_board", + "inherits": "default" + }, { "name": "pybadge", "inherits": "default" @@ -1007,6 +1015,11 @@ "description": "Build preset for the apard32690 board", "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", @@ -1637,6 +1650,11 @@ "description": "Build preset for the portenta_c33 board", "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", @@ -2257,6 +2275,19 @@ } ] }, + { + "name": "apm32f072_dev_board", + "steps": [ + { + "type": "configure", + "name": "apm32f072_dev_board" + }, + { + "type": "build", + "name": "apm32f072_dev_board" + } + ] + }, { "name": "arduino_nano33_ble", "steps": [ @@ -3895,6 +3926,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) {} + // Configure HSE and PLL for a 48 MHz system clock + SystemClockConfig(); - // Select HSI48 as USB clock source - RCM_ConfigUSBCLK(RCM_USBCLK_HSI48); - - // 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'],