尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32免驱HID设备:从报告描述符到Windows上位机通信

STM32免驱HID设备:从报告描述符到Windows上位机通信 简介针对需要在Windows宿主机上实现免驱USB人机交互设备的嵌入式开发场景这份STM32 USB HID资源包将协议原理与工程实践融为一体。内容先从HID协议基础讲起说明报告描述符如何定义数据格式再依托STM32的USB外设控制器解析SETUP、IN、OUT三类事务的处理流程并以RVMDK工程示例演示完整的设备枚举与数据收发。包内共188个文件核心为69个C源文件与75个头文件辅以36个汇编启动文件、2份PDF、1份docx说明文档以及uvproj/uvopt等可直接导入Keil MDK的工程配置整体仅2.16MB紧凑而不失完整。代码、文档、硬件库、工程四类内容分目录存放User目录提供用户层示例Libraries目录内置固件库doc中附有协议与调试说明RVMDK目录则给出可直接编译运行的开发环境配置。已有173人学习适合从零搭建自定义HID键盘、鼠标等设备也可作为USB协议学习和固件二次开发的参考实现。1. 把 STM32 枚举成 Windows 免驱 HID 设备的完整链路搜到 usb_hid.rar 与 HID协议、STM32、Windows usb通信 这几个关键词时想做的事情通常很明确让 STM32 枚举成一个 HID 设备在 Windows 里不装厂商驱动就能收发数据。HID 协议的特点是免驱、即插即用、实时性好适合参数下发、状态回传和简单文件传输但不适合大带宽吞吐。这篇文章按选型、CubeMX 配置、上位机三种实现、抓包定位四个层次展开给出可直接复现的工程片段和排错路径。不管手里是有现成的 usb_hid.rar 工程包要改还是从零开始建工程都能对应上。2. HID 协议与 STM32 USB 外设的选型前提2.1 HID 协议为什么能免驱报告描述符是核心HIDHuman Interface Device协议的免驱特性来自 Windows 内置的 hidclass 和 hidserv 驱动栈系统不关心设备是鼠标、键盘还是自定义仪表只按报告描述符解析数据。设备描述符、配置描述符、接口描述符和端点描述符决定了 USB 枚举的基本形态而报告描述符Report Descriptor决定了一次数据交换里每个字段的 bit 宽度、数值范围和用途。这个描述符是 HID 协议区别于其他类别的关键它是一张“数据布局图”主机靠它知道哪几个字节是坐标、哪几个字节是按键、整个报表一共多长。标准 HID 设备必须在枚举阶段对 GET_DESCRIPTOR(Report) 请求返回这份描述符主机走完这套流程后设备就会出现在设备管理器的“人机接口设备”分类下。对自定义通信设备来说最稳妥的做法是把 Usage Page 设成 Vendor Defined0xFF00。这么做有两个理由一是 Windows 不会把数据当作键盘或鼠标去做系统级过滤用户态程序可以直接打开设备进行读写二是如果用了标准的 Generic Desktop 鼠标页系统会尝试消费其中的按钮和坐标数据即使上位机强行打开设备底层报表解析也会出乱子。2.2 STM32 的 USB 控制器48MHz 时钟与端点资源STM32 各系列在 USB 外设上的实现策略不同。以 STM32F103C8T6 为代表的 USB FS Device 控制器要求 PLL 输出精确的 48MHz 时钟供 USB 内核使用。外部晶振 8MHz倍频到 72MHz 后经过 USB 预分频 1.5 得到 48MHz如果板子上用的是 HSI也需要通过 PLL 尽量校准到 48MHz偏差稍微大一点Windows 侧就直接表现为枚举失败或者插拔几次后设备消失。CubeMX 的 Clock Configuration 页面中USB 时钟来源必须显示为 PLLQ数值要精确落在 48.000MHz不是“大约”也不是“48.01”。端点资源方面STM32F103 的 USB 控制器提供 8 个双向端点端点 0 固定做控制传输其余 7 个可以分配给应用。HID 类设备默认用端点 1 的中断 IN 和中断 OUT 即可完成双向通信一个端点号全部覆盖。相比之下CDC 虚拟串口方案需要两个批量端点加一个中断通知端点占用的端点资源和寄存器配置复杂度都会增加。这也是 HID 在小数据量双向通信场景里更省事的原因。2.3 中断传输就是 HID 的数据通道间隔与带宽HID 的数据交互走中断传输。端点描述符里的 bInterval 字段决定主机多久轮询一次设备全速设备最小值是 1ms最大值 255ms。单个中断事务的最大有效负载是 64 字节也就是说即使设备在数据变化最频繁时也只能等下一个轮询周期把包送出去。这类链路适合单次 864 字节、毫秒级交互的控制指令、状态读取和应答包不适合把固件镜像拆成 64 字节一包慢慢推除非你的升级时间预算足够宽裕。中断传输的轮询间隔设置需要结合产品体验来权衡。bInterval 设为 1ms 时上报延迟最低但也意味着主机每毫秒发起一次轮询总线上会持续出现信任令包设成 10ms 则吞吐量下降上位机 read 的响应时间会拉长到十几毫秒。很多“设备识别了但很卡”的问题其实是 bInterval 被配成了 255主机端每次 read 要等约四分之一秒。Windows 对 HID 轮询并不是每毫秒精确执行实际间隔还会叠加系统调度和 hub 延迟所以需要低延迟时直接设 1不要留人为裕量。3. CubeMX 里生成 STM32 自定义 HID 工程及报告描述符修改3.1 CubeMX 最小配置步骤3.1.1 RCC 和 USB FS 的时钟设置在 CubeMX 里新建 STM32F103C8T6 工程System Core RCC 中 HSE 选择 Crystal/Ceramic Resonator芯片上 PA11 和 PA12 自动复用为 USB DM 和 USB DP。左侧 Connectivity 里打开 USB 外设在 Device (FS) 的 Class for FS IP 下拉里选择 Human Interface Device。然后切到 Clock Configuration把 HCLK 拉成 72MHz确认 USB 时钟源显示 PLLQ 且数值是 48MHz。这一步如果不对生成的代码枚举时会不定时失败。3.1.2 中断优先级与 VID/PIDNVIC 设置里给 USB_LP_CAN1_RX0_IRQn 一个中等偏高的抢占优先级低于 SysTick但高于外部业务中断。USB_DEVICE 配置页里能找到默认的 VID0x0455和 PID0x5723量产产品改为自己的值调试阶段保持不变也可以。生成工程后代码集中在 usbd_hid_desc.c、usbd_hid.c 和 usbd_conf.c。默认生成的 usbd_hid.c 里带的是鼠标报告描述符要整段替换成自定义版本。3.2 自定义报告描述符怎么写下面这段是 64 字节双向报告描述符Usage Page 设成 Vendor DefinedIN 和 OUT 各带一个 64 字节数据数组__ALIGN_BEGIN static const uint8_t HID_ReportDesc[] __ALIGN_END { 0x06, 0x00, 0xFF, /* Usage Page (Vendor Defined 0xFF00) */ 0x09, 0x01, /* Usage (Vendor Usage 1) */ 0xA1, 0x01, /* Collection (Application) */ 0x19, 0x01, /* Usage Minimum (1) */ 0x29, 0x40, /* Usage Maximum (64) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ 0x75, 0x08, /* Report Size (8 bits) */ 0x95, 0x40, /* Report Count (64) */ 0x81, 0x02, /* Input (Data, Var, Abs) */ 0x19, 0x01, /* Usage Minimum (1) */ 0x29, 0x40, /* Usage Maximum (64) */ 0x75, 0x08, /* Report Size (8 bits) */ 0x95, 0x40, /* Report Count (64) */ 0x91, 0x02, /* Output (Data, Var, Abs) */ 0xC0 /* End Collection */ };描述符里的 0x75 和 0x95 必须成对出现Report Size 定义每个字段位数Report Count 定义字段个数。把这两个值相乘就是报表的字节数。这里把 Input 报表定义为 64 字节Output 报表也定义为 64 字节正好对应端点描述符里的 wMaxPacketSize。如果将 Report Count 改成 8那么端点最大包长可以保持 64 不变上位机读到的报文会按实际长度返回不强制补零到 64 字节。反之如果报表长度超过端点最大包长USB 层会自动分帧上位机端必须自行拼包才能还原完整数据。3.3 端点的发送接收与回调函数usbd_hid.h 中定义了端点地址和数据长度宏#define HID_EPIN_ADDR 0x81 #define HID_EPOUT_ADDR 0x01 #define HID_IN_PACKET_SIZE 64 #define HID_OUT_PACKET_SIZE 64发送一包数据直接调用uint8_t buf[64] {0}; uint8_t res USBD_HID_SendReport(hUsbDeviceFS, buf, sizeof(buf)); if (res ! USBD_OK) { /* 返回 USBD_BUSY 表示上一包还没发送完成需要重试或丢弃 */ }USBD_HID_SendReport 内部走 USBD_LL_Transmit 把数据提交到端点寄存器返回 USBD_OK 只代表数据已经进硬件缓冲区不表示主机已读取。对低功耗场景发送后立刻修改 buf 内容是危险的DMA 传输可能读到半包脏数据。稳妥做法是维护一个发送完成信号量在 HID_DataIn 回调里置位。接收方向有个高频踩坑点CubeMX 默认不会主动让 OUT 端点进入接收就绪。需要在初始化阶段手动调用USBD_LL_PrepareReceive(hUsbDeviceFS, HID_EPOUT_ADDR, rx_buf, HID_OUT_PACKET_SIZE);数据到达后usbd_hid.c 里的 HID_DataOut 被调用static int8_t HID_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum) { /* 处理 rx_buf 中的数据处理后重新 PrepareReceive 收下一包 */ USBD_LL_PrepareReceive(pdev, HID_EPOUT_ADDR, rx_buf, HID_OUT_PACKET_SIZE); return USBD_OK; }第一包能收、第二包开始上位机写超时基本就是没有重新调用 PrepareReceive。端点只启动一次接收用完就停了。异常现象最可能原因检查点Windows 报告“无法识别的 USB 设备”USB 时钟不是 48MHzClock Configuration 中 PLLQ 是否精确枚举正常但上位机读写全超时OUT 端点未 PrepareReceiveHID_DataOut 后是否重新调用设备出现但被系统当作鼠标/键盘报告描述符用了标准 Usage Page改为 Vendor Defined 0xFF00收发都成功但内容错位报告描述符长度与端点长度不一致核对 Report Count 与 wMaxPacketSize4. Windows 端 USB HID 通信的读写实现Python 与 C#4.1 先确认枚举结果再写代码在设备管理器里展开“人机接口设备”能看到 HID-compliant device 等条目。右键选“详细信息”-“设备实例路径”里面带 VID_xxxxPID_yyyy 的字符串就是设备的身份标识。这一步能快速定位问题层级如果设备出现在“通用串行总线设备”下面而不是“人机接口设备”说明接口描述符里的类代码或 HID 描述符有误程序写得再好也打不开设备。用 PowerShell 可以更精确地过滤Get-PnpDevice -Class HIDClass | Where-Object { $_.InstanceId -like *VID_1234* } | Select-Object FriendlyName, Status命令中的 VID_1234 换成烧录到 STM32 的实际 VID。Status 为 OK 表示 USB 枚举完成且 HID 类驱动已绑定接下来才能进入数据收发层。如果这里找不到设备优先检查时钟和描述符而不是折腾上位机代码。4.2 Python hidapi 的最小读写片段Windows 上先安装 Python 的 hidapi 包pip install hidapi下面的代码完成枚举、打开、发送和读取import hid VID, PID 0x1234, 0x5678 # 枚举目标设备 for dev in hid.enumerate(VID, PID): print(dev[path], dev[product_string], dev[interface_number]) dev hid.device() dev.open(VID, PID) dev.set_nonblocking(False) payload bytes([0x01, 0x02, 0x03]) b\x00 * 61 dev.write(b\x00 payload) # 第一个字节是 report ID没有 ID 也要占位 report dev.read(64, timeout_ms500) print(report)这段代码里 write 的入参长度比实际报表多 1 字节因为 hidapi 约定第一个字节放 report ID。报告描述符里没有定义 report ID 时这个位置固定填 0下位机收到的第一个数据字节是 0x01 而不是 0x00。这是新手最容易搞反的地方。read 的返回长度取决于设备实际上报的字节数64 字节报表如果只填充了 32 字节设备端也会按 64 字节发送但缓冲区内未初始化部分是脏数据需要下位机侧做清零或用长度字段约束。当系统里同时插着多个相同 VID/PID 的 HID 设备时按 path 打开比按 VID/PID 更可靠dev.open_path(b\\\\?\\hid#vid_1234pid_5678#7...)path 从 enumerate 输出里取避免每次打开都指向第一个设备。4.3 C# 用 HidLibrary 做上位机收发Windows 上位机界面如果用 WPF 或 WinForms我用 HidLibrary 比较多。NuGet 安装后在代码里写using HidLibrary; var devices HidDevices.Enumerate(0x1234, 0x5678); var device devices.FirstOrDefault(); if (device null) return; device.OpenDevice(); device.ReadReport(OnReport, timeout: 1000); var outReport device.CreateReport(64); outReport.Data new byte[64]; outReport.Data[0] 0x01; device.WriteReport(outReport);ReadReport 的回调签名是 Action 收一包触发一次。因为 HID 中断传输在系统线程里执行回调里直接操作 WPF 控件会引发跨线程异常或界面卡顿。常见做法是把原始包放进 ConcurrentQueue再由 UI 线程的 DispatcherTimer 定时消费队列。热插拔事件 Inserted 和 Removed 在设备路径变化后会自动失效拔出再插入后原 device 对象指向的是旧路径需要重新 Enumerate所以我在实际项目里并不依赖这两个事件做复杂逻辑只在 Removed 事件里触发延迟重扫。4.4 Windows 端高优先级检查点按出现的频率排列Windows 端收不到数据有三个原因。第一报告描述符的长度与端点长度不一致上位机 read 的阻塞时间和实际数据不匹配。第二上位机打开设备的瞬间下位机还没有完成 USB 配置阶段第一包发送就直接超时需要在设备打开后做一次握手重试。第三设备被系统当作标准输入设备独占这个问题的根源是报告描述符里用了鼠标或键盘的 Usage Page上位机程序在打开设备时会得到 access denied。把这三点先排除再考虑业务层的解析逻辑。如果碰上使用 usb抓包 才能定位的场景Windows 下拉 Wireshark 加 USBPcap 是标准路径。抓包能看到设备层实际交互比上位机日志和下位机调试打印更接近真相下一章重点讲。5. USB 抓包验证 STM32 HID 枚举与收发异常的定性方法5.1 设备管理器报错对应哪层问题设备管理器报“无法识别的 USB 设备”时问题基本在 USB 设备层48MHz 时钟偏差、DP/DM 上拉异常、描述符字节错误。如果设备已经显示为 HID-compliant device 但数据收发超时问题在报告描述符、端点和应用层。先按这个分层定位再去动代码避免在下位机业务逻辑里反复加打印却看不到全局。USB 枚举过程可以比喻成一次握手主机请求设备描述符、设置地址、再请求配置描述符任何一步响应超时或返回长度错误Windows 都会放弃加载驱动。5.2 Wireshark 抓中断 IN 端点数据安装 USBPcap 驱动后用 Wireshark 选择对应的 USB Root Hub 接口设置过滤条件usb.idVendor 0x1234 (usb.transfer_type 0x03)transfer_type 0x03 表示中断传输。抓包结果里能看到设备地址、端点和数据长度。如果只有主机发出的 IN 令牌而没有设备应答说明设备端没有调用 SendReport如果应答的数据长度和上位机期望不一致回去比对报告描述符的 Report Count 和端点 wMaxPacketSize。枚举阶段的 GET_REPORT_DESCRIPTOR 请求也能在 URB 详情里展开对照返回的字节流可以精确看出描述符在设备的第几个字节处写错。5.3 一次改动描述符后的验证方案改完报告描述符后先重新插拔设备确认设备管理器里的设备类型没有变化再由上位机连续发送 100 包回显数据下位机按序号校验并返回上位机比对丢帧率。中断传输本身有握手机制单包丢失的情况较少更多是时序抖动和端点忙导致的重试。如果怀疑上报太慢检查 bInterval 是否被人为设大。这个验证方案不依赖额外的硬件设备一套 STM32 开发板加一根 USB 线就能完成能在 10 分钟内定位大部分枚举失败和数据异常的问题。本文还有配套的精品资源点击获取
返回列表