简介:面向 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规范里的HID(Human 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字段。鼠标把接口类设置为0x03(HID类),并附带一个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描述符和端点描述符。鼠标例程的配置描述符最长也就几十个字节。接口描述符里的bInterfaceClass=0x03、bInterfaceSubClass=0x01(启动设备)、bInterfaceProtocol=0x02(鼠标协议)。这三个字段组合起来,就是告诉系统:我的接口属于HID类,而且是鼠标,你按鼠标驱动来处理我。
接着是HID描述符,里面有个bcdHID表示HID规范版本,还有bNumDescriptors=1说明后面跟着一份报表描述符。报表描述符是最容易劝退新人的部分,一长串十六进制字节像天书一样。以四字节鼠标报表为例,大概长这样:
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设备”。
端点描述符里,bEndpointAddress=0x81表示端点1的IN方向,bmAttributes=0x03表示中断传输,wMaxPacketSize=0x08表示最大包长度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.3V,Full Speed设备靠D+上拉让主机感知到设备插入。很多同学画PCB时图省事把这个电阻省了,结果USB永远识别不到。
代码层面,如果D+上拉了,但设备复位异常,枚举也会失败。我记得有一次改完描述符后,设备死活不枚举,最后用示波器发现上电后USB模块一直在复位状态,原因是PLL配置不稳,时钟从48MHz漂到了50多MHz。后来重新按数据手册配置了USB分频系数,稳定后一插就识别。所以遇到“无法枚举”的问题,先测时钟波形,再查描述符,别一上来就怀疑协议栈。
4.2 设备管理器显示未知设备或设备描述符请求失败
这通常说明设备枚举过程中的控制传输出了问题。控制传输的第一个请求是GET_DESCRIPTOR(Device Descriptor),如果设备返回的数据长度或内容不对,主机就会放弃枚举。常见原因有:设备描述符数组长度不是18;bMaxPacketSize0填的值不对,比如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设备都会顺畅很多。
本文还有配套的精品资源,点击获取