STM32 USB HID 教程:TinyUSB 实现键盘设备,从描述符到枚举

背景:STM32 的 USB 开发为什么劝退

USB 协议栈是嵌入式领域学习曲线最陡的技术之一,而 STM32 的 USB 开发又把这层难度放大了:ST HAL 库把协议栈藏在层层回调后面,USB 复位、枚举、描述符交换全是黑盒,改一个字段可能牵动六个地方,出错时又不知道从哪里查------USB 分析仪一台几千块,多数人只能靠 printf 盲调。

TinyUSB 走的是反方向:把描述符从黑盒里拿出来,变成你亲手写的字节数组。本文给出在 STM32 上实现一个 USB HID 键盘设备的完整流程。

环境:STM32F4 / F7 / H7 / L4 / G4(TinyUSB 已内置移植层);CubeMX 配时钟;编译链 arm-none-eabi-gcc。

一、一个宏定下设备类型

c 复制代码
// tusb_config.h ------ 三行宏,USB 设备类型就定下来了
#define CFG_TUD_HID   1
#define CFG_TUD_CDC   0
#define CFG_TUD_MSC   0

插上 USB 线,STM32 就是一台标准 HID 设备,Windows / Linux / macOS 自带驱动。先建立直觉:USB 本质是一套问答游戏,主机问「你是什么设备」设备答「我是键盘」,主机问「数据格式」设备答「8 字节,1 字节修饰键 + 6 字节按键码」。这套问答的标准答案模板,就是描述符。

对比两种写法的差别:

c 复制代码
// HAL 的方式:描述符藏在结构体赋值里
pdev->DevDesc.bDeviceClass = 0x00;

// TinyUSB 的方式:描述符就是手写的字节数组,你写什么总线上就是什么
uint8_t const desc_hid_report[] = {
    TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(RID_KEYBOARD))
};

从数据到总线的映射是透明的------出了 bug 把数组 dump 出来跟 USB 规范对照即可。

二、HID 描述符:三份身份材料

USB 枚举的本质是设备向主机递交三份材料:

1. 设备描述符(我是谁):

c 复制代码
tusb_desc_device_t const desc_device = {
    .bLength            = sizeof(tusb_desc_device_t),
    .bDescriptorType    = TUSB_DESC_DEVICE,
    .bcdUSB             = 0x0200,
    .idVendor           = 0xCafe,   // 你的 VID
    .idProduct          = 0x4001,   // 你的 PID
    .bNumConfigurations = 1,
};

VID/PID 建议:自己玩用 0xCAFE / 0xBEEF 这类测试 ID;正式产品向 USB-IF 申请或从芯片厂商获取子授权。别用 STM32 官方 VID(0x0483),否则 Linux 上可能把设备认成 ST-Link。

2. 配置描述符(我能干什么,HID 通常只用一种配置)。

3. HID 报告描述符 (我的数据长什么样,HID 设备的灵魂)。TUD_HID_REPORT_DESC_KEYBOARD 宏展开约 60 字节,定义修饰键 1 字节、保留 1 字节、按键码 6 字节。

三、五步从编译到枚举

Step 1:CubeMX 配时钟。 USB 外设要 48MHz 精准时钟,用 HSI48 + CRS(时钟恢复)最省事,无需外部晶振。勾上 USB 外设,不要勾 ST 的 USB Device 中间件

Step 2:拉源码。 git clone https://github.com/hathach/tinyusb.git,把 src/ 拷到项目 Middlewares/ 下,移植 tusb_config.h 和芯片移植层(STM32F4 已内置)。

Step 3:写描述符。usb_descriptors.c 定义四种描述符,实现回调:

c 复制代码
uint8_t const *tud_descriptor_device_cb(void) {
    return (uint8_t const *)&desc_device;
}

Step 4:实现 HID 回调。

c 复制代码
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id,
                                hid_report_type_t report_type,
                                uint8_t *buffer, uint16_t reqlen) {
    return 0;   // 主机读报告(枚举完读一次)
}

void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id,
                           hid_report_type_t report_type,
                           uint8_t const *buffer, uint16_t bufsize) {
    // 主机发报告(键盘 LED 状态)
}

Step 5:主循环发数据。

c 复制代码
void hid_task(void) {
    if (!tud_hid_ready()) return;  // 还没枚举完,跳过

    uint8_t report[8] = {0};
    report[2] = HID_KEY_A;
    tud_hid_report(0, report, sizeof(report));   // 发送 'A'

    memset(report, 0, sizeof(report));
    tud_hid_report(0, report, sizeof(report));   // 发空报告释放按键
}

连上 USB 线,lsusb 或设备管理器里就能看到 CAFE:4001 的新设备。

四、局限

  1. 不是所有 STM32 系列都有移植层:官方支持 F4 / F7 / H7 / L4 / G4,较新的 H5 / H7RS / U5(带 USB 3.0 PHY)移植还在开发,选型前到 TinyUSB 的 supported MCU 列表确认。
  2. 多实例 HID 有限制:可同时做键盘+鼠标,但端点配置在 DCD 层复用资源池,不同实例要不同轮询间隔需改移植层。
  3. Isochronous 传输不成熟:HID 用的中断传输没问题,但 USB 麦克风/摄像头这类 Isochronous 端点只在部分 MCU(如 ESP32-S2/S3)稳定。
  4. 调试难度依然在 :代码比 HAL 透明,但协议本身没变简单。无分析仪时把 CFG_TUD_LOG_LEVEL 开到 3 看描述符请求和枚举过程,是可行的替代。

五、总结

TinyUSB 把描述符从 HAL 黑盒里拿出来,变成你直接可控的字节数组------出了问题能追、改了能懂。想要更省事可用 Adafruit 的 TinyUSB Arduino 库(枚举全自动,灵活度打折);想理解协议、要灵活控制,就用 TinyUSB。剩下的事:找根 USB 线、烧程序、看 dmesg 里弹出新设备的那一刻。

相关推荐
H_oRIZoN_8 分钟前
Linux入门DAY41(51 单片机 串口与通信协议)
linux·运维·单片机
jianqiang.xue32 分钟前
ESP-IDF保姆级入门41|产品级故障排查与稳定性优化全解:死机复位排查/内存泄漏定位/性能瓶颈分析/长期稳定性测试,掌握量产运维问题定位方法论
单片机·mcu·物联网·esp32
单片机仿真设计1 小时前
【proteus仿真】基于 STM32 的林区环境监控系统设计
stm32·单片机·proteus
归零鸟2 小时前
笔记本内屏黑屏,系统却说 “显示器一切正常“:一次 Intel PSR2 导致的 eDP 排查实录
计算机外设·电脑
Doraemomo3 小时前
IMX6ULL裸机开发——GPIO的使用
单片机·嵌入式硬件
笨笨饿3 小时前
#138_解决Codex要五次回复的问题
linux·stm32·单片机·嵌入式硬件·mcu·物联网·嵌入式实时数据库
晊晌_h4 小时前
嵌入式从0到精通——51单片机(二)
嵌入式硬件·51单片机
云泽8084 小时前
STM32 USART 详解(七):超时设置、非阻塞发送与 ReadLine 实现
stm32·单片机·嵌入式硬件
2401_862880825 小时前
UART 通信协议
单片机·51单片机
一条破秋裤5 小时前
24_MPU6050结构参数与寄存器基础
stm32·嵌入式硬件·学习