example(usbtest): device-side peer for the Linux kernel usbtest battery

Gadget-Zero style source/sink on a vendor interface (alt0 empty, alt1
bulk+int+iso) plus EP0 ctrl_out; tier advertised in bcdDevice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeF2gZ1M7GWkz6Av4BpKPg
This commit is contained in:
hathach
2026-07-09 23:38:48 +07:00
parent 90d48c2a63
commit 5dbc15370c
11 changed files with 988 additions and 0 deletions

View File

@ -35,6 +35,7 @@ set(EXAMPLE_LIST
printer_to_cdc
uac2_headset
uac2_speaker_fb
usbtest
usbtmc
video_capture
video_capture_2ch

View File

@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.20)
include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake)
project(usbtest C CXX ASM)
# Checks this example is valid for the family and initializes the project
family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR})
# Espressif has its own cmake build system
if(FAMILY STREQUAL "espressif")
return()
endif()
add_executable(${PROJECT_NAME})
# Example source
target_sources(${PROJECT_NAME} PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src/main.c
${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.c
)
# Example include
target_include_directories(${PROJECT_NAME} PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
)
# Configure compilation flags and libraries for the example without RTOS.
# See the corresponding function in hw/bsp/FAMILY/family.cmake for details.
family_configure_device_example(${PROJECT_NAME} noos)

View File

@ -0,0 +1,6 @@
{
"version": 6,
"include": [
"../../../hw/bsp/BoardPresets.json"
]
}

View File

@ -0,0 +1,11 @@
include ../../../hw/bsp/family_support.mk
INC += \
src \
# Example source
EXAMPLE_SOURCE += $(wildcard src/*.c)
SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE))
include ../../../hw/bsp/family_rules.mk

View File

@ -0,0 +1,94 @@
# usbtest
Device-side peer of the Linux kernel USB test pair:
- `usbtest.ko` — host kernel module (`drivers/usb/misc/usbtest.c`) containing ~30 numbered
test cases over bulk/control/interrupt/isochronous transfers.
- `testusb` — userspace dispatcher (`tools/usb/testusb.c`) that tells the module which case
to run via usbfs ioctl.
This example implements the Gadget-Zero style *source/sink* protocol on a vendor-specific
interface so the whole battery can exercise TinyUSB device controller drivers:
- bulk IN = infinite source (usbtest pattern 0: all zeros)
- bulk OUT = infinite sink (data discarded)
## Tiers
The firmware advertises its capability tier in `bcdDevice` (`0x01TT`); the host script picks
the matching test battery automatically.
| Tier | Capability | usbtest cases |
|------|------------|---------------|
| 1 | bulk source/sink | 0, 9, 10, 18, 11, 12, 24, 13, 29, 1720, 27, 28 |
| 2 | + vendor control `0x5b`/`0x5c` (ctrl_out) | + 14, 21 |
| 3 | + interrupt source/sink | + 25, 26 |
| 4 | + isochronous source/sink | + 15, 16, 22, 23 |
This example implements all four tiers using the vendor class with the interrupt
(`CFG_TUD_VENDOR_EP_INT_OUT/IN`) and isochronous (`CFG_TUD_VENDOR_EP_ISO_OUT/IN`)
endpoint pairs and altsetting support (`CFG_TUD_VENDOR_ALT_SETTINGS`): alt 0
carries no endpoints, alt 1 the full source/sink set, per USB 2.0 5.6.3 (the host
usbtest driver selects alt 1 itself).
## Test cases
Directions are from the host's point of view: *write* = host→device (OUT endpoint, device
sinks and discards), *read* = device→host (IN endpoint, device sources zeros and the host
verifies every byte). All checking happens host-side in `usbtest.ko`; a case fails on a data
mismatch, an unexpected short packet/STALL, or a timeout. What each case stresses on the
device/DCD side:
| # | Name | What it does / what it exercises |
|---|------|----------------------------------|
| 0 | NOP | ioctl round-trip sanity, no USB traffic — proves the interface bound with the right capability profile |
| 9 | ch9 subset | chapter-9 standard control requests (GET_DESCRIPTOR, GET_STATUS, SET/CLEAR_FEATURE, SET_INTERFACE, …) — the EP0 state machine, incl. status stages and ZLPs |
| 10 | queued control | many control URBs in flight at once — EP0 under sustained back-to-back SETUPs |
| 1 / 2 | bulk write / read | plain OUT sink / IN source streams of whole max-size packets — FIFO handling, multi-packet transfers |
| 3 / 4 | bulk write / read vary | same with transfer sizes varying per URB — short packets and packet-boundary edge cases |
| 58 | bulk sg write/read (+vary) | scatter-gather queued URBs — continuous packet pressure with no inter-URB gap; classic overflow/babble catcher |
| 11 / 12 | unlink reads / writes | URBs submitted then cancelled mid-flight — the device keeps streaming while the host aborts; DCD abort/cleanup paths |
| 24 | unlink queued writes | unlink from a deep OUT queue — same, under queue pressure |
| 13 | ep halt set/clear | SET_FEATURE(ENDPOINT_HALT), verify the endpoint really STALLs, then CLEAR_FEATURE and verify traffic resumes at DATA0 — stall must abort an armed transfer (and flush any loaded FIFO) |
| 29 | toggle clear | CLEAR_FEATURE(HALT) on a **non-halted** endpoint mid-traffic, purely to reset the data toggle — the DCD must reset DATA0 *without* disarming the queued transfer (historically the most common per-DCD bug in this battery) |
| 17 / 18 | bulk write / read unaligned | bulk streams from oddly-offset host buffers — host DMA-alignment path; the device sees normal traffic |
| 19 / 20 | bulk write / read premapped | bulk streams using host pre-mapped DMA buffers — another host memory path |
| 27 / 28 | bulk write / read perf | sustained maximum-throughput streams, reported in MB/s — real-time FIFO servicing under load |
| 14 | ctrl_out write/read | vendor EP0 request `0x5b` stores wLength bytes, `0x5c` reads them back, sizes varying — multi-packet control-OUT data stages and buffer persistence across requests |
| 21 | ctrl_out unaligned | same from odd host buffer offsets |
| 25 / 26 | int write / read | interrupt OUT sink / IN source at the descriptor's polling interval — interrupt endpoint arming and completion |
| 15 / 16 | iso write / read | isochronous OUT sink / IN source, one packet per (micro)frame with per-packet status — no handshake/retry, DATA0-only; the IN source must re-arm fast enough to make every frame deadline |
| 22 / 23 | iso write / read unaligned | same from odd host buffer offsets |
Per-case iteration counts and sizes are chosen by `test/hil/usbtest.py` for the negotiated
speed (see its `PARAMS` table); the authoritative case implementations live in the kernel's
`drivers/usb/misc/usbtest.c`.
## Running
Use the host script (handles driver binding, per-case parameters, result parsing):
```bash
python3 test/hil/usbtest.py --serial <board-uid>
```
Requirements on the host: `usbtest` kernel module (`CONFIG_USB_TEST`, `modprobe usbtest`),
the `testusb` binary built from kernel `tools/usb/testusb.c`, and sudo (usbfs ioctls +
driver bind/unbind).
Manual runs are possible but beware `testusb` defaults: always pass explicit `-s`/`-v`
values that are multiples of 512 — the device streams whole max-size packets, so a
non-packet-aligned read length overflows (`-EOVERFLOW`), and never run bare `testusb -a`
(the default parameter set includes cases with invalid parameters and hour-long runtimes
at full speed).
```bash
# bind: MUST use the 5-field form referencing Gadget Zero (0525:a4a0) so the
# dynamic id inherits its capability profile. A plain "cafe 4010" id leaves
# driver_info NULL, which usbtest_probe() dereferences -> kernel oops.
sudo modprobe usbtest
echo "cafe 4010 0 0525 a4a0" | sudo tee /sys/bus/usb/drivers/usbtest/new_id
# example: bulk write/read
sudo testusb -D /dev/bus/usb/<BBB>/<DDD> -t 1 -c 128 -s 1024 -v 512
sudo testusb -D /dev/bus/usb/<BBB>/<DDD> -t 2 -c 128 -s 1024 -v 512
```

View File

@ -0,0 +1,15 @@
mcu:MSP430x5xx
mcu:NUC121
mcu:SAMD11
# DCD has no isochronous support (dcd_edpt_iso_alloc refuses), tier-4 cannot enumerate:
mcu:CXD56
mcu:FT90X
mcu:LPC175X_6X
mcu:LPC40XX
mcu:NUC100
mcu:NUC120
mcu:NUC505
mcu:PIC32MZ
mcu:SAMG
mcu:SAMX7X
mcu:VALENTYUSB_EPTRI

View File

@ -0,0 +1,4 @@
# This file is for ESP-IDF only
idf_component_register(SRCS "main.c" "usb_descriptors.c"
INCLUDE_DIRS "."
REQUIRES boards tinyusb_src)

View File

@ -0,0 +1,336 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2026 Ha Thach (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
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
/* Device-side peer of the Linux kernel host test driver drivers/usb/misc/usbtest.c
* (driven from userspace by tools/usb/testusb.c). Implements the Gadget-Zero
* style source/sink protocol on a vendor interface (alt 0 = no endpoints,
* alt 1 = full set, selected by the host usbtest driver):
* - bulk/interrupt/isochronous IN = infinite source (pattern 0: all zeros)
* - bulk/interrupt/isochronous OUT = infinite sink (data discarded)
* - EP0 0x5b/0x5c = control write then read-back (ctrl_out tests)
* See examples/device/usbtest/README.md and test/hil/usbtest.py for usage.
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "bsp/board_api.h"
#include "tusb.h"
#include "usb_descriptors.h"
//--------------------------------------------------------------------+
// MACRO CONSTANT TYPEDEF PROTYPES
//--------------------------------------------------------------------+
/* Blink pattern
* - 250 ms : device not mounted
* - 1000 ms : device mounted
*/
enum {
BLINK_NOT_MOUNTED = 250,
BLINK_MOUNTED = 1000,
};
static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED;
// Source data, all zeros = usbtest pattern 0. Sizes are a multiple of the
// endpoint max packet size: transfers are always whole packets, never an
// unintended short packet or ZLP.
static uint8_t const tx_chunk[CFG_TUD_VENDOR_TX_EPSIZE];
static uint8_t const int_tx_chunk[USBTEST_INT_EP_MPS];
static uint8_t const iso_tx_chunk[USBTEST_ISO_EP_MPS];
// Interrupt/iso submit one packet per (micro)frame, sized to the NEGOTIATED speed's mps — a
// high-speed build enumerated at full speed must submit the FS length, not the HS-capacity buffer
// size (bulk is exempt: it streams multi-packet transfers). See usb_descriptors.h.
static inline uint16_t usbtest_int_len(void) {
return (tud_speed_get() == TUSB_SPEED_HIGH) ? USBTEST_INT_EP_MPS_HS : USBTEST_INT_EP_MPS_FS;
}
static inline uint16_t usbtest_iso_len(void) {
return (tud_speed_get() == TUSB_SPEED_HIGH) ? USBTEST_ISO_EP_MPS_HS : USBTEST_ISO_EP_MPS_FS;
}
//------------- prototypes -------------//
void led_blinking_task(void* param);
void usbtest_task(void* param);
#if CFG_TUSB_OS == OPT_OS_FREERTOS
void freertos_init(void);
#endif
/*------------- MAIN -------------*/
int main(void) {
board_init();
// If using FreeRTOS: create blinky, tinyusb device, and usbtest source/sink tasks
#if CFG_TUSB_OS == OPT_OS_FREERTOS
freertos_init();
#else
// init device stack on configured roothub port
tusb_rhport_init_t dev_init = {
.role = TUSB_ROLE_DEVICE,
.speed = TUSB_SPEED_AUTO
};
tusb_init(BOARD_TUD_RHPORT, &dev_init);
board_init_after_tusb();
while (1) {
tud_task(); // tinyusb device task
usbtest_task(NULL);
led_blinking_task(NULL);
}
#endif
}
//--------------------------------------------------------------------+
// Source/sink pumps
//--------------------------------------------------------------------+
// Polling keeps all four endpoints armed and self-heals after endpoint halt
// (set/clear feature tests): stall marks the endpoint busy so the calls fail
// quietly until the host clears the halt, then the next tick re-arms.
static void usbtest_pump(void) {
if (tud_vendor_mounted()) {
tud_vendor_read_xfer(); // bulk sink: arm/re-arm, quiet fail if armed or halted
if (tud_vendor_write_available()) { // 0 while bulk IN is busy or halted
tud_vendor_write(tx_chunk, sizeof(tx_chunk));
}
tud_vendor_int_read_xfer(); // interrupt sink
if (tud_vendor_int_write_available()) {
tud_vendor_int_write(int_tx_chunk, usbtest_int_len());
}
tud_vendor_iso_read_xfer(); // isochronous sink
if (tud_vendor_iso_write_available()) {
tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len());
}
}
}
void usbtest_task(void* param) {
(void) param;
#if CFG_TUSB_OS == OPT_OS_FREERTOS
while (1) {
usbtest_pump();
vTaskDelay(1); // yield; tx/rx completion callbacks keep the pipes saturated between polls
}
#else
usbtest_pump(); // called from the main loop, one tick per call
#endif
}
// Invoked when received data from host: discard and immediately re-arm
void tud_vendor_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) {
(void) idx;
(void) buffer;
(void) bufsize;
tud_vendor_read_xfer();
}
// Invoked when last bulk tx transfer finished: keep the source saturated
void tud_vendor_tx_cb(uint8_t idx, uint32_t sent_bytes) {
(void) idx;
(void) sent_bytes;
tud_vendor_write(tx_chunk, sizeof(tx_chunk));
}
// Interrupt pair: same discard/refill pumps as bulk
void tud_vendor_int_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) {
(void) idx;
(void) buffer;
(void) bufsize;
tud_vendor_int_read_xfer();
}
void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes) {
(void) idx;
(void) sent_bytes;
tud_vendor_int_write(int_tx_chunk, usbtest_int_len());
}
// Isochronous pair: same discard/refill pumps; a completion may be a missed
// frame, re-arm regardless
void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) {
(void) idx;
(void) buffer;
(void) bufsize;
tud_vendor_iso_read_xfer();
}
void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes) {
(void) idx;
(void) sent_bytes;
tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len());
}
//--------------------------------------------------------------------+
// Vendor control requests (EP0)
//--------------------------------------------------------------------+
// Control write/read-back for the ctrl_out tests (14/21), same protocol as
// Gadget Zero: 0x5b stores the host's wLength bytes, 0x5c returns them.
static uint8_t ctrl_buf[1024];
// Invoked on vendor control transfers, and by usbd for forwarded standard
// endpoint requests (halt set/clear) whose return value it ignores — return
// false for anything that is not a supported vendor request.
bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) {
if (request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR) {
return false;
}
switch (request->bRequest) {
case 0x5b: // control WRITE: receive wLength bytes into ctrl_buf
TU_VERIFY(request->bmRequestType_bit.direction == TUSB_DIR_OUT);
TU_VERIFY(request->wValue == 0 && request->wIndex == 0);
TU_VERIFY(request->wLength <= sizeof(ctrl_buf));
if (stage == CONTROL_STAGE_SETUP) {
return tud_control_xfer(rhport, request, ctrl_buf, request->wLength);
}
return true; // DATA/ACK: payload already landed in ctrl_buf
case 0x5c: // control READ: send back the previously written bytes
TU_VERIFY(request->bmRequestType_bit.direction == TUSB_DIR_IN);
TU_VERIFY(request->wValue == 0 && request->wIndex == 0);
TU_VERIFY(request->wLength <= sizeof(ctrl_buf));
if (stage == CONTROL_STAGE_SETUP) {
return tud_control_xfer(rhport, request, ctrl_buf, request->wLength);
}
return true;
default:
return false;
}
}
//--------------------------------------------------------------------+
// Device callbacks
//--------------------------------------------------------------------+
// Invoked when device is mounted
void tud_mount_cb(void) {
blink_interval_ms = BLINK_MOUNTED;
}
// Invoked when device is unmounted
void tud_umount_cb(void) {
blink_interval_ms = BLINK_NOT_MOUNTED;
}
//--------------------------------------------------------------------+
// BLINKING TASK
//--------------------------------------------------------------------+
void led_blinking_task(void* param) {
(void) param;
static uint32_t start_ms = 0;
static bool led_state = false;
while (1) {
#if CFG_TUSB_OS == OPT_OS_FREERTOS
vTaskDelay(blink_interval_ms / portTICK_PERIOD_MS);
#else
// Blink every interval ms
if (tusb_time_millis_api() - start_ms < blink_interval_ms) {
return; // not enough time
}
#endif
start_ms += blink_interval_ms;
board_led_write(led_state);
led_state = 1 - led_state; // toggle
}
}
//--------------------------------------------------------------------+
// FreeRTOS
//--------------------------------------------------------------------+
#if CFG_TUSB_OS == OPT_OS_FREERTOS
#define BLINKY_STACK_SIZE configMINIMAL_STACK_SIZE
#define USBTEST_STACK_SIZE (configMINIMAL_STACK_SIZE*2)
#ifdef ESP_PLATFORM
#define USBD_STACK_SIZE 4096
int main(void);
void app_main(void) {
main();
}
#else
// Increase stack size when debug log is enabled
#define USBD_STACK_SIZE (3*configMINIMAL_STACK_SIZE/2) * (CFG_TUSB_DEBUG ? 2 : 1)
#endif
// static task allocation
#if configSUPPORT_STATIC_ALLOCATION
StackType_t blinky_stack[BLINKY_STACK_SIZE];
StaticTask_t blinky_taskdef;
StackType_t usb_device_stack[USBD_STACK_SIZE];
StaticTask_t usb_device_taskdef;
StackType_t usbtest_stack[USBTEST_STACK_SIZE];
StaticTask_t usbtest_taskdef;
#endif
// USB Device Driver task: processes all usb events and invokes callbacks
void usb_device_task(void* param) {
(void) param;
// init device stack on configured roothub port. Must be called after the
// scheduler starts: the USB IRQ handler uses RTOS queue APIs.
tusb_rhport_init_t dev_init = {
.role = TUSB_ROLE_DEVICE,
.speed = TUSB_SPEED_AUTO
};
tusb_init(BOARD_TUD_RHPORT, &dev_init);
board_init_after_tusb();
// RTOS forever loop
while (1) {
tud_task(); // put thread to waiting state until there is a new event
}
}
void freertos_init(void) {
#if configSUPPORT_STATIC_ALLOCATION
xTaskCreateStatic(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, blinky_stack, &blinky_taskdef);
xTaskCreateStatic(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES-1, usb_device_stack, &usb_device_taskdef);
xTaskCreateStatic(usbtest_task, "usbtest", USBTEST_STACK_SIZE, NULL, configMAX_PRIORITIES-2, usbtest_stack, &usbtest_taskdef);
#else
xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL);
xTaskCreate(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES-1, NULL);
xTaskCreate(usbtest_task, "usbtest", USBTEST_STACK_SIZE, NULL, configMAX_PRIORITIES-2, NULL);
#endif
// only start scheduler for non-espressif mcu (espressif starts it in startup code)
#ifndef ESP_PLATFORM
vTaskStartScheduler();
#endif
}
#endif

