TinyUSB 移植到 STM32F407实现Audio+Midi+Cdc复合设备

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 引脚配置)
      • [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 未定义)
    • [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))
      • [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)
    • [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.husb_descriptors.cTinyUSB/ 根目录。

复制代码
# 从示例拷贝
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。主要修改点:

  1. 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))
  1. 接口号枚举:从硬编码改为条件编译
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
};
  1. 配置描述符数组:从静态改为条件编译组装
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
};
  1. 字符串描述符:追加新 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.cTinyUSB/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 关键注意事项

  1. 中断冲突OTG_FS_IRQHandlerstartup_stm32f407xx.s 的中断向量表中是固定名称。如果 CubeMX 在 stm32f4xx_it.c 中也生成了同名函数,必须删除那一份,否则链接报重复定义错误。

  2. 时间戳来源tusb_time_millis_api() 返回 HAL_GetTick(),因此 HAL 的 Timebase 必须正常工作。我们在 CubeMX 中已将 Timebase 配置为 TIM1(参考 [3.4 节](#3.4 节)),避免与 USB 中断优先级冲突。

  3. GPIO 配置 :如果 CubeMX 已在 gpio.c 中配置了 PA11/PA12 为 USB_OTG_FS_DM/DP(参考 [3.5 节](#3.5 节)),TinyUsb_board_init() 中可以省略 GPIO 初始化部分,只保留时钟使能和中断配置。

  4. 回调函数 :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]

关键原则

  1. 同一 EP 号的 IN 和 OUT 方向可以给不同 Class 使用
  2. Isochronous 端点(Audio)有固定带宽保证
  3. HID 只需要 IN 端点(设备到主机)
  4. 如果使用 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 Descriptor
  • tud_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.cUSER 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() 等核心 API
  • TinyUsb.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 设备识别不到

排查步骤

  1. 确认 PLLQ 产生了 48MHz USB 时钟
  2. 确认 __HAL_RCC_USB_OTG_FS_CLK_ENABLE() 已调用
  3. 确认 PA11/PA12 配置为 GPIO_AF10_OTG_FS
  4. 确认 OTG_FS_IRQHandler 中调用了 tusb_int_handler()
  5. 确认 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. 参考资源

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 移植的参考模板。


本文基于实际移植经验编写,如有问题欢迎交流。

相关推荐
若风的雨2 小时前
【deepseek】PCIe上电时序的详细
嵌入式硬件
’长谷深风‘2 小时前
51单片机入门
c语言·单片机·嵌入式硬件·51单片机
张海森-1688202 小时前
cv608_aac_8k_16bit_mono编码较慢,所以存为MP4,音频数据会对不齐视频数据?
单片机
沐欣工作室_lvyiyi2 小时前
基于物联网的体温心率监测系统(论文+源码)
stm32·单片机·嵌入式硬件·物联网·体温心率
电子工程师成长日记-C512 小时前
51单片机蓝牙智能台灯
单片机·嵌入式硬件·51单片机
电子工程师成长日记-C512 小时前
51单片机无线病床呼叫系统
单片机·嵌入式硬件·51单片机
somi72 小时前
51单片机-05-DHT11 温湿度传感器 | DS1302 实时时钟
单片机·嵌入式硬件·51单片机
沐欣工作室_lvyiyi3 小时前
基于腾讯云的智能家居监控系统的设计开发(论文+源码)
单片机·云计算·毕业设计·智能家居·腾讯云
辰哥单片机设计3 小时前
STM32心率血氧手环(机智云)
stm32·单片机·嵌入式硬件