test/hil: fold openocd_wch into openocd, verify per board, resolve firmware by flasher extension (#3804)

test/hil: one openocd flasher, per-board verify and firmware extension

The four WCH boards move to `openocd`, leaving one flasher for all.

`verify` is now a per-board opt-out, not dropped fleet-wide: WCH cannot read flash back
over the WCH-Link sdi transport; the other seven openocd boards can, and say so explicitly.

FLASHER_SUFFIX decides each flasher's extension once — find_firmware returns the full path
and the flashers pass it through, so a build with only the wrong artifact is skipped rather
than failed mid-flash. --skip-flash bypasses the filter.

rescue_openocd() power-on-resets a wedged RP2040/RP2350 via its Rescue DP from the flash
retry; the probe has no reset line.

Drops unused openocd_adi, stflash, wlink_rs and uniflash, parks the unstable ra6m5_ek, and
tests that every roster flasher name dispatches.
diff --git a/test/hil/hil_flash.py b/test/hil/hil_flash.py
index 8142580..da81fcc 100755
--- a/test/hil/hil_flash.py
+++ b/test/hil/hil_flash.py
@@ -26,7 +26,7 @@
 CMD_TIMEOUT = int(os.getenv('HIL_CMD_TIMEOUT', '180'))
 
 # flasher names (dispatch key, board['flasher']['name'].lower()) whose reset_* is a no-op
-RESET_NOOP = {'esptool', 'lm4flash', 'stflash', 'uniflash'}
+RESET_NOOP = {'esptool', 'lm4flash'}
 
 # extra parents find_firmware ALSO searches after build_dir. Empty by default so
 # hil_test's -B stays authoritative (a board missing there must report "Skip (no
@@ -46,7 +46,6 @@
 # -------------------------------------------------------------
 # Path
 # -------------------------------------------------------------
-OPENCOD_ADI_PATH = Path.home() / 'app' / 'openocd_adi'
 TINYUSB_ROOT = Path(__file__).resolve().parents[2]
 
 # get usb serial by id
@@ -129,7 +128,7 @@
 
 def flash_jlink(board: Board, firmware: str) -> subprocess.CompletedProcess:
     flasher = board['flasher']
-    script = ['halt', 'r', f'loadfile {firmware}.elf', 'r', 'go', 'exit']
+    script = ['halt', 'r', f'loadfile {firmware}', 'r', 'go', 'exit']
     f_jlink = Path(f'{board["name"]}_{Path(firmware).name}.jlink')
     with f_jlink.open('w') as f:
         f.writelines(f'{s}\n' for s in script)
@@ -151,88 +150,84 @@
 
 def flash_stlink(board, firmware):
     flasher = board['flasher']
-    return run_cmd(f'STM32_Programmer_CLI --connect port=swd sn={flasher["uid"]} --write {firmware}.elf --go')
+    return run_cmd(f'STM32_Programmer_CLI --connect port=swd sn={flasher["uid"]} --write {firmware} --go')
 
 
 def reset_stlink(board):
     flasher = board['flasher']
     return run_cmd(f'STM32_Programmer_CLI --connect port=swd sn={flasher["uid"]} --rst --go')
 
-def flash_stflash(board, firmware):
-    flasher = board['flasher']
-    ret = run_cmd(f'st-flash --serial {flasher["uid"]} write {firmware}.bin 0x8000000')
-    return ret
+
+def _openocd_cmd_base(flasher):
+    return (f'openocd -c "tcl_port disabled" -c "gdb_port disabled" -c "telnet_port disabled" '
+            f'-c "adapter serial {flasher["uid"]}" {flasher["args"]}')
 
 
-def reset_stflash(board):
-    flasher = board['flasher']
-    return subprocess.CompletedProcess(args=['dummy'], returncode=0)
-
-
+# `verify` is on by default and opted out per board with "verify": false in the roster.
+# WCH targets must opt out: flash read-back over the WCH-Link sdi transport returns a
+# repeated word instead of memory contents, so verification always reports a mismatch and
+# fails the flash (measured on ch32v103r and ch32v307v, 2026-07-30). Do NOT drop verify
+# fleet-wide to accommodate them — every other openocd board can read back, and without it
+# a partial or corrupt write exits 0 and the test phase runs bad firmware.
 def flash_openocd(board, firmware):
     flasher = board['flasher']
-    ret = run_cmd(f'openocd -c "tcl_port disabled" -c "gdb_port disabled" -c "adapter serial {flasher["uid"]}" '
-                  f'{flasher["args"]} -c "init; halt; program {firmware}.elf verify; reset; exit"')
+    verify = ' verify' if flasher.get('verify', True) else ''
+    ret = run_cmd(f'{_openocd_cmd_base(flasher)} -c "program {firmware}{verify} reset exit"')
     return ret
 
 
 def reset_openocd(board):
     flasher = board['flasher']
-    ret = run_cmd(f'openocd -c "tcl_port disabled" -c "gdb_port disabled" -c "adapter serial {flasher["uid"]}" '
-                  f'{flasher["args"]} -c "init; reset run; exit"')
+    ret = run_cmd(f'{_openocd_cmd_base(flasher)} -c "init; reset run; exit"')
     return ret
 
 
-def flash_openocd_wch(board, firmware):
+# OpenOCD's messages for "the target's debug port did not answer". The probe is fine when
+# these appear (the log still shows "CMSIS-DAP: Interface ready"); the chip's debug clock
+# is gone, which no reset the probe can drive would fix -- the CMSIS-DAP Debug Probe has no
+# nRESET line at all. Which message you get depends on the DAP topology, NOT on the board:
+# rp2040.cfg creates three multidrop DAPs (cores 0/1 and the Rescue DP at instance 0xf) so
+# it fails in swd_multidrop_select, while rp2350.cfg creates a single plain ADIv6 DAP that
+# fails earlier in swd_connect. A dead RP2040 can also produce the second one if the very
+# first DP read never gets through, so both are accepted for both chips -- it is the target
+# cfg in the roster args, below, that picks how to rescue.
+DAP_WEDGED = ('Failed to connect multidrop', 'Error connecting DP: cannot read IDR')
+
+# How each RP target reaches its Rescue DP, keyed by the target cfg named in flasher args.
+# (cfg substitution, extra args): rp2040.cfg drives the Rescue DP itself behind a RESCUE
+# flag and calls init/shutdown on its own; rp2350 has a separate cfg that pokes the rescue
+# bit via an AP register but never shuts down, so it would sit in the server loop until
+# CMD_TIMEOUT without an explicit one.
+RESCUE_CFG = {
+    'target/rp2040.cfg': ('target/rp2040.cfg', '-c "set RESCUE 1" ', ''),
+    'target/rp2350.cfg': ('target/rp2350-rescue.cfg', '', ' -c "shutdown"'),
+}
+
+
+def rescue_openocd(board, flash_out: str = '') -> bool:
+    """Power-on-reset a wedged RP2040/RP2350 through its Rescue DP, the one debug port not
+    gated by the system clock (RP2040 datasheet 2.3.4.2): setting CDBGPWRUPREQ hard-resets
+    the chip, and the bootrom halts it in a safe state ready to be flashed. This is the
+    only way back for a target whose cores have stopped answering -- otherwise the board
+    needs a physical replug, since the probe carries no reset line.
+
+    No-op (returns False) unless this is an openocd RP board AND the flash output shows the
+    wedge, so a flash that failed for any other reason still just retries. Returns True
+    when a rescue was attempted; the caller should retry the flash afterwards."""
     flasher = board['flasher']
-    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']
-    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
-
-
-def flash_openocd_adi(board: Board, firmware: str) -> subprocess.CompletedProcess:
-    flasher = board['flasher']
-    openocd = OPENCOD_ADI_PATH / 'src' / 'openocd'
-    tcl_dir = OPENCOD_ADI_PATH / 'tcl'
-    ret = run_cmd(f'{openocd} -c "adapter serial {flasher["uid"]}" -s {tcl_dir} '
-                  f'{flasher["args"]} -c "program {firmware}.elf reset exit"')
-    return ret
-
-
-def reset_openocd_adi(board: Board) -> subprocess.CompletedProcess:
-    flasher = board['flasher']
-    openocd = OPENCOD_ADI_PATH / 'src' / 'openocd'
-    tcl_dir = OPENCOD_ADI_PATH / 'tcl'
-    ret = run_cmd(f'{openocd} -c "adapter serial {flasher["uid"]}" -s {tcl_dir} '
-                  f'{flasher["args"]} -c "program reset exit"')
-    return ret
-
-
-def flash_wlink_rs(board, firmware):
-    flasher = board['flasher']
-    # wlink use index for probe selection and lacking usb serial support
-    ret = run_cmd(f'wlink flash {firmware}.elf')
-    return ret
-
-
-def reset_wlink_rs(board):
-    flasher = board['flasher']
-    # wlink use index for probe selection and lacking usb serial support
-    ret = run_cmd(f'wlink reset')
-    return ret
+    if flasher['name'].lower() != 'openocd' or not any(m in flash_out for m in DAP_WEDGED):
+        return False
+    for cfg, (rescue_cfg, pre, post) in RESCUE_CFG.items():
+        if cfg in flasher['args']:
+            args = flasher['args'].replace(cfg, rescue_cfg)
+            return run_cmd(f'{_openocd_cmd_base({**flasher, "args": pre + args})}{post}').returncode == 0
+    return False
 
 
 def flash_esptool(board: Board, firmware: str) -> subprocess.CompletedProcess:
     flasher = board['flasher']
     port = get_serial_dev(flasher["uid"], None, None, 0)
-    fw_dir = Path(f'{firmware}.bin').parent
+    fw_dir = Path(firmware).parent
     with (fw_dir / 'config.env').open() as f:
         idf_target = json.load(f)['IDF_TARGET']
     with (fw_dir / 'flash_args').open() as f:
@@ -248,21 +243,10 @@
     return subprocess.CompletedProcess(args=['dummy'], returncode=0)
 
 
-def flash_uniflash(board, firmware):
-    flasher = board['flasher']
-    ret = run_cmd(f'dslite.sh {flasher["args"]} -f {firmware}.hex')
-    return ret
-
-
-def reset_uniflash(board):
-    flasher = board['flasher']
-    return subprocess.CompletedProcess(args=['dummy'], returncode=0)
-
-
 def flash_lm4flash(board, firmware):
     # TI Tiva-C / Stellaris ICDI: lightweight lm4flash, resets and runs after write
     flasher = board['flasher']
-    ret = run_cmd(f'lm4flash -s {flasher["uid"]} {flasher["args"]} {firmware}.bin')
+    ret = run_cmd(f'lm4flash -s {flasher["uid"]} {flasher["args"]} {firmware}')
     return ret
 
 
@@ -272,22 +256,42 @@
     return subprocess.CompletedProcess(args=['dummy'], returncode=0)
 
 
-def find_firmware(variant: str, example: str, roots: list | None = None):
-    """Locate a built example's firmware base path (no extension) under
-    <build_dir>/cmake-build-<variant>/<example>/, then under EXTRA_BUILD_DIRS
-    (empty unless the caller opts in — see its comment). `roots` overrides that
-    search list entirely for one call (e.g. to find a build just produced by
-    tools/build.py in its fixed cmake-build/ layout without widening the global
-    policy). Accepts the single-config layout (firmware directly in the example
-    dir) or Ninja Multi-Config (a per-config subdir like RelWithDebInfo/).
-    Returns the base Path, or None if not built."""
+# The one place a flasher's firmware extension is decided: find_firmware resolves the
+# path with it and the flash_* functions pass that path through untouched. A flasher
+# added here without an entry falls back to .elf-or-.bin and can be handed the wrong
+# file — test_hil_select's TestRosterFlashersDispatch fails if a roster names one.
+FLASHER_SUFFIX = {
+    'esptool': '.bin',
+    'jlink': '.elf',
+    'lm4flash': '.bin',
+    'openocd': '.elf',
+    'stlink': '.elf',
+}
+
+
+def find_firmware(variant: str, example: str, roots: list | None = None, flasher: str | None = None):
+    """Locate a built example's firmware under <build_dir>/cmake-build-<variant>/<example>/,
+    then under EXTRA_BUILD_DIRS (empty unless the caller opts in — see its comment).
+    `roots` overrides that search list entirely for one call (e.g. to find a build just
+    produced by tools/build.py in its fixed cmake-build/ layout without widening the
+    global policy). `flasher` is the roster flasher name: it selects which extension
+    counts (see FLASHER_SUFFIX), so a build that produced only the other one is reported
+    missing — a clean "Skip (no binary)" — instead of being handed to the flasher, which
+    would fail opaquely on the absent file and burn every retry plus the board lock.
+    Accepts the single-config layout (firmware directly in the example dir) or Ninja
+    Multi-Config (a per-config subdir like RelWithDebInfo/).
+    Returns the full Path INCLUDING extension, or None if not built."""
     base = Path(example).name
+    suffixes = [FLASHER_SUFFIX.get(flasher.lower())] if flasher else []
+    if not suffixes or suffixes == [None]:
+        suffixes = ['.elf', '.bin']
     for bd in dict.fromkeys(roots if roots is not None else [build_dir, *EXTRA_BUILD_DIRS]):
         fw_dir = TINYUSB_ROOT / bd / f'cmake-build-{variant}' / example
         if not fw_dir.is_dir():
             continue
         for cand in [fw_dir / base, fw_dir / 'RelWithDebInfo' / base,
-                     *(p.with_suffix('') for p in sorted(fw_dir.glob(f'*/{base}.elf')))]:
-            if cand.with_suffix('.elf').exists() or cand.with_suffix('.bin').exists():
-                return cand
+                     *(p.with_suffix('') for s in suffixes for p in sorted(fw_dir.glob(f'*/{base}{s}')))]:
+            for s in suffixes:
+                if cand.with_suffix(s).exists():
+                    return cand.with_suffix(s)
     return None
