TinyUSB 移植到 STM32Cube MX生产的STM32F407 代码上(Keil MDK / ARMCC V5)
基于 STM32F407VET6 + TinyUSB,在 Keil MDK (ARMCC V5) 环境下实现 USB 复合设备:HID + MIDI + Audio (UAC1)。
目录
- [TinyUSB 移植到 STM32Cube MX生产的STM32F407 代码上(Keil MDK / ARMCC V5)](#TinyUSB 移植到 STM32Cube MX生产的STM32F407 代码上(Keil MDK / ARMCC V5))
- 目录
- [1. 概述](#1. 概述)
- [2. 硬件与开发环境](#2. 硬件与开发环境)
- [3. STM32CubeMX 工程配置](#3. STM32CubeMX 工程配置)
- [3.1 芯片选择](#3.1 芯片选择)
- [3.2 RCC 时钟源配置](#3.2 RCC 时钟源配置)
- [3.3 时钟树配置(关键)](#3.3 时钟树配置(关键))
- [3.4 SYS 配置](#3.4 SYS 配置)
- [3.5 GPIO 引脚配置](#3.5 GPIO 引脚配置)
- [USB 引脚(PA11 / PA12)](#USB 引脚(PA11 / PA12))
- 其他引脚(按需配置)
- [3.6 USART1 配置(调试串口)](#3.6 USART1 配置(调试串口))
- [3.7 I2S2 配置(音频接口,可选)](#3.7 I2S2 配置(音频接口,可选))
- [3.8 NVIC 中断配置](#3.8 NVIC 中断配置)
- [3.9 工程生成设置](#3.9 工程生成设置)
- [3.10 CubeMX 配置要点总结](#3.10 CubeMX 配置要点总结)
- [4. 工程目录结构](#4. 工程目录结构)
- [5. 移植步骤](#5. 移植步骤)
- [5.1 获取 TinyUSB 源码](#5.1 获取 TinyUSB 源码)
- [5.2 参考 cdc_msc 示例拷贝应用文件](#5.2 参考 cdc_msc 示例拷贝应用文件)
- [5.2.1 cdc_msc 示例的文件结构](#5.2.1 cdc_msc 示例的文件结构)
- [5.2.2 拷贝与改造步骤](#5.2.2 拷贝与改造步骤)
- [5.2.3 对拷贝文件的关键修改](#5.2.3 对拷贝文件的关键修改)
- [5.2.4 最终文件对应关系总结](#5.2.4 最终文件对应关系总结)
- [5.3 添加源文件到 Keil 工程](#5.3 添加源文件到 Keil 工程)
- [5.4 配置头文件路径](#5.4 配置头文件路径)
- [5.5 添加全局宏定义](#5.5 添加全局宏定义)
- [5.6 解决 ARMCC V5 编译兼容性问题](#5.6 解决 ARMCC V5 编译兼容性问题)
- [问题 1:
__has_attribute不支持](#问题 1:__has_attribute 不支持) - [问题 2:
__builtin_bswap16未定义](#问题 2:__builtin_bswap16 未定义)
- [问题 1:
- [6. 驱动层适配](#6. 驱动层适配)
- [6.1 必须实现的函数一览](#6.1 必须实现的函数一览)
- [6.2 关键注意事项](#6.2 关键注意事项)
- [7. TinyUSB 配置 (tusb_config.h)](#7. TinyUSB 配置 (tusb_config.h))
- [Class 组合灵活性](#Class 组合灵活性)
- [8. USB 描述符](#8. USB 描述符)
- [8.1 描述符头文件 (usb_descriptors.h)](#8.1 描述符头文件 (usb_descriptors.h))
- [8.2 端点分配策略](#8.2 端点分配策略)
- [8.3 设备描述符](#8.3 设备描述符)
- [8.4 配置描述符](#8.4 配置描述符)
- [8.5 字符串描述符](#8.5 字符串描述符)
- [9. 各 Class 应用层实现](#9. 各 Class 应用层实现)
- [9.1 CDC 虚拟串口](#9.1 CDC 虚拟串口)
- [9.2 MSC 大容量存储](#9.2 MSC 大容量存储)
- [9.3 MIDI 设备](#9.3 MIDI 设备)
- [9.4 HID 复合设备](#9.4 HID 复合设备)
- [9.5 Audio 音频设备 (UAC1)](#9.5 Audio 音频设备 (UAC1))
- 描述符拓扑
- 关键配置选择
- 数据流处理(中断驱动)
- [UAC1 控制请求](#UAC1 控制请求)
- [9.6 Vendor 自定义设备](#9.6 Vendor 自定义设备)
- [10. 主循环集成(初始化与任务调度)](#10. 主循环集成(初始化与任务调度))
- [10.1 头文件引入](#10.1 头文件引入)
- [10.2 初始化调用顺序](#10.2 初始化调用顺序)
- [10.3 主循环任务调度](#10.3 主循环任务调度)
- [10.4 回调函数的调用机制](#10.4 回调函数的调用机制)
- [10.5 完整 main.c 代码](#10.5 完整 main.c 代码)
- [11. STM32 各系列 USB 端点数量参考](#11. STM32 各系列 USB 端点数量参考)
- [12. 常见问题与解决](#12. 常见问题与解决)
- [Q1: 编译报错
__has_attribute](#Q1: 编译报错 __has_attribute) - [Q2: USB 设备识别不到](#Q2: USB 设备识别不到)
- [Q3: Audio 设备显示"代码 10"](#Q3: Audio 设备显示"代码 10")
- [Q4: MSC 单独使用时识别不到](#Q4: MSC 单独使用时识别不到)
- [Q5: 切换 Class 组合后设备异常](#Q5: 切换 Class 组合后设备异常)
- [Q6: Vendor 设备显示"代码 28"](#Q6: Vendor 设备显示"代码 28")
- [Q7:
#define CONFIG_TOTAL_LEN中不能使用#if](#define CONFIG_TOTAL_LEN 中不能使用 #if)
- [Q1: 编译报错
- [13. 参考资源](#13. 参考资源)
- [14. 完整工程源码](#14. 完整工程源码)
1. 概述
TinyUSB 是一个开源的跨平台 USB 协议栈,支持 Device 和 Host 模式,具有以下特点:
- 纯 C 编写,无外部依赖
- 支持 CDC、MSC、HID、MIDI、Audio、Vendor 等多种 USB Class
- 支持复合设备(单个 USB 口同时枚举多个 Class)
- 已适配 STM32、ESP32、RP2040 等主流 MCU
本教程基于实际项目,详细记录了在 STM32F407VET6 + Keil MDK (ARMCC V5) 环境下移植 TinyUSB 的完整过程,并实现以下 USB Class 组合:
| Class | 功能 | 端点 |
|---|---|---|
| HID | 键盘 + 鼠标 + 多媒体 + 游戏手柄 | EP3 IN |
| MIDI | MIDI 乐器 | EP1 IN/OUT |
| Audio (UAC1) | 麦克风 (1ch) + 扬声器 (2ch) | EP2 IN / EP2 OUT |
STM32F407 OTG FS 仅有 4 个双向端点 (EP0-EP3),扣除 EP0 后只剩 3 个,本工程刚好用满。
2. 硬件与开发环境
| 项目 | 规格 |
|---|---|
| MCU | STM32F407VET6 (Cortex-M4, 168MHz) |
| USB 外设 | OTG FS (Full Speed 12Mbps) |
| USB 引脚 | PA11 (DM) / PA12 (DP) |
| 晶振 | 8MHz HSE |
| 编译器 | ARMCC V5.06 update 7 (Keil MDK) |
| TinyUSB 版本 | 最新 master 分支 |
时钟配置要点 :USB OTG FS 需要 48MHz 时钟源。本工程使用:
HSE 8MHz → PLL → SYSCLK 168MHz
→ PLLQ = 7 → USB 48MHz (168/7 ≈ 48)
对应代码中 PLLM=4, PLLN=168, PLLP=2, PLLQ=7。
3. STM32CubeMX 工程配置
本工程使用 STM32CubeMX 6.17 生成基础 HAL 工程,然后手动集成 TinyUSB。以下是 CubeMX 中需要配置的关键项。
3.1 芯片选择
- 搜索并选择 STM32F407VETx(LQFP100 封装)
3.2 RCC 时钟源配置
进入 Pinout & Configuration → System Core → RCC:
- HSE (High Speed Clock) :
Crystal/Ceramic Resonator(使用外部 8MHz 晶振) - HSI 保持默认
必须启用 HSE,因为内部 HSI 精度不足以满足 USB 48MHz 时钟要求。
3.3 时钟树配置(关键)
进入 Clock Configuration 页面,配置如下:
HSE = 8 MHz
↓
PLL Source Mux → HSE
↓
PLLM = 4 (VCO Input = 8/4 = 2 MHz)
PLLN = 168 (VCO Output = 2 × 168 = 336 MHz)
PLLP = /2 → SYSCLK = 336/2 = 168 MHz
PLLQ = 7 → USB CLK = 336/7 = 48 MHz ✅
↓
System Clock Mux → PLLCLK
↓
AHB Prescaler = /1 → HCLK = 168 MHz
APB1 Prescaler = /4 → APB1 = 42 MHz (max 42MHz)
APB2 Prescaler = /2 → APB2 = 84 MHz (max 84MHz)
最关键的参数是 PLLQ = 7 ,它产生 USB OTG FS 所需的 48MHz 时钟。如果这个频率不对,USB 将完全无法工作。
在 CubeMX 时钟树界面中,确认 48MHz Clocks 显示为 48 MHz。
3.4 SYS 配置
进入 Pinout & Configuration → System Core → SYS:
- Debug :
Serial Wire(PA13/PA14 用于 SWD 调试) - Timebase Source :
TIM1
重要:Timebase 必须改为定时器(如 TIM1),不能用默认的 SysTick。因为 TinyUSB 内部也依赖 SysTick/HAL_GetTick(),如果 HAL 的 Timebase 也用 SysTick 会存在优先级冲突。使用 TIM1 作为 HAL Timebase 可以避免问题。
3.5 GPIO 引脚配置
USB 引脚(PA11 / PA12)
在 CubeMX Pinout view 中直接点击引脚配置:
| 引脚 | 功能 | 标签 | 说明 |
|---|---|---|---|
| PA11 | USB_OTG_FS_DM |
USBF_DM | USB D- 信号线 |
| PA12 | USB_OTG_FS_DP |
USBF_DP | USB D+ 信号线 |
操作方式:左键点击 PA11 → 选择 USB_OTG_FS_DM,PA12 → 选择 USB_OTG_FS_DP。
注意:不要在 CubeMX 的 Connectivity 中启用 USB_OTG_FS 外设! 只需要把引脚配置为 USB 功能即可。USB 外设的初始化完全由 TinyUSB 接管,如果 CubeMX 也初始化 USB 会产生冲突。
CubeMX 生成的 gpio.c 会自动配置这两个引脚为复用推挽模式:
c
/* gpio.c 中自动生成的 USB 引脚配置 */
GPIO_InitStruct.Pin = USBF_DM_Pin | USBF_DP_Pin; // PA11 | PA12
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // 最高速
GPIO_InitStruct.Alternate = GPIO_AF10_OTG_FS; // AF10 = OTG FS
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
其他引脚(按需配置)
| 引脚 | 功能 | 标签 | 说明 |
|---|---|---|---|
| PA0 | GPIO_Input | KEY1 | 用户按键(上拉输入) |
| PA15 | GPIO_Output | LED1 | 状态 LED |
| PA9/PA10 | USART1 TX/RX | - | 调试串口 |
| PB10/PB12/PC2/PC3 | I2S2 | - | 音频编解码器接口(可选) |
| PH0/PH1 | RCC_OSC | - | HSE 晶振 |
3.6 USART1 配置(调试串口)
进入 Connectivity → USART1:
- Mode :
Asynchronous - Baud Rate :
115200(默认) - 引脚自动分配:PA9 (TX) / PA10 (RX)
3.7 I2S2 配置(音频接口,可选)
如果需要连接音频编解码器(如 WM8978、CS4344 等),配置 I2S2:
进入 Multimedia → I2S2:
- Mode :
Full-Duplex Master - Audio Frequency :
48K - 引脚自动分配:PB10 (CK) / PB12 (WS) / PC3 (SD) / PC2 (ext_SD)
3.8 NVIC 中断配置
进入 System Core → NVIC:
- 确认 TIM1 中断已启用(HAL Timebase)
- 不需要在 CubeMX 中启用 OTG_FS 中断,由 TinyUSB 驱动层手动配置
3.9 工程生成设置
进入 Project Manager:
| 设置项 | 值 |
|---|---|
| Project Name | STM32F407_TinyUsb |
| Toolchain / IDE | MDK-ARM V5.32 |
| Heap Size | 0x1000 |
| Stack Size | 0x1000 |
点击 Generate Code 生成基础工程。
3.10 CubeMX 配置要点总结
✅ HSE 外部晶振启用
✅ PLLQ = 7 → USB 48MHz 时钟
✅ SYS Timebase = TIM1(不用 SysTick)
✅ PA11/PA12 配置为 USB_OTG_FS_DM/DP
✅ 生成 MDK-ARM 工程
❌ 不要在 CubeMX 中启用 USB_OTG_FS 外设(Connectivity 中不勾选)
❌ 不要在 CubeMX 中配置 USB 中断(由 TinyUSB 管理)
❌ 不要使用 SysTick 作为 HAL Timebase
4. 工程目录结构
STM32F407_TinyUsb/
├── Core/
│ ├── Inc/ # HAL 配置头文件
│ └── Src/
│ └── main.c # 主程序,调用 tud_task() 和各 class task
├── Drivers/ # STM32 HAL 库
├── MDK-ARM/ # Keil 工程文件
├── RTT/ # Segger RTT (调试打印)
└── TinyUSB/
├── tusb_config.h # TinyUSB 全局配置(启用哪些 Class)
├── usb_descriptors.h # 描述符集中定义(接口号/端点/长度宏)
├── usb_descriptors.c # USB 描述符实现
├── driver/
│ ├── TinyUsb.h # 板级驱动头文件
│ └── TinyUsb.c # 板级驱动(GPIO/时钟/中断/回调)
├── usb_app/
│ ├── cdc_app.c # CDC 虚拟串口应用
│ ├── msc_app.c # MSC 大容量存储应用
│ ├── midi_app.c # MIDI 应用
│ ├── hid_app.c # HID 复合设备应用
│ ├── audio_app.c # Audio (UAC1) 应用
│ └── vendor_app.c # Vendor 自定义应用
└── src/ # TinyUSB 源码(原封不动)
├── tusb.c
├── common/
├── device/
├── class/
│ ├── cdc/
│ ├── msc/
│ ├── hid/
│ ├── midi/
│ ├── audio/
│ └── vendor/
└── portable/
└── synopsys/dwc2/ # STM32F4 OTG FS 驱动
5. 移植步骤
5.1 获取 TinyUSB 源码
bash
git clone https://github.com/hathach/tinyusb.git
克隆完成后,得到的目录结构如下(列出关键部分):
tinyusb/
├── src/ # ← 核心源码,需要拷贝到工程
│ ├── tusb.h / tusb.c # TinyUSB 入口
│ ├── common/ # 通用工具(FIFO、编译器适配等)
│ ├── device/ # USB Device 核心(usbd.c / usbd_control.c)
│ ├── class/ # 各 USB Class 实现
│ │ ├── cdc/cdc_device.c
│ │ ├── msc/msc_device.c
│ │ ├── hid/hid_device.c
│ │ ├── midi/midi_device.c
│ │ ├── audio/audio_device.c
│ │ └── vendor/vendor_device.c
│ └── portable/ # MCU 底层驱动
│ └── synopsys/dwc2/ # ← STM32F4 使用此驱动
│ ├── dcd_dwc2.c
│ └── dwc2_common.c
└── examples/device/ # ← 官方示例,参考其结构
├── cdc_msc/src/ # CDC + MSC 复合设备示例
│ ├── main.c
│ ├── tusb_config.h
│ ├── usb_descriptors.c
│ └── msc_disk.c
├── hid_composite/src/ # HID 复合设备示例
├── midi_test/src/ # MIDI 示例
└── audio_4_channel_mic/src/ # Audio 示例
5.2 参考 cdc_msc 示例拷贝应用文件
TinyUSB 的移植思路是:src/ 目录原封不动拷贝 (不修改 TinyUSB 库源码),应用层文件则参考官方 examples/device/cdc_msc 示例,拷贝后按需修改。
5.2.1 cdc_msc 示例的文件结构
官方 examples/device/cdc_msc/src/ 包含 4 个文件:
| 文件 | 作用 | 移植后对应文件 |
|---|---|---|
tusb_config.h |
TinyUSB 全局配置(启用哪些 Class、FIFO 大小等) | TinyUSB/tusb_config.h |
usb_descriptors.c |
USB 描述符(设备/配置/字符串描述符 + 回调函数) | TinyUSB/usb_descriptors.c + TinyUSB/usb_descriptors.h |
msc_disk.c |
MSC 应用逻辑(RAM Disk 实现) | TinyUSB/usb_app/msc_app.c |
main.c |
主程序(board_init + 主循环 + CDC 应用逻辑 + 设备回调) | 拆分为多个文件 |
5.2.2 拷贝与改造步骤
第一步 :在工程根目录创建 TinyUSB/ 文件夹,将 tinyusb/src/ 整个目录拷贝到 TinyUSB/src/。
TinyUSB/
└── src/ ← 原封不动拷贝 tinyusb/src/
第二步 :从 cdc_msc/src/ 拷贝 tusb_config.h 和 usb_descriptors.c 到 TinyUSB/ 根目录。
# 从示例拷贝
cp tinyusb/examples/device/cdc_msc/src/tusb_config.h TinyUSB/tusb_config.h
cp tinyusb/examples/device/cdc_msc/src/usb_descriptors.c TinyUSB/usb_descriptors.c
第三步 :拆分 main.c 中的功能,创建自定义文件。
cdc_msc 示例的 main.c 将所有功能混在一个文件中(初始化、CDC 逻辑、设备回调、LED 闪烁),我们按职责拆分为独立文件,方便后续扩展更多 Class:
| 示例 main.c 中的功能 | 拆分到的文件 | 说明 |
|---|---|---|
board_init() / USB GPIO 时钟 / tusb_init() / OTG_FS_IRQHandler() / tusb_time_millis_api() / board_usb_get_serial() / 设备回调 (tud_mount_cb 等) |
TinyUSB/driver/TinyUsb.c + .h |
新建:替代示例中的 BSP 层和设备回调 |
cdc_task() / tud_cdc_line_state_cb() / tud_cdc_rx_cb() |
TinyUSB/usb_app/cdc_app.c |
新建:CDC 应用独立文件 |
while(1) { tud_task(); ... } |
Core/Src/main.c |
保留:CubeMX 生成的 main.c 中添加调用 |
同样,从示例拷贝 msc_disk.c 并重命名:
cp tinyusb/examples/device/cdc_msc/src/msc_disk.c TinyUSB/usb_app/msc_app.c
第四步 :新建 usb_descriptors.h 头文件。
示例中描述符定义全在 .c 文件中,我们将接口号枚举、端点地址、描述符长度宏等提取到独立头文件 TinyUSB/usb_descriptors.h,方便多个文件引用。
5.2.3 对拷贝文件的关键修改
拷贝后需要对示例代码进行以下适配修改:
tusb_config.h 的修改:
c
// 示例原始内容(只有 CDC 和 MSC):
#define CFG_TUD_CDC 1
#define CFG_TUD_MSC 1
#define CFG_TUD_HID 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 0
// 修改为你需要的 Class 组合,例如 HID + MIDI + Audio:
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_HID 1
#define CFG_TUD_MIDI 1
#define CFG_TUD_VENDOR 0
#define CFG_TUD_AUDIO 1 // 示例中没有,需新增
usb_descriptors.c 的修改:
示例中的描述符只包含 CDC + MSC,需要扩展以支持更多 Class。主要修改点:
USB_PID宏:追加新 Class 的位映射
c
// 示例原始:
#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \
PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4))
// 修改后:追加 AUDIO
#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \
PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) | PID_MAP(AUDIO, 5))
- 接口号枚举:从硬编码改为条件编译
c
// 示例原始(硬编码 CDC + MSC):
enum { ITF_NUM_CDC = 0, ITF_NUM_CDC_DATA, ITF_NUM_MSC, ITF_NUM_TOTAL };
// 修改后(条件编译,支持任意组合):
enum {
ITF_NUM_IDX = 0, // 占位,使有效接口从 0 开始
#if CFG_TUD_MIDI
ITF_NUM_MIDI, ITF_NUM_MIDI_STREAMING,
#endif
#if CFG_TUD_AUDIO
ITF_NUM_AUDIO_CONTROL,
ITF_NUM_AUDIO_STREAMING_MIC,
ITF_NUM_AUDIO_STREAMING_SPK,
#endif
#if CFG_TUD_HID
ITF_NUM_HID,
#endif
ITF_NUM_TOTAL
};
- 配置描述符数组:从静态改为条件编译组装
c
// 示例原始(硬编码):
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN)
static uint8_t const desc_fs_configuration[] = {
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),
TUD_CDC_DESCRIPTOR(...),
TUD_MSC_DESCRIPTOR(...),
};
// 修改后(按宏开关动态组装):
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CDC_DESC_LEN + MSC_DESC_LEN \
+ MIDI_DESC_LEN + HID_DESC_LEN + AUDIO_DESC_LEN + VENDOR_DESC_LEN)
static uint8_t const desc_fs_configuration[] = {
TUD_CONFIG_DESCRIPTOR(1, (ITF_NUM_TOTAL - 1), 0, CONFIG_TOTAL_LEN, 0x00, 100),
#if CFG_TUD_MIDI
TUD_MIDI_DESCRIPTOR(...),
#endif
#if CFG_TUD_HID
TUD_HID_DESCRIPTOR(...),
#endif
#if CFG_TUD_AUDIO
// Audio 描述符(较复杂,使用自定义宏封装)
...
#endif
};
- 字符串描述符:追加新 Class 的接口名称
c
// 示例原始:
static char const *string_desc_arr[] = {
(const char[]){0x09, 0x04}, // 0: English
"TinyUSB", // 1: Manufacturer
"TinyUSB Device", // 2: Product
NULL, // 3: Serial (自动生成)
"TinyUSB CDC", // 4: CDC Interface
"TinyUSB MSC", // 5: MSC Interface
};
// 修改后:追加 Audio、Vendor、HID 等
static char const *string_desc_arr[] = {
(const char[]){0x09, 0x04},
"TinyUSB",
"TinyUSB Device",
NULL,
"TinyUSB CDC", // 4
"TinyUSB MSC", // 5
"TinyUSB Audio", // 6
"TinyUSB Vendor", // 7
"TinyUSB HID", // 8
};
TinyUsb.c 和 TinyUsb.h 的创建:
示例的 main.c 中调用了 board_init() 和 board_xxx() 系列 BSP 函数。在 CubeMX 工程中不使用 TinyUSB 自带的 BSP,而是新建 TinyUSB/driver/TinyUsb.c 和 TinyUSB/driver/TinyUsb.h,直接调用 STM32 HAL 实现 TinyUSB 要求的全部平台接口。
TinyUsb.c 中包含 7 个必须实现的函数,缺少任何一个都会导致链接错误或 USB 无法正常工作:
| 函数 | 类型 | 说明 |
|---|---|---|
TinyUsb_board_init() |
自定义 | USB 外设初始化入口,main() 中调用 |
tusb_time_millis_api() |
TinyUSB 必须 | TinyUSB 内部超时管理依赖此函数获取毫秒时间戳 |
OTG_FS_IRQHandler() |
ARM 中断 | USB OTG FS 中断服务函数,转发给 TinyUSB |
board_get_unique_id() |
自定义 | 读取 STM32 唯一 ID,供序列号生成使用 |
board_usb_get_serial() |
TinyUSB 必须 | 生成 USB 序列号字符串,usb_descriptors.c 中调用 |
tud_mount_cb() / tud_umount_cb() / tud_suspend_cb() / tud_resume_cb() |
TinyUSB 必须 | 设备状态回调,TinyUSB 内部会调用,必须实现(可空函数体) |
以下是 TinyUsb.h 的完整内容:
c
#ifndef _TINYUSB_H__
#define _TINYUSB_H__
#include <stdint.h>
#include <stddef.h>
#include "class/cdc/cdc.h"
void TinyUsb_board_init(void);
void cdc_task(void);
void midi_task(void);
void audio_task(void);
void vendor_task(void);
void hid_task(void);
const cdc_line_coding_t* cdc_get_line_coding(void);
size_t board_usb_get_serial(uint16_t desc_str1[], size_t max_chars);
#endif // !_TINYUSB_H__
以下是 TinyUsb.c 的完整内容(不省略任何函数):
c
#include "tusb.h"
#include "main.h"
#include "gpio.h"
#include "TinyUsb.h"
//--------------------------------------------------------------------+
// 1. 板级初始化(main.c 中调用,替代示例的 board_init)
//--------------------------------------------------------------------+
void board_init_after_tusb(void)
{
// 预留:tusb_init 之后的额外初始化,当前无需操作
}
void TinyUsb_board_init(void)
{
// 使能 GPIOA 时钟(USB 引脚在 PA11/PA12)
__HAL_RCC_GPIOA_CLK_ENABLE();
// 使能 USB OTG FS 外设时钟(必须,否则 USB 控制器无法工作)
__HAL_RCC_USB_OTG_FS_CLK_ENABLE();
// 配置 USB 引脚 PA11(DM) PA12(DP) 为复用推挽
// 注:如果 CubeMX 已在 gpio.c 中配置了 PA11/PA12,这里可以省略 GPIO 部分
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = USBF_DM_Pin | USBF_DP_Pin; // PA11 | PA12
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // 最高速率
GPIO_InitStruct.Alternate = GPIO_AF10_OTG_FS; // AF10 = OTG FS
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置 USB OTG FS 中断优先级并使能
HAL_NVIC_SetPriority(OTG_FS_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(OTG_FS_IRQn);
// 调用 TinyUSB 核心初始化
tusb_rhport_init_t dev_init = {
.role = TUSB_ROLE_DEVICE, // USB 设备模式
.speed = TUSB_SPEED_AUTO // 自动选择速度(STM32F407 OTG FS = Full Speed)
};
tusb_init(BOARD_TUD_RHPORT, &dev_init); // BOARD_TUD_RHPORT 在 tusb_config.h 中定义为 0
board_init_after_tusb();
}
//--------------------------------------------------------------------+
// 2. 时间戳接口(TinyUSB 必须实现)
// TinyUSB 内部用于超时判断、轮询间隔控制等
//--------------------------------------------------------------------+
uint32_t tusb_time_millis_api(void)
{
return (uint32_t)HAL_GetTick();
}
//--------------------------------------------------------------------+
// 3. USB 中断处理(TinyUSB 必须实现)
// 将 USB 硬件中断转发给 TinyUSB 协议栈处理
// 注意:如果 stm32f4xx_it.c 中已有此函数,必须删除以避免重复定义
//--------------------------------------------------------------------+
void OTG_FS_IRQHandler(void)
{
tusb_int_handler(0, true); // rhport=0, in_isr=true
}
void OTG_HS_IRQHandler(void)
{
tusb_int_handler(1, true); // rhport=1(STM32F407 OTG HS,本工程未使用)
}
//--------------------------------------------------------------------+
// 4. 设备唯一 ID 读取
// STM32 在 0x1FFF7A10 地址有 96-bit (12 字节) 出厂唯一 ID
//--------------------------------------------------------------------+
size_t board_get_unique_id(uint8_t id[], size_t max_len)
{
(void) max_len;
volatile uint32_t *stm32_uuid = (volatile uint32_t *) UID_BASE; // UID_BASE = 0x1FFF7A10
uint32_t *id32 = (uint32_t *) (uintptr_t) id;
uint8_t const len = 12;
id32[0] = stm32_uuid[0];
id32[1] = stm32_uuid[1];
id32[2] = stm32_uuid[2];
return len;
}
//--------------------------------------------------------------------+
// 5. USB 序列号字符串生成(TinyUSB 必须实现)
// 将 96-bit 唯一 ID 转为 HEX 字符串,供 usb_descriptors.c 中
// tud_descriptor_string_cb() 的 STRID_SERIAL 分支调用
//--------------------------------------------------------------------+
size_t board_usb_get_serial(uint16_t desc_str1[], size_t max_chars)
{
uint8_t uid[16];
size_t uid_len = board_get_unique_id(uid, sizeof(uid));
if (uid_len > max_chars / 2) {
uid_len = max_chars / 2;
}
for (size_t i = 0; i < uid_len; i++) {
for (size_t j = 0; j < 2; j++) {
const char nibble_to_hex[16] = {
'0','1','2','3','4','5','6','7',
'8','9','A','B','C','D','E','F'
};
uint8_t nibble = (uid[i] >> (4 - j * 4)) & 0xf;
desc_str1[i * 2 + j] = nibble_to_hex[nibble];
}
}
return uid_len * 2;
}
//--------------------------------------------------------------------+
// 6. 设备状态回调(TinyUSB 必须实现,可以是空函数体)
// 这 4 个回调由 tud_task() 内部在状态变化时调用
//--------------------------------------------------------------------+
// USB 设备被主机枚举成功(mounted)
void tud_mount_cb(void)
{
// 可在此处点亮 LED、设置标志位等
}
// USB 设备被主机断开(unmounted)
void tud_umount_cb(void)
{
// 可在此处熄灭 LED、清除标志位等
}
// USB 总线挂起(主机停止发送 SOF 包)
// remote_wakeup_en: 主机是否允许设备发起远程唤醒
// USB 规范要求:7ms 内设备必须将平均电流降至 2.5mA 以下
void tud_suspend_cb(bool remote_wakeup_en)
{
(void)remote_wakeup_en;
// 可在此处进入低功耗模式
}
// USB 总线恢复(主机重新发送 SOF 包)
void tud_resume_cb(void)
{
// 可在此处退出低功耗模式
}
提醒 :
OTG_FS_IRQHandler是 STM32 的中断向量表中定义的固定函数名(在startup_stm32f407xx.s中),如果 CubeMX 在stm32f4xx_it.c中也生成了同名函数,会导致链接时"重复定义"错误,必须删除stm32f4xx_it.c中的那一份。
5.2.4 最终文件对应关系总结
tinyusb 官方仓库 本工程 TinyUSB/ 目录
───────────────────── ────────────────────
src/ (整个目录) ──拷贝→ src/ (不修改)
│
examples/device/cdc_msc/src/ │
├── tusb_config.h ──拷贝→ tusb_config.h (修改 Class 开关)
├── usb_descriptors.c ──拷贝→ usb_descriptors.c (扩展描述符)
│ ──新建→ usb_descriptors.h (提取公共定义)
├── msc_disk.c ──拷贝→ usb_app/msc_app.c (重命名)
└── main.c ──拆分→ driver/TinyUsb.c (BSP + 回调)
usb_app/cdc_app.c (CDC 逻辑)
Core/Src/main.c (主循环)
│
(无对应示例,自行编写) ──新建→ usb_app/midi_app.c
usb_app/hid_app.c
usb_app/audio_app.c
usb_app/vendor_app.c
重要原则 :
TinyUSB/src/下的库源码绝对不要修改 (唯一例外是tusb_compiler.h需为 ARMCC V5 做兼容适配,详见 [5.5 节](#5.5 节)),所有自定义代码都放在TinyUSB/src/之外的文件中。
5.3 添加源文件到 Keil 工程
在 Keil 中创建以下 Group 并添加对应的 .c 文件:
| Keil Group | 源文件 |
|---|---|
| TinyUSB/Core | src/tusb.c src/common/tusb_fifo.c src/device/usbd.c src/device/usbd_control.c |
| TinyUSB/Portable | src/portable/synopsys/dwc2/dcd_dwc2.c src/portable/synopsys/dwc2/dwc2_common.c |
| TinyUSB/Class/CDC | src/class/cdc/cdc_device.c |
| TinyUSB/Class/MSC | src/class/msc/msc_device.c |
| TinyUSB/Class/MIDI | src/class/midi/midi_device.c |
| TinyUSB/Class/HID | src/class/hid/hid_device.c |
| TinyUSB/Class/Audio | src/class/audio/audio_device.c |
| TinyUSB/Class/Vendor | src/class/vendor/vendor_device.c |
| TinyUSB/Driver | driver/TinyUsb.c usb_descriptors.c |
| TinyUSB/USB_App | usb_app/cdc_app.c usb_app/msc_app.c usb_app/midi_app.c usb_app/hid_app.c usb_app/audio_app.c usb_app/vendor_app.c |
未使用的 Class 源文件也可以添加,通过
tusb_config.h中的宏控制编译(CFG_TUD_xxx = 0时对应的xxx_device.c内部代码不会编译)。
5.4 配置头文件路径
在 Keil 的 Options → C/C++ → Include Paths 中添加:
../TinyUSB/src
../TinyUSB
../TinyUSB/driver
5.5 添加全局宏定义
在 Keil 的 Options → C/C++ → Define 中添加:
CFG_TUSB_MCU=OPT_MCU_STM32F4
同时建议在 Misc Controls 中添加 --gnu 以启用 GNU 扩展支持。
5.6 解决 ARMCC V5 编译兼容性问题
TinyUSB 默认面向 GCC/Clang,ARMCC V5 会遇到以下问题:
问题 1:__has_attribute 不支持
ARMCC V5 不支持 __has_attribute 关键字,会导致编译错误:
error: #109: expression preceding parentheses of apparent call must have
(pointer-to-) function type
解决方法 :修改 src/common/tusb_compiler.h,在 __has_attribute 检测之前添加 ARMCC V5 判断:
c
#if defined(__ARMCC_VERSION) && (__ARMCC_VERSION < 6000000)
#define TU_ATTR_FALLTHROUGH do {} while (0) /* fallthrough */
#else
#if (defined(__has_attribute) && __has_attribute(__fallthrough__)) || ...
#define TU_ATTR_FALLTHROUGH __attribute__((fallthrough))
#else
...
#endif
#endif
问题 2:__builtin_bswap16 未定义
ARMCC V5 不支持 GCC 内建字节序转换函数。
解决方法 :在 tusb_compiler.h 中为 ARMCC V5 添加手动实现:
c
#elif defined(__ARMCC_VERSION) && (__ARMCC_VERSION < 6000000)
#define TU_BSWAP16(u16) ((uint16_t)((((u16) & 0xff00u) >> 8) | \
(((u16) & 0x00ffu) << 8)))
#define TU_BSWAP32(u32) ((uint32_t)((((u32) & 0xff000000u) >> 24) | \
(((u32) & 0x00ff0000u) >> 8) | \
(((u32) & 0x0000ff00u) << 8) | \
(((u32) & 0x000000ffu) << 24)))
6. 驱动层适配
TinyUSB/driver/TinyUsb.c 是连接 STM32 HAL 与 TinyUSB 协议栈的桥梁。完整的源代码和每个函数的详细说明已在 [5.2.3 节](#5.2.3 节) 中给出,此处对各函数的职责做概要说明。
6.1 必须实现的函数一览
| 函数 | TinyUSB 是否强制要求 | 职责 |
|---|---|---|
TinyUsb_board_init() |
否(自定义入口) | USB 外设时钟使能 → GPIO 配置 → 中断配置 → tusb_init() |
tusb_time_millis_api() |
是 | 提供毫秒时间戳,TinyUSB 内部超时和轮询依赖此接口 |
OTG_FS_IRQHandler() |
是(中断向量) | 将 USB 硬件中断转发给 tusb_int_handler() |
board_usb_get_serial() |
是 | 生成 USB 序列号字符串,被 usb_descriptors.c 中的字符串描述符回调调用 |
tud_mount_cb() |
是 | USB 设备被主机枚举成功时的回调 |
tud_umount_cb() |
是 | USB 设备断开时的回调 |
tud_suspend_cb() |
是 | USB 总线挂起时的回调 |
tud_resume_cb() |
是 | USB 总线恢复时的回调 |
6.2 关键注意事项
-
中断冲突 :
OTG_FS_IRQHandler在startup_stm32f407xx.s的中断向量表中是固定名称。如果 CubeMX 在stm32f4xx_it.c中也生成了同名函数,必须删除那一份,否则链接报重复定义错误。 -
时间戳来源 :
tusb_time_millis_api()返回HAL_GetTick(),因此 HAL 的 Timebase 必须正常工作。我们在 CubeMX 中已将 Timebase 配置为 TIM1(参考 [3.4 节](#3.4 节)),避免与 USB 中断优先级冲突。 -
GPIO 配置 :如果 CubeMX 已在
gpio.c中配置了 PA11/PA12 为USB_OTG_FS_DM/DP(参考 [3.5 节](#3.5 节)),TinyUsb_board_init()中可以省略 GPIO 初始化部分,只保留时钟使能和中断配置。 -
回调函数 :4 个设备状态回调(mount/umount/suspend/resume)即使不做任何事情,也必须定义,否则链接会报未定义符号错误。可以留空函数体。
7. TinyUSB 配置 (tusb_config.h)
这是 TinyUSB 的核心配置文件,决定了启用哪些 USB Class:
c
#ifndef TUSB_CONFIG_H_
#define TUSB_CONFIG_H_
// MCU 类型(必须通过编译器宏定义 CFG_TUSB_MCU=OPT_MCU_STM32F4)
#ifndef CFG_TUSB_MCU
#error CFG_TUSB_MCU must be defined
#endif
// 无操作系统
#define CFG_TUSB_OS OPT_OS_NONE
// 使能 Device 模式
#define CFG_TUD_ENABLED 1
// USB 端点 0 大小
#define CFG_TUD_ENDPOINT0_SIZE 64
// 内存对齐(STM32 DMA 要求 4 字节对齐)
#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4)))
//------------- 启用的 USB Class -------------//
#define CFG_TUD_CDC 0 // 虚拟串口
#define CFG_TUD_MSC 0 // U盘
#define CFG_TUD_HID 1 // 键鼠/游戏手柄
#define CFG_TUD_MIDI 1 // MIDI 乐器
#define CFG_TUD_AUDIO 1 // 音频 (UAC1)
#define CFG_TUD_VENDOR 0 // 自定义设备
//------------- Audio 配置 -------------//
#define CFG_TUD_AUDIO_ENABLE_MIC 1 // 麦克风
#define CFG_TUD_AUDIO_ENABLE_SPK 1 // 扬声器
#define CFG_TUD_AUDIO_FUNC_1_SAMPLE_RATE 48000
#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE 2 // 16-bit
#define CFG_TUD_AUDIO_FUNC_1_N_BITS_PER_SAMPLE 16
// MIC: 1 声道 IN
#if CFG_TUD_AUDIO_ENABLE_MIC
#define CFG_TUD_AUDIO_ENABLE_EP_IN 1
#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX 1
#define CFG_TUD_AUDIO_EP_SZ_IN TUD_AUDIO_EP_SIZE(...)
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ (4 * CFG_TUD_AUDIO_EP_SZ_IN)
#endif
// SPK: 2 声道 OUT, 无反馈端点
#if CFG_TUD_AUDIO_ENABLE_SPK
#define CFG_TUD_AUDIO_ENABLE_EP_OUT 1
#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX 2
#define CFG_TUD_AUDIO_EP_SZ_OUT TUD_AUDIO_EP_SIZE(...)
#define CFG_TUD_AUDIO_ENABLE_FEEDBACK_EP 0 // 不使用反馈端点,节省 EP
#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ (4 * CFG_TUD_AUDIO_EP_SZ_OUT)
#endif
// HID 端点缓冲大小
#define CFG_TUD_HID_EP_BUFSIZE 64
#endif
Class 组合灵活性
通过修改宏值 (0/1) 可以自由切换 USB 功能组合,例如:
| 组合 | CDC | MSC | HID | MIDI | Audio | Vendor |
|---|---|---|---|---|---|---|
| 串口 + U盘 | 1 | 1 | 0 | 0 | 0 | 0 |
| MIDI + Audio | 0 | 0 | 0 | 1 | 1 | 0 |
| HID + MIDI + Audio | 0 | 0 | 1 | 1 | 1 | 0 |
| 全功能(需更多EP) | 1 | 1 | 1 | 1 | 1 | 1 |
注意:STM32F407 OTG FS 只有 4 个端点,全功能组合需要端点复用或换用端点更多的芯片。
8. USB 描述符
8.1 描述符头文件 (usb_descriptors.h)
将接口编号、端点地址、描述符长度集中定义,便于各文件引用:
c
// 接口编号枚举(根据启用的 Class 自动编号)
enum {
ITF_NUM_IDX = 0,
#if CFG_TUD_MIDI
ITF_NUM_MIDI,
ITF_NUM_MIDI_STREAMING,
#endif
#if CFG_TUD_AUDIO
ITF_NUM_AUDIO_CONTROL,
ITF_NUM_AUDIO_STREAMING_MIC, // 条件编译
ITF_NUM_AUDIO_STREAMING_SPK, // 条件编译
#endif
#if CFG_TUD_HID
ITF_NUM_HID,
#endif
ITF_NUM_TOTAL
};
// 端点地址
#define EPNUM_MIDI_OUT 0x01
#define EPNUM_MIDI_IN 0x81
#define EPNUM_AUDIO_MIC_IN 0x82
#define EPNUM_AUDIO_SPK_OUT 0x02
#define EPNUM_HID_IN 0x83
8.2 端点分配策略
STM32F407 OTG FS 端点分配(EP0 为控制端点,不可自由使用):
EP0 ─── 控制端点 (系统占用)
EP1 ─── MIDI OUT (0x01) + MIDI IN (0x81) [Bulk]
EP2 ─── Audio SPK OUT (0x02) + Audio MIC IN (0x82) [Isochronous]
EP3 ─── HID IN (0x83) [Interrupt]
关键原则:
- 同一 EP 号的 IN 和 OUT 方向可以给不同 Class 使用
- Isochronous 端点(Audio)有固定带宽保证
- HID 只需要 IN 端点(设备到主机)
- 如果使用 Audio 反馈端点会多占一个 EP,本工程通过 Adaptive 模式避免
8.3 设备描述符
复合设备需要使用 IAD(Interface Association Descriptor),设备描述符的 Class 必须设为:
c
.bDeviceClass = TUSB_CLASS_MISC, // 0xEF
.bDeviceSubClass = MISC_SUBCLASS_COMMON, // 0x02
.bDeviceProtocol = MISC_PROTOCOL_IAD, // 0x01
PID 通过宏自动生成,不同 Class 组合会产生不同的 PID,避免 Windows 驱动缓存冲突:
c
#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0)
#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | \
PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 5))
8.4 配置描述符
使用 TinyUSB 提供的描述符宏,按顺序排列各 Class 描述符:
c
static uint8_t const desc_fs_configuration[] = {
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL-1, 0, CONFIG_TOTAL_LEN, 0x00, 100),
#if CFG_TUD_MIDI
TUD_MIDI_DESCRIPTOR(ITF_NUM_MIDI, 0, EPNUM_MIDI_OUT, 0x80|EPNUM_MIDI_IN, 64),
#endif
#if CFG_TUD_HID
TUD_HID_DESCRIPTOR(ITF_NUM_HID, 8, HID_ITF_PROTOCOL_NONE,
sizeof(desc_hid_report), EPNUM_HID_IN, 64, 5),
#endif
#if CFG_TUD_AUDIO
// Audio 描述符(IAD + AC + MIC AS + SPK AS)...
#endif
};
8.5 字符串描述符
c
static char const *string_desc_arr[] = {
(const char[]){0x09, 0x04}, // 0: 语言 ID (English)
"TinyUSB", // 1: 厂商
"TinyUSB Device", // 2: 产品名
NULL, // 3: 序列号(自动从 UID 生成)
"TinyUSB CDC", // 4: CDC 接口
"TinyUSB MSC", // 5: MSC 接口
"TinyUSB Audio", // 6: Audio 接口
"TinyUSB Vendor", // 7: Vendor 接口
"TinyUSB HID", // 8: HID 接口
};
9. 各 Class 应用层实现
每个 USB Class 对应一个 xxx_app.c 文件,实现 TinyUSB 要求的回调函数和应用逻辑。所有文件用 #if CFG_TUD_XXX ... #endif 包裹,未启用的 Class 不会编译。
9.1 CDC 虚拟串口
- 接口数:2(Control + Data)
- 端点:Notification IN + Bulk IN + Bulk OUT
- 用途:虚拟 COM 口,替代传统 UART 调试
- 关键回调:
tud_cdc_line_coding_cb()(波特率变更)、tud_cdc_rx_cb()(数据接收)
9.2 MSC 大容量存储
- 接口数:1
- 端点:Bulk IN + Bulk OUT
- 用途:模拟 U 盘(RAM Disk / SPI Flash)
- 关键回调:
tud_msc_read10_cb()/tud_msc_write10_cb()(读写扇区)
9.3 MIDI 设备
- 接口数:2(Audio Control + MIDI Streaming)
- 端点:Bulk IN + Bulk OUT
- 用途:USB MIDI 乐器,可发送/接收 MIDI 消息
- 关键函数:
tud_midi_stream_write()/tud_midi_packet_read()
9.4 HID 复合设备
HID 通过 Report ID 在单个接口中复合多种设备:
| Report ID | 类型 | 说明 |
|---|---|---|
| 1 | Keyboard | 标准 6 键无冲键盘 |
| 2 | Mouse | 3 按键 + XY + 滚轮 |
| 3 | Consumer Control | 多媒体键(音量/播放/暂停) |
| 4 | Gamepad | 16 按键 + 帽子开关 + 6 轴 |
Report Descriptor 定义:
c
static uint8_t const desc_hid_report[] = {
TUD_HID_REPORT_DESC_KEYBOARD ( HID_REPORT_ID(REPORT_ID_KEYBOARD) ),
TUD_HID_REPORT_DESC_MOUSE ( HID_REPORT_ID(REPORT_ID_MOUSE) ),
TUD_HID_REPORT_DESC_CONSUMER ( HID_REPORT_ID(REPORT_ID_CONSUMER_CONTROL) ),
TUD_HID_REPORT_DESC_GAMEPAD ( HID_REPORT_ID(REPORT_ID_GAMEPAD) ),
};
链式报告发送 :参考官方 hid_composite 示例,通过 tud_hid_report_complete_cb 回调实现链式发送,每 10ms 发送一轮完整报告:
hid_task() → send(Keyboard) → complete_cb → send(Mouse) → complete_cb → send(Consumer) → ...
关键回调:
tud_hid_descriptor_report_cb():返回 Report Descriptortud_hid_get_report_cb()/tud_hid_set_report_cb():处理 Host 的报告请求tud_hid_report_complete_cb():链式发送触发
9.5 Audio 音频设备 (UAC1)
这是最复杂的 Class,本工程实现 单声道 MIC + 立体声 SPK,不使用反馈端点。
描述符拓扑
MIC 链路: InputTerm(Mic) → FeatureUnit(Mute/Volume) → OutputTerm(USB Streaming)
SPK 链路: InputTerm(USB Streaming) → FeatureUnit(Mute/Volume) → OutputTerm(Speaker)
关键配置选择
| 配置项 | MIC | SPK |
|---|---|---|
| 同步方式 | Asynchronous | Adaptive(无反馈EP) |
| 声道数 | 1 (单声道) | 2 (立体声) |
| 采样率 | 48000 Hz | 48000 Hz |
| 位深 | 16 bit | 16 bit |
数据流处理(中断驱动)
MIC 和 SPK 数据在 ISR 中处理(每 1ms 一次),而不是在主循环轮询:
c
// MIC: 每 1ms ISO IN 完成中断,填充下一帧数据
bool tud_audio_tx_done_isr(...) {
memset(mic_buf, 0, sizeof(mic_buf)); // 替换为真实采集数据
tud_audio_write((uint8_t*)mic_buf, sizeof(mic_buf));
return true;
}
// SPK: 每 1ms ISO OUT 完成中断,读取 Host 发来的音频
bool tud_audio_rx_done_isr(...) {
uint16_t count = tud_audio_read(spk_buf, avail);
// 将 spk_buf 送到 I2S/DAC 播放
return true;
}
UAC1 控制请求
必须正确处理 Host 的 GET_CUR/GET_MIN/GET_MAX/GET_RES 请求,否则设备会显示"代码 10"错误:
- 采样率 :
tud_audio_get_req_ep_cb()返回 3 字节小端采样率 - 音量 :
tud_audio_get_req_entity_cb()返回 Mute (1 byte) / Volume (2 bytes) - Mute:同上
9.6 Vendor 自定义设备
- 接口数:1
- 端点:Bulk IN + Bulk OUT
- 用途:自定义协议通信(需安装 WinUSB 驱动,可用 Zadig 工具)
- 关键函数:
tud_vendor_read()/tud_vendor_write()
10. 主循环集成(初始化与任务调度)
这是将 TinyUSB 集成到 CubeMX 工程中最关键的一步:需要在正确的位置调用初始化函数,并在主循环中持续运行 TinyUSB 核心任务和各 Class 应用任务。
10.1 头文件引入
在 main.c 的 USER CODE BEGIN Includes 区域添加两个头文件:
c
/* USER CODE BEGIN Includes */
#include "tusb.h" // TinyUSB 核心 API(tud_task 等)
#include "TinyUsb.h" // 板级驱动接口(TinyUsb_board_init 等)
/* USER CODE END Includes */
tusb.h:TinyUSB 库的统一入口头文件,提供tud_task()、tud_mounted()等核心 APITinyUsb.h:我们自己编写的板级驱动头文件,声明了TinyUsb_board_init()和各 Class 的 task 函数
10.2 初始化调用顺序
在 main() 函数中,必须在 CubeMX 外设初始化完成之后再调用 TinyUSB 初始化:
c
int main(void)
{
/* ① HAL 初始化(SysTick、NVIC 分组等) */
HAL_Init();
/* ② 系统时钟配置(HSE → PLL → 168MHz, PLLQ=7 → USB 48MHz) */
SystemClock_Config();
/* ③ CubeMX 生成的外设初始化 */
MX_GPIO_Init(); // GPIO(含 PA11/PA12 USB 引脚配置)
MX_I2S2_Init(); // I2S(音频编解码器,可选)
MX_USART1_UART_Init(); // 调试串口
/* ④ TinyUSB 初始化(必须在 GPIO 初始化之后) */
TinyUsb_board_init();
调用顺序的关键约束:
| 顺序 | 调用 | 为什么必须在这个位置 |
|---|---|---|
| ① | HAL_Init() |
初始化 HAL 库,配置 TIM1 作为 Timebase(提供 HAL_GetTick()) |
| ② | SystemClock_Config() |
配置 PLL 时钟,PLLQ=7 产生 48MHz USB 时钟,必须在 USB 初始化之前 |
| ③ | MX_GPIO_Init() |
配置 PA11/PA12 为 USB_OTG_FS 复用功能,必须在 USB 时钟使能之前完成 |
| ④ | TinyUsb_board_init() |
使能 USB OTG FS 时钟 → 配置 USB 中断 → 调用 tusb_init() 启动协议栈 |
TinyUsb_board_init() 内部实际做了以下事情:
c
void TinyUsb_board_init(void)
{
// 1. 使能 USB OTG FS 外设时钟
__HAL_RCC_USB_OTG_FS_CLK_ENABLE();
// 2. 配置 USB 中断优先级并使能
HAL_NVIC_SetPriority(OTG_FS_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(OTG_FS_IRQn);
// 3. 调用 TinyUSB 库的初始化函数(这是 TinyUSB 的入口)
tusb_rhport_init_t dev_init = {
.role = TUSB_ROLE_DEVICE, // 设备模式
.speed = TUSB_SPEED_AUTO // 自动速度(FS for STM32F407 OTG FS)
};
tusb_init(BOARD_TUD_RHPORT, &dev_init); // BOARD_TUD_RHPORT = 0
}
tusb_init()是 TinyUSB 的核心初始化函数,它会:读取tusb_config.h的配置 → 初始化 DWC2 USB 控制器 → 注册已启用的 Class 驱动 → 等待主机连接。
10.3 主循环任务调度
初始化完成后,主循环中需要持续轮询 TinyUSB 核心任务和各 Class 的应用任务:
c
while (1)
{
/* ⑤ TinyUSB 核心任务(必须调用,且频率越高越好) */
tud_task();
/* ⑥ 各 Class 应用任务(按需调用) */
#if CFG_TUD_CDC
cdc_task(); // CDC 虚拟串口:轮询接收数据并回显
#endif
#if CFG_TUD_MIDI
midi_task(); // MIDI:发送 MIDI 音符消息
#endif
#if CFG_TUD_AUDIO
audio_task(); // Audio:填充 MIC 数据 / 读取 SPK 数据
#endif
#if CFG_TUD_HID
hid_task(); // HID:发送键盘/鼠标/手柄报告
#endif
#if CFG_TUD_VENDOR
vendor_task(); // Vendor:处理自定义数据传输
#endif
/* ⑦ 用户其他任务(LED 闪烁等) */
}
}
各函数的职责说明:
| 函数 | 来源 | 职责 | 调用频率要求 |
|---|---|---|---|
tud_task() |
TinyUSB 库 (tusb.h) |
USB 协议栈核心:处理 Setup 请求、端点数据收发、状态机切换 | 必须调用,尽量不被阻塞 |
cdc_task() |
usb_app/cdc_app.c |
读取主机发来的串口数据,执行回显或其他处理 | 有数据时处理 |
midi_task() |
usb_app/midi_app.c |
定时发送 MIDI Note On/Off 消息 | 内部自带 10ms 间隔控制 |
audio_task() |
usb_app/audio_app.c |
MIC 数据填充、SPK 数据读取(中断驱动为主,此任务为辅) | 每轮调用 |
hid_task() |
usb_app/hid_app.c |
定时发送 HID 报告(键盘/鼠标/多媒体/手柄),使用链式发送机制 | 内部自带 10ms 间隔控制 |
vendor_task() |
usb_app/vendor_app.c |
读取主机发来的自定义数据并回环返回 | 有数据时处理 |
关键理解 :
tud_task()是整个 USB 协议栈的"心跳",它处理来自中断的 USB 事件(枚举、数据传输、挂起/恢复等)。如果tud_task()长时间得不到调用(例如主循环中有耗时的阻塞操作),USB 通信将出现超时或断连。
10.4 回调函数的调用机制
除了主循环中主动调用的 task 函数,TinyUSB 还通过回调函数 通知应用层各种事件。这些回调函数在 tud_task() 内部被调用(不是在中断中),用户只需实现它们:
tud_task() 内部调用链:
│
├─ USB 枚举完成 → tud_mount_cb() // 在 TinyUsb.c 中实现
├─ USB 断开 → tud_umount_cb() // 在 TinyUsb.c 中实现
├─ USB 挂起 → tud_suspend_cb() // 在 TinyUsb.c 中实现
├─ USB 恢复 → tud_resume_cb() // 在 TinyUsb.c 中实现
│
├─ CDC 收到数据 → tud_cdc_rx_cb() // 在 cdc_app.c 中实现
├─ CDC 线路状态 → tud_cdc_line_state_cb() // 在 cdc_app.c 中实现
│
├─ HID 报告发完 → tud_hid_report_complete_cb() // 在 hid_app.c 中实现(链式发送)
├─ HID 主机设置 → tud_hid_set_report_cb() // 在 hid_app.c 中实现(LED 状态)
│
├─ Audio 接口切换 → tud_audio_set_itf_cb() // 在 audio_app.c 中实现
├─ Audio 控制请求 → tud_audio_get_req_entity_cb()// 在 audio_app.c 中实现
├─ Audio 数据到达 → tud_audio_rx_done_post_read_cb() // 在 audio_app.c 中实现
│
├─ MSC SCSI 命令 → tud_msc_read10_cb() 等 // 在 msc_app.c 中实现
│
└─ Vendor 数据 → 通过 tud_vendor_read() 轮询 // 在 vendor_app.c 中实现
10.5 完整 main.c 代码
以下是工程中 main.c 的完整关键代码(CubeMX 生成部分 + 用户代码部分):
c
/* USER CODE BEGIN Includes */
#include "tusb.h"
#include "TinyUsb.h"
/* USER CODE END Includes */
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2S2_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
TinyUsb_board_init(); // ← USB 初始化入口
/* USER CODE END 2 */
while (1)
{
/* USER CODE BEGIN 3 */
tud_task(); // ← USB 核心任务(必须)
#if CFG_TUD_CDC
cdc_task();
#endif
#if CFG_TUD_MIDI
midi_task();
#endif
#if CFG_TUD_AUDIO
audio_task();
#endif
#if CFG_TUD_VENDOR
vendor_task();
#endif
#if CFG_TUD_HID
hid_task();
#endif
// LED 闪烁指示运行状态
static uint32_t lastTick = 0;
if (HAL_GetTick() - lastTick >= 500) {
lastTick = HAL_GetTick();
LED_Toggle();
}
/* USER CODE END 3 */
}
}
USER CODE BEGIN/END 标记 :所有用户代码必须写在 CubeMX 的
USER CODE标记区域内,否则重新生成代码时会被覆盖。
11. STM32 各系列 USB 端点数量参考
选择芯片时,端点数量是决定能支持多少 USB Class 组合的关键因素:
| 系列 | USB 外设 | 端点数量 | 速度 |
|---|---|---|---|
| STM32F0 / F1(103) / F3 | USB FS | 8 个双向 | Full Speed |
| STM32F1 (105/107) | OTG FS | 4 个双向 | Full Speed |
| STM32F4 (405/407/427/429) | OTG FS | 4 个双向 | Full Speed |
| STM32F4 (405/407/427/429) | OTG HS | 6 个双向 | High Speed |
| STM32F7 | OTG FS | 6 个双向 | Full Speed |
| STM32F7 | OTG HS | 9 个双向 | High Speed |
| STM32G0 / G4 / L0 / L1 / L4 | USB FS | 8 个双向 | Full Speed |
| STM32H7 | OTG FS | 6 个双向 | Full Speed |
| STM32H7 (A3/B3) | OTG HS | 9 个双向 | High Speed |
| STM32U5 | OTG HS | 6 个双向 | High Speed |
STM32F407 OTG FS 的 4 个端点是最紧张的,复杂组合需要精心规划端点分配。
12. 常见问题与解决
Q1: 编译报错 __has_attribute
原因:ARMCC V5 不支持此关键字。
解决 :见 [4.5 节](#4.5 节)。
Q2: USB 设备识别不到
排查步骤:
- 确认
PLLQ产生了 48MHz USB 时钟 - 确认
__HAL_RCC_USB_OTG_FS_CLK_ENABLE()已调用 - 确认 PA11/PA12 配置为
GPIO_AF10_OTG_FS - 确认
OTG_FS_IRQHandler中调用了tusb_int_handler() - 确认
stm32f4xx_it.c中没有重复定义OTG_FS_IRQHandler
Q3: Audio 设备显示"代码 10"
原因 :tud_audio_get_req_entity_cb() 和 tud_audio_get_req_ep_cb() 返回了 false,没有正确响应 Host 的 GET_CUR/GET_MIN/GET_MAX/GET_RES 请求。
解决 :必须用 tud_control_xfer() 回复 Mute、Volume、采样率等控制请求的数据。
Q4: MSC 单独使用时识别不到
原因:当只有 MSC 一个 Class 时,设备描述符不应该使用 IAD 的 Class 码。
解决 :当非复合设备时,将 bDeviceClass/SubClass/Protocol 设为 0。
Q5: 切换 Class 组合后设备异常
原因:Windows 会根据 VID/PID 缓存驱动。不同 Class 组合使用相同 PID 会导致冲突。
解决 :USB_PID 宏通过 PID_MAP 自动根据启用的 Class 生成不同 PID。如果仍有问题,在设备管理器中卸载设备并勾选"删除驱动程序"。
Q6: Vendor 设备显示"代码 28"
原因:Vendor Class 没有自带驱动,需要手动安装。
解决 :使用 Zadig 工具安装 WinUSB 驱动。
Q7: #define CONFIG_TOTAL_LEN 中不能使用 #if
原因 :C 预处理器不允许在 #define 宏体内使用 #if 指令。
解决:将各 Class 的描述符长度定义为独立宏,未启用时为 0,最后相加:
c
#if CFG_TUD_MIDI
#define MIDI_DESC_LEN TUD_MIDI_DESC_LEN
#else
#define MIDI_DESC_LEN 0
#endif
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + MIDI_DESC_LEN + HID_DESC_LEN + AUDIO_DESC_LEN)
13. 参考资源
- TinyUSB 官方仓库
- TinyUSB 官方文档
- TinyUSB Device 示例
- STM32F407 参考手册 (RM0090)
- USB 2.0 规范
- USB Audio Class 1.0 规范
- Zadig - USB 驱动安装工具
14. 完整工程源码
本教程对应的完整工程代码已开源,可直接克隆编译:
Gitee 仓库 :https://gitee.com/haihuiqiu/stm32f407_tinyusb
bash
git clone https://gitee.com/haihuiqiu/stm32f407_tinyusb.git
仓库包含本文所述的所有代码,包括 TinyUSB 源码、驱动适配层、USB 描述符、各 Class 应用实现(CDC / MSC / MIDI / HID / Audio / Vendor)以及 Python 测试工具等,可作为 STM32F407 + TinyUSB 移植的参考模板。
本文基于实际移植经验编写,如有问题欢迎交流。