- 嵌入式
- 驱动开发
- 通信
- 物联网
【免费下载链接】tinyusb
An open source cross-platform USB stack for embedded system
本指南以 TinyUSB 仓库中的dynamic_switch双角色示例(examples/dual/dynamic_switch/README.md)为核心,深入讲解如何在一块同时具备设备(Device)与主机(Host)能力的 USB 控制器上,通过按键在两种模式之间运行时来回切换:设备模式下板卡作为 CDC 虚拟串口回显字符,主机模式下板卡枚举接入的 USB 设备并打印其描述符信息。读完本文,你将掌握 TinyUSB 新一代tusb_init(rhport, rh_init)/tusb_deinit(rhport)双角色 API 的使用方式、模式切换的状态机设计,以及无 RTOS 与 FreeRTOS 两种环境下的任务组织方法。
示例概览:TinyUSB 的双角色(Dual-Role)能力
TinyUSB 是一个开源、跨平台的嵌入式 USB 协议栈,设备(Device)栈与主机(Host)栈可以同时编译进同一个固件。传统的 USB 应用只使用其中一个角色,而本示例演示的是 TinyUSB 的**双角色(dual-role)**能力——在运行时通过tusb_init()/tusb_deinit()让同一个根端口(roothub port)在 USB 设备与 USB 主机两种角色之间动态切换:
- 作为USB 设备(Device):板卡以 CDC 类(虚拟串口)枚举到 PC,回显终端输入的所有字符;
- 作为USB 主机(Host):板卡枚举接入的 USB 设备(如鼠标、键盘、U 盘),并把设备的厂商 ID、产品 ID、序列号以及完整设备描述符打印到调试串口;
- 两种模式通过板载按键一键切换,切换过程会先反初始化当前栈,再以新角色重新初始化,全程无需复位 MCU。
该示例属于examples/dual/目录下的三个双角色示例之一(examples/dual/CMakeLists.txt 中与host_hid_to_device_cdc、host_info_to_device_cdc并列),与另外两个"常驻双角色"示例不同,dynamic_switch强调的是单一时刻只运行一个角色、且角色可动态互换。
功能特性一览
按原文档整理,本示例的核心特性包括:
- 按键触发模式切换:按下板载按键即可在设备模式与主机模式之间来回切换;
- 设备模式(Device Mode):作为 USB CDC(虚拟串口)工作,回显所有收到的数据;
- 主机模式(Host Mode):枚举接入的 USB 设备并打印设备信息(VID/PID、序列号、描述符等);
- 动态切换(Dynamic switching):先反初始化当前栈,再以新模式重新初始化。
工程结构与构建方式
示例的完整工程位于examples/dual/dynamic_switch/,结构如下:
src/main.c:主循环、模式切换逻辑、CDC 回显任务、主机设备信息打印任务、LED 闪烁任务;src/tusb_config.h:TinyUSB 栈与类驱动的编译期配置;src/usb_descriptors.c:设备/配置/字符串描述符;Makefile与CMakeLists.txt:传统 make 与 CMake 两套构建入口;only.txt:该示例适用的 MCU 系列白名单。
从 Makefile 可以看到,它同时拉入了设备栈的src/class/cdc/cdc_device.c与主机栈的src/host/hub.c、src/host/usbh.c,即设备与主机两套栈代码在同一固件中并存,这正是双角色切换的编译前提。CMake 侧则通过family_configure_dual_usb_example(${PROJECT_NAME} noos)(CMakeLists.txt)按 no-OS 配置初始化工程。
only.txt 列出的支持系列包括:espressif、LPC43XX、MIMXRT1XXX、STM32C0、STM32C5、STM32G0、STM32H5、STM32F2、STM32F4、STM32U5、STM32F7、STM32H7、STM32H7RS。构建前请确认目标板卡属于上述系列之一,且硬件上具备可供 Host 模式使用的 USB 控制器(通常为 OTG 类双模式控制器)。
配置解析:tusb_config.h
原文档列出了 4 个关键配置项,结合 src/tusb_config.h 完整解读如下:
#define CFG_TUD_CDC 1 #define CFG_TUH_HUB 1 #define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) #define CFG_TUH_ENUMERATION_BUFSIZE 256各配置项的深层含义:
CFG_TUD_CDC 1:启用设备端 CDC 类驱动,这是设备模式下虚拟串口回显功能的支撑。同段配置中CFG_TUD_MSC / CFG_TUD_HID / CFG_TUD_MIDI / CFG_TUD_VENDOR均被置 0,即设备侧只保留 CDC 一个类(src/tusb_config.h);CFG_TUH_HUB 1:启用主机端 Hub 驱动。注意启用后CFG_TUH_DEVICE_MAX会按"hub 通常有 4 个端口"的经验值扩展到 4(不含 hub 设备本身),即最多可同时接入 4 个下游设备(src/tusb_config.h);CFG_TUH_ENUMERATION_BUFSIZE 256:主机枚举阶段用于暂存描述符等数据的缓冲区大小,256 字节足以容纳典型设备在枚举期需要读取的设备/配置描述符数据(src/tusb_config.h);- 主机侧其余类(
CFG_TUH_CDC / HID / MSC / VENDOR)均置 0(src/tusb_config.h),因为本示例在主机模式下只做"枚举 + 读描述符",不挂载具体类驱动,CFG_TUH_ENDPOINT_MAX 16则限制了每个设备可打开的最大端点对数量。
文件开头的公共与双栈配置同样值得注意(src/tusb_config.h):
// Enable Device and Host stacks (dual role) #define CFG_TUD_ENABLED 1 #define CFG_TUH_ENABLED 1 #define CFG_TUD_MAX_SPEED BOARD_MAX_SPEED #define CFG_TUH_MAX_SPEED BOARD_MAX_SPEEDCFG_TUD_ENABLED与CFG_TUH_ENABLED同时为 1 是双角色模式切换的配置前提;BOARD_MAX_SPEED默认取硬件控制器配合片内 PHY 能支持的最大速度(OPT_MODE_DEFAULT_SPEED)。CDC 缓冲配置CFG_TUD_CDC_RX_BUFSIZE / TX_BUFSIZE与端点传输缓冲CFG_TUD_CDC_RX_EPSIZE / TX_EPSIZE均按TUD_OPT_HIGH_SPEED在三态(HS 512 / FS 64)之间选择(src/tusb_config.h)。
核心原理:运行时模式切换 API 与调用链
新一代双角色初始化 API
TinyUSB 为双角色场景引入了显式指定角色与速度的初始化结构。角色与速度枚举定义在 src/common/tusb_types.h:
typedef enum { TUSB_ROLE_INVALID = 0u, TUSB_ROLE_DEVICE = 0x1, TUSB_ROLE_HOST = 0x2, } tusb_role_t; typedef enum { TUSB_SPEED_FULL = 0, TUSB_SPEED_LOW = 1, TUSB_SPEED_HIGH = 2, TUSB_SPEED_AUTO = 0xaa, TUSB_SPEED_INVALID = 0xff, } tusb_speed_t;初始化参数结构tusb_rhport_init_t只有两个字段(src/common/tusb_types.h):
typedef struct { tusb_role_t role; // 希望该根端口扮演的角色:TUSB_ROLE_DEVICE 或 TUSB_ROLE_HOST tusb_speed_t speed; // 期望链路速度,示例统一使用 TUSB_SPEED_AUTO } tusb_rhport_init_t;顶层入口tusb_init(...)是支持 0~2 个参数的宏(src/tusb.h):传(void)时为向后兼容的旧式双栈同时初始化,传(rhport, rh_init)时则为显式单角色初始化。真正实现位于 src/tusb.c:它会先把角色写入全局数组_tusb_rhport_role[rhport],再按角色分发到tud_rhport_init()(设备栈)或tuh_rhport_init()(主机栈),二者向下分别调用 DCD 的dcd_init()与 HCD 的hcd_init()(如 src/host/hcd.h 所声明)。配套的tusb_deinit(rhport)同样依据_tusb_rhport_role决定反初始化设备还是主机栈,并把角色置回TUSB_ROLE_INVALID(src/tusb.c)。中断分发tusb_int_handler()也依赖该角色数组把 IRQ 路由给dcd_int_handler()或hcd_int_handler()(src/tusb.c)。
注意:当使用 RTOS 时,
tusb_init()应在调度器启动之后调用,因为 USB 中断处理会使用 RTOS 队列 API(src/tusb.h)。
usb_mode_switch():切换状态机
模式切换的核心实现在 src/main.c,其流程为:
- 快照并清空角色:先把
prev_role = current_role,随即把current_role置为TUSB_ROLE_INVALID。这一步是关键的状态边界——在 RTOS 环境下,并发的cdc_task/print_devinfo_task看到 INVALID 后就不会再调用已反初始化的设备/主机栈 API; - 反初始化当前栈:
tusb_deinit(BOARD_RHPORT); - 切换缓冲:FreeRTOS 下
vTaskDelay(pdMS_TO_TICKS(100)),无 OS 下tusb_time_delay_ms_api(100),留出干净的切换过渡时间; - 以另一角色重新初始化:构造新的
tusb_rhport_init_t(角色取反、速度TUSB_SPEED_AUTO),调用tusb_init(BOARD_RHPORT, &init),并更新current_role; - 复位 LED 闪烁周期为"未连接"档,打印
Mode switch complete!。
核心代码结构如下(节选自 src/main.c):
const tusb_role_t prev_role = current_role; current_role = TUSB_ROLE_INVALID; // 让并发任务看到切换边界 tusb_deinit(BOARD_RHPORT); // ... 100ms 延迟 ... if (prev_role == TUSB_ROLE_DEVICE) { tusb_rhport_init_t host_init = { .role = TUSB_ROLE_HOST, .speed = TUSB_SPEED_AUTO }; tusb_init(BOARD_RHPORT, &host_init); current_role = TUSB_ROLE_HOST; } else { tusb_rhport_init_t dev_init = { .role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_AUTO }; tusb_init(BOARD_RHPORT, &dev_init); current_role = TUSB_ROLE_DEVICE; }启动路径与默认角色
无 RTOS 时,main()默认以设备角色启动(src/main.c):
tusb_rhport_init_t dev_init = { .role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_AUTO }; tusb_init(BOARD_RHPORT, &dev_init); current_role = TUSB_ROLE_DEVICE; board_init_after_tusb();BOARD_RHPORT默认取 0,可由board.mk覆盖(src/tusb_config.h)。board_init_after_tusb()由 BSP 提供,用于在 USB 栈初始化后再完成板级初始化(hw/bsp/board_api.h)。
主循环与任务划分:无 OS 与 FreeRTOS 两套运行模型
示例同时支持CFG_TUSB_OS == OPT_OS_NONE与OPT_OS_FREERTOS两种模型。
无 OS 模型(src/main.c):main()的while(1)中先用board_button_read()检测按键——按键持续按住时只触发一次切换(pending_switch标志去抖),然后按当前角色分发任务:
if (current_role == TUSB_ROLE_DEVICE) { tud_task(); // 处理设备栈事件 cdc_task(NULL); // CDC 回显 } else { tuh_task(); // 处理主机栈事件 print_devinfo_task(NULL); // 打印设备信息 } led_blinking_task(NULL);FreeRTOS 模型(src/main.c):创建 4 个任务——blinky(LED 闪烁)、usb(最高优先级,处理 USB 事件与模式切换)、cdc(CDC 回显)、devinfo(设备信息打印),支持动态与静态分配(configSUPPORT_STATIC_ALLOCATION)。其中usb_task在调度器启动后调用tusb_init(),并改用tud_task_ext(10, false)/tuh_task_ext(10, false)以便带超时返回、及时读取按键状态(src/main.c)。Espressif 平台则通过app_main()入口并在main()中跳过vTaskStartScheduler()(src/main.c)。
设备模式:CDC 虚拟串口回显
cdc_task()只在current_role == TUSB_ROLE_DEVICE时触碰设备端 CDC API(src/main.c):用tud_cdc_available()/tud_cdc_read()读取 PC 发来的数据,再以tud_cdc_write()原样写回,最后tud_cdc_write_flush()冲刷发送缓冲——即"回显所有收到的数据"。在 FreeRTOS 下该任务每 10ms 轮询一次。
配套的设备描述符定义在 src/usb_descriptors.c:VID 0xCafe、PID 0x4020(每个示例使用唯一 PID,保证重新烧录后触发重新枚举与主机驱动重新匹配,见 src/usb_descriptors.c)、bcdUSB 0x0200、设备类为TUSB_CLASS_MISC + MISC_SUBCLASS_COMMON + MISC_PROTOCOL_IAD,配置描述符由TUD_CONFIG_DESCRIPTOR+TUD_CDC_DESCRIPTOR组成并带远程唤醒属性(src/usb_descriptors.c)。序列号通过board_usb_get_serial()从芯片唯一 ID 生成(hw/bsp/board_api.h)。
设备事件回调(src/main.c)负责维护 LED 状态并打印状态信息:
tud_mount_cb():挂载成功 → 打印[DEVICE] Mounted,LED 切到"已挂载"档;tud_umount_cb():拔出 → 打印[DEVICE] Unmounted,LED 回到"未挂载"档;tud_suspend_cb()/tud_resume_cb():总线挂起/恢复,挂起时 LED 切到最慢档。
主机模式:枚举设备并打印描述符
主机侧的事件回调(src/main.c):
tuh_mount_cb(daddr):设备枚举完成(configured)后触发,打印[HOST] Device attached, address = %d,并把对应地址的need_devinfo[daddr]标志置位;tuh_umount_cb(daddr):设备拔出,打印移除消息并清标志。
注意:回调运行在主机任务上下文,要求保持极简——真正读取描述符的工作放在print_devinfo_task()中完成(这也是"不要在主机栈回调里做同步描述符读取"的典型工程实践,源码注释明确指出 sync 辅助函数应避免在回调内使用)。print_devinfo_task()扫描need_devinfo[]标志数组(其大小为CFG_TUH_DEVICE_MAX + 1,地址从 1 开始),对每个待打印设备调用print_one_device()(src/main.c)。
print_one_device()(src/main.c)使用的都是主机栈的同步描述符 API,定义于 src/host/usbh.h:
tuh_descriptor_get_device_sync(daddr, &desc.device, 18):读取 18 字节设备描述符;tuh_descriptor_get_serial_string_sync(daddr, LANGUAGE_ID, ...):读取序列号字符串(LANGUAGE_ID = 0x0409即英语);tuh_descriptor_get_manufacturer_string_sync()/tuh_descriptor_get_product_string_sync():厂商与产品字符串。
打印内容覆盖设备描述符全部标准字段(bLength、bDescriptorType、bcdUSB、bDeviceClass/SubClass/Protocol、bMaxPacketSize0、idVendor/idProduct、bcdDevice、iManufacturer/iProduct/iSerialNumber、bNumConfigurations),序列号读取失败时(如设备未提供)回退输出n/a。字符串以 UTF-16 存储,print_utf16()负责转成 ASCII 打印(非 ASCII 字符暂以?代替,源码中有 TODO 标注,见 src/main.c)。为 DMA 访问安全,描述符缓冲通过CFG_TUH_MEM_SECTION声明(src/main.c)。
LED 指示模式
板载 LED 反映 USB 连接状态,由led_blinking_task()以blink_interval_ms为周期翻转(src/main.c):
| 闪烁周期 | 含义 |
|---|---|
| 快速闪烁(250ms) | 未挂载 / 未连接 |
| 慢速闪烁(1000ms) | 成功挂载 / 连接 |
| 极慢闪烁(2500ms) | 总线挂起(仅设备模式) |
对应枚举常量定义在 src/main.c。模式切换完成后会先把闪烁周期复位为 250ms(未挂载档),等待新角色下的挂载/连接事件回调更新。
串口输出示例
示例通过调试 UART 打印状态信息,原文档给出的典型输出如下(注意[HOST] Device attached后是print_devinfo_task异步打印的 VID/PID、序列号与设备描述符):
====================================== TinyUSB Dynamic Switch Example Press button to switch between device and host modes Starting in DEVICE mode... ====================================== [DEVICE] Mounted --- Switching USB mode --- Stopping DEVICE mode... Starting HOST mode... Mode switch complete! [HOST] Device attached, address = 1 Device 1: ID 1234:5678 SN ABC123 Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 0200 bDeviceClass 239 ...使用步骤
按原文档整理,完整操作流程如下:
- 构建并烧录:将示例编译后烧录到你的板卡(构建前请确认板卡系列在 only.txt 白名单内,且 USB 控制器支持双角色);
- 默认行为:板卡上电后以设备模式启动;
- 设备模式操作:
- 将板卡连接到 PC;
- 打开串口终端(例如
screen /dev/ttyACM0或 PuTTY); - 输入字符——它们会被原样回显给你;
- 切换到主机模式:
- 按下板载按键;
- 将 USB 设备接到板卡上;
- 板卡会枚举该设备,并在调试控制台打印其描述符信息(地址、VID:PID、序列号、设备描述符等);
- 切回设备模式:再次按下按键即可。
小结与工程启示
dynamic_switch示例为"单控制器、单时刻单角色、按键动态互换"给出了完整的参考实现,几个可直接复用的工程要点:
- 双栈共存:
CFG_TUD_ENABLED与CFG_TUH_ENABLED同时开启,设备类与主机类驱动按需裁剪(本示例设备侧仅 CDC、主机侧仅 Hub + 枚举); - 角色状态机:用
tusb_role_t全局变量跟踪当前角色,切换前先置TUSB_ROLE_INVALID作为并发任务的安全边界,再tusb_deinit()+ 100ms 缓冲 + 反向角色tusb_init(); - 上下文纪律:主机/设备回调保持极简,描述符同步读取下沉到专用任务;FreeRTOS 下使用
_ext版本任务 API 保证超时返回以响应按键; - 状态可视化:以 LED 三档闪烁周期区分未挂载 / 已挂载 / 挂起,是嵌入式调试 USB 状态的低成本手段。
若需在此基础上扩展,可从 src/main.c 出发,将 CDC 回显替换为任意设备类逻辑、把描述符打印升级为真正的类驱动(如 MSC/HID 挂载),或结合 TinyUSB 的 Type-C / 电源管理能力实现更复杂的角色协商方案。
- 嵌入式
- 驱动开发
- 通信
- 物联网
【免费下载链接】tinyusb
An open source cross-platform USB stack for embedded system
相关推荐
ESP32 USB OTG 动态切换 Host/Device 模式实战:CDC 设备与 MSC 主机手动切换(esp-iot-solution)
ESP32 USB OTG 动态切换 Host/Device 模式实战:CDC 设备与 MSC 主机手动切换(esp iot solution) 本文基于 es
物联网嵌入式驱动开发硬件开发lottie-web渲染模式切换:运行时动态切换SVG/Canvas
lottie web渲染模式切换:运行时动态切换SVG/Canvas 引言:为什么需要动态切换渲染模式? 你是否曾遇到过这样的困境:开发复杂动画时希望使用SVG
前端图形学pyvideotrans语音合成角色切换:动态切换不同音色
pyvideotrans语音合成角色切换:动态切换不同音色 pyvideotrans 是一款强大的视频翻译和配音工具,它不仅能将视频从一种语言翻译成另一种语言,
音视频AI 应用语音本地部署
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考