diff --git a/test/hil/hil_pool_check.py b/test/hil/hil_pool_check.py
index 6328421..98f2428 100644
--- a/test/hil/hil_pool_check.py
+++ b/test/hil/hil_pool_check.py
@@ -201,7 +201,7 @@
     name = board['name']
     for v in board.get('variant') or [{'name': name}]:
         vn = v['name']
-        if hil_flash.find_firmware(vn, example):
+        if hil_flash.find_firmware(vn, example, flasher=board['flasher']['name']):
             if vn != name and note is not None and f'variant: {vn}' not in note:
                 note.append(f'variant: {vn}')
             return vn
@@ -211,8 +211,8 @@
 def pick_example(board: dict, note: list, build_missing: bool = True):
     """(example, kind, variant, fw) with built firmware for this board; kind is
     'device' (uid check) or 'host' (serial-output check); variant is the resolved
-    build-dir variant that has it (see resolve_variant); fw is the firmware base
-    path to flash. When nothing is built and build_missing is set (the default —
+    build-dir variant that has it (see resolve_variant); fw is the firmware path to
+    flash, extension included. When nothing is built and build_missing is set (the default —
     never skip a board for lack of a build), the preferred candidate is built on
     the spot via ensure_fw."""
     tests = board.get('tests', {})
