summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-08-17 19:04:37 +0700
committerGitHub <[email protected]>2026-08-17 19:04:37 +0700
commit2465ea8f435114af3b3c935cc4fbed423d9eac69 (patch)
treea47ef0f4ecb66ecba5d00e286fc4bf6f067096a9
parent9fb2f9cb3f8e30fa9ca2a32f4cdc6f0fb07a5be3 (diff)
parent8ccd0d549798c66d484e5a4b4c57edf49e8bb097 (diff)
Merge pull request #3790 from hathach/fix/lpc43-hfp-reliability
Fix HFP HIL reliability issue
-rw-r--r--hw/bsp/lpc43/family.c24
-rw-r--r--hw/bsp/stm32l4/boards/stm32l412nucleo/board.h48
-rw-r--r--src/class/mtp/mtp_device.c18
-rw-r--r--src/portable/chipidea/ci_hs/ci_hs_imxrt.h3
-rw-r--r--src/portable/chipidea/ci_hs/ci_hs_lpc18_43.h5
-rw-r--r--src/portable/chipidea/ci_hs/ci_hs_type.h9
-rw-r--r--src/portable/chipidea/ci_hs/dcd_ci_hs.c4
-rw-r--r--src/portable/chipidea/ci_hs/hcd_ci_hs.c4
-rw-r--r--src/portable/synopsys/dwc2/dcd_dwc2.c7
-rw-r--r--test/hil/hfp.json5
-rwxr-xr-xtest/hil/hil_test.py195
-rw-r--r--test/hil/pymtp.py10
-rw-r--r--test/hil/requirements.txt3
13 files changed, 228 insertions, 107 deletions
diff --git a/hw/bsp/lpc43/family.c b/hw/bsp/lpc43/family.c
index 411ea7d58..7f0722a33 100644
--- a/hw/bsp/lpc43/family.c
+++ b/hw/bsp/lpc43/family.c
@@ -59,6 +59,30 @@ 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.
+ 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;
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 */
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);
diff --git a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h
index f0f918fe2..8f0d6083e 100644
--- a/src/portable/chipidea/ci_hs/ci_hs_imxrt.h
+++ b/src/portable/chipidea/ci_hs/ci_hs_imxrt.h
@@ -36,6 +36,9 @@ static const ci_hs_controller_t _ci_controller[] =
#define CI_HS_REG(_port) ((ci_hs_regs_t*) _ci_controller[_port].reg_base)
+// 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)
#define CI_DCD_INT_DISABLE(_p) NVIC_DisableIRQ((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 f2061bd7a..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,4 +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)
+// 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 fa98d6882..8c08c6bd5 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;
+ #ifdef CI_HS_SET_AHB_BURST
+ CI_HS_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..0f24f5bb6 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
+ #ifdef CI_HS_SET_AHB_BURST
+ CI_HS_SET_AHB_BURST(rhport);
+ #endif
+
#if !TUH_OPT_HIGH_SPEED
hcd_reg->PORTSC1 |= PORTSC1_FORCE_FULL_SPEED;
#endif
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);
diff --git a/test/hil/hfp.json b/test/hil/hfp.json
index 735d5a402..2babcaaf3 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"
}
},
{
diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py
index e7f82bd7f..5d9407883 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_*)
# openocd unified openocd from https://github.com/hathach/openocd (branch tinyusb) for wch, rp2040/rp2350, analog max32
@@ -68,7 +69,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
# Enumeration wait budget. The first attempt gets ENUM_TIMEOUT; retry attempts get the
@@ -86,11 +87,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:
@@ -285,23 +286,86 @@ 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_detail = None
+ deadline = time.monotonic() + 2 * enum_timeout()
+
+ def find_ready_mtp():
+ nonlocal last_detail
+ for marker_name in glob.glob('/dev/libmtp-*'):
+ marker = Path(marker_name)
+ serial = ''
+ try:
+ # 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
- 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()
+ busnum = int((dev_path / 'busnum').read_text())
+ devnum = int((dev_path / 'devnum').read_text())
+ usb_node = Path('/dev/bus/usb') / f'{busnum:03d}' / f'{devnum:03d}'
+ 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
- return wait_until(try_open)
+ def remaining() -> float:
+ return max(0.0, deadline - time.monotonic())
+
+ target = wait_until(find_ready_mtp, step=0.05, timeout=remaining())
+ if target is None:
+ 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.
+ 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
+
+ # 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)
+ 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):
@@ -1003,15 +1067,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'
@@ -1037,7 +1099,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')
@@ -1149,26 +1213,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
@@ -1231,24 +1306,16 @@ 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 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
-
- 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})'
+ # 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]}')
- 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):
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