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.
This commit is contained in:
Ha Thach
2026-07-31 23:17:36 +07:00
committed by GitHub
parent eef5af86aa
commit f3021b337f
5 changed files with 194 additions and 127 deletions

View File

@ -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
<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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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"
}
}
]
}