@@ -229,7 +229,7 @@
         if ex in skip:
             continue
         variant = resolve_variant(board, ex, note)
-        fw = hil_flash.find_firmware(variant, ex)
+        fw = hil_flash.find_firmware(variant, ex, flasher=board['flasher']['name'])
         if fw:
             return ex, kind, variant, fw
     if not build_missing:
@@ -472,7 +472,7 @@
     (variant, example) per run, success or failure — memoized in _builds, so a
     repeat call (park, under the held flock) resolves instantly even when an
     exclusive -B hides the fresh cmake-build/ artifact from the global search."""
-    fw = hil_flash.find_firmware(variant, example)
+    fw = hil_flash.find_firmware(variant, example, flasher=board['flasher']['name'])
     if fw:
         return fw
     key, base = (variant, example), Path(example).name
@@ -523,7 +523,8 @@
     # look there too even when an explicit -B narrowed the global search — this is
     # OUR fresh build, not a stale-candidate fallback
     fw = hil_flash.find_firmware(variant, example,
-                                 roots=[hil_flash.build_dir, 'cmake-build'])
+                                 roots=[hil_flash.build_dir, 'cmake-build'],
+                                 flasher=board['flasher']['name'])
     _builds[key] = (fw, 'ok' if fw else 'no-fw')
     note.append(f'built {base}' if fw else f'build produced no firmware: {base}')
     return fw
@@ -533,7 +534,7 @@
     """board_test firmware for parking, building it if absent (via ensure_fw).
     Espressif included — tools/build.py builds board_test for that family too;
     the build just needs the ESP-IDF env (127 → noted, park is then skipped)."""
-    fw = hil_flash.find_firmware(variant, 'device/board_test')
+    fw = hil_flash.find_firmware(variant, 'device/board_test', flasher=board['flasher']['name'])
     if fw:
         return fw
     variants = board.get('variant') or [{'name': board['name']}]
@@ -706,7 +707,8 @@
     # even under --no-park; --no-build gates EVERY build, board_test included
     need_bt = (not args.no_build
                and (not args.no_park or kind == 'host')
-               and hil_flash.find_firmware(bt_variant, 'device/board_test') is None)
+               and hil_flash.find_firmware(bt_variant, 'device/board_test',
+                                           flasher=board['flasher']['name']) is None)
     if need_example or need_bt:
         # builds are long and run BEFORE locking (park must never hold the flock
         # through a build); peek the lock first so minutes of building are not
diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py
index 96d52e6..e7f82bd 100755
--- a/test/hil/hil_test.py
+++ b/test/hil/hil_test.py
@@ -1370,13 +1370,17 @@
 
     test_name = f'{variant:40} {example:30} ...'
 
-    fw_name = hil_flash.find_firmware(variant, example)
+    # --skip-flash runs whatever is already on the board, so any build counts as present:
+    # only the flashing path needs the artifact this board's flasher actually consumes.
+    # Filtering there too would skip the test as "no binary" over an extension it never uses.
+    fw_name = hil_flash.find_firmware(variant, example,
+                                      flasher=None if skip_flash else board['flasher']['name'])
     if fw_name is None:
         log_line(f'{test_name} Skip (no binary)')
         return 0, 'skip', None
 
     if verbose:
-        log_line(f'Flashing {fw_name}.elf')
+        log_line(f'Firmware {fw_name}')
 
     # flash firmware (unless --skip-flash), then run the test. Both may fail randomly,
     # retry a few times.
@@ -1396,7 +1400,13 @@
                     if PROFILE:
                         log_line(f'[prof] {variant} {example} flash attempt {i + 1}: '
                                  f'{time.monotonic() - t_flash:.1f}s rc={ret.returncode}')
-                flash_ok = (ret.returncode == 0)
+                    flash_ok = (ret.returncode == 0)
+                    # A wedged RP2040/RP2350 DAP answers nothing and the probe has no reset
+                    # line, so the retry would fail identically; POR it via the Rescue DP
+                    # first. No-op for every other board and every other flash failure.
+                    if not flash_ok and i + 1 < max_retry and \
+                            hil_flash.rescue_openocd(board, hil_flash.cmd_stdout_text(ret.stdout)):
+                        log_line(f'{variant} {example}: DAP wedged, rescued via Rescue DP')
             if flash_ok:
                 try:
                     tret = globals()[f'test_{example.replace("/", "_")}'](board)
diff --git a/test/hil/test_hil_select.py b/test/hil/test_hil_select.py
index 6a2bf62..5e6b167 100644
--- a/test/hil/test_hil_select.py
+++ b/test/hil/test_hil_select.py
@@ -9,6 +9,7 @@
 import unittest
 
 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import hil_flash
 import hil_select
 from hil_examples import device_tests, dual_tests, host_test
 
@@ -26,6 +27,19 @@
     return rosters
 
 
+def roster_flashers():
+    """(roster path, board) for every board in the live rosters, `boards-skip`
+    included: a parked board's flasher name must still dispatch, so that unparking it
+    is not what discovers the name went stale."""
+    for name in ('tinyusb.json', 'hfp.json'):
+        path = os.path.join(REPO, 'test/hil', name)
+        with open(path) as f:
+            cfg = json.load(f)
+        for key in ('boards', 'boards-skip'):
+            for b in cfg.get(key, []):
+                yield f'test/hil/{name}', b
+
+
 def on_roster(tc, *names):
     """The subset of `names` currently in the live rig rosters, skipping the test
     when none are. Parking/unparking a board is routine rig maintenance and must not
