From a3b2b4217630eea3f514ee042ee399e9ee397b8a Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 17 Mar 2026 14:58:45 +0700 Subject: [PATCH 1/8] add hil host cdc test --- AGENTS.md | 1 - hw/bsp/stm32h7/boards/stm32h743eval/board.h | 20 ++++-- hw/bsp/stm32h7/family.c | 20 +++++- test/hil/hil_test.py | 75 +++++++++++++++++++++ test/hil/tinyusb.json | 16 ++--- 5 files changed, 117 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4e510b01e..d491f94f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,6 @@ information that does not match the info here. ## Build Examples Choose ONE of these approaches: - **Option 1: Individual Example with CMake and Ninja (RECOMMENDED)** ```bash diff --git a/hw/bsp/stm32h7/boards/stm32h743eval/board.h b/hw/bsp/stm32h7/boards/stm32h743eval/board.h index d2f61a5ce..3914d7aac 100644 --- a/hw/bsp/stm32h7/boards/stm32h743eval/board.h +++ b/hw/bsp/stm32h7/boards/stm32h743eval/board.h @@ -205,13 +205,25 @@ static int32_t board_i2c_deinit(void) { } static int32_t i2c_readreg(uint16_t DevAddr, uint16_t Reg, uint8_t *pData, uint16_t Length) { - TU_ASSERT (HAL_OK == HAL_I2C_Mem_Read(&i2c_handle, DevAddr, Reg, I2C_MEMADD_SIZE_8BIT, pData, Length, 10000)); - return 0; + for (int retry = 0; retry < 3; retry++) { + if (HAL_OK == HAL_I2C_Mem_Read(&i2c_handle, DevAddr, Reg, I2C_MEMADD_SIZE_8BIT, pData, Length, 10000)) { + return 0; + } + HAL_Delay(10); + } + TU_ASSERT(0); + return -1; } static int32_t i2c_writereg(uint16_t DevAddr, uint16_t Reg, uint8_t *pData, uint16_t Length) { - TU_ASSERT(HAL_OK == HAL_I2C_Mem_Write(&i2c_handle, DevAddr, Reg, I2C_MEMADD_SIZE_8BIT, pData, Length, 10000)); - return 0; + for (int retry = 0; retry < 3; retry++) { + if (HAL_OK == HAL_I2C_Mem_Write(&i2c_handle, DevAddr, Reg, I2C_MEMADD_SIZE_8BIT, pData, Length, 10000)) { + return 0; + } + HAL_Delay(10); + } + TU_ASSERT(0); + return -1; } static int32_t i2c_get_tick(void) { diff --git a/hw/bsp/stm32h7/family.c b/hw/bsp/stm32h7/family.c index c94c2e755..a95674217 100644 --- a/hw/bsp/stm32h7/family.c +++ b/hw/bsp/stm32h7/family.c @@ -275,9 +275,25 @@ size_t board_get_unique_id(uint8_t id[], size_t max_len) { } int board_uart_read(uint8_t *buf, int len) { - (void) buf; - (void) len; +#ifdef UART_DEV + int count = 0; + // clear overrun error if any + if (__HAL_UART_GET_FLAG(&UartHandle, UART_FLAG_ORE)) { + __HAL_UART_CLEAR_FLAG(&UartHandle, UART_CLEAR_OREF); + } + for (int i = 0; i < len; i++) { + if (__HAL_UART_GET_FLAG(&UartHandle, UART_FLAG_RXNE)) { + buf[i] = (uint8_t) UartHandle.Instance->RDR; + count++; + } else { + break; + } + } + return count; +#else + (void) buf; (void) len; return 0; +#endif } int board_uart_write(void const *buf, int len) { diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 7cffd2da8..65585d929 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -416,6 +416,80 @@ def test_host_device_info(board): return 0 +def test_host_cdc_msc_hid(board): + flasher = board['flasher'] + cdc_devs = [d for d in board['tests'].get('dev_attached', []) if d.get('is_cdc')] + if not cdc_devs: + return + + port = get_serial_dev(flasher["uid"], None, None, 0) + ser = open_serial_dev(port) + ser.timeout = 0.1 + + # reset device to catch mount messages + ret = globals()[f'reset_{flasher["name"].lower()}'](board) + assert ret.returncode == 0, 'Failed to reset device' + + # Wait for CDC mounted message + data = b'' + timeout = ENUM_TIMEOUT + while timeout > 0: + new_data = ser.read(ser.in_waiting or 1) + if new_data: + data += new_data + if b'CDC Interface is mounted' in data: + break + time.sleep(0.1) + timeout -= 0.1 + assert b'CDC Interface is mounted' in data, 'CDC device not mounted on host' + + # Lookup serial chip name from vid_pid + vid_pid_name = { + '0403_6001': 'FTDI', '0403_6010': 'FTDI', '0403_6011': 'FTDI', '0403_6014': 'FTDI', + '10c4_ea60': 'CP210x', '10c4_ea70': 'CP210x', + '067b_2303': 'PL2303', '067b_23a3': 'PL2303', + '1a86_7523': 'CH340', '1a86_7522': 'CH340', + '1a86_55d3': 'CH9102', '1a86_55d4': 'CH9102', + } + dev = cdc_devs[0] + chip_name = vid_pid_name.get(dev['vid_pid'], dev['vid_pid']) + for l in data.decode('utf-8', errors='ignore').splitlines(): + if 'CDC Interface is mounted' in l: + print(f'\r\n {chip_name}: {l} ', end='') + + # CDC echo test via flasher serial + time.sleep(2) + ser.reset_input_buffer() + + def rand_ascii(length): + return "".join(random.choices(string.ascii_letters + string.digits, k=length)).encode("ascii") + + sizes = [8, 32, 64, 128] + for size in sizes: + test_data = rand_ascii(size) + ser.reset_input_buffer() + + # Write byte-by-byte with delay to avoid UART overrun + for b in test_data: + ser.write(bytes([b])) + ser.flush() + time.sleep(0.001) + + # Read echo back with timeout + echo = b'' + t = 5.0 + while t > 0 and len(echo) < size: + rd = ser.read(max(1, ser.in_waiting)) + if rd: + echo += rd + time.sleep(0.05) + t -= 0.05 + assert echo == test_data, (f'CDC echo wrong data ({size} bytes):\n' + f' expected: {test_data}\n received: {echo}') + + ser.close() + + # ------------------------------------------------------------- # Tests: device # ------------------------------------------------------------- @@ -763,6 +837,7 @@ dual_tests = [ ] host_test = [ + 'host/cdc_msc_hid', 'host/device_info', ] diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index e8449ba74..eaf60c6ce 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -8,7 +8,7 @@ }, "tests": { "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "host/device_info"], - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002427"}] + "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002427", "is_cdc": true}] }, "flasher": { "name": "esptool", @@ -26,7 +26,7 @@ }, "tests": { "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "host/device_info"], - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2005402"}] + "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2005402", "is_cdc": true}] }, "flasher": { "name": "esptool", @@ -67,7 +67,7 @@ }, "tests": { "device": true, "host": false, "dual": true, - "dev_attached": [{"vid_pid": "067b_2303", "serial": "0"}], + "dev_attached": [{"vid_pid": "067b_2303", "serial": "0", "is_cdc": true}], "comment": "pl23x" }, "flasher": { @@ -93,7 +93,7 @@ "uid": "BAE96FB95AFA6DBB8F00005002001200", "tests": { "device": true, "host": true, "dual": true, - "dev_attached": [{"vid_pid": "10c4_ea60", "serial": "0001"}], + "dev_attached": [{"vid_pid": "10c4_ea60", "serial": "0001", "is_cdc": true}], "comment": "cp2102" }, "flasher": { @@ -136,7 +136,7 @@ }, "tests": { "device": true, "host": true, "dual": true, - "dev_attached": [{"vid_pid": "1a86_7523", "serial": "0"}], + "dev_attached": [{"vid_pid": "1a86_7523", "serial": "0", "is_cdc": true}], "comment": "ch34x" }, "flasher": { @@ -150,7 +150,7 @@ "uid": "E6614C311B764A37", "tests": { "device": false, "host": true, "dual": false, - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2023934"}] + "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2023934", "is_cdc": true}] }, "flasher": { "name": "openocd", @@ -167,7 +167,7 @@ }, "tests": { "device": true, "host": true, "dual": true, - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "533D004242"}] + "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "533D004242", "is_cdc": true}] }, "flasher": { "name": "openocd", @@ -193,7 +193,7 @@ }, "tests": { "device": true, "host": true, "dual": false, - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2003414"}] + "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2003414", "is_cdc": true}] }, "flasher": { "name": "jlink", From 45e80a1042ea802b74ebe97db350f9da17cb2fe4 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 17 Mar 2026 21:38:46 +0700 Subject: [PATCH 2/8] add hil test for host msc and cdc --- AGENTS.md | 7 + LICENSE | 2 +- examples/host/msc_file_explorer/src/msc_app.c | 11 +- .../board.cmake | 1 + .../boards/adafruit_fruit_jam/board.cmake | 2 + .../boards/adafruit_metro_rp2350/board.cmake | 2 + hw/bsp/rp2040/family.cmake | 6 + test/hil/hil_test.py | 169 +++++++++++++++--- test/hil/tinyusb.json | 19 ++ 9 files changed, 195 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d491f94f1..eb6b737c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -151,6 +151,13 @@ openocd -f interface/stlink.cfg -f target/stm32h7x.cfg openocd -f interface/jlink.cfg -f target/stm32h7x.cfg ``` +For **rp2040/rp2350** with a CMSIS-DAP probe (e.g. Picoprobe, debugprobe): +```bash +openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" +# or for rp2350: +openocd -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000" +``` + For boards that define `OPENOCD_OPTION` in `board.cmake`, use those options directly: ```bash openocd $(cat hw/bsp/FAMILY/boards/BOARD/board.cmake | grep OPENOCD_OPTION | ...) diff --git a/LICENSE b/LICENSE index ddd4ab410..0680c2f05 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018, hathach (tinyusb.org) +Copyright (c) 2012-2026, hathach (tinyusb.org) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/examples/host/msc_file_explorer/src/msc_app.c b/examples/host/msc_file_explorer/src/msc_app.c index 6ac63e937..238af5431 100644 --- a/examples/host/msc_file_explorer/src/msc_app.c +++ b/examples/host/msc_file_explorer/src/msc_app.c @@ -130,11 +130,16 @@ static bool inquiry_complete_cb(uint8_t dev_addr, const tuh_msc_complete_data_t drive_path[0] += drive_num; if (f_mount(&fatfs[drive_num], drive_path, 1) != FR_OK) { - puts("mount failed"); + printf("mount failed\r\n"); + return true; } // change to newly mounted drive - f_chdir(drive_path); + f_chdrive(drive_path); + FRESULT rc = f_chdir("/"); + if (rc != FR_OK) { + printf("chdir failed: %d\r\n", rc); + } // print the drive label // char label[34]; @@ -148,7 +153,7 @@ static bool inquiry_complete_cb(uint8_t dev_addr, const tuh_msc_complete_data_t //------------- IMPLEMENTATION -------------// void tuh_msc_mount_cb(uint8_t dev_addr) { - printf("A MassStorage device is mounted\r\n"); + printf("A MassStorage device (addr = %u) is mounted\r\n", dev_addr); const uint8_t lun = 0; tuh_msc_inquiry(dev_addr, lun, &scsi_resp.inquiry, inquiry_complete_cb, 0); diff --git a/hw/bsp/rp2040/boards/adafruit_feather_rp2040_usb_host/board.cmake b/hw/bsp/rp2040/boards/adafruit_feather_rp2040_usb_host/board.cmake index 41897f644..66758d7d0 100644 --- a/hw/bsp/rp2040/boards/adafruit_feather_rp2040_usb_host/board.cmake +++ b/hw/bsp/rp2040/boards/adafruit_feather_rp2040_usb_host/board.cmake @@ -1,2 +1,3 @@ set(PICO_PLATFORM rp2040) set(PICO_BOARD adafruit_feather_rp2040_usb_host) +set(CFG_TUH_RPI_PIO_USB 1) diff --git a/hw/bsp/rp2040/boards/adafruit_fruit_jam/board.cmake b/hw/bsp/rp2040/boards/adafruit_fruit_jam/board.cmake index 4ab8a5477..d3535fa36 100644 --- a/hw/bsp/rp2040/boards/adafruit_fruit_jam/board.cmake +++ b/hw/bsp/rp2040/boards/adafruit_fruit_jam/board.cmake @@ -2,3 +2,5 @@ set(PICO_PLATFORM rp2350-arm-s) set(PICO_BOARD adafruit_fruit_jam) set(PICO_BOARD_HEADER_DIRS ${CMAKE_CURRENT_LIST_DIR}) #set(OPENOCD_SERIAL E6614103E78E8324) + +set(CFG_TUH_RPI_PIO_USB 1) diff --git a/hw/bsp/rp2040/boards/adafruit_metro_rp2350/board.cmake b/hw/bsp/rp2040/boards/adafruit_metro_rp2350/board.cmake index 9a58821a5..f1f54d217 100644 --- a/hw/bsp/rp2040/boards/adafruit_metro_rp2350/board.cmake +++ b/hw/bsp/rp2040/boards/adafruit_metro_rp2350/board.cmake @@ -2,3 +2,5 @@ set(PICO_PLATFORM rp2350-arm-s) set(PICO_BOARD adafruit_metro_rp2350) set(PICO_BOARD_HEADER_DIRS ${CMAKE_CURRENT_LIST_DIR}) #set(OPENOCD_SERIAL E6614103E78E8324) + +set(CFG_TUH_RPI_PIO_USB 1) diff --git a/hw/bsp/rp2040/family.cmake b/hw/bsp/rp2040/family.cmake index e31abe50b..2e2cd436a 100644 --- a/hw/bsp/rp2040/family.cmake +++ b/hw/bsp/rp2040/family.cmake @@ -72,6 +72,12 @@ target_compile_definitions(tinyusb_common_base INTERFACE CFG_TUSB_DEBUG=${TINYUSB_DEBUG_LEVEL} ) +if (CFG_TUH_RPI_PIO_USB) + target_compile_definitions(tinyusb_common_base INTERFACE + CFG_TUH_RPI_PIO_USB=1 + ) +endif() + target_link_libraries(tinyusb_common_base INTERFACE hardware_structs hardware_irq diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 65585d929..a2c054e61 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -80,6 +80,11 @@ flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 $_TARGETNAME.0 echo "Ready for Remote Connections" """ +MSC_README_TXT = \ +b"This is tinyusb's MassStorage Class demo.\r\n\r\n\ +If you find any bugs or get any questions, feel free to file an\r\n\ +issue at github.com/hathach/tinyusb" + # ------------------------------------------------------------- # Path # ------------------------------------------------------------- @@ -360,10 +365,27 @@ def test_dual_host_info_to_device_cdc(board): declared_devs = [f'{d["vid_pid"]}_{d["serial"]}' for d in board['tests']['dev_attached']] port = get_serial_dev(uid, 'TinyUSB', "TinyUSB_Device", 0) ser = open_serial_dev(port) + ser.timeout = 0.1 - # read from cdc, first line should contain vid/pid and serial - data = ser.read(10000) + # read until all expected devices are enumerated + data = b'' + timeout = ENUM_TIMEOUT + while timeout > 0: + new_data = ser.read(ser.in_waiting or 1) + if new_data: + data += new_data + # check if all devices found + enum_dev_sn = [] + for l in data.decode('utf-8', errors='ignore').splitlines(): + vid_pid_sn = re.search(r'ID ([0-9a-fA-F]+):([0-9a-fA-F]+) SN (\w+)', l) + if vid_pid_sn: + enum_dev_sn.append(f'{vid_pid_sn.group(1)}_{vid_pid_sn.group(2)}_{vid_pid_sn.group(3)}') + if set(declared_devs).issubset(set(enum_dev_sn)): + break + time.sleep(0.1) + timeout -= 0.1 ser.close() + if len(data) == 0: assert False, 'No data from device' lines = data.decode('utf-8', errors='ignore').splitlines() @@ -391,13 +413,31 @@ def test_host_device_info(board): port = get_serial_dev(flasher["uid"], None, None, 0) ser = open_serial_dev(port) + ser.timeout = 0.1 # reset device since we can miss the first line ret = globals()[f'reset_{flasher["name"].lower()}'](board) - assert ret.returncode == 0, 'Failed to reset device' + assert ret.returncode == 0, 'Failed to reset device' - data = ser.read(10000) + # read until all expected devices are enumerated + data = b'' + timeout = ENUM_TIMEOUT + while timeout > 0: + new_data = ser.read(ser.in_waiting or 1) + if new_data: + data += new_data + # check if all devices found + enum_dev_sn = [] + for l in data.decode('utf-8', errors='ignore').splitlines(): + vid_pid_sn = re.search(r'ID ([0-9a-fA-F]+):([0-9a-fA-F]+) SN (\w+)', l) + if vid_pid_sn: + enum_dev_sn.append(f'{vid_pid_sn.group(1)}_{vid_pid_sn.group(2)}_{vid_pid_sn.group(3)}') + if set(declared_devs).issubset(set(enum_dev_sn)): + break + time.sleep(0.1) + timeout -= 0.1 ser.close() + if len(data) == 0: assert False, 'No data from device' lines = data.decode('utf-8', errors='ignore').splitlines() @@ -416,10 +456,25 @@ def test_host_device_info(board): return 0 +def print_msc_info(lines): + """Print MSC inquiry and disk size on a single line""" + inquiry = '' + disk_size = '' + for l in lines: + if re.match(r'^[A-Za-z].*\s+rev\s+', l): + inquiry = l.strip() + if 'Disk Size' in l: + disk_size = l.strip() + if inquiry or disk_size: + print(f'\r\n {inquiry} {disk_size} ', end='') + + def test_host_cdc_msc_hid(board): flasher = board['flasher'] - cdc_devs = [d for d in board['tests'].get('dev_attached', []) if d.get('is_cdc')] - if not cdc_devs: + dev_attached = board['tests'].get('dev_attached', []) + cdc_devs = [d for d in dev_attached if d.get('is_cdc')] + msc_devs = [d for d in dev_attached if d.get('is_msc')] + if not cdc_devs and not msc_devs: return port = get_serial_dev(flasher["uid"], None, None, 0) @@ -430,18 +485,21 @@ def test_host_cdc_msc_hid(board): ret = globals()[f'reset_{flasher["name"].lower()}'](board) assert ret.returncode == 0, 'Failed to reset device' - # Wait for CDC mounted message + # Wait for all expected mount messages data = b'' timeout = ENUM_TIMEOUT + wait_cdc = len(cdc_devs) > 0 + wait_msc = len(msc_devs) > 0 while timeout > 0: new_data = ser.read(ser.in_waiting or 1) if new_data: data += new_data - if b'CDC Interface is mounted' in data: + cdc_ok = (not wait_cdc) or (b'CDC Interface is mounted' in data) + msc_ok = (not wait_msc) or (b'Disk Size' in data) + if cdc_ok and msc_ok: break time.sleep(0.1) timeout -= 0.1 - assert b'CDC Interface is mounted' in data, 'CDC device not mounted on host' # Lookup serial chip name from vid_pid vid_pid_name = { @@ -451,13 +509,29 @@ def test_host_cdc_msc_hid(board): '1a86_7523': 'CH340', '1a86_7522': 'CH340', '1a86_55d3': 'CH9102', '1a86_55d4': 'CH9102', } - dev = cdc_devs[0] - chip_name = vid_pid_name.get(dev['vid_pid'], dev['vid_pid']) - for l in data.decode('utf-8', errors='ignore').splitlines(): - if 'CDC Interface is mounted' in l: - print(f'\r\n {chip_name}: {l} ', end='') + + lines = data.decode('utf-8', errors='ignore').splitlines() + + # Verify and print CDC mount + if cdc_devs: + assert b'CDC Interface is mounted' in data, 'CDC device not mounted on host' + dev = cdc_devs[0] + chip_name = vid_pid_name.get(dev['vid_pid'], dev['vid_pid']) + for l in lines: + if 'CDC Interface is mounted' in l: + print(f'\r\n {chip_name}: {l} ', end='') + + # Verify and print MSC mount (inquiry + disk size) + if msc_devs: + assert b'MassStorage device is mounted' in data, 'MSC device not mounted on host' + assert b'Disk Size' in data, 'MSC Disk Size not reported' + print_msc_info(lines) # CDC echo test via flasher serial + if not cdc_devs: + ser.close() + return + time.sleep(2) ser.reset_input_buffer() @@ -490,6 +564,65 @@ def test_host_cdc_msc_hid(board): ser.close() +def test_host_msc_file_explorer(board): + flasher = board['flasher'] + msc_devs = [d for d in board['tests'].get('dev_attached', []) if d.get('is_msc')] + if not msc_devs: + return + + port = get_serial_dev(flasher["uid"], None, None, 0) + ser = open_serial_dev(port) + ser.timeout = 0.1 + + # reset device to catch mount messages + ret = globals()[f'reset_{flasher["name"].lower()}'](board) + assert ret.returncode == 0, 'Failed to reset device' + + # Wait for MSC mount (Disk Size message) + data = b'' + timeout = ENUM_TIMEOUT + while timeout > 0: + new_data = ser.read(ser.in_waiting or 1) + if new_data: + data += new_data + if b'Disk Size' in data: + break + time.sleep(0.1) + timeout -= 0.1 + assert b'Disk Size' in data, 'MSC device not mounted' + lines = data.decode('utf-8', errors='ignore').splitlines() + print_msc_info(lines) + + # Send "cat README.TXT" and read response + time.sleep(1) + ser.reset_input_buffer() + for ch in 'cat README.TXT\r': + ser.write(ch.encode()) + ser.flush() + time.sleep(0.002) + + # Read response + resp = b'' + t = 10.0 + while t > 0: + rd = ser.read(max(1, ser.in_waiting)) + if rd: + resp += rd + # wait for prompt after command output + if b'>' in resp and resp.rstrip().endswith(b'>'): + break + time.sleep(0.05) + t -= 0.05 + + # Verify response contains README content + resp_text = resp.decode('utf-8', errors='ignore') + assert MSC_README_TXT.decode() in resp_text, (f'MSC README.TXT not found in response:\n' + f' received: {resp_text}') + print('README.TXT matched ', end='') + + ser.close() + + # ------------------------------------------------------------- # Tests: device # ------------------------------------------------------------- @@ -565,12 +698,7 @@ def test_device_cdc_msc(board): # MSC Block test data = read_disk_file(uid, 0, 'README.TXT') - readme = \ - b"This is tinyusb's MassStorage Class demo.\r\n\r\n\ -If you find any bugs or get any questions, feel free to file an\r\n\ -issue at github.com/hathach/tinyusb" - - assert data == readme, f'MSC wrong data in README.TXT\n expected: {readme.decode()}\n received: {data.decode()}' + assert data == MSC_README_TXT, f'MSC wrong data in README.TXT\n expected: {MSC_README_TXT.decode()}\n received: {data.decode()}' def test_device_cdc_msc_freertos(board): @@ -838,6 +966,7 @@ dual_tests = [ host_test = [ 'host/cdc_msc_hid', + 'host/msc_file_explorer', 'host/device_info', ] diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index eaf60c6ce..e84e9672b 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -175,6 +175,25 @@ "args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\"" } }, + { + "name": "adafruit_fruit_jam", + "uid": "2B0DC7A45781189E", + "tests": { + "device": false, + "host": true, + "dual": true, + "dev_attached": [ + {"vid_pid": "0403_6001", "serial": "0", "is_cdc": true}, + {"vid_pid": "058f_6387", "serial": "A8BEE062633D", "is_msc": true, + "msc_disk_size": 3730, "msc_inquiry": "Generic Flash Disk rev 8.07"} + ] + }, + "flasher": { + "name": "openocd", + "uid": "E6614103E78E8324", + "args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\"" + } + }, { "name": "stm32f072disco", "uid": "3A001A001357364230353532", From 55994bc1d5ac80ced3f7a8383e7537967a572ed2 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 17 Mar 2026 22:25:23 +0700 Subject: [PATCH 3/8] add hil tests for device msc_dual_lun and midi functionality --- test/hil/hil_test.py | 72 +++++++++++++++++++++++++++++++++++++++++++ test/hil/tinyusb.json | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index a2c054e61..2100fb7fc 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -944,6 +944,76 @@ def test_device_mtp(board): mtp.disconnect() +def test_device_msc_dual_lun(board): + uid = board['uid'] + + # Read README from LUN 0 + data0 = read_disk_file(uid, 0, 'README0.TXT') + readme0 = b"LUN0: " + MSC_README_TXT + assert data0 == readme0, f'MSC LUN0 wrong data in README0.TXT\n expected: {readme0}\n received: {data0}' + + # Read README from LUN 1 + data1 = read_disk_file(uid, 1, 'README1.TXT') + readme1 = b"LUN1: " + MSC_README_TXT + assert data1 == readme1, f'MSC LUN1 wrong data in README1.TXT\n expected: {readme1}\n received: {data1}' + + +def test_device_midi_test(board): + uid = board['uid'] + + # Find MIDI device via /dev/snd/by-id using board UID + timeout = ENUM_TIMEOUT + midi_port = None + while timeout > 0: + pattern = f'/dev/snd/by-id/usb-*_{uid}-*' + devs = glob.glob(pattern) + if devs: + # by-id entry points to controlCX, derive card number for midiCXD0 + link = os.path.basename(os.readlink(devs[0])) # e.g. "controlC2" + card_num = link.replace('controlC', '') + midi_path = f'/dev/snd/midiC{card_num}D0' + if os.path.exists(midi_path): + midi_port = midi_path + break + time.sleep(1) + timeout -= 1 + assert midi_port is not None, f'MIDI device not found for {uid}' + + # Read MIDI messages and verify note on/off + import select + with open(midi_port, 'rb') as f: + notes = [] + # Read for up to 3 seconds to capture a few notes (286ms interval) + end_time = time.time() + 3 + while time.time() < 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 + + assert len(notes) >= 2, f'Expected at least 2 MIDI notes, got {len(notes)}' + # Verify notes are from the expected sequence + note_sequence = [ + 74, 78, 81, 86, 90, 93, 98, 102, 57, 61, 66, 69, 73, 78, 81, 85, + 88, 92, 97, 100, 97, 92, 88, 85, 81, 78, 74, 69, 66, 62, 57, 62, + 66, 69, 74, 78, 81, 86, 90, 93, 97, 102, 97, 93, 90, 85, 81, 78, + 73, 68, 64, 61, 56, 61, 64, 68, 74, 78, 81, 86, 90, 93, 98, 102 + ] + for n in notes: + assert n in note_sequence, f'Unexpected MIDI note {n}' + + # ------------------------------------------------------------- # Main # ------------------------------------------------------------- @@ -956,7 +1026,9 @@ device_tests = [ 'device/dfu_runtime', 'device/cdc_msc_freertos', 'device/hid_boot_interface', + 'device/msc_dual_lun', 'device/printer_to_cdc', + 'device/midi_test', 'device/mtp' ] diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index e84e9672b..5a42d852e 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -179,7 +179,7 @@ "name": "adafruit_fruit_jam", "uid": "2B0DC7A45781189E", "tests": { - "device": false, + "device": true, "host": true, "dual": true, "dev_attached": [ From b2a592d42a871cadca269ff496ed723daa3c51b7 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 18 Mar 2026 00:02:47 +0700 Subject: [PATCH 4/8] add hil test for device hid_generic_inout functionality --- test/hil/hil_test.py | 37 +++++++++++++++++++++++++++++++++++++ test/hil/requirements.txt | 2 ++ 2 files changed, 39 insertions(+) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 2100fb7fc..41e9fad88 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -1014,6 +1014,42 @@ def test_device_midi_test(board): assert n in note_sequence, f'Unexpected MIDI note {n}' +def test_device_hid_generic_inout(board): + uid = board['uid'] + import hid + + # Find HID device by UID (VID=0xCafe) + timeout = ENUM_TIMEOUT + dev = None + while timeout > 0: + for d in hid.enumerate(0xCafe): + if d['serial_number'] == uid: + dev = d + break + if dev: + break + time.sleep(1) + timeout -= 1 + assert dev is not None, f'HID device not found for {uid}' + + h = hid.Device(vid=dev['vendor_id'], pid=dev['product_id'], serial=uid) + + # Echo test: send random data and verify echo + for size in [8, 32, 63]: + # Report ID (0) + payload, padded to 64 bytes + payload = bytes([random.randint(1, 255) for _ in range(size)]) + report = bytes([0]) + payload + bytes(64 - size) + h.write(report) + echo = h.read(64, timeout=2000) + assert echo is not None and len(echo) >= size, ( + f'HID echo timeout or short read ({size} bytes)') + assert bytes(echo[:size]) == payload, ( + f'HID echo wrong data ({size} bytes):\n' + f' expected: {payload.hex()}\n received: {bytes(echo[:size]).hex()}') + + h.close() + + # ------------------------------------------------------------- # Main # ------------------------------------------------------------- @@ -1027,6 +1063,7 @@ device_tests = [ 'device/cdc_msc_freertos', 'device/hid_boot_interface', 'device/msc_dual_lun', + 'device/hid_generic_inout', 'device/printer_to_cdc', 'device/midi_test', 'device/mtp' diff --git a/test/hil/requirements.txt b/test/hil/requirements.txt index c33980c9d..ef2fecebe 100644 --- a/test/hil/requirements.txt +++ b/test/hil/requirements.txt @@ -1,2 +1,4 @@ fs +hid pyfatfs +pyserial From 3e47f1fcce4d7b445a0ed161cbd860a87eafff30 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 18 Mar 2026 00:20:42 +0700 Subject: [PATCH 5/8] add board_uart_read() for f7 --- .github/actions/get_deps/action.yml | 8 ++++-- hw/bsp/stm32f7/family.c | 28 ++++++++++++++++++--- hw/bsp/stm32h7/boards/stm32h743eval/board.h | 2 -- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.github/actions/get_deps/action.yml b/.github/actions/get_deps/action.yml index bbe94f0fa..ff6972af6 100644 --- a/.github/actions/get_deps/action.yml +++ b/.github/actions/get_deps/action.yml @@ -9,8 +9,12 @@ runs: using: "composite" steps: - name: Checkout pico-sdk for rp2040 - if: contains(inputs.arg, 'rp2040') || contains(inputs.arg, 'raspberry_pi_pico') - uses: actions/checkout@v4 + if: >- + contains(inputs.arg, 'rp2040') || + contains(inputs.arg, 'rp2350') || + contains(inputs.arg, 'raspberry_pi_pico') || + contains(inputs.arg, 'adafruit_fruit_jam') + uses: actions/checkout@v6 with: repository: raspberrypi/pico-sdk ref: master diff --git a/hw/bsp/stm32f7/family.c b/hw/bsp/stm32f7/family.c index ce9049abe..f8145cd63 100644 --- a/hw/bsp/stm32f7/family.c +++ b/hw/bsp/stm32f7/family.c @@ -289,14 +289,31 @@ size_t board_get_unique_id(uint8_t id[], size_t max_len) { } int board_uart_read(uint8_t *buf, int len) { - (void) buf; - (void) len; +#ifdef UART_DEV + int count = 0; + // clear overrun error if any + if (__HAL_UART_GET_FLAG(&UartHandle, UART_FLAG_ORE)) { + __HAL_UART_CLEAR_FLAG(&UartHandle, UART_CLEAR_OREF); + } + for (int i = 0; i < len; i++) { + if (__HAL_UART_GET_FLAG(&UartHandle, UART_FLAG_RXNE)) { + buf[i] = (uint8_t) UartHandle.Instance->RDR; + count++; + } else { + break; + } + } + return count; +#else + (void) buf; (void) len; return 0; +#endif } int board_uart_write(void const *buf, int len) { #ifdef UART_DEV - HAL_UART_Transmit(&UartHandle, (uint8_t *) (uintptr_t) buf, len, 0xffff); + HAL_UART_Transmit(&UartHandle, (uint8_t * )(uintptr_t) + buf, len, 0xffff); return len; #else (void) buf; (void) len; @@ -316,6 +333,11 @@ uint32_t tusb_time_millis_api(void) { return system_ticks; } +#elif CFG_TUSB_OS == OPT_OS_THREADX +// Keep HAL_GetTick() working for HAL functions called from board_init() +void osal_threadx_tick_cb(void) { + HAL_IncTick(); +} #endif void HardFault_Handler(void) { diff --git a/hw/bsp/stm32h7/boards/stm32h743eval/board.h b/hw/bsp/stm32h7/boards/stm32h743eval/board.h index 3914d7aac..ea91976c8 100644 --- a/hw/bsp/stm32h7/boards/stm32h743eval/board.h +++ b/hw/bsp/stm32h7/boards/stm32h743eval/board.h @@ -211,7 +211,6 @@ static int32_t i2c_readreg(uint16_t DevAddr, uint16_t Reg, uint8_t *pData, uint1 } HAL_Delay(10); } - TU_ASSERT(0); return -1; } @@ -222,7 +221,6 @@ static int32_t i2c_writereg(uint16_t DevAddr, uint16_t Reg, uint8_t *pData, uint } HAL_Delay(10); } - TU_ASSERT(0); return -1; } From 6c895e7af45107702705cc532af4abb35d2546cf Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 18 Mar 2026 00:26:18 +0700 Subject: [PATCH 6/8] Update several actions to latest version --- .github/actions/setup_toolchain/download/action.yml | 2 +- .github/actions/setup_toolchain/espressif/action.yml | 2 +- .github/workflows/build.yml | 6 +++--- .github/workflows/build_util.yml | 4 ++-- .github/workflows/cifuzz.yml | 2 +- .github/workflows/static_analysis.yml | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/actions/setup_toolchain/download/action.yml b/.github/actions/setup_toolchain/download/action.yml index f691b0499..77d3bcf19 100644 --- a/.github/actions/setup_toolchain/download/action.yml +++ b/.github/actions/setup_toolchain/download/action.yml @@ -13,7 +13,7 @@ runs: steps: - name: Cache Toolchain if: ${{ !startsWith(inputs.toolchain_url, 'https://github.com') }} - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-toolchain-download with: path: ~/cache/${{ inputs.toolchain }} diff --git a/.github/actions/setup_toolchain/espressif/action.yml b/.github/actions/setup_toolchain/espressif/action.yml index 90ef753c4..ec1ff2e91 100644 --- a/.github/actions/setup_toolchain/espressif/action.yml +++ b/.github/actions/setup_toolchain/espressif/action.yml @@ -21,7 +21,7 @@ runs: shell: bash - name: Cache Docker Image - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-toolchain-espressif with: path: ${{ env.DOCKER_ESP_IDF }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 960ccf8ee..f549bb1e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 2 # Needed for push commit comparison - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@v4 id: filter with: filters: | @@ -124,7 +124,7 @@ jobs: - name: Upload Metrics Artifact if: github.event_name == 'push' || github.event_name == 'release' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: metrics-tinyusb path: metrics.json @@ -179,7 +179,7 @@ jobs: - name: Upload Metrics Comment Artifact if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: metrics-comment path: | diff --git a/.github/workflows/build_util.yml b/.github/workflows/build_util.yml index c9b0d36d9..69b6f28d5 100644 --- a/.github/workflows/build_util.yml +++ b/.github/workflows/build_util.yml @@ -90,14 +90,14 @@ jobs: - name: Upload Artifacts for Metrics if: inputs.upload-metrics == true && inputs.code-changed == true - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: metrics-${{ matrix.arg }} path: cmake-build/cmake-build-*/metrics.json - name: Upload Artifacts for Hardware Testing if: inputs.upload-artifacts == true && inputs.code-changed == true - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: binaries-${{ matrix.arg }} path: | diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 9b3756a72..ff75a8ba6 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -29,7 +29,7 @@ jobs: fuzz-seconds: 400 - name: Upload Crash - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 if: failure() && steps.build.outcome == 'success' with: name: artifacts diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index a78682d7a..d440bf69e 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -84,7 +84,7 @@ jobs: category: CodeQL - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: codeql-${{ matrix.board }} path: ${{ steps.analyze.outputs.sarif-output }} @@ -136,7 +136,7 @@ jobs: category: PVS-Studio - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: pvs-studio-${{ matrix.board }} path: pvs-studio-${{ matrix.board }}.sarif @@ -236,7 +236,7 @@ jobs: category: IAR-CStat - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: iar-cstat-${{ matrix.board }} path: iar-cstat-${{ matrix.board }}.sarif From b7656561b86b68763e5aa2da5261e5a79e5ff53d Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 18 Mar 2026 12:56:54 +0700 Subject: [PATCH 7/8] fix hid_generic_inout for TUD_ENDPOINT_ONE_DIRECTION_ONLY MCUs Use separate endpoint numbers (EP1 OUT, EP2 IN) on MCUs with shared FIFO that cannot support the same endpoint number in both directions. Also add missing static qualifier to print_musb_info(). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../device/hid_generic_inout/src/usb_descriptors.c | 12 ++++++++++-- src/portable/mentor/musb/dcd_musb.c | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/device/hid_generic_inout/src/usb_descriptors.c b/examples/device/hid_generic_inout/src/usb_descriptors.c index f26333d50..929b2fd3a 100644 --- a/examples/device/hid_generic_inout/src/usb_descriptors.c +++ b/examples/device/hid_generic_inout/src/usb_descriptors.c @@ -97,7 +97,15 @@ enum #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN) -#define EPNUM_HID 0x01 +#if defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) + // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h + // e.g EP1 OUT & EP1 IN cannot exist together + #define EPNUM_HID_OUT 0x01 + #define EPNUM_HID_IN 0x82 +#else + #define EPNUM_HID_OUT 0x01 + #define EPNUM_HID_IN 0x81 +#endif uint8_t const desc_configuration[] = { @@ -105,7 +113,7 @@ uint8_t const desc_configuration[] = TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), // Interface number, string index, protocol, report descriptor len, EP Out & In address, size & polling interval - TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, 0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10) + TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID_OUT, EPNUM_HID_IN, CFG_TUD_HID_EP_BUFSIZE, 10) }; // Invoked when received GET CONFIGURATION DESCRIPTOR diff --git a/src/portable/mentor/musb/dcd_musb.c b/src/portable/mentor/musb/dcd_musb.c index d329285e9..339048473 100644 --- a/src/portable/mentor/musb/dcd_musb.c +++ b/src/portable/mentor/musb/dcd_musb.c @@ -494,7 +494,7 @@ static void process_bus_reset(uint8_t rhport) { *------------------------------------------------------------------*/ #if CFG_TUSB_DEBUG >= MUSB_DEBUG -void print_musb_info(musb_regs_t* musb_regs) { +static void print_musb_info(musb_regs_t* musb_regs) { // print version, epinfo, raminfo, config_data0, fifo_size TU_LOG1("musb version = %u.%u\r\n", musb_regs->hwvers_bit.major, musb_regs->hwvers_bit.minor); TU_LOG1("Number of endpoints: %u TX, %u RX\r\n", musb_regs->epinfo_bit.tx_ep_num, musb_regs->epinfo_bit.rx_ep_num); From 0521e6698eb0719c27ef7c2473c9152be30ae172 Mon Sep 17 00:00:00 2001 From: Ha Thach Date: Wed, 18 Mar 2026 14:56:25 +0700 Subject: [PATCH 8/8] hil pico2 test with native host --- test/hil/tinyusb.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index 5a42d852e..b4c6aaefe 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -162,12 +162,9 @@ { "name": "raspberry_pi_pico2", "uid": "560AE75E1C7152C9", - "build" : { - "flags_on": ["CFG_TUH_RPI_PIO_USB"] - }, "tests": { - "device": true, "host": true, "dual": true, - "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "533D004242", "is_cdc": true}] + "device": false, "host": true, "dual": false, + "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002694", "is_cdc": true}] }, "flasher": { "name": "openocd",