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 里弹出新设备的那一刻。

相关推荐
蓝速科技1 小时前
蓝速鸿蒙信创终端长效流畅性深度评测
arm开发·华为·harmonyos
泛凡(Linyongui)2 小时前
第三篇、生命的bug分两种:一种是Flash坏了,一种是RAM乱了
人工智能·单片机·程序人生
恒锐丰小瑞2 小时前
率能SS8844T 40V/2.5A/四通道1/2H桥驱动芯片,IN/EN独立控制接口与低导通电阻(350mΩ),用于纺织机械/工业自动化/机器人
单片机·嵌入式硬件·机器人·自动化
Hello_Damon_Nikola2 小时前
TPA3116D2DADR从入门到精通
人工智能·单片机·嵌入式硬件·物联网
zlinear数据采集卡2 小时前
数据采集卡从入门到精通(46):趋势展望——软件定义硬件、AI分析与开源生态
arm开发·stm32·单片机·嵌入式硬件·fpga开发·开源
智购科技自动售货机工厂2 小时前
2026自动售货机电机驱动芯片选型:从L298N到DRV8870的工程实践~YH
大数据·开发语言·数据库·人工智能·单片机·嵌入式硬件·scikit-learn
恒锐丰科技林技术员3 小时前
SS8918T 微步进电机驱动芯片:高性能步进运动控制解决方案
经验分享·嵌入式硬件·硬件工程
2601_966799043 小时前
酷嗨米H300三路超清采集盒深度解读:重构三机位直播的硬件解决方案
嵌入式硬件
hongmai6668883 小时前
把玩ESP8684-WROOM-04C-H4X:一颗耐高温的RISC-V小钢炮
笔记·单片机·嵌入式硬件·物联网·risc-v