@@ -538,5 +552,30 @@
         self.assertTrue(any('no board family' in r for r in s['reasons']), s['reasons'])
 
 
+class TestRosterFlashersDispatch(unittest.TestCase):
+    """hil_test and hil_pool_check resolve a board's flasher with a bare
+    getattr(hil_flash, f'flash_{name}'), and hil_test does it inside a redirect_stdout —
+    so a renamed or typo'd roster name raises an AttributeError whose output is swallowed,
+    with nothing pointing at the roster as the thing to edit. Renaming a flash_*/reset_*
+    pair without updating every roster must fail here instead."""
+
+    def test_flash_and_reset_exist_for_every_roster_flasher(self):
+        for path, board in roster_flashers():
+            name = board['flasher']['name'].lower()
+            for fn in (f'flash_{name}', f'reset_{name}'):
+                self.assertTrue(callable(getattr(hil_flash, fn, None)),
+                                f'{path}: {board["name"]} uses flasher "{name}" '
+                                f'but hil_flash.{fn} does not exist')
+
+    def test_firmware_suffix_known_for_every_roster_flasher(self):
+        """find_firmware falls back to accepting .elf-or-.bin when a flasher is missing
+        from FLASHER_SUFFIX, silently restoring the mismatch that map exists to catch."""
+        for path, board in roster_flashers():
+            name = board['flasher']['name'].lower()
+            self.assertIn(name, hil_flash.FLASHER_SUFFIX,
+                          f'{path}: {board["name"]} uses flasher "{name}" '
+                          f'with no hil_flash.FLASHER_SUFFIX entry')
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=1)
diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json
index 8f321ba..c9b3899 100644
--- a/test/hil/tinyusb.json
+++ b/test/hil/tinyusb.json
@@ -149,7 +149,8 @@
             "flasher": {
                 "name": "openocd",
                 "uid": "E6614C311B597D32",
-                "args": "-f interface/cmsis-dap.cfg -f target/max32665.cfg"
+                "args": "-f interface/cmsis-dap.cfg -f target/max32665.cfg",
+                "verify": true
             }
         },
         {
@@ -239,7 +240,8 @@
             "flasher": {
                 "name": "openocd",
                 "uid": "E6614103E72C1D2F",
-                "args": "-f interface/cmsis-dap.cfg -f target/rp2040.cfg -c \"adapter speed 5000\""
+                "args": "-f interface/cmsis-dap.cfg -f target/rp2040.cfg -c \"adapter speed 5000\"",
+                "verify": true
             }
         },
         {
@@ -268,7 +270,8 @@
             "flasher": {
                 "name": "openocd",
                 "uid": "E6633861A3819D38",
-                "args": "-f interface/cmsis-dap.cfg -f target/rp2040.cfg -c \"adapter speed 5000\""
+                "args": "-f interface/cmsis-dap.cfg -f target/rp2040.cfg -c \"adapter speed 5000\"",
+                "verify": true
             },
             "comment": "Test native host"
         },