View File

@ -0,0 +1,151 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2026 Ha Thach (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
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#ifndef TUSB_CONFIG_H_
#define TUSB_CONFIG_H_
#ifdef __cplusplus
extern "C" {
#endif
//--------------------------------------------------------------------+
// Board Specific Configuration
//--------------------------------------------------------------------+
// RHPort number used for device can be defined by board.mk, default to port 0
#ifndef BOARD_TUD_RHPORT
#define BOARD_TUD_RHPORT 0
#endif
// RHPort max operational speed can defined by board.mk
#ifndef BOARD_TUD_MAX_SPEED
#define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED
#endif
//--------------------------------------------------------------------
// Common Configuration
//--------------------------------------------------------------------
// defined by compiler flags for flexibility
#ifndef CFG_TUSB_MCU
#error CFG_TUSB_MCU must be defined
#endif
#ifndef CFG_TUSB_OS
#define CFG_TUSB_OS OPT_OS_NONE
#endif
// Espressif IDF requires "freertos/" prefix in include path
#ifdef ESP_PLATFORM
#define CFG_TUSB_OS_INC_PATH freertos/
#endif
#ifndef CFG_TUSB_DEBUG
#define CFG_TUSB_DEBUG 0
#endif
// Enable Device stack
#define CFG_TUD_ENABLED 1
// Default is max speed that hardware controller could support with on-chip PHY
#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED
/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment.
* Tinyusb use follows macros to declare transferring memory so that they can be put
* into those specific section.
* e.g
* - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") ))
* - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4)))
*/
#ifndef CFG_TUSB_MEM_SECTION
#define CFG_TUSB_MEM_SECTION
#endif
#ifndef CFG_TUSB_MEM_ALIGN
#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4)))
#endif
//--------------------------------------------------------------------
// DEVICE CONFIGURATION
//--------------------------------------------------------------------
#ifndef CFG_TUD_ENDPOINT0_SIZE
#define CFG_TUD_ENDPOINT0_SIZE 64
#endif
//------------- CLASS -------------//
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_HID 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 1
// Non-buffered mode: every transfer is submitted with an exact length so the
// host never sees an unexpected short packet or ZLP mid-transfer, which the
// usbtest data-integrity cases treat as failure.
#define CFG_TUD_VENDOR_RX_BUFSIZE 0
#define CFG_TUD_VENDOR_TX_BUFSIZE 0
// App re-arms RX itself: required to recover the sink after halt tests
// (SET_FEATURE/CLEAR_FEATURE endpoint halt) where no completion ever fires.
#define CFG_TUD_VENDOR_RX_MANUAL_XFER 1
// Multi-packet IN transfers; must be a multiple of bulk MPS at both speeds (64/512).
// LPC11/13 (ip3511 FS) keep endpoint buffers in a dedicated 2 KB USB RAM: a 2048 B bulk epbuf
// overflows it once the int/iso buffers join, so those parts use 512 (= 8 FS packets).
#if TU_CHECK_MCU(OPT_MCU_LPC11UXX, OPT_MCU_LPC13XX)
#define CFG_TUD_VENDOR_TX_EPSIZE 512
#else
#define CFG_TUD_VENDOR_TX_EPSIZE 2048
#endif
// Interrupt IN/OUT source/sink pair (usbtest cases 25/26). Buffer sizes track the
// per-speed endpoint max packet size (see usb_descriptors.c) so full-speed builds
// don't over-allocate the scarce USB DMA section.
#define CFG_TUD_VENDOR_EP_INT_OUT 1
#define CFG_TUD_VENDOR_EP_INT_IN 1
#define CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
#define CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
// Isochronous IN/OUT source/sink pair (usbtest cases 15/16/22/23), placed in
// altsetting 1: alt 0 has no endpoints so no iso bandwidth is claimed by default
#define CFG_TUD_VENDOR_EP_ISO_OUT 1
#define CFG_TUD_VENDOR_EP_ISO_IN 1
#define CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 128)
#define CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 128)
#define CFG_TUD_VENDOR_ALT_SETTINGS 1
// CH32V20X fsdev port has only 512 B PMA and single-buffered iso can't keep the iso IN endpoint
// fed under load. Double-buffer iso; the descriptor drops iso mps to 32 there so 2x32 = 64 B/ep
// keeps the same PMA budget (see usb_descriptors.h). Other fsdev parts have room and stay single.
#if CFG_TUSB_MCU == OPT_MCU_CH32V20X
#define CFG_TUD_FSDEV_DOUBLE_BUFFERED_ISO_EP 1
#endif
#ifdef __cplusplus
}
#endif
#endif /* TUSB_CONFIG_H_ */

