Skip to content

Commit e1cb1b5

Browse files
authored
Merge pull request #3640 from hathach/dwc2_iso_in
dcd/dwc2: fix ISO IN endpoint become disabled after incomplete transfer
2 parents 08f9855 + 460ce56 commit e1cb1b5

3 files changed

Lines changed: 176 additions & 33 deletions

File tree

src/portable/synopsys/dwc2/dcd_dwc2.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1136,8 +1136,9 @@ static void handle_incomplete_iso_in(uint8_t rhport) {
11361136
}
11371137
epin->diepctl = depctl.value;
11381138
} else {
1139-
// too many retries, give up
1139+
// too many retries, give up, but keep endpoint activated
11401140
edpt_disable(rhport, epnum | TUSB_DIR_IN_MASK, false);
1141+
epin->diepctl |= DIEPCTL_USBAEP;
11411142
dcd_event_xfer_complete(rhport, epnum | TUSB_DIR_IN_MASK, 0, XFER_RESULT_FAILED, true);
11421143
}
11431144
}

test/hil/hil_test.py

Lines changed: 172 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
# ACTION=="add", SUBSYSTEM=="block", SUBSYSTEMS=="usb", ENV{ID_FS_USAGE}=="filesystem", MODE="0666", PROGRAM="/bin/sh -c 'echo $$ID_SERIAL_SHORT | rev | cut -c -8 | rev'", RUN{program}+="/usr/bin/systemd-mount --no-block --automount=yes --collect $devnode /media/blkUSB_%c.%s{bInterfaceNumber}"
2828

2929
import argparse
30+
import io
3031
import os
3132
import random
3233
import re
@@ -35,6 +36,7 @@
3536
import time
3637
import warnings
3738
import signal
39+
from contextlib import redirect_stdout
3840
from pathlib import Path
3941
from typing import Any, TypedDict, NotRequired, cast
4042

@@ -47,7 +49,8 @@
4749
import subprocess
4850
import json
4951
import glob
50-
from multiprocessing import Pool
52+
import shutil
53+
from multiprocessing import Pool, Lock
5154
from multiprocessing import TimeoutError as MpTimeoutError
5255
import fs
5356
import hashlib
@@ -66,6 +69,28 @@
6669
board_test = {}
6770
build_dir = 'cmake-build'
6871
skip_flash = False
72+
print_lock = None
73+
74+
75+
def init_worker(lock):
76+
global print_lock
77+
print_lock = lock
78+
79+
80+
def log_line(msg: str) -> None:
81+
out = sys.__stdout__ if sys.__stdout__ is not None else sys.stdout
82+
if print_lock is not None:
83+
with print_lock:
84+
print(msg, file=out, flush=True)
85+
else:
86+
print(msg, file=out, flush=True)
87+
88+
89+
def compact_output(raw: str) -> str:
90+
if not raw:
91+
return ''
92+
lines = [ln.strip() for ln in raw.replace('\r', '\n').split('\n') if ln.strip()]
93+
return ' | '.join(lines)
6994

7095
class FlasherCfg(TypedDict):
7196
name: str
@@ -174,6 +199,19 @@ def get_hid_dev(id, vendor_str, product_str, event):
174199
return f'/dev/input/by-id/usb-{vendor_str}_{product_str}_{id}-{event}'
175200

176201

202+
def get_alsa_capture_dev(id):
203+
pattern = f'/dev/snd/by-id/usb-*_{id}-*'
204+
for dev in glob.glob(pattern):
205+
try:
206+
link = os.path.basename(os.path.realpath(dev))
207+
except OSError:
208+
continue
209+
m = re.match(r'controlC(\d+)', link)
210+
if m:
211+
return f'hw:{m.group(1)},0'
212+
return None
213+
214+
177215
def open_serial_dev(port: str):
178216
timeout = ENUM_TIMEOUT
179217
ser = None
@@ -1289,6 +1327,79 @@ def test_device_midi_test(board):
12891327
assert n in note_sequence, f'Unexpected MIDI note {n}'
12901328

12911329