@@ -293,7 +296,8 @@
             "flasher": {
                 "name": "openocd",
                 "uid": "E6633861A3978538",
-                "args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\""
+                "args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\"",
+                "verify": true
             }
         },
         {
@@ -322,7 +326,8 @@
             "flasher": {
                 "name": "openocd",
                 "uid": "E663AC91D3359B38",
-                "args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\""
+                "args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\"",
+                "verify": true
             }
         },
         {
@@ -404,7 +409,8 @@
             "flasher": {
                 "name": "openocd",
                 "uid": "004C00343137510F39383538",
-                "args": "-f interface/stlink.cfg -f target/stm32h7x.cfg"
+                "args": "-f interface/stlink.cfg -f target/stm32h7x.cfg",
+                "verify": true
             }
         },
         {
@@ -418,7 +424,8 @@
             "flasher": {
                 "name": "openocd",
                 "uid": "066FFF495087534867063844",
-                "args": "-f interface/stlink.cfg -f target/stm32g0x.cfg"
+                "args": "-f interface/stlink.cfg -f target/stm32g0x.cfg",
+                "verify": true
             },
             "comment": "32-bit scheme, 2KB USB SRAM"
         },
@@ -463,9 +470,10 @@
                 "dual": false
             },
             "flasher": {
-                "name": "openocd_wch",
+                "name": "openocd",
                 "uid": "A76D8F062C2A",
-                "args": "-f target/wch-riscv.cfg"
+                "args": "-f target/wch-riscv.cfg",
+                "verify": false
             }
         },
         {
@@ -478,9 +486,10 @@
                 "dual": false
             },
             "flasher": {
-                "name": "openocd_wch",
+                "name": "openocd",
                 "uid": "BC4954081051",
-                "args": "-f target/wch-riscv.cfg"
+                "args": "-f target/wch-riscv.cfg",
+                "verify": false
             }
         },
         {
@@ -497,9 +506,10 @@
                 "dual": false
             },
             "flasher": {
-                "name": "openocd_wch",
+                "name": "openocd",
                 "uid": "BC5DA47360D0",
-                "args": "-f target/wch-riscv.cfg"
+                "args": "-f target/wch-riscv.cfg",
+                "verify": false
             }
         },
         {
@@ -512,9 +522,10 @@
                 "dual": false
             },
             "flasher": {
-                "name": "openocd_wch",
+                "name": "openocd",
                 "uid": "57468F06DC03",
-                "args": "-f target/wch-riscv.cfg"
+                "args": "-f target/wch-riscv.cfg",
+                "verify": false
             }
         },
         {
@@ -561,22 +572,6 @@
                 "uid": "1051856258",
                 "args": "-device NRF54LM20A_M33"
             }
