| /* |
| * 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]; |
| #if USBTEST_TIER >= 4 |
| static uint8_t const iso_tx_chunk[USBTEST_ISO_EP_MPS]; |
| #endif |
| |
| // 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; |
| } |
| #if USBTEST_TIER >= 4 |
| 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; |
| } |
| #endif |
| |
| //------------- 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()); |
| } |
| |
| #if USBTEST_TIER >= 4 |
| tud_vendor_iso_read_xfer(); // isochronous sink |
| if (tud_vendor_iso_write_available()) { |
| tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len()); |
| } |
| #endif |
| } |
| } |
| |
| 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 |
| #if USBTEST_TIER >= 4 |
| 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()); |
| } |
| #endif |
| |
| //--------------------------------------------------------------------+ |
| // 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 |