1330+
def test_device_audio_test_freertos(board):
1331+
uid = board['uid']
1332+
1333+
if os.name == 'nt':
1334+
return 'skipped'
1335+
1336+
arecord = shutil.which('arecord')
1337+
if arecord is None:
1338+
return 'skipped'
1339+
1340+
pcm = None
1341+
timeout = ENUM_TIMEOUT
1342+
while timeout > 0:
1343+
pcm = get_alsa_capture_dev(uid)
1344+
if pcm:
1345+
break
1346+
time.sleep(1)
1347+
timeout -= 1
1348+
1349+
assert pcm is not None, f'ALSA capture device not found for {uid}'
1350+
1351+
raw_path = f'/tmp/tinyusb_audio_{uid}.raw'
1352+
cmd = [
1353+
arecord,
1354+
'-D', pcm,
1355+
'-q',
1356+
'-f', 'S16_LE',
1357+
'-c', '1',
1358+
'-r', '48000',
1359+
'-d', '2',
1360+
'-t', 'raw',
1361+
raw_path,
1362+
]
1363+
1364+
ret = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
1365+
assert ret.returncode == 0, f'arecord failed: {ret.stderr.strip() or ret.stdout.strip()}'
1366+
1367+
try:
1368+
with open(raw_path, 'rb') as f:
1369+
raw = f.read()
1370+
finally:
1371+
try:
1372+
os.remove(raw_path)
1373+
except OSError:
1374+
pass
1375+
1376+
assert len(raw) >= 48000, f'Captured too little audio: {len(raw)} bytes'
1377+
assert (len(raw) % 2) == 0, f'Invalid 16-bit audio length: {len(raw)}'
1378+
1379+
sample_count = len(raw) // 2
1380+
samples = [int.from_bytes(raw[i:i + 2], 'little', signed=False) for i in range(0, len(raw), 2)]
1381+
assert sample_count > 1024, f'Not enough samples captured: {sample_count}'
1382+
1383+
# The firmware sends a continuous uint16 ramp. Using ALSA hw: capture bypasses
1384+
# PulseAudio processing, so most adjacent samples should differ by exactly 1.
1385+
total_diffs = sample_count - 1
1386+
one_step = 0
1387+
near_step = 0
1388+
for i in range(total_diffs):
1389+
d = (samples[i + 1] - samples[i]) & 0xFFFF
1390+
if d == 1:
1391+
one_step += 1
1392+
if d in (0, 1, 2, 47, 48, 49):
1393+
near_step += 1
1394+
1395+
one_ratio = one_step / total_diffs
1396+
near_ratio = near_step / total_diffs
1397+
assert one_ratio >= 0.85, f'Unexpected audio pattern (strict ratio={one_ratio:.3f})'
1398+
assert near_ratio >= 0.98, f'Unexpected audio pattern (relaxed ratio={near_ratio:.3f})'
1399+
1400+
print(f' ALSA {pcm} strict={one_ratio:.3f} relaxed={near_ratio:.3f}', end='')
1401+
1402+
12921403
def test_device_hid_generic_inout(board):
12931404
uid = board['uid']
12941405
import hid
@@ -1335,6 +1446,7 @@ def test_device_hid_generic_inout(board):
13351446
'device/dfu',
13361447
'device/cdc_msc',
13371448
'device/cdc_msc_throughput',
1449+
'device/audio_test_freertos',
13381450
'device/dfu_runtime',
13391451
'device/cdc_msc_freertos',
13401452
'device/hid_boot_interface',
@@ -1374,47 +1486,76 @@ def test_example(board: Board, f1: str, example: str) -> int:
13741486

13751487
fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{name}{f1_str}' / example
13761488
fw_name = fw_dir / Path(example).name
1377-
print(f'{name+f1_str:40} {example:30} ...', end='')
1489+
test_name = f'{name+f1_str:40} {example:30} ...'
13781490

13791491
if not fw_dir.exists() or not ((fw_name.with_suffix('.elf')).exists() or (fw_name.with_suffix('.bin')).exists()):
1380-
print('Skip (no binary)')
1492+
log_line(f'{test_name} Skip (no binary)')
13811493
return 0
13821494

13831495
if verbose:
1384-
print(f'Flashing {fw_name}.elf')
1496+
log_line(f'Flashing {fw_name}.elf')
13851497