View File

@ -0,0 +1,271 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2026 Ha Thach (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
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#include "bsp/board_api.h"
#include "tusb.h"
#include "usb_descriptors.h"
//--------------------------------------------------------------------+
// Device Descriptors
//--------------------------------------------------------------------+
static tusb_desc_device_t const desc_device = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = 0x00, // per-interface class
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0xCafe,
.idProduct = 0x4010,
.bcdDevice = 0x0100 | USBTEST_TIER,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01
};
// Invoked when received GET DEVICE DESCRIPTOR
uint8_t const* tud_descriptor_device_cb(void) {
return (uint8_t const*) &desc_device;
}
//--------------------------------------------------------------------+
// Configuration Descriptor
//--------------------------------------------------------------------+
// Interface must be number 0: testusb -D issues its ioctls against interface 0
enum {
ITF_NUM_VENDOR = 0,
ITF_NUM_TOTAL
};
// Vendor interface, Gadget-Zero style altsettings: alt 0 carries no endpoints (an
// isochronous endpoint must not claim bandwidth in the default altsetting, USB 2.0
// 5.6.3), alt 1 carries bulk + interrupt + isochronous IN/OUT. The host usbtest
// driver skips altsettings without pipes and selects alt 1 itself. No TUD_ macro
// covers this layout, hand-rolled.
#define USBTEST_DESC_LEN (9 + 9 + 6*7)
#define USBTEST_DESCRIPTOR(_itfnum, _stridx, _epout, _epin, _bulk_mps, _intout, _intin, _int_mps, _int_interval, _isoout, _isoin, _iso_mps, _iso_interval) \
/* alt 0: zero bandwidth, no endpoints */\
9, TUSB_DESC_INTERFACE, _itfnum, 0, 0, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\
/* alt 1: full source/sink set */\
9, TUSB_DESC_INTERFACE, _itfnum, 1, 6, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\
7, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_bulk_mps), 0,\
7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_bulk_mps), 0,\
7, TUSB_DESC_ENDPOINT, _intout, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval,\
7, TUSB_DESC_ENDPOINT, _intin, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval,\
7, TUSB_DESC_ENDPOINT, _isoout, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval,\
7, TUSB_DESC_ENDPOINT, _isoin, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + USBTEST_DESC_LEN)
#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX
// LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number
// 0 control, 1 Interrupt, 2 Bulk, 3 Iso, 4 Interrupt etc ...
#define EPNUM_BULK_OUT 0x02
#define EPNUM_BULK_IN 0x85
#define EPNUM_INT_OUT 0x01
#define EPNUM_INT_IN 0x84
#define EPNUM_ISO_OUT 0x03
#define EPNUM_ISO_IN 0x86
#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY
// MCUs that don't support a same endpoint number with different direction IN and OUT
// e.g EP1 OUT & EP1 IN cannot exist together
#if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002)
// Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering
#define EPNUM_BULK_OUT 0x08
#define EPNUM_BULK_IN 0x89
#define EPNUM_INT_OUT 0x02
#define EPNUM_INT_IN 0x83
#define EPNUM_ISO_OUT 0x04
#define EPNUM_ISO_IN 0x85
#else
#define EPNUM_BULK_OUT 0x01
#define EPNUM_BULK_IN 0x82
#define EPNUM_INT_OUT 0x03
#define EPNUM_INT_IN 0x84
#define EPNUM_ISO_OUT 0x05
#define EPNUM_ISO_IN 0x86
#endif
#elif CFG_TUSB_MCU == OPT_MCU_NRF5X
// nRF5x: ISO endpoints are hardware-fixed to EP8 (ISOOUT/ISOIN)
#define EPNUM_BULK_OUT 0x01
#define EPNUM_BULK_IN 0x81
#define EPNUM_INT_OUT 0x02
#define EPNUM_INT_IN 0x82
#define EPNUM_ISO_OUT 0x08
#define EPNUM_ISO_IN 0x88
#else
#define EPNUM_BULK_OUT 0x01
#define EPNUM_BULK_IN 0x81
#define EPNUM_INT_OUT 0x02
#define EPNUM_INT_IN 0x82
#define EPNUM_ISO_OUT 0x03
#define EPNUM_ISO_IN 0x83
#endif
static uint8_t const desc_fs_configuration[] = {
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),
// Interface number, string index, bulk out/in + mps, int out/in + mps + interval, iso out/in + mps + interval
USBTEST_DESCRIPTOR(ITF_NUM_VENDOR, 4, EPNUM_BULK_OUT, 0x80 | EPNUM_BULK_IN, 64,
EPNUM_INT_OUT, 0x80 | EPNUM_INT_IN, USBTEST_INT_EP_MPS_FS, 1,
EPNUM_ISO_OUT, 0x80 | EPNUM_ISO_IN, USBTEST_ISO_EP_MPS_FS, 1)
};
#if TUD_OPT_HIGH_SPEED
// Per USB specs: high speed capable device must report device_qualifier and other_speed_configuration
static uint8_t const desc_hs_configuration[] = {
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),
// Interface number, string index, bulk out/in + mps, int out/in + mps + interval, iso out/in + mps + interval (1 ms)
USBTEST_DESCRIPTOR(ITF_NUM_VENDOR, 4, EPNUM_BULK_OUT, 0x80 | EPNUM_BULK_IN, 512,
EPNUM_INT_OUT, 0x80 | EPNUM_INT_IN, USBTEST_INT_EP_MPS_HS, 4,
EPNUM_ISO_OUT, 0x80 | EPNUM_ISO_IN, USBTEST_ISO_EP_MPS_HS, 4)
};
// other speed configuration
static uint8_t desc_other_speed_config[CONFIG_TOTAL_LEN];
// device qualifier is mostly similar to device descriptor since we don't change configuration based on speed
static tusb_desc_device_qualifier_t const desc_device_qualifier = {
.bLength = sizeof(tusb_desc_device_qualifier_t),
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
.bcdUSB = 0x0200,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.bNumConfigurations = 0x01,
.bReserved = 0x00
};
// Invoked when received GET DEVICE QUALIFIER DESCRIPTOR request
uint8_t const* tud_descriptor_device_qualifier_cb(void) {
return (uint8_t const*) &desc_device_qualifier;
}
// Invoked when received GET OTHER SPEED CONFIGURATION DESCRIPTOR request
// Configuration descriptor in the other speed e.g if high speed then this is for full speed and vice versa
uint8_t const* tud_descriptor_other_speed_configuration_cb(uint8_t index) {
(void) index; // for multiple configurations
// if link speed is high return fullspeed config, and vice versa
// Note: the descriptor type is OTHER_SPEED_CONFIG instead of CONFIG
memcpy(desc_other_speed_config,
(tud_speed_get() == TUSB_SPEED_HIGH) ? desc_fs_configuration : desc_hs_configuration,
CONFIG_TOTAL_LEN);
desc_other_speed_config[1] = TUSB_DESC_OTHER_SPEED_CONFIG;
return desc_other_speed_config;
}
#endif // highspeed
// Invoked when received GET CONFIGURATION DESCRIPTOR
uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
(void) index; // for multiple configurations
#if TUD_OPT_HIGH_SPEED
// Although we are highspeed, host may be fullspeed.
return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration;
#else
return desc_fs_configuration;
#endif
}
//--------------------------------------------------------------------+
// String Descriptors
//--------------------------------------------------------------------+
// String Descriptor Index
enum {
STRID_LANGID = 0,
STRID_MANUFACTURER,
STRID_PRODUCT,
STRID_SERIAL,
};
// array of pointer to string descriptors
static char const* string_desc_arr[] = {
(const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409)
"TinyUSB", // 1: Manufacturer
"TinyUSB usbtest", // 2: Product
NULL, // 3: Serials will use unique ID if possible
"TinyUSB usbtest source/sink" // 4: Vendor Interface
};
static uint16_t _desc_str[32 + 1];
// Invoked when received GET STRING DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
(void) langid;
size_t chr_count;
switch (index) {
case STRID_LANGID:
memcpy(&_desc_str[1], string_desc_arr[0], 2);
chr_count = 1;
break;
case STRID_SERIAL:
chr_count = board_usb_get_serial(_desc_str + 1, 32);
break;
default:
if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) return NULL;
const char* str = string_desc_arr[index];
// Cap at max char
chr_count = strlen(str);
size_t const max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1; // -1 for string type
if (chr_count > max_count) chr_count = max_count;
// Convert ASCII string into UTF-16
for (size_t i = 0; i < chr_count; i++) {
_desc_str[1 + i] = str[i];
}
break;
}
// first byte is length (including header), second byte is string type
_desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
return _desc_str;
}

