diff --git a/test/hil/hil_flash.py b/test/hil/hil_flash.py index 814258072..da81fcc97 100755 --- a/test/hil/hil_flash.py +++ b/test/hil/hil_flash.py @@ -26,7 +26,7 @@ build_dir = 'cmake-build' 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 @@ def cmd_stdout_text(out: Any) -> str: # ------------------------------------------------------------- # 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 run_cmd(cmd: str, cwd: str | None = None, timeout: int = CMD_TIMEOUT) -> sub 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 reset_jlink(board: Board) -> subprocess.CompletedProcess: 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 reset_stflash(board): - flasher = board['flasher'] - return subprocess.CompletedProcess(args=['dummy'], returncode=0) + +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"]}') +# `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 @@ def reset_esptool(board): 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 @@ def reset_lm4flash(board): 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 - /cmake-build-//, 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 /cmake-build-//, + 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 63284213e..98f24288a 100644 --- a/test/hil/hil_pool_check.py +++ b/test/hil/hil_pool_check.py @@ -201,7 +201,7 @@ def resolve_variant(board: dict, example: str, note: list | None = None) -> str: 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 resolve_variant(board: dict, example: str, note: list | None = None) -> str: 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 @@ def pick_example(board: dict, note: list, build_missing: bool = True): 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 @@ def ensure_fw(board: dict, variant: str, example: str, note: list): (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 @@ def ensure_fw(board: dict, variant: str, example: str, note: list): # 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 @@ def ensure_board_test(board: dict, variant: str, note: list): """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 @@ def check_board(board: dict, args, allow_recovery: bool, seen: dict) -> dict: # 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 96d52e601..e7f82bd7f 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -1370,13 +1370,17 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st 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 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st 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 6a2bf6210..5e6b16759 100644 --- a/test/hil/test_hil_select.py +++ b/test/hil/test_hil_select.py @@ -9,6 +9,7 @@ import sys 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 @@ def real_rosters(): 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 @@ class TestPortWithoutFamilyIsFull(unittest.TestCase): 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 8f321baef..c9b38992c 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" + } } ] }