13861498
# flash firmware (unless --skip-flash), then run the test. Both may fail randomly,
13871499
# retry a few times.
13881500
start_s = time.time()
13891501
flash_ok = True
1502+
last_err = ''
1503+
last_detail = ''
13901504
for i in range(max_retry):
1391-
if not skip_flash:
1392-
ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name))
1393-
flash_ok = (ret.returncode == 0)
1394-
if flash_ok:
1395-
try:
1396-
tret = globals()[f'test_{example.replace("/", "_")}'](board)
1397-
if tret == 'skipped':
1398-
print(f' {STATUS_SKIPPED}', end='')
1399-
else:
1400-
print(' OK', end='')
1401-
break
1402-
except Exception as e:
1403-
if i == max_retry - 1:
1404-
err_count += 1
1405-
print(f'{STATUS_FAILED}: {e}')
1406-
else:
1407-
print(f'\n Test failed: {e}, retry {i+2}/{max_retry}', end='')
1408-
time.sleep(0.5)
1409-
else:
1410-
print(f'\n Flash failed, retry {i+2}/{max_retry}', end='')
1411-
time.sleep(0.5)
1505+
attempt_out = io.StringIO()
1506+
with redirect_stdout(attempt_out):
1507+
if not skip_flash:
1508+
ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name))
1509+
flash_ok = (ret.returncode == 0)
1510+
if flash_ok:
1511+
try:
1512+
tret = globals()[f'test_{example.replace("/", "_")}'](board)
1513+
last_detail = compact_output(attempt_out.getvalue())
1514+
if tret == 'skipped':
1515+
status = STATUS_SKIPPED
1516+
else:
1517+
status = STATUS_OK
1518+
msg = f'{test_name} {status}'
1519+
if last_detail:
1520+
msg += f' {last_detail}'
1521+
msg += f' in {time.time() - start_s:.1f}s'
1522+
log_line(msg)
1523+
break
1524+
except Exception as e:
1525+
last_err = str(e)
1526+
last_detail = compact_output(attempt_out.getvalue())
1527+
if i == max_retry - 1:
1528+
err_count += 1
1529+
msg = f'{test_name} {STATUS_FAILED}: {e}'
1530+
if last_detail:
1531+
msg += f' {last_detail}'
1532+
msg += f' in {time.time() - start_s:.1f}s'
1533+
log_line(msg)
1534+
else:
1535+
msg = f'{test_name} retry {i+2}/{max_retry}: test failed: {e}'
1536+
if last_detail:
1537+
msg += f' {last_detail}'
1538+
log_line(msg)
1539+
time.sleep(0.5)
1540+
else:
1541+
last_err = 'Flash failed'
1542+
last_detail = compact_output(attempt_out.getvalue())
1543+
if i < max_retry - 1:
1544+
msg = f'{test_name} retry {i+2}/{max_retry}: flash failed'
1545+
if last_detail:
1546+
msg += f' {last_detail}'
1547+
log_line(msg)
1548+
time.sleep(0.5)
14121549

14131550
if not flash_ok:
14141551
err_count += 1
1415-
print(f' Flash {STATUS_FAILED}', end='')
1416-
1417-
print(f' in {time.time() - start_s:.1f}s')
1552+
msg = f'{test_name} Flash {STATUS_FAILED}'
1553+
if last_err:
1554+
msg += f': {last_err}'
1555+
if last_detail:
1556+
msg += f' {last_detail}'
1557+
msg += f' in {time.time() - start_s:.1f}s'
1558+
log_line(msg)
14181559

14191560
return err_count
14201561

@@ -1471,7 +1612,7 @@ def test_board(board: Board) -> tuple[str, int, list[str]]:
14711612
for skip in board_tests['skip']:
14721613
if skip in test_list:
14731614
test_list.remove(skip)
1474-
print(f'{name:25} {skip:30} ... Skip')
1615+
log_line(f'{name:25} {skip:30} ... Skip')
14751616

14761617
err_count = 0
14771618
failed_tests = []
@@ -1560,14 +1701,15 @@ def main() -> None:
15601701
print(f'Build phase done: {build_err} failed')
15611702
print('-' * 30)
15621703

1563-
with Pool(processes=os.cpu_count() or 1) as pool:
1704+
with Pool(processes=os.cpu_count() or 1, initializer=init_worker, initargs=(Lock(),)) as pool:
15641705
async_ret = pool.map_async(test_board, config_boards)
15651706
try:
15661707
mret = async_ret.get(timeout=POOL_TIMEOUT)
15671708
except MpTimeoutError:
15681709
pool.terminate()
15691710
pool.join()
15701711
raise RuntimeError(f'HIL worker pool timed out after {POOL_TIMEOUT}s')
1712+
15711713
err_count = build_err + sum(e[1] for e in mret)
15721714
# generate skip list for next re-run if failed: skip boards that fully passed,
15731715
# and emit -bt BOARD:t1,t2 so each failed board only re-runs its own failed tests.

test/hil/tinyusb.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"]
88
},
99
"tests": {
10-
"only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "host/device_info"],
10+
"only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", "host/device_info"],
1111
"dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002427", "is_cdc": true}]
1212
},
1313
"flasher": {
@@ -25,7 +25,7 @@
2525
"flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"]
2626
},
2727
"tests": {
28-
"only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "host/device_info"],
28+
"only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", "host/device_info"],
2929
"dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2005402", "is_cdc": true}]
3030
},
3131
"flasher": {

0 commit comments

Comments
 (0)