ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GD32 USB鼠标例程深度解析:从HID协议到枚举调试实战

GD32 USB鼠标例程深度解析:从HID协议到枚举调试实战 简介面向 GD32 微控制器开发者的 USB 触控鼠标完整例程包基于 USB OTG 控制器和电容式触摸传感器实现鼠标模拟涵盖 USB 协议配置、枚举、端点管理、触控坐标读取与鼠标事件上报等关键环节适合嵌入式入门及有一定基础的开发者参考移植。整个资源包共 182 个文件以 C 源码和头文件为主导78 个 c、80 个 h同时包含 Keil 工程配置文件、批处理脚本、hex 固件以及图片和文本说明等辅助材料压缩包体积约 979KB。例程还提供了触摸传感器驱动、USB 描述符配置和异常处理代码通过文档和源码对照可以快速跑通 GD32 触控鼠标实验理解 USB 协议栈与中断处理流程。目前已有 1593 人学习下载源码目录结构清晰便于按模块学习或直接迁移到其他 GD32 项目中。 做嵌入式的朋友应该都见过GD32官方库里的USB鼠标例程一个标准HID设备插上电脑动一动光标就跟着走。看起来简单实际上把这个例程吃透USB HID协议、端点通信、描述符设计、中断传输这些基本功全都能串起来。这篇我直接以GD32F30x系列标准库里的HID鼠标例程为蓝本讲清楚它是怎么跑到电脑上的以及你在移植和调试时最容易踩的那些坑。这个例程最大的价值不是“能动”而是给你一个最轻量的USB设备参考。如果你是刚接触USB协议的人鼠标是最好下手的对象结构简单、报表短、枚举过程全连驱动都不用装。如果你想做键盘、游戏手柄、自定义HID设备甚至复合设备改这个例程比从零写快得多。所以这文章既适合刚入门的同学也适合想快速把USB功能移植到产品的工程师。1. 项目思路与整体设计1.1 先搞清楚“USB鼠标”到底是个啥东西USB鼠标属于USB规范里的HIDHuman Interface Device类设备也就是人机交互设备。这类设备的共同特点是操作系统自带驱动你插上去不需要装任何东西就能用。这背后靠的是一套标准协议设备只需要告诉电脑“我是什么、我发什么格式的数据”电脑就自动按标准方式解析。GD32的鼠标例程本质上是把单片机模拟成一个标准的USB鼠标通过内部定时器或外部按键触发向上位机发送鼠标数据。数据内容一般就四到八个字节按键状态、X轴位移、Y轴位移、滚轮位移。电脑收到后HID驱动负责把位移转成光标移动。这个过程用生活的话说就是你把自己的USB外设做得“有身份证”了电脑一看就认识不用额外打招呼。选择GD32来做这个事原因很实在成本比同等级的ST低一些供货稳定库函数和HAL风格跟STM32非常接近几乎可以平替调试。而且现在GD32的生态做得相当不错标准外设库、USB协议栈、例程手册都齐全。尤其是USB部分官方例程里不光有鼠标还有键盘、U盘、自定义HID你把这个鼠标例程跑通另外几个也就在路上了。1.2 例程的整体代码结构GD32官方USB鼠标例程的结构并不复杂一般就是这几个文件usbd_hid_core.c负责HID类处理逻辑usbd_hid_core.h定义接口和结构体usbd_hid_dev.c是设备层实现里面能看到鼠标上报函数usbd_desc.c放着各种描述符数组usbd_conf.c和usbd_conf.h负责USB配置和中断处理最上面还有一个usbd_hid_demo.c之类的应用入口把初始化、按键扫描和鼠标数据绑定在一起。如果你打开例程第一眼有点懵不用慌。我的建议是先别逐行抠代码先按住四个关键点描述符数据、HID上报接口、USB中断处理、时钟配置。这四个点串起来整条数据通路就通了。单片机里的USB模块收到主机发来的事务中断标志置位然后库函数去解析令牌包若是IN请求就调用usbd_hid_report_send把数据放到端点的FIFO里主机会在下一次IN事务时把数据拿走。2. USB鼠标例程的底层逻辑2.1 为什么鼠标不需要驱动很多初学者会问为什么鼠标插上就能用但串口转USB芯片非要装驱动答案在设备描述符的bDeviceClass和bInterfaceClass字段。鼠标把接口类设置为0x03HID类并附带一个HID描述符说明自己和标准HID兼容。Windows、Linux、macOS这些系统里都有通用的HID类驱动所以枚举完成后基本无需额外安装驱动。这个设计也是典型的“标准化思维”USB只是管道真正让设备免驱动的是你声明自己属于哪一类设备。所以做USB开发理解描述符比理解代码函数更重要。描述符就像简历你的专业技能、能干什么全写在上面系统HR看到简历后决定派哪个部门来处理你。鼠标例程最值得学习的地方就是它用最少的字节数写了一份“简历”而且语法完整。2.2 描述符一步步拆解鼠标例程里通常有四个核心描述符数组都在usbd_desc.c或usbd_hid_core.c里。首先是设备描述符固定18字节。重点看idVendor和idProduct这是厂商ID和产品ID一定要改掉否则不同产品插到电脑上会认成同一个设备。bcdUSB一般设成0x0200或0x0110表示USB 2.0或1.1只要总线速度匹配就行。然后是配置描述符它会引用接口描述符、HID描述符和端点描述符。鼠标例程的配置描述符最长也就几十个字节。接口描述符里的bInterfaceClass0x03、bInterfaceSubClass0x01启动设备、bInterfaceProtocol0x02鼠标协议。这三个字段组合起来就是告诉系统我的接口属于HID类而且是鼠标你按鼠标驱动来处理我。接着是HID描述符里面有个bcdHID表示HID规范版本还有bNumDescriptors1说明后面跟着一份报表描述符。报表描述符是最容易劝退新人的部分一长串十六进制字节像天书一样。以四字节鼠标报表为例大概长这样0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x02, /* USAGE (Mouse) */ 0xA1, 0x01, /* COLLECTION (Application) */ 0x09, 0x01, /* USAGE (Pointer) */ 0xA1, 0x00, /* COLLECTION (Physical) */ 0x05, 0x09, /* USAGE_PAGE (Button) */ 0x19, 0x01, /* USAGE_MINIMUM (Button 1) */ 0x29, 0x03, /* USAGE_MAXIMUM (Button 3) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ 0x95, 0x03, /* REPORT_COUNT (3) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x81, 0x02, /* INPUT (Data,Var,Abs) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x05, /* REPORT_SIZE (5) */ 0x81, 0x01, /* INPUT (Const,Var,Abs) */ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x30, /* USAGE (X) */ 0x09, 0x31, /* USAGE (Y) */ 0x09, 0x38, /* USAGE (Wheel) */ 0x15, 0x81, /* LOGICAL_MINIMUM (-127) */ 0x25, 0x7F, /* LOGICAL_MAXIMUM (127) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x95, 0x03, /* REPORT_COUNT (3) */ 0x81, 0x06, /* INPUT (Data,Var,Rel) */ 0xC0, 0xC0这段报表描述符定义一个四字节的输入报表第一字节的低三位是三个鼠标按键高五位固定为0后面三个字节分别代表X、Y、滚轮的相对位移取值在-127到127之间。电脑收到的每一包数据都按这个格式解析所以如果改了报文字段个数报表描述符必须同步改不然设备可能被识别为“无法识别的USB设备”。端点描述符里bEndpointAddress0x81表示端点1的IN方向bmAttributes0x03表示中断传输wMaxPacketSize0x08表示最大包长度8字节。鼠标这种小数据量用中断传输就够了每轮USB传输周期轮询一次端点延迟在毫秒级完全够用。2.3 数据上报的关键流程枚举完成后主机与设备建立通信USB控制器会按配置的总线轮询间隔来定时向端点1的IN方向发IN令牌包。设备收到令牌后如果FIFO里有数据就放到总线上如果FIFO是空的设备返回NAK。主机会在下一个间隔继续尝试。在GD32库例程里上报数据的函数一般叫usbd_hid_report_send你传入一个缓冲区指针和长度库函数会帮你把数据写入对应的端点寄存器。应用层的核心操作是先定义结构体typedef struct { uint8_t button; int8_t x; int8_t y; int8_t wheel; } mouse_report_t; mouse_report_t mouse;然后根据按键状态和坐标增量填好结构体调用上报函数mouse.button (按键按下) ? 1 : 0; mouse.x move_x; mouse.y move_y; mouse.wheel 0; usbd_hid_report_send(usb_dev, (uint8_t *)mouse, sizeof(mouse_report_t));要特别注意的是x、y是有符号相对位移不是绝对坐标。如果填正数指针向右或向上移动填负数则向左或向下。用int8_t正好对应报表描述符里的LOGICAL_MINIMUM和LOGICAL_MAXIMUM范围不会越界。3. 实操把GD32鼠标例程跑起来3.1 获取官方例程和工程准备第一步先把GD32官方固件库下载下来去GD32官网或者你使用的IDE内置的包管理器找。以GD32F30x系列为例标准固件库解压后Template或者USB目录下能找到多个USB例程其中usbd_hid_mouse就是目标工程。这个工程依赖外设库里的USB驱动和系统时钟初始化文件所以整个文件夹不能随便删东西最好保留原有目录结构。打开工程前检查一下编译器的设备型号选对没。比如Keil里Target选项里的Device要选中你的具体型号GD32F303CBT6和GD32F303RCT6的外设资源不同选错可能导致链接失败。如果用的是Eclipse加GCC工具链还要注意链接脚本(.ld文件)里的Flash和RAM大小要和芯片匹配这个坑很多人在Linux环境编译时遇到过原本工程是128KB Flash你的芯片只有64KB烧写后直接跑飞。3.2 时钟配置与USB外设使能USB模块工作在480Mbps的USB 2.0 Full Speed速率下内部时钟必须是48MHz。GD32的USB时钟一般由PLL分频而来不同型号分频路径不同但有一点是一致的USB时钟不对设备绝对枚举不上。在例程的system_gd32f30x.c或usb_rcu_config里面你会看到专门为USB配置PLL和USB时钟分频的代码。如果你自己从其它例程移植过来优先检查这条时钟链。调试时想确认USB时钟是否正常最简单的办法是看设备插入电脑后有没有产生“叮咚”的连接音。有提示音但设备管理器里还是感叹号多半是描述符或HID报表有问题连提示音都没有先回头查时钟和电源。GD32的USB模块对VBUS检测引脚也有要求有些例程需要你在usb_int.c里配置VBUS外部中断检测USB插拔。如果你把它关掉了设备永远进不了枚举流程。3.3 修改按键控制和坐标增量官方鼠标例程默认的做法是用开发板上的某个按键来触发事件按下按键鼠标往一个方向移动一定距离。这很好能直观验证USB通信没问题但离真正能用还差一步。实际产品里鼠标数据往往来自传感器比如陀螺仪、光电传感器或者来自其它外部MCU的串口/SPI数据。我建议在第一阶段的调试里你先保留例程的定时上报逻辑但在外围加一个简易的按键映射把三个按键分别映射成左键、中键、右键用独立按键或矩阵键盘扫描。比如每10ms扫描一次按键扫描结果更新到mouse.button同时维护一个坐标增量变量由摇杆或编码器产生位移if (KEY_LEFT_PRESSED) { mouse.x -5; } else if (KEY_RIGHT_PRESSED) { mouse.x 5; } else { mouse.x 0; }这样测试起来比固定移动直观得多。等跑通了再把这个数据源替换成你的实际传感器接口。3.4 编译与烧录要点用Keil打开工程后直接编译通过的概率很高。下载烧录时如果提示读保护或连接失败先按住芯片的BOOT0引脚再上电复位把下器重新连接一下一般就能解决。GD32的烧录接口兼容标准SWD一个ST-Link或者DAP-Link都能用不需要额外的专用工具。烧录完成后插上USB线打开设备管理器正常情况下会看到“鼠标和其他指针设备”里多出一个“HID-compliant mouse”。如果你的开发板上有LED或者串口打印可以在初始化函数里加一段测试代码确认程序已经运行到主循环。4. 调试实战那些年踩过的USB鼠标坑4.1 插入后毫无反应这是最常见的现象而且原因往往不在代码而在硬件。首先是USB线上的D和D-有没有接反这在自制板子上特别容易出问题。其次是D上有没有接1.5kΩ或2.2kΩ的上拉电阻到3.3VFull Speed设备靠D上拉让主机感知到设备插入。很多同学画PCB时图省事把这个电阻省了结果USB永远识别不到。代码层面如果D上拉了但设备复位异常枚举也会失败。我记得有一次改完描述符后设备死活不枚举最后用示波器发现上电后USB模块一直在复位状态原因是PLL配置不稳时钟从48MHz漂到了50多MHz。后来重新按数据手册配置了USB分频系数稳定后一插就识别。所以遇到“无法枚举”的问题先测时钟波形再查描述符别一上来就怀疑协议栈。4.2 设备管理器显示未知设备或设备描述符请求失败这通常说明设备枚举过程中的控制传输出了问题。控制传输的第一个请求是GET_DESCRIPTOR(Device Descriptor)如果设备返回的数据长度或内容不对主机就会放弃枚举。常见原因有设备描述符数组长度不是18bMaxPacketSize0填的值不对比如Full Speed HID设备一般填64或者在USB中断处理里没有正确响应SETUP包。你可以用USB抓包工具看一眼总线上的数据比如Bus Hound或者USBlyzer。抓包时重点看SETUP阶段和数据阶段设备是否按主机请求返回了正确的描述符。如果你看到的返回值全是00或者FF基本可以断定是端点地址、描述符索引或中断处理逻辑有遗漏。4.3 鼠标能识别但光标不动能识别说明枚举成功光标不动说明后续中断IN传输没有正常上报。先检查主循环里有没有一直调用上报函数例程里如果只在按键边沿触发一次那松开按键之后光标当然不动。如果确认在周期上报再用抓包工具看有没有IN事务若设备一直返回NAK说明FIFO里没有数据问题出在数据写入端点的路径上。另一个容易被忽略的点是GD32库里的usbd_hid_report_send函数可能要求你在主循环里先等待上一次上报完成否则会覆盖前一个包。有的例程是通过回调函数或标志位告诉你上个月底是否已发送完毕。处理方法是加一个标志发送完后清标志主循环里检测标志再填入新数据。4.4 坐标乱跳或按键错乱坐标乱跳通常是符号问题。int8_t的取值范围是-128到127如果你从外部传感器读回一个16位有符号数直接截断到int8_t大数会变成负数表现为鼠标往相反方向跳。解决办法是提前做饱和运算大于127就钳到127小于-127就钳到-127。还见过SPI或I2C读取时序错误导致数据错位的也会表现为乱跳。按键错乱则多和报表描述符有关。比如你把报表描述符里的按键数量改成4但上报数据还是3个bit那么高位会被解释成坐标的符号位导致光标自己乱飘。记住一条原则改动报表结构改上报数据两者必须一一对应。4.5 常见问题速查表现象可能原因处理方向插上后电脑无任何反应D/D-接反D无上拉电源不稳USB时钟异常检查硬件连线量D波形确认3.3V供电设备管理器显示未知设备设备描述符错误SETUP处理异常抓包看控制传输核对描述符长度和内容枚举成功但光标不动主循环未上报端点FIFO未写入接口被占满检查上报函数调用抓包看IN端点是否返回NAK光标乱跳或反向坐标符号截断传感器数据异常用饱和运算限幅逐字打印原始数据偶尔掉线或识别慢电源毛刺USB线材差PCB布线太长加强滤波电容换短USB线检查地线回路5. 把例程改成你自己的产品跑通鼠标例程之后别急着关项目。USB HID这套体系的价值在于鼠标只是最简单的一种你完全可以顺着这条路继续扩展。最简单的扩展是把四个字节的报表改成八个字节做成自定义HID设备上位机用一个自写的小软件就能收发数据。这种方案在工业设备、数据采集、医疗仪器里很常见比虚拟串口更稳还不用装驱动。再进一步可以把这个设置成“复合设备”把键盘、鼠标、自定义HID接口做到同一个USB配置描述符里电脑会同时识别出多个设备。但要注意这时候配置描述符要包含两个接口描述符和两组端点描述符HID报表描述符也要分成两个集合不然系统只能识别其中一个。如果用GD32的 USB 功能做的是电池供电的小设备记得考虑低功耗。鼠标类设备一般支持USB挂起电脑睡眠时主机会发送挂起信号设备进入挂起状态后电流能降下来。例程里不会主动做这个需要你自己在usbd_suspend回调里关掉无关外设时钟并设置USB唤醒功能。我做过一个无线鼠标接收器的小项目这部分的功耗压下来之后整体续航提升非常明显。最后再分享一个经验调试USB这类有时序要求的外设不要靠眼睛看代码判断问题老老实实抓包。抓包工具不贵甚至可以先用逻辑分析仪在D和D-上量波形。很多看起来玄学的问题比如枚举到一半断了、数据偶尔丢包看到波形和包内容之后基本都能定位到是时钟、电阻、还是描述符的问题。把鼠标例程这个基础打牢后面做任何基于GD32的USB设备都会顺畅很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进