summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-03-18 18:10:32 +0700
committerGitHub <[email protected]>2026-03-18 18:10:32 +0700
commit766ac4f7a2c223bb85312c8bb99f8c478c211bae (patch)
tree795c7b40c1d8567378ae052a783b0d8b28531ec5
parent8f7f286ae347ec1adbfd0888d65fb1aeb263b7d2 (diff)
parent0521e6698eb0719c27ef7c2473c9152be30ae172 (diff)
Merge pull request #3557 from hathach/hil-host-cdc
add more hil tests
-rw-r--r--.github/actions/get_deps/action.yml8
-rw-r--r--.github/actions/setup_toolchain/download/action.yml2
-rw-r--r--.github/actions/setup_toolchain/espressif/action.yml2
-rw-r--r--.github/workflows/build.yml6
-rw-r--r--.github/workflows/build_util.yml4
-rw-r--r--.github/workflows/cifuzz.yml2
-rw-r--r--.github/workflows/static_analysis.yml6
-rw-r--r--AGENTS.md8
-rw-r--r--LICENSE2
-rw-r--r--examples/device/hid_generic_inout/src/usb_descriptors.c12
-rw-r--r--examples/host/msc_file_explorer/src/msc_app.c11
-rw-r--r--hw/bsp/rp2040/boards/adafruit_feather_rp2040_usb_host/board.cmake1
-rw-r--r--hw/bsp/rp2040/boards/adafruit_fruit_jam/board.cmake2
-rw-r--r--hw/bsp/rp2040/boards/adafruit_metro_rp2350/board.cmake2
-rw-r--r--hw/bsp/rp2040/family.cmake6
-rw-r--r--hw/bsp/stm32f7/family.c28
-rw-r--r--hw/bsp/stm32h7/boards/stm32h743eval/board.h18
-rw-r--r--hw/bsp/stm32h7/family.c20
-rw-r--r--src/portable/mentor/musb/dcd_musb.c2
-rwxr-xr-xtest/hil/hil_test.py333
-rw-r--r--test/hil/requirements.txt2
-rw-r--r--test/hil/tinyusb.json40
22 files changed, 465 insertions, 52 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/.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
diff --git a/AGENTS.md b/AGENTS.md
index 4e510b01e..eb6b737c3 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
@@ -152,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/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/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/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 d2f61a5ce..ea91976c8 100644
--- a/hw/bsp/stm32h7/boards/stm32h743eval/board.h
+++ b/hw/bsp/stm32h7/boards/stm32h743eval/board.h
@@ -205,13 +205,23 @@ 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);
+ }
+ 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);
+ }
+ 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/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);
diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py
index 7cffd2da8..41e9fad88 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,6 +456,173 @@ 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']
+ 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)
+ 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 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
+ 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
+
+ # 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',
+ }
+
+ 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()
+
+ 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()
+
+
+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
# -------------------------------------------------------------
@@ -491,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):
@@ -742,6 +944,112 @@ 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}'
+
+
+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
# -------------------------------------------------------------
@@ -754,7 +1062,10 @@ device_tests = [
'device/dfu_runtime',
'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'
]
@@ -763,6 +1074,8 @@ dual_tests = [
]
host_test = [
+ 'host/cdc_msc_hid',
+ 'host/msc_file_explorer',
'host/device_info',
]
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
diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json
index e8449ba74..b4c6aaefe 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",
@@ -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"}]
+ "device": false, "host": true, "dual": false,
+ "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002694", "is_cdc": true}]
},
"flasher": {
"name": "openocd",
@@ -176,6 +173,25 @@
}
},
{
+ "name": "adafruit_fruit_jam",
+ "uid": "2B0DC7A45781189E",
+ "tests": {
+ "device": true,
+ "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",
"flasher": {
@@ -193,7 +209,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",