View File

@ -0,0 +1,69 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2026 Ha Thach (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
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#ifndef USB_DESCRIPTORS_H_
#define USB_DESCRIPTORS_H_
// Device-capability tier advertised in bcdDevice low byte (0x01TT), read by the
// host script to select which usbtest cases to run:
// 1: bulk source/sink
// 2: + vendor control 0x5b/0x5c (ctrl_out)
// 3: + interrupt source/sink
// 4: + isochronous source/sink
#define USBTEST_TIER 4
// Interrupt/isochronous endpoint max packet sizes, must match the configuration descriptor.
// TUD_OPT_HIGH_SPEED is a compile-time capability flag, NOT the live bus speed, so the full-speed
// config descriptor (and the OTHER_SPEED descriptor served to a HS host) must use full-speed-legal
// sizes regardless of it: interrupt <= 64 B, isochronous <= 1023 B (and both iso EPs must fit the
// 1023 B/frame FS periodic budget). Hence separate _FS / _HS descriptor sizes; the plain macro
// below is the compile-time capability maximum that sizes the source buffers (runtime write
// lengths follow the negotiated speed via tud_speed_get(), see main.c).
//
// The CH32 USB IPs have tiny per-endpoint buffers so tier-4's six endpoints don't fit at the usual
// FS sizes: usbfs gives 64 B/ep (iso must drop to 64), and the CH32V20X fsdev port shares one 512 B
// PMA across every endpoint (needs iso 32 AND a small interrupt mps to fit alongside EP0+bulk+iso).
#if CFG_TUSB_MCU == OPT_MCU_CH32V20X && defined(CFG_TUD_WCH_USBIP_FSDEV) && CFG_TUD_WCH_USBIP_FSDEV
#define USBTEST_INT_EP_MPS_FS 16
#define USBTEST_ISO_EP_MPS_FS 32 // double-buffered on fsdev: 2x32=64/ep, same 512 B PMA budget
#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V103, OPT_MCU_CH32F20X, OPT_MCU_CH32V307, OPT_MCU_CH583)
// WCH USBFS parts cap every endpoint (except EP3 IN) at 64 B. For the CH32V307 this applies to its
// full-speed (usbfs) port; its high-speed (usbhs) port uses the _HS sizes below via desc_hs.
#define USBTEST_INT_EP_MPS_FS 64
#define USBTEST_ISO_EP_MPS_FS 64
#else
#define USBTEST_INT_EP_MPS_FS 64
#define USBTEST_ISO_EP_MPS_FS 128
#endif
#define USBTEST_INT_EP_MPS_HS 512
#define USBTEST_ISO_EP_MPS_HS 512
// Compile-time capability maximum: sizes the source buffers / vendor epbufs for the largest
// packet the build can negotiate. Runtime write lengths follow tud_speed_get() (see main.c) —
// a high-speed build enumerated at full speed submits the _FS lengths.
#define USBTEST_INT_EP_MPS (TUD_OPT_HIGH_SPEED ? USBTEST_INT_EP_MPS_HS : USBTEST_INT_EP_MPS_FS)
#define USBTEST_ISO_EP_MPS (TUD_OPT_HIGH_SPEED ? USBTEST_ISO_EP_MPS_HS : USBTEST_ISO_EP_MPS_FS)
#endif /* USB_DESCRIPTORS_H_ */