-        },
-        {
-            "name": "ra6m5_ek",
-            "uid": "8419032D32363657364EF4622D294B4E",
-            "tests": {
-                "device": true,
-                "host": false,
-                "dual": false,
-                "skip": ["device/cdc_msc_throughput", "device/msc_dual_lun"],
-                "comment": "MSC writes wedge the uPD720201 host (URBs queued, zero wire activity, bus-15 ctrl xfers time out until the device's URBs are killed); reproduced identically with master firmware - device side armed+BUF and exonerated. MSC reads and usbtest bulk (15.8 MB/s) are fine"
-            },
-            "flasher": {
-                "name": "jlink",
-                "uid": "000831915224",
-                "args": "-device R7FA6M5BH"
-            }
         }
     ],
     "boards-skip": [
@@ -612,6 +607,23 @@
                 "uid": "000778170924",
                 "args": "-device stm32f769ni"
             }
+        },
+        {
+            "name": "ra6m5_ek",
+            "uid": "8419032D32363657364EF4622D294B4E",
+            "comment": "Unstable in CI: intermittent usbtest failures plus cdc_dual_ports/hid_boot_interface/midi_test/mtp/printer_to_cdc flapping. Parked until diagnosed",
+            "tests": {
+                "device": true,
+                "host": false,
+                "dual": false,
+                "skip": ["device/cdc_msc_throughput", "device/msc_dual_lun"],
+                "comment": "MSC writes wedge the uPD720201 host (URBs queued, zero wire activity, bus-15 ctrl xfers time out until the device's URBs are killed); reproduced identically with master firmware - device side armed+BUF and exonerated. MSC reads and usbtest bulk (15.8 MB/s) are fine"
+            },
+            "flasher": {
+                "name": "jlink",
+                "uid": "000831915224",
+                "args": "-device R7FA6M5BH"
+            }
         }
     ]
 }