From a2f4786865e85f9cfe7f58c86fbb9355bbd2d701 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Mon, 27 Jul 2026 14:39:02 +0200 Subject: [PATCH 01/16] portable/chipidea: configure LPC USB0 AHB bursts --- src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h | 14 ++++++++++++++ src/portable/chipidea/ci_hs/dcd_ci_hs.c | 4 ++++ src/portable/chipidea/ci_hs/hcd_ci_hs.c | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h index f2061bd7a..dec3a34b1 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h +++ b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h @@ -34,4 +34,18 @@ static const ci_hs_controller_t _ci_controller[] = #define CI_HCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum) #define CI_HCD_INT_DISABLE(_p) NVIC_DisableIRQ((IRQn_Type)_ci_controller[_p].irqnum) +enum { + CI_HS_LPC18_43_SBUSCFG_OFFSET = 0x90u, + CI_HS_LPC18_43_AHBBRST_INCR16_UNSPEC = 0x07u, +}; + +TU_ATTR_ALWAYS_INLINE static inline void ci_hs_lpc18_43_set_ahb_burst(uint8_t rhport) { + // USB0 SBUSCFG is at offset 0x90. NXP recommends AHBBRST=0x7: + // INCR16 with non-multiple transfers decomposed into smaller unspecified bursts. + if (rhport == 0) { + volatile uint32_t *sbuscfg = (volatile uint32_t *)(_ci_controller[rhport].reg_base + CI_HS_LPC18_43_SBUSCFG_OFFSET); + *sbuscfg = CI_HS_LPC18_43_AHBBRST_INCR16_UNSPEC; + } +} + #endif diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c index fa98d6882..32c701bfa 100644 --- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c @@ -237,6 +237,10 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { usbmode |= USBMODE_CM_DEVICE; dcd_reg->USBMODE = usbmode; + #if TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) + ci_hs_lpc18_43_set_ahb_burst(rhport); + #endif + #ifdef CFG_TUD_CI_HS_VBUS_CHARGE dcd_reg->OTGSC = OTGSC_VBUS_CHARGE | OTGSC_OTG_TERMINATION; #else diff --git a/src/portable/chipidea/ci_hs/hcd_ci_hs.c b/src/portable/chipidea/ci_hs/hcd_ci_hs.c index 3cb69acfa..c94ce810f 100644 --- a/src/portable/chipidea/ci_hs/hcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/hcd_ci_hs.c @@ -82,6 +82,10 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { hcd_reg->USBMODE = USBMODE_CM_HOST; #endif + #if TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) + ci_hs_lpc18_43_set_ahb_burst(rhport); + #endif + #if !TUH_OPT_HIGH_SPEED hcd_reg->PORTSC1 |= PORTSC1_FORCE_FULL_SPEED; #endif From 80ffbff6e98a9c5053bba008ae2c5087f0351300 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Mon, 27 Jul 2026 14:39:20 +0200 Subject: [PATCH 02/16] test/hil: separate LPC43 stress test flashes --- test/hil/hfp.json | 6 ++++- test/hil/hil_test.py | 62 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/test/hil/hfp.json b/test/hil/hfp.json index 735d5a402..17fbb7605 100644 --- a/test/hil/hfp.json +++ b/test/hil/hfp.json @@ -36,7 +36,11 @@ "flasher": { "name": "jlink", "uid": "728973776", - "args": "-device LPC43S67_M4" + "args": "-device LPC43S67_M4", + "pre_flash": { + "device/usbtest": "device/board_test", + "device/cdc_msc_throughput": "device/board_test" + } } } ] diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 0efc6826f..72af6c697 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -321,6 +321,7 @@ class FlasherCfg(TypedDict): name: str uid: str args: str + pre_flash: NotRequired[dict[str, str]] # target example -> USB-off separator example class AttachedDevCfg(TypedDict, total=False): @@ -1830,6 +1831,19 @@ def find_firmware(variant: str, example: str): return None +def usb_uid_paths(uid: str) -> set[str]: + """Return sysfs device paths currently exposing the requested USB serial.""" + paths = set() + for f in glob.glob('/sys/bus/usb/devices/*/serial'): + try: + with open(f) as serial_file: + if serial_file.read().strip().lower() == uid.lower(): + paths.add(os.path.dirname(f)) + except OSError: + pass + return paths + + def test_example(board: Board, variant: str, example: str) -> tuple[int, str, str | None]: """ Test example firmware @@ -1852,7 +1866,16 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st log_line(f'{test_name} Skip (no binary)') return 0, 'skip', None + pre_flash_example = None if skip_flash else board['flasher'].get('pre_flash', {}).get(example) + pre_flash_name = find_firmware(variant, pre_flash_example) if pre_flash_example else None + if pre_flash_example and pre_flash_name is None: + log_line(f'{test_name} {STATUS_FAILED}: ' + f'pre-flash firmware {pre_flash_example} not found') + return 1, 'fail', None + if verbose: + if pre_flash_name is not None: + log_line(f'Pre-flashing {pre_flash_name}.elf') log_line(f'Flashing {fw_name}.elf') # flash firmware (unless --skip-flash), then run the test. Both may fail randomly, @@ -1867,13 +1890,36 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st attempt_out = io.StringIO() with redirect_stdout(attempt_out): if not skip_flash: + flash_ok = True + flash_error = '' with flash_permit(board['uid']): - t_flash = time.monotonic() - ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name)) - 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) + if pre_flash_name is not None: + previous_usb_paths = usb_uid_paths(board['uid']) + ret = globals()[f'flash_{board["flasher"]["name"].lower()}']( + board, str(pre_flash_name)) + flash_ok = (ret.returncode == 0) + if not flash_ok: + flash_error = f'Pre-flash {pre_flash_example} failed' + elif previous_usb_paths: + disconnected = wait_until( + lambda: all(not os.path.exists(p) for p in previous_usb_paths), step=0.1) + if not disconnected: + flash_ok = False + flash_error = (f'Pre-flash {pre_flash_example} did not disconnect ' + f'USB device {board["uid"]}') + else: + time.sleep(0.1) + + if flash_ok: + t_flash = time.monotonic() + ret = globals()[f'flash_{board["flasher"]["name"].lower()}']( + board, str(fw_name)) + 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) + if not flash_ok: + flash_error = 'Flash failed' if flash_ok: try: tret = globals()[f'test_{example.replace("/", "_")}'](board) @@ -1911,10 +1957,10 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st log_line(msg) time.sleep(0.5) else: - last_err = 'Flash failed' + last_err = flash_error last_detail = compact_output(attempt_out.getvalue()) if i < max_retry - 1: - msg = f'{test_name} retry {i+2}/{max_retry}: flash failed' + msg = f'{test_name} retry {i+2}/{max_retry}: {flash_error}' if last_detail: msg += f' {last_detail}' log_line(msg) From f0a8a1483bd4e89ab3adf40d8c61777a5ddadc7f Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Mon, 27 Jul 2026 14:58:13 +0200 Subject: [PATCH 03/16] portable/chipidea: configure i.MX RT AHB bursts --- src/portable/chipidea/ci_hs/ci_hs_imxrt.h | 10 ++++++++++ src/portable/chipidea/ci_hs/dcd_ci_hs.c | 4 +++- src/portable/chipidea/ci_hs/hcd_ci_hs.c | 4 +++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h index f0f918fe2..601e4d1c9 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h +++ b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h @@ -36,6 +36,16 @@ static const ci_hs_controller_t _ci_controller[] = #define CI_HS_REG(_port) ((ci_hs_regs_t*) _ci_controller[_port].reg_base) +enum { + // INCR16/8/4 followed by an unspecified-length burst for the remainder. + CI_HS_IMXRT_AHBBRST_INCR16_UNSPEC = 0x07u, +}; + +TU_ATTR_ALWAYS_INLINE static inline void ci_hs_imxrt_set_ahb_burst(uint8_t rhport) { + USB_Type *usb = (USB_Type *)_ci_controller[rhport].reg_base; + usb->SBUSCFG = USB_SBUSCFG_AHBBRST(CI_HS_IMXRT_AHBBRST_INCR16_UNSPEC); +} + //------------- DCD -------------// #define CI_DCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum) #define CI_DCD_INT_DISABLE(_p) NVIC_DisableIRQ((IRQn_Type)_ci_controller[_p].irqnum) diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c index 32c701bfa..62d75b4d3 100644 --- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c @@ -237,7 +237,9 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { usbmode |= USBMODE_CM_DEVICE; dcd_reg->USBMODE = usbmode; - #if TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) + #if CFG_TUSB_MCU == OPT_MCU_MIMXRT1XXX + ci_hs_imxrt_set_ahb_burst(rhport); + #elif TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) ci_hs_lpc18_43_set_ahb_burst(rhport); #endif diff --git a/src/portable/chipidea/ci_hs/hcd_ci_hs.c b/src/portable/chipidea/ci_hs/hcd_ci_hs.c index c94ce810f..0fc8e4d70 100644 --- a/src/portable/chipidea/ci_hs/hcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/hcd_ci_hs.c @@ -82,7 +82,9 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { hcd_reg->USBMODE = USBMODE_CM_HOST; #endif - #if TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) + #if CFG_TUSB_MCU == OPT_MCU_MIMXRT1XXX + ci_hs_imxrt_set_ahb_burst(rhport); + #elif TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) ci_hs_lpc18_43_set_ahb_burst(rhport); #endif From b3708aebd985a2c21a361903b1ca60d44e457e92 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Mon, 27 Jul 2026 15:55:20 +0200 Subject: [PATCH 04/16] hw/bsp/lpc43: reset peripherals on IAR restart --- hw/bsp/lpc43/family.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/hw/bsp/lpc43/family.c b/hw/bsp/lpc43/family.c index 411ea7d58..0f3c62fef 100644 --- a/hw/bsp/lpc43/family.c +++ b/hw/bsp/lpc43/family.c @@ -59,6 +59,19 @@ void SystemInit(void); // Invoked by startup code void SystemInit(void) { +#if defined(__ICCARM__) && !defined(DONT_RESET_ON_RESTART) + // A debugger restart resets the M4 core, but can leave LPC43 peripherals and + // pending interrupts active. Match the GCC startup sequence, which the IAR + // startup lacks, before the C runtime can reuse peripheral DMA memory. + __disable_irq(); + LPC_RGU->RESET_CTRL[0] = 0x10DF1000u; + LPC_RGU->RESET_CTRL[1] = 0x01DFF7FFu; + for (uint32_t i = 0; i < 8; i++) { + NVIC->ICPR[i] = UINT32_MAX; + } + __enable_irq(); +#endif + #ifdef __USE_LPCOPEN unsigned int *pSCB_VTOR = (unsigned int *) 0xE000ED08; From 9a4d71162ba3b317091513fb51f7bcdaf22427dd Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Mon, 27 Jul 2026 17:11:46 +0200 Subject: [PATCH 05/16] test/hil: bound MIDI reads by deadline --- test/hil/hil_test.py | 47 +++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 72af6c697..c9c31c820 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -1579,26 +1579,37 @@ def test_device_midi_test(board): # Read MIDI messages and verify note on/off import select - with open(midi_port, 'rb') as f: - notes = [] + midi_fd = os.open(midi_port, os.O_RDONLY | os.O_NONBLOCK) + try: + data = bytearray() # Read for up to 3 seconds to capture a few notes (286ms interval) end_time = time.monotonic() + 3 - while time.monotonic() < end_time: - ready, _, _ = select.select([f], [], [], 0.5) - if ready: - data = f.read(64) - if data: - # Parse MIDI bytes: note_on = 0x90, note_off = 0x80 - i = 0 - while i + 2 < len(data): - status = data[i] - if (status & 0xF0) == 0x90: # Note On - notes.append(data[i + 1]) - i += 3 - elif (status & 0xF0) == 0x80: # Note Off - i += 3 - else: - i += 1 + while (remaining := end_time - time.monotonic()) > 0: + ready, _, _ = select.select([midi_fd], [], [], min(0.5, remaining)) + if not ready: + continue + try: + chunk = os.read(midi_fd, 64) + except BlockingIOError: + continue + if not chunk: + break + data.extend(chunk) + finally: + os.close(midi_fd) + + notes = [] + # Parse MIDI bytes: note_on = 0x90, note_off = 0x80 + i = 0 + while i + 2 < len(data): + status = data[i] + if (status & 0xF0) == 0x90: # Note On + notes.append(data[i + 1]) + i += 3 + elif (status & 0xF0) == 0x80: # Note Off + i += 3 + else: + i += 1 assert len(notes) >= 2, f'Expected at least 2 MIDI notes, got {len(notes)}' # Verify notes are from the expected sequence From edb4a0744a1f7ce95fac35fc2798a5dea052571d Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Mon, 27 Jul 2026 20:44:18 +0200 Subject: [PATCH 06/16] hw/bsp/lpc43: configure safe flash timing --- hw/bsp/lpc43/family.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/hw/bsp/lpc43/family.c b/hw/bsp/lpc43/family.c index 0f3c62fef..7f0722a33 100644 --- a/hw/bsp/lpc43/family.c +++ b/hw/bsp/lpc43/family.c @@ -59,11 +59,22 @@ void SystemInit(void); // Invoked by startup code void SystemInit(void) { +#if defined(__ICCARM__) && !defined(DONT_RESET_ON_RESTART) + __disable_irq(); +#endif + + if (Chip_CREG_OnChipFlashIsPresent()) { + // The boot ROM configures flash for its 96 MHz clock, and debugger core + // resets can preserve it. Use safe timing before switching the M4 to 204 MHz. + Chip_CREG_SetFLASHAccess(FLASHTIM_SAFE_SETTING); + __DSB(); + __ISB(); + } + #if defined(__ICCARM__) && !defined(DONT_RESET_ON_RESTART) // A debugger restart resets the M4 core, but can leave LPC43 peripherals and // pending interrupts active. Match the GCC startup sequence, which the IAR // startup lacks, before the C runtime can reuse peripheral DMA memory. - __disable_irq(); LPC_RGU->RESET_CTRL[0] = 0x10DF1000u; LPC_RGU->RESET_CTRL[1] = 0x01DFF7FFu; for (uint32_t i = 0; i < 8; i++) { From 98bce6952497a5d2dc64b72af287d13f890b9a8e Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 28 Jul 2026 16:53:32 +0200 Subject: [PATCH 07/16] test/hil: use stlink for stm32l412nucleo Signed-off-by: Zixun LI --- test/hil/hfp.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/hil/hfp.json b/test/hil/hfp.json index 17fbb7605..10c613d09 100644 --- a/test/hil/hfp.json +++ b/test/hil/hfp.json @@ -7,9 +7,8 @@ "device": true, "host": false, "dual": false }, "flasher": { - "name": "jlink", - "uid": "774470029", - "args": "-device STM32L412KB" + "name": "stlink", + "uid": "0673FF575051717867034946" } }, { From 9d9b2ef21c4663e740e687dcc403f9df0add11ce Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 28 Jul 2026 18:12:59 +0200 Subject: [PATCH 08/16] Revert 'test/hil: separate LPC43 stress test flashes' This reverts commit 80ffbff6e98a9c5053bba008ae2c5087f0351300. --- test/hil/hfp.json | 6 +---- test/hil/hil_test.py | 62 ++++++-------------------------------------- 2 files changed, 9 insertions(+), 59 deletions(-) diff --git a/test/hil/hfp.json b/test/hil/hfp.json index 10c613d09..2babcaaf3 100644 --- a/test/hil/hfp.json +++ b/test/hil/hfp.json @@ -35,11 +35,7 @@ "flasher": { "name": "jlink", "uid": "728973776", - "args": "-device LPC43S67_M4", - "pre_flash": { - "device/usbtest": "device/board_test", - "device/cdc_msc_throughput": "device/board_test" - } + "args": "-device LPC43S67_M4" } } ] diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index c9c31c820..58452f64a 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -321,7 +321,6 @@ class FlasherCfg(TypedDict): name: str uid: str args: str - pre_flash: NotRequired[dict[str, str]] # target example -> USB-off separator example class AttachedDevCfg(TypedDict, total=False): @@ -1842,19 +1841,6 @@ def find_firmware(variant: str, example: str): return None -def usb_uid_paths(uid: str) -> set[str]: - """Return sysfs device paths currently exposing the requested USB serial.""" - paths = set() - for f in glob.glob('/sys/bus/usb/devices/*/serial'): - try: - with open(f) as serial_file: - if serial_file.read().strip().lower() == uid.lower(): - paths.add(os.path.dirname(f)) - except OSError: - pass - return paths - - def test_example(board: Board, variant: str, example: str) -> tuple[int, str, str | None]: """ Test example firmware @@ -1877,16 +1863,7 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st log_line(f'{test_name} Skip (no binary)') return 0, 'skip', None - pre_flash_example = None if skip_flash else board['flasher'].get('pre_flash', {}).get(example) - pre_flash_name = find_firmware(variant, pre_flash_example) if pre_flash_example else None - if pre_flash_example and pre_flash_name is None: - log_line(f'{test_name} {STATUS_FAILED}: ' - f'pre-flash firmware {pre_flash_example} not found') - return 1, 'fail', None - if verbose: - if pre_flash_name is not None: - log_line(f'Pre-flashing {pre_flash_name}.elf') log_line(f'Flashing {fw_name}.elf') # flash firmware (unless --skip-flash), then run the test. Both may fail randomly, @@ -1901,36 +1878,13 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st attempt_out = io.StringIO() with redirect_stdout(attempt_out): if not skip_flash: - flash_ok = True - flash_error = '' with flash_permit(board['uid']): - if pre_flash_name is not None: - previous_usb_paths = usb_uid_paths(board['uid']) - ret = globals()[f'flash_{board["flasher"]["name"].lower()}']( - board, str(pre_flash_name)) - flash_ok = (ret.returncode == 0) - if not flash_ok: - flash_error = f'Pre-flash {pre_flash_example} failed' - elif previous_usb_paths: - disconnected = wait_until( - lambda: all(not os.path.exists(p) for p in previous_usb_paths), step=0.1) - if not disconnected: - flash_ok = False - flash_error = (f'Pre-flash {pre_flash_example} did not disconnect ' - f'USB device {board["uid"]}') - else: - time.sleep(0.1) - - if flash_ok: - t_flash = time.monotonic() - ret = globals()[f'flash_{board["flasher"]["name"].lower()}']( - board, str(fw_name)) - 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) - if not flash_ok: - flash_error = 'Flash failed' + t_flash = time.monotonic() + ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name)) + 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) if flash_ok: try: tret = globals()[f'test_{example.replace("/", "_")}'](board) @@ -1968,10 +1922,10 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st log_line(msg) time.sleep(0.5) else: - last_err = flash_error + last_err = 'Flash failed' last_detail = compact_output(attempt_out.getvalue()) if i < max_retry - 1: - msg = f'{test_name} retry {i+2}/{max_retry}: {flash_error}' + msg = f'{test_name} retry {i+2}/{max_retry}: flash failed' if last_detail: msg += f' {last_detail}' log_line(msg) From a240ee5be90a8d5e45f4f93788e307a1ba840b31 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 28 Jul 2026 19:15:20 +0200 Subject: [PATCH 09/16] test/hil: require exact audio ramp --- test/hil/hil_test.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 58452f64a..922668041 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -1672,23 +1672,13 @@ def test_device_audio_test_freertos(board): assert sample_count > 1024, f'Not enough samples captured: {sample_count}' # The firmware sends a continuous uint16 ramp. Using ALSA hw: capture bypasses - # PulseAudio processing, so most adjacent samples should differ by exactly 1. - total_diffs = sample_count - 1 - one_step = 0 - near_step = 0 - for i in range(total_diffs): - d = (samples[i + 1] - samples[i]) & 0xFFFF - if d == 1: - one_step += 1 - if d in (0, 1, 2, 47, 48, 49): - near_step += 1 + # PulseAudio processing, so every adjacent sample must differ by exactly 1. + for i in range(sample_count - 1): + expected = (samples[i] + 1) & 0xFFFF + assert samples[i + 1] == expected, ( + f'Audio mismatch at sample {i + 1}: expected {expected}, got {samples[i + 1]}') - one_ratio = one_step / total_diffs - near_ratio = near_step / total_diffs - assert one_ratio >= 0.85, f'Unexpected audio pattern (strict ratio={one_ratio:.3f})' - assert near_ratio >= 0.98, f'Unexpected audio pattern (relaxed ratio={near_ratio:.3f})' - - print(f' ALSA {pcm} strict={one_ratio:.3f} relaxed={near_ratio:.3f}', end='') + print(f' ALSA {pcm}', end='') def test_device_hid_generic_inout(board): From a20cf74e6a62f5b833baacfe1198648b237b3e7f Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 28 Jul 2026 21:15:38 +0200 Subject: [PATCH 10/16] portable/dwc2: rewind DMA on ISO IN retry --- src/portable/synopsys/dwc2/dcd_dwc2.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 86aa54510..b2f1a93a4 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -1143,7 +1143,12 @@ static void handle_incomplete_iso_in(uint8_t rhport) { xfer_ctl_t *xfer = XFER_CTL_BASE(epnum, TUSB_DIR_IN); if (xfer->iso_retry > 0) { xfer->iso_retry--; - // Restart ISO transfe: re-write TSIZ and CTL + // Restart ISO transfer: re-write DMA address, TSIZ, and CTL + #if CFG_TUD_DWC2_DMA_ENABLE + if (dma_device_enabled(dwc2)) { + epin->diepdma = (uintptr_t) xfer->buffer; + } + #endif dwc2_ep_tsize_t deptsiz = {.value = 0}; deptsiz.xfer_size = xfer->total_len; deptsiz.packet_count = tu_div_ceil(xfer->total_len, xfer->max_size); From 8895e94b7faed15b5367dcb1e6715e8ed4c56955 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 28 Jul 2026 21:15:49 +0200 Subject: [PATCH 11/16] hw/bsp/stm32l4: stabilize L412 USB clock --- hw/bsp/stm32l4/boards/stm32l412nucleo/board.h | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/hw/bsp/stm32l4/boards/stm32l412nucleo/board.h b/hw/bsp/stm32l4/boards/stm32l412nucleo/board.h index a5250eda9..7f63ec431 100644 --- a/hw/bsp/stm32l4/boards/stm32l412nucleo/board.h +++ b/hw/bsp/stm32l4/boards/stm32l412nucleo/board.h @@ -64,9 +64,10 @@ * AHB Prescaler = 1 * APB1 Prescaler = 1 * APB2 Prescaler = 1 - * MSI Frequency(Hz) = 8000000 - * PLL_M = 1 - * PLL_N = 10 + * MSI Frequency(Hz) = 48000000 + * LSE Frequency(Hz) = 32768 + * PLL_M = 6 + * PLL_N = 20 * PLL_Q = 2 * PLL_R = 2 * VDD(V) = 3.3 @@ -78,29 +79,35 @@ static inline void board_clock_init(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; - RCC_CRSInitTypeDef RCC_CRSInitStruct = {0}; RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; /** Configure the main internal regulator output voltage */ HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1); + /* HAL clock setup reconfigures its tick while MSI is the reset SYSCLK. */ + HAL_InitTick((1UL << __NVIC_PRIO_BITS) - 1UL); + /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ - RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI48|RCC_OSCILLATORTYPE_HSI; - RCC_OscInitStruct.HSIState = RCC_HSI_ON; - RCC_OscInitStruct.HSI48State = RCC_HSI48_ON; - RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE | RCC_OSCILLATORTYPE_MSI; + RCC_OscInitStruct.LSEState = RCC_LSE_ON; + RCC_OscInitStruct.MSIState = RCC_MSI_ON; + RCC_OscInitStruct.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; + RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; - RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; - RCC_OscInitStruct.PLL.PLLM = 1; - RCC_OscInitStruct.PLL.PLLN = 10; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_MSI; + RCC_OscInitStruct.PLL.PLLM = 6; + RCC_OscInitStruct.PLL.PLLN = 20; RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; HAL_RCC_OscConfig(&RCC_OscInitStruct); + /* Stabilize MSI against the on-board 32.768 kHz LSE crystal. */ + HAL_RCCEx_EnableMSIPLLMode(); + /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK @@ -112,24 +119,9 @@ static inline void board_clock_init(void) HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4); - /** Enable the SYSCFG APB clock - */ - __HAL_RCC_CRS_CLK_ENABLE(); - - /** Configures CRS - */ - RCC_CRSInitStruct.Prescaler = RCC_CRS_SYNC_DIV1; - RCC_CRSInitStruct.Source = RCC_CRS_SYNC_SOURCE_USB; - RCC_CRSInitStruct.Polarity = RCC_CRS_SYNC_POLARITY_RISING; - RCC_CRSInitStruct.ReloadValue = __HAL_RCC_CRS_RELOADVALUE_CALCULATE(48000000,1000); - RCC_CRSInitStruct.ErrorLimitValue = 34; - RCC_CRSInitStruct.HSI48CalibrationValue = 32; - - HAL_RCCEx_CRSConfig(&RCC_CRSInitStruct); - - /* Select HSI48 output as USB clock source */ + /* Use the same LSE-trimmed MSI source for USB and the CPU PLL. */ PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_USB; - PeriphClkInitStruct.UsbClockSelection = RCC_USBCLKSOURCE_HSI48; + PeriphClkInitStruct.UsbClockSelection = RCC_USBCLKSOURCE_MSI; HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct); /* Select PLL output as UART clock source */ From d8595dafcd9996312a95d06e6532920570f93850 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 28 Jul 2026 21:15:58 +0200 Subject: [PATCH 12/16] test/hil: allow audio startup transition --- test/hil/hil_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 922668041..6ba756a77 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -1671,9 +1671,11 @@ def test_device_audio_test_freertos(board): samples = [int.from_bytes(raw[i:i + 2], 'little', signed=False) for i in range(0, len(raw), 2)] assert sample_count > 1024, f'Not enough samples captured: {sample_count}' - # The firmware sends a continuous uint16 ramp. Using ALSA hw: capture bypasses - # PulseAudio processing, so every adjacent sample must differ by exactly 1. - for i in range(sample_count - 1): + # The producer is already running while ALSA activates streaming, so the + # initial overwritable software FIFO (at most 224 samples) can transition + # between ramp generations. After that startup window, require an exact ramp. + startup_samples = 256 + for i in range(startup_samples, sample_count - 1): expected = (samples[i] + 1) & 0xFFFF assert samples[i + 1] == expected, ( f'Audio mismatch at sample {i + 1}: expected {expected}, got {samples[i + 1]}') From b868d6d268cab403eb843f3d0a190162a4e5d0db Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Tue, 28 Jul 2026 23:27:54 +0200 Subject: [PATCH 13/16] test/hil: avoid parallel MTP probe races --- test/hil/hil_test.py | 150 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 27 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 6ba756a77..7bc3e0868 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -39,6 +39,7 @@ import argparse import io import itertools +import math import os import random import re @@ -64,7 +65,7 @@ _mp = multiprocessing.get_context('fork') Pool, Lock, Semaphore, Manager = _mp.Pool, _mp.Lock, _mp.Semaphore, _mp.Manager import hashlib import ctypes -from pymtp import MTP +from pymtp import LIBMTP_DeviceEntry, LIBMTP_RawDevice, MTP import string # --- per-board dev-session locks (see test/hil/board_lock.py) ------------ @@ -127,11 +128,11 @@ def enum_timeout() -> int: return _enum_timeout -def wait_until(predicate, step: float = 1.0): +def wait_until(predicate, step: float = 1.0, timeout: float | None = None): """Poll predicate under the per-attempt enum budget. Deadline-based so a slow predicate - body (subprocess, libmtp scan) counts against the budget. Returns the first truthy - predicate value, or None on timeout.""" - deadline = time.monotonic() + enum_timeout() + body (subprocess, libmtp scan) counts against the budget. An explicit timeout overrides + that budget. Returns the first truthy predicate value, or None on timeout.""" + deadline = time.monotonic() + (enum_timeout() if timeout is None else timeout) while True: r = predicate() if r: @@ -503,23 +504,120 @@ def read_disk_file(uid: str, lun: int, fname: str) -> bytes: return data -def open_mtp_dev(uid): +def open_mtp_dev(uid: str): mtp = MTP() + last_usb = None + deadline = time.monotonic() + 2 * enum_timeout() - def try_open(): - # unmount gio/gvfs MTP mount which blocks libmtp from accessing the device - subprocess.run(f"gio mount -u mtp://TinyUsb_TinyUsb_Device_{uid}/", - shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - for raw in mtp.detect_devices(): - mtp.device = mtp.mtp.LIBMTP_Open_Raw_Device(ctypes.byref(raw)) - if mtp.device: - sn = mtp.get_serialnumber().decode('utf-8') - if sn == uid: - return mtp - mtp.disconnect() + def find_usb(): + nonlocal last_usb + for serial_fname in glob.glob('/sys/bus/usb/devices/*/serial'): + dev_path = Path(serial_fname).parent + try: + if (Path(serial_fname).read_text().strip().lower() != uid.lower() + or (dev_path / 'idVendor').read_text().strip() != 'cafe' + or (dev_path / 'idProduct').read_text().strip() != '4017'): + continue + busnum = int((dev_path / 'busnum').read_text()) + devnum = int((dev_path / 'devnum').read_text()) + last_usb = (dev_path.name, busnum, devnum) + usb_node = Path('/dev/bus/usb') / f'{busnum:03d}' / f'{devnum:03d}' + if usb_node.exists(): + return dev_path, busnum, devnum + except (OSError, ValueError): + pass return None - return wait_until(try_open) + def remaining() -> float: + return max(0.0, deadline - time.monotonic()) + + target = wait_until(find_usb, step=0.05, timeout=remaining()) + if target is None: + if last_usb: + name, busnum, devnum = last_usb + raise AssertionError( + f'MTP USB node not ready for {uid} at {name} ({busnum:03d}/{devnum:03d})') + raise AssertionError(f'MTP USB device not enumerated for {uid}') + + dev_path, busnum, devnum = target + wait_seconds = max(1, math.ceil(remaining())) + try: + udev_wait = subprocess.run( + ['udevadm', 'wait', '--initialized=yes', f'--timeout={wait_seconds}', str(dev_path)], + capture_output=True, text=True, timeout=wait_seconds + 2) + except FileNotFoundError: + udev_wait = None + except subprocess.TimeoutExpired as e: + raise AssertionError( + f'udev initialization timed out for MTP {uid} at {busnum:03d}/{devnum:03d}') from e + + if udev_wait is not None and udev_wait.returncode != 0: + try: + wait_help = subprocess.run( + ['udevadm', 'wait', '--help'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=2) + wait_supported = wait_help.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + wait_supported = False + + if wait_supported: + detail = (udev_wait.stderr or udev_wait.stdout).strip().replace('\n', ' ') + detail = detail[-300:] or 'no diagnostic' + raise AssertionError( + f'udevadm wait failed for MTP {uid} at {busnum:03d}/{devnum:03d}: {detail}') + udev_wait = None + + if udev_wait is None: + # systemd < 251 has no target-specific udev wait. Its libmtp rule creates + # this link only after synchronous mtp-probe has released the interface. + def find_libmtp_marker(): + found = find_usb() + if found is None: + return None + found_path, found_busnum, found_devnum = found + marker = Path('/dev') / f'libmtp-{found_path.name}' + usb_node = Path('/dev/bus/usb') / f'{found_busnum:03d}' / f'{found_devnum:03d}' + if marker.exists() and marker.resolve() == usb_node: + return found + return None + + target = wait_until(find_libmtp_marker, step=0.05, timeout=remaining()) + if target is None: + raise AssertionError( + f'udevadm wait unsupported and libmtp marker absent for MTP {uid}; ' + 'install libmtp-runtime') + dev_path, busnum, devnum = target + elif find_usb() != target: + raise AssertionError(f'MTP USB device {uid} changed while waiting for udev initialization') + + # A desktop GVFS session may claim MTP after udev probing. This is a no-op on + # headless runners, but preserves support for rigs where the mount exists. + try: + subprocess.run(['gio', 'mount', '-u', f'mtp://TinyUsb_TinyUsb_Device_{uid}/'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + # TinyUSB needs no libmtp device quirks. Construct its raw entry directly so + # this test never probes another MTP board that is still being initialized. + entry = LIBMTP_DeviceEntry(None, 0xcafe, None, 0x4017, 0) + raw = LIBMTP_RawDevice(entry, busnum, devnum) + mtp.device = mtp.mtp.LIBMTP_Open_Raw_Device(ctypes.byref(raw)) + if not mtp.device: + raise AssertionError(f'libmtp could not open MTP {uid} at {busnum:03d}/{devnum:03d}') + + try: + serial_raw = mtp.get_serialnumber() + serial = serial_raw.decode('utf-8') if serial_raw else '' + if serial.lower() != uid.lower(): + raise AssertionError(f'MTP serial mismatch at {busnum:03d}/{devnum:03d}: {serial}') + except Exception: + try: + mtp.disconnect() + except Exception: + pass + raise + return mtp def get_printer_dev(id: str, vendor_str, product_str, ifnum: int): @@ -1432,15 +1530,13 @@ def test_device_mtp(board): _null = os.open(os.devnull, os.O_WRONLY) os.dup2(_null, fd) - mtp = open_mtp_dev(uid) - - # --- AFTER: restore stderr --- - os.dup2(_saved, fd) - os.close(_null) - os.close(_saved) - - if mtp is None or mtp.device is None: - assert False, 'MTP device not found' + try: + mtp = open_mtp_dev(uid) + finally: + # --- AFTER: restore stderr --- + os.dup2(_saved, fd) + os.close(_null) + os.close(_saved) try: assert b"TinyUSB" == mtp.get_manufacturer(), 'MTP wrong manufacturer' From 3e3e9f8a978b274b8fe1f5a9b5fd41a94f928606 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Wed, 29 Jul 2026 00:34:59 +0200 Subject: [PATCH 14/16] class/mtp: preserve final OUT payload before ZLP --- src/class/mtp/mtp_device.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/class/mtp/mtp_device.c b/src/class/mtp/mtp_device.c index 7657899ec..275c9f858 100644 --- a/src/class/mtp/mtp_device.c +++ b/src/class/mtp/mtp_device.c @@ -437,8 +437,11 @@ bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t TU_LOG_DRV(" MTP Data %s CB: xferred_bytes=%lu, xferred_len/total_len=%lu/%lu, is_complete=%d\r\n", is_data_in ? "IN" : "OUT", xferred_bytes, p_mtp->xferred_len, p_mtp->total_len, is_complete ? 1 : 0); - // Send/queue ZLP if packet is full-sized but transfer is complete - if (is_complete && xferred_bytes > 0 && !(xferred_bytes & (threshold - 1))) { + // Send/queue ZLP if packet is full-sized but transfer is complete. + // OUT must deliver this final payload to the application before receiving + // its terminating ZLP below. + const bool need_zlp = is_complete && xferred_bytes > 0 && !(xferred_bytes & (threshold - 1)); + if (is_data_in && need_zlp) { TU_LOG_DRV(" queue ZLP\r\n"); TU_VERIFY(usbd_edpt_claim(p_mtp->rhport, ep_addr)); TU_ASSERT(usbd_edpt_xfer(p_mtp->rhport, ep_addr, NULL, 0, false)); @@ -466,9 +469,16 @@ bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t cb_data.io_container = headerless_packet; cb_data.io_container.payload_bytes = xferred_bytes; } - tud_mtp_data_xfer_cb(&cb_data); + if (xferred_bytes > 0) { + tud_mtp_data_xfer_cb(&cb_data); + } - if (is_complete) { + if (need_zlp) { + TU_LOG_DRV(" queue ZLP\r\n"); + TU_VERIFY(usbd_edpt_claim(p_mtp->rhport, ep_addr)); + TU_ASSERT(usbd_edpt_xfer(p_mtp->rhport, ep_addr, NULL, 0, false)); + return true; + } else if (is_complete) { // back to header + payload for response cb_data.io_container = headered_packet; cb_data.io_container.header->len = sizeof(mtp_container_header_t); From 192e0bd872608b4a39b36047e0d5c1d18c2a8f02 Mon Sep 17 00:00:00 2001 From: Zixun LI Date: Wed, 29 Jul 2026 00:35:21 +0200 Subject: [PATCH 15/16] test/hil: make MTP checks deterministic --- test/hil/hil_test.py | 110 ++++++++++++++------------------------ test/hil/pymtp.py | 10 ++-- test/hil/requirements.txt | 3 +- 3 files changed, 46 insertions(+), 77 deletions(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 7bc3e0868..e3073faba 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -23,9 +23,10 @@ # THE SOFTWARE. # Host setup (required: a missing tool fails its test rather than skipping it): -# - System packages: sudo apt install mtools libmtp9 alsa-utils iperf +# - 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_*) # - device/usbtest: usbtest kernel module + testusb binary (kernel tools/usb/testusb.c) on PATH, @@ -39,7 +40,6 @@ import argparse import io import itertools -import math import os import random import re @@ -506,89 +506,48 @@ def read_disk_file(uid: str, lun: int, fname: str) -> bytes: def open_mtp_dev(uid: str): mtp = MTP() - last_usb = None + last_detail = None deadline = time.monotonic() + 2 * enum_timeout() - def find_usb(): - nonlocal last_usb - for serial_fname in glob.glob('/sys/bus/usb/devices/*/serial'): - dev_path = Path(serial_fname).parent + def find_ready_mtp(): + nonlocal last_detail + for marker_name in glob.glob('/dev/libmtp-*'): + marker = Path(marker_name) + serial = '' try: - if (Path(serial_fname).read_text().strip().lower() != uid.lower() + # libmtp-runtime publishes libmtp-%k only after its synchronous + # mtp-probe has accepted the device. Starting from that small, ready-only + # set avoids a broad sysfs scan racing unrelated parallel re-enumerations. + sysname = marker.name[len('libmtp-'):] + dev_path = Path('/sys/bus/usb/devices') / sysname + serial = (dev_path / 'serial').read_text().strip() + if (serial.lower() != uid.lower() or (dev_path / 'idVendor').read_text().strip() != 'cafe' or (dev_path / 'idProduct').read_text().strip() != '4017'): continue + busnum = int((dev_path / 'busnum').read_text()) devnum = int((dev_path / 'devnum').read_text()) - last_usb = (dev_path.name, busnum, devnum) usb_node = Path('/dev/bus/usb') / f'{busnum:03d}' / f'{devnum:03d}' - if usb_node.exists(): - return dev_path, busnum, devnum - except (OSError, ValueError): - pass + if marker.resolve(strict=True) != usb_node or not os.access( + usb_node, os.R_OK | os.W_OK): + last_detail = f'{marker} did not resolve to an accessible {usb_node}' + continue + return busnum, devnum + except (OSError, ValueError) as e: + # A marker can disappear while another board flashes. Only retain + # diagnostics for this board's marker, not unrelated MTP devices. + if serial.lower() == uid.lower(): + last_detail = f'{marker}: {e}' return None def remaining() -> float: return max(0.0, deadline - time.monotonic()) - target = wait_until(find_usb, step=0.05, timeout=remaining()) + target = wait_until(find_ready_mtp, step=0.05, timeout=remaining()) if target is None: - if last_usb: - name, busnum, devnum = last_usb - raise AssertionError( - f'MTP USB node not ready for {uid} at {name} ({busnum:03d}/{devnum:03d})') - raise AssertionError(f'MTP USB device not enumerated for {uid}') - - dev_path, busnum, devnum = target - wait_seconds = max(1, math.ceil(remaining())) - try: - udev_wait = subprocess.run( - ['udevadm', 'wait', '--initialized=yes', f'--timeout={wait_seconds}', str(dev_path)], - capture_output=True, text=True, timeout=wait_seconds + 2) - except FileNotFoundError: - udev_wait = None - except subprocess.TimeoutExpired as e: - raise AssertionError( - f'udev initialization timed out for MTP {uid} at {busnum:03d}/{devnum:03d}') from e - - if udev_wait is not None and udev_wait.returncode != 0: - try: - wait_help = subprocess.run( - ['udevadm', 'wait', '--help'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, timeout=2) - wait_supported = wait_help.returncode == 0 - except (FileNotFoundError, subprocess.TimeoutExpired): - wait_supported = False - - if wait_supported: - detail = (udev_wait.stderr or udev_wait.stdout).strip().replace('\n', ' ') - detail = detail[-300:] or 'no diagnostic' - raise AssertionError( - f'udevadm wait failed for MTP {uid} at {busnum:03d}/{devnum:03d}: {detail}') - udev_wait = None - - if udev_wait is None: - # systemd < 251 has no target-specific udev wait. Its libmtp rule creates - # this link only after synchronous mtp-probe has released the interface. - def find_libmtp_marker(): - found = find_usb() - if found is None: - return None - found_path, found_busnum, found_devnum = found - marker = Path('/dev') / f'libmtp-{found_path.name}' - usb_node = Path('/dev/bus/usb') / f'{found_busnum:03d}' / f'{found_devnum:03d}' - if marker.exists() and marker.resolve() == usb_node: - return found - return None - - target = wait_until(find_libmtp_marker, step=0.05, timeout=remaining()) - if target is None: - raise AssertionError( - f'udevadm wait unsupported and libmtp marker absent for MTP {uid}; ' - 'install libmtp-runtime') - dev_path, busnum, devnum = target - elif find_usb() != target: - raise AssertionError(f'MTP USB device {uid} changed while waiting for udev initialization') + detail = f': {last_detail}' if last_detail else '; install libmtp-runtime' + raise AssertionError(f'MTP udev device not ready for {uid}{detail}') # A desktop GVFS session may claim MTP after udev probing. This is a no-op on # headless runners, but preserves support for rigs where the mount exists. @@ -598,6 +557,13 @@ def open_mtp_dev(uid: str): except (FileNotFoundError, subprocess.TimeoutExpired): pass + # GIO can race a disconnect/re-enumeration. Resolve the completed marker again + # rather than opening a stale bus/device tuple. + target = wait_until(find_ready_mtp, step=0.05, timeout=remaining()) + if target is None: + raise AssertionError(f'MTP udev device disappeared for {uid}') + busnum, devnum = target + # TinyUSB needs no libmtp device quirks. Construct its raw entry directly so # this test never probes another MTP board that is still being initialized. entry = LIBMTP_DeviceEntry(None, 0xcafe, None, 0x4017, 0) @@ -1562,7 +1528,9 @@ def test_device_mtp(board): assert f2_md5_expect == hashlib.md5(f2_data).hexdigest(), 'MTP file2 wrong data' # test send file with open(f3, "wb") as file: - f3_data = os.urandom(random.randint(1024, 3*1024)) + # 1524-byte payload + 12-byte MTP header = 3 full 512-byte buffers. + # This exercises delivery of the final OUT payload before its ZLP. + f3_data = bytes((i % 251) + 1 for i in range(1524)) file.write(f3_data) file.close() fid = mtp.send_file_from_file(f3, b'file3') diff --git a/test/hil/pymtp.py b/test/hil/pymtp.py index 8b694df94..fc0c66104 100644 --- a/test/hil/pymtp.py +++ b/test/hil/pymtp.py @@ -420,6 +420,8 @@ _libmtp.LIBMTP_Get_Playlist.restype = ctypes.POINTER(LIBMTP_Playlist) _libmtp.LIBMTP_Get_Folder_List.restype = ctypes.POINTER(LIBMTP_Folder) _libmtp.LIBMTP_Find_Folder.restype = ctypes.POINTER(LIBMTP_Folder) _libmtp.LIBMTP_Get_Errorstack.restype = ctypes.POINTER(LIBMTP_Error) +_libmtp.LIBMTP_Dump_Errorstack.argtypes = [ctypes.POINTER(LIBMTP_MTPDevice)] +_libmtp.LIBMTP_Dump_Errorstack.restype = None _libmtp.LIBMTP_Open_Raw_Device.restype = ctypes.POINTER(LIBMTP_MTPDevice) _libmtp.LIBMTP_Open_Raw_Device.argtypes = [ctypes.POINTER(LIBMTP_RawDevice)] @@ -451,16 +453,14 @@ class MTP: def debug_stack(self): """ - Checks if __DEBUG__ is set, if so, prints and clears the - errorstack. + Checks if __DEBUG__ is set, and if so prints the error stack. @rtype: None @return: None """ - if __DEBUG__: - self.mtp.LIBMTP_Dump_Errorstack() - #self.mtp.LIBMTP_Clear_Errorstack() + if __DEBUG__ and self.device: + self.mtp.LIBMTP_Dump_Errorstack(self.device) def detect_devices(self): """ diff --git a/test/hil/requirements.txt b/test/hil/requirements.txt index ef1cf575b..abfb93783 100644 --- a/test/hil/requirements.txt +++ b/test/hil/requirements.txt @@ -1,7 +1,8 @@ # System packages (install separately): -# sudo apt install mtools libmtp9 alsa-utils iperf +# 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_*) hidapi From 8ccd0d549798c66d484e5a4b4c57edf49e8bb097 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 13 Aug 2026 14:35:01 +0700 Subject: [PATCH 16/16] portable/chipidea: name SBUSCFG in ci_hs_regs_t, unify AHB burst hook Replace the duplicated per-MCU dispatch in dcd_init/hcd_init and the two helper flavors (USB_Type access on iMX RT, raw offset 0x90 on LPC18/43) with one SBUSCFG register field plus a per-header CI_HS_SET_AHB_BURST() hook, compiled only where defined. The LPC USB0-only policy is now visible at the macro definition. --- src/portable/chipidea/ci_hs/ci_hs_imxrt.h | 11 ++--------- src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h | 17 ++++------------- src/portable/chipidea/ci_hs/ci_hs_type.h | 9 ++++++++- src/portable/chipidea/ci_hs/dcd_ci_hs.c | 6 ++---- src/portable/chipidea/ci_hs/hcd_ci_hs.c | 6 ++---- 5 files changed, 18 insertions(+), 31 deletions(-) diff --git a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h index 601e4d1c9..8f0d6083e 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h +++ b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h @@ -36,15 +36,8 @@ static const ci_hs_controller_t _ci_controller[] = #define CI_HS_REG(_port) ((ci_hs_regs_t*) _ci_controller[_port].reg_base) -enum { - // INCR16/8/4 followed by an unspecified-length burst for the remainder. - CI_HS_IMXRT_AHBBRST_INCR16_UNSPEC = 0x07u, -}; - -TU_ATTR_ALWAYS_INLINE static inline void ci_hs_imxrt_set_ahb_burst(uint8_t rhport) { - USB_Type *usb = (USB_Type *)_ci_controller[rhport].reg_base; - usb->SBUSCFG = USB_SBUSCFG_AHBBRST(CI_HS_IMXRT_AHBBRST_INCR16_UNSPEC); -} +// NXP recommends AHBBRST = INCR16 (remainder as unspecified-length bursts) +#define CI_HS_SET_AHB_BURST(_p) (CI_HS_REG(_p)->SBUSCFG = SBUSCFG_AHBBRST_INCR16_UNSPEC) //------------- DCD -------------// #define CI_DCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum) diff --git a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h index dec3a34b1..c7dc7e69f 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h +++ b/src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h @@ -34,18 +34,9 @@ static const ci_hs_controller_t _ci_controller[] = #define CI_HCD_INT_ENABLE(_p) NVIC_EnableIRQ ((IRQn_Type)_ci_controller[_p].irqnum) #define CI_HCD_INT_DISABLE(_p) NVIC_DisableIRQ((IRQn_Type)_ci_controller[_p].irqnum) -enum { - CI_HS_LPC18_43_SBUSCFG_OFFSET = 0x90u, - CI_HS_LPC18_43_AHBBRST_INCR16_UNSPEC = 0x07u, -}; - -TU_ATTR_ALWAYS_INLINE static inline void ci_hs_lpc18_43_set_ahb_burst(uint8_t rhport) { - // USB0 SBUSCFG is at offset 0x90. NXP recommends AHBBRST=0x7: - // INCR16 with non-multiple transfers decomposed into smaller unspecified bursts. - if (rhport == 0) { - volatile uint32_t *sbuscfg = (volatile uint32_t *)(_ci_controller[rhport].reg_base + CI_HS_LPC18_43_SBUSCFG_OFFSET); - *sbuscfg = CI_HS_LPC18_43_AHBBRST_INCR16_UNSPEC; - } -} +// USB0 (high-speed) only: NXP recommends AHBBRST = INCR16 (remainder as +// unspecified-length bursts) +#define CI_HS_SET_AHB_BURST(_p) \ + do { if ((_p) == 0) { CI_HS_REG(_p)->SBUSCFG = SBUSCFG_AHBBRST_INCR16_UNSPEC; } } while (0) #endif diff --git a/src/portable/chipidea/ci_hs/ci_hs_type.h b/src/portable/chipidea/ci_hs/ci_hs_type.h index 70817a6e3..b209c7545 100644 --- a/src/portable/chipidea/ci_hs/ci_hs_type.h +++ b/src/portable/chipidea/ci_hs/ci_hs_type.h @@ -71,11 +71,18 @@ enum { USBMODE_VBUS_POWER_SELECT = TU_BIT(5), // Need to be enabled for LPC18XX/43XX in host mode }; +// SBUSCFG +enum { + SBUSCFG_AHBBRST_INCR16_UNSPEC = 7, // INCR16 burst, remainder as unspecified-length bursts +}; + // Device Registers typedef struct { //------------- ID + HW Parameter Registers-------------// - volatile uint32_t TU_RESERVED[64]; ///< For iMX RT10xx, but not used by LPC18XX/LPC43XX + volatile uint32_t TU_RESERVED[36]; ///< ID/HW parameter registers, not used by this driver + volatile uint32_t SBUSCFG; ///< System Bus Interface Configuration (not present on every MCU) + volatile uint32_t TU_RESERVED[27]; //------------- Capability Registers-------------// volatile uint8_t CAPLENGTH; ///< Capability Registers Length diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c index 62d75b4d3..8c08c6bd5 100644 --- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c @@ -237,10 +237,8 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { usbmode |= USBMODE_CM_DEVICE; dcd_reg->USBMODE = usbmode; - #if CFG_TUSB_MCU == OPT_MCU_MIMXRT1XXX - ci_hs_imxrt_set_ahb_burst(rhport); - #elif TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) - ci_hs_lpc18_43_set_ahb_burst(rhport); + #ifdef CI_HS_SET_AHB_BURST + CI_HS_SET_AHB_BURST(rhport); #endif #ifdef CFG_TUD_CI_HS_VBUS_CHARGE diff --git a/src/portable/chipidea/ci_hs/hcd_ci_hs.c b/src/portable/chipidea/ci_hs/hcd_ci_hs.c index 0fc8e4d70..0f24f5bb6 100644 --- a/src/portable/chipidea/ci_hs/hcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/hcd_ci_hs.c @@ -82,10 +82,8 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { hcd_reg->USBMODE = USBMODE_CM_HOST; #endif - #if CFG_TUSB_MCU == OPT_MCU_MIMXRT1XXX - ci_hs_imxrt_set_ahb_burst(rhport); - #elif TU_CHECK_MCU(OPT_MCU_LPC18XX, OPT_MCU_LPC43XX) - ci_hs_lpc18_43_set_ahb_burst(rhport); + #ifdef CI_HS_SET_AHB_BURST + CI_HS_SET_AHB_BURST(rhport); #endif #if !TUH_OPT_HIGH_SPEED