Merge pull request #3557 from hathach/hil-host-cdc

add more hil tests
This commit is contained in:
Ha Thach
2026-03-18 18:10:32 +07:00
committed by GitHub
22 changed files with 465 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
set(PICO_PLATFORM rp2040)
set(PICO_BOARD adafruit_feather_rp2040_usb_host)
set(CFG_TUH_RPI_PIO_USB 1)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',
]

View File

@ -1,2 +1,4 @@
fs
hid
pyfatfs
pyserial

View File

@ -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",
@ -175,6 +172,25 @@
"args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\""
}
},
{
"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",
@ -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",