十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RK3568 USB鼠标驱动开发实战:从枚举到HID解析的完整链路

RK3568 USB鼠标驱动开发实战:从枚举到HID解析的完整链路 RK3568这块板子做USB鼠标驱动开发很多人第一反应是这有什么好开发的内核不是自带驱动吗。这话对了一半等你真正把鼠标插上去发现设备节点没出来、按键乱跳、或者上报坐标对着增量的数值一脸茫然的时候才会明白USB设备的驱动开发完全不是写个字符设备那么简单。它牵扯到USB协议栈、HID子系统的解析、input子系统的上报机制还有设备树里那一堆phy和控制器节点的配置。本文我会从RK3568的USB控制器资源开始讲把鼠标从物理插上到应用层收到事件的整条链路拆开然后给出设备树、内核配置、三种驱动实现路线和完整的排查方法。这个内容是面向实际开发的适合要在RK3568上做USB外设适配的工程师也适合对Linux USB设备驱动完全没概念的入门者——我尽量把协议层面的概念说得像聊天一样但后面涉及代码的部分不会省。1. RK3568的USB控制器资源开工前先搞清楚硬件底牌1.1 三路USB控制器各管什么RK3568这颗SoC的USB控制器资源和RK3399那一代比变化不小。它集成了三路独立的USB控制器一路是USB 3.0 OTGDWC3控制器另外两路是USB 2.0 Host通常以EHCI/OHCI的形式出现在设备树里。如果你拿到的是带有Type-C口的核心板通常那个Type-C口连的就是USB 3.0 OTG控制器它向下兼容USB 2.0和USB 1.1也就是说USB 2.0的Hub、USB键盘鼠标、U盘它都能带得动。剩下两路USB 2.0 Host一般从板子上引出来做标准A口或者排针。有些板子还会在中间接一颗USB Hub芯片把一路Host扩展成四路甚至更多这个在设备树里是看不出来的因为Hub对软件来说是透明的——只要系统里能看到usb 1-1.3这样的拓扑层级说明中间有Hub存在。看清楚控制器分布的意义在于你在调设备树的时候得知道改的是哪个节点以及这个节点对应的是板子上的哪个物理接口。改错了节点鼠标插哪个口都没反应排查起来非常折磨。1.2 为什么鼠标驱动不能当普通字符设备写很多从单片机转过来的工程师有个惯性思维写个驱动注册一个file_operationsopen/read/write就完事了。但USB设备不是这么玩的。USB是一个host和设备一对多通信的总线架构主机端通过地址区分设备设备内部又通过接口interface区分功能。鼠标属于人机交互设备HID它采用的是中断传输方式以固定的间隔比如8ms主动向主机上报数据。整个数据的传输方向、时间、格式USB控制器硬件和Linux内核的usbcore早就定义好了。换句话说USB鼠标驱动开发的核心工作不是去写怎么把数据从硬件读出来而是要理解Linux已经搭好的框架里你的设备会被哪一层认领、数据在哪一层被解析、之后怎么路由到input子系统。你要是企图绕过这套框架自己从端点读原始数据那等于自己重新实现一个USB协议栈完全不现实系统也不会允许。2. USB鼠标的底层工作原理枚举、描述符与中断传输2.1 从插上鼠标到/dev/input出现中间发生了什么把鼠标插到RK3568的USB A口到系统里出现/dev/input/eventX节点这个看似瞬间完成的过程实际上经历了一套复杂的握手流程叫USB枚举。这个过程大致是这样的鼠标插上后USB主机控制器在Hub端口的D线上检测到电平变化判断有设备接入。主机给这个端口发送复位信号复位完成后设备地址为0主机通过端点0默认控制端点向设备发送获取设备描述符请求。设备返回18字节的设备描述符里面包含了设备类型bDeviceClass0表示类型在接口描述符里定义、厂商IDidVendor、产品IDidProduct等信息。接着主机会继续读取配置描述符。这一步稍微特殊一点主机先请求9字节的配置描述符从里面拿到这个配置的总长度然后再一次性把所有描述符读完。这些描述符可能包括接口描述符、端点描述符、HID描述符如果是复合设备比如带键盘的鼠标还会有多个接口描述符。读完后主机给设备分配地址再次设置配置Set Configuration设备才算真正进入工作状态之后通过中断端点周期性地往主机发送鼠标运动数据。2.2 描述符和报表描述符到底在说什么描述符是USB设备用来自我介绍的数据结构。鼠标枚举的时候主机最关心的几个字段是这么用的接口描述符里的bInterfaceClass必须是0x03HID类bInterfaceProtocol如果是0x02表示是鼠标boot protocol模式0x01是键盘。这一步在内核里决定了这个设备会被usbhid驱动认领。端点描述符里bEndpointAddress标明中断端点的地址bInterval标明查询间隔。低速鼠标固定是10ms的间隔全速鼠标可以是1ms、2ms、4ms或8ms。接口描述符后面还跟着一个HID描述符它里面有一个关键的字段叫wReportDescriptorLength这个值表示后面跟着的报表描述符有多少字节。报表描述符是最让人头晕的东西。它不像标准的USB描述符那样字段是固定的而是一堆不定长的条目组成的、描述数据格式的脚本。鼠标的报表描述符大致会定义两个用途页Usage Page一个是Generic Desktop通用桌面里面定义鼠标Mouse、X轴X、Y轴Y、滚轮Wheel另一个是Button按键定义左键、右键、中键。每个字段都会有逻辑最小值、物理最小值、报告大小、报告数量这些属性。Linux的hid-input驱动拿到这些信息后用hid_parser逐条解析然后创建对应的input设备注册按键和相对位移轴。2.3 鼠标是低速设备这个身份影响了什么USB鼠标绝大多数是低速设备Low-Speed1.5Mbps少数游戏鼠标是全速设备Full-Speed12Mbps。这个身份在驱动开发中会带来几个实际影响。第一低速设备不支持某些传输类型比如等时传输Isochronous和批量传输Bulk所以鼠标只能通过中断端点传输数据。第二低速设备的中断端点最大包大小上限是8字节一个鼠标报告如果超过8字节比如带侧键、带RGB灯控制的鼠标它就必须是全速设备。第三某些USB控制器对低速设备有兼容性问题比如早期的RK方案上偶尔会遇到低速设备枚举失败的情况排查方向多半得落在端接电阻和phy配置上。你拿到一个鼠标第一步用lsusb看它的bcdUSB和bMaxPacketSize0基本就能判断它是低速还是全速。这个判断影响的不只是理论带宽更重要的是驱动里你对上报频率和数据包大小的预期。3. Linux内核的USB HID链路鼠标事件从硬件到用户态走过的四层3.1 协议栈各层职责Linux内核的USB子系统从下往上分四层鼠标数据包的旅程是一层一层递送的。你如果没把这四层搞清楚写驱动的时候就会经常迷失方向。最底层是USB主机控制器驱动。在RK3568上USB 3.0 OTG用的是DWC3控制器驱动USB 2.0 Host用的传统EHCI/OHCI兼容驱动。这一层负责和硬件打交道收发URBUSB Request BlockUSB请求块。对鼠标来说host控制器驱动会在枚举完成后创建一个中断URB提交给usbhid驱动。URB往上是usbcore这一层它管理总线拓扑、设备地址、配置选择。再往上是usbhid驱动它负责和HID类设备通信提交中断URB、收集原始报告、解析HID报表描述符。usbhid驱动有一个核心的机制叫HID protocol它会根据解析结果调用hid-input的回调函数。最上面就是hid-input、hid-generic以及input核心了。hid-input把HID的Usage映射成Linux input子系统的事件比如把Usage Page0x01, Usage0x30映射成REL_X事件。3.2 一个完整的鼠标事件上报路径以常见的3键滚轮XY轴鼠标为例当鼠标移动时硬件把位移封装成7字节的报告1字节modifier 1字节保留 1字节X 1字节Y 1字节滚轮 2字节按键状态通过中断端点发送。DWC3控制器收到数据后通过DMA把数据放到内存产生中断xhci-hcd驱动的中断处理函数在中断上下文里调用usb_hcd_giveback_urb把这个URB的回调函数usbhid的回调调度执行。usbhid拿到这个报告后会把它交给hid_input_report再进入hid-core的解析流程根据HID报表描述符里定义的结构把字节流拆解成一个个field的值然后调用hidinput_report_event。这里它会判断这个field对应的是绝对位移轴还是相对位移轴对应的按键码是BTN_LEFT还是BTN_RIGHT然后调用input_report_rel/input_report_key往input子系统上报。input子系统拿到事件后会更新对应设备的状态再调用input_event把事件写入evdev客户端的缓冲区。你用户态用evtest去读/dev/input/eventX看到的就是struct input_event结构的原始事件列表里面有type、code、value三个关键字段。type2表示相对位移EV_RELcode0是X轴value是这一次的位移增量。这个链路的每一层你都可以通过内核日志、动态调试、以及usbmon抓包来观测。掌握这条链路后调试思路就会非常清晰鼠标没数据先看URB有没有提交再看中断端点有没有数据回传再看hid-input有没有把事件映射出来。4. RK3568设备树与内核配置让鼠标被系统认出来的前提4.1 设备树中与USB相关的节点在RK3568上做USB鼠标适配设备树通常是第一个会被打开的文件。以常见的RK3568方案设备树为例里面会有两个关键层级的节点USB控制器节点和USB PHY节点。DWC3控制器节点长这样usbdrd3_0 { status okay; }; usbdrd_dwc3_0 { dr_mode otg; extcon u2phy0; status okay; }; usb2phy0 { status okay; }; u2phy0_host { phy-supply vcc5v0_host; status okay; }; u2phy0_otg { vbus-supply vcc5v0_otg; status okay; };这里usbdrd3_0是DWC3控制器的父节点usbdrd_dwc3_0是真正起作用的子节点。dr_mode的取值直接决定这个控制器跑在什么模式下——如果你希望RK3568的Type-C口接鼠标dr_mode不能设成peripheral得是host或者otg。很多板子出厂默认在device模式插鼠标上去内核里连枚举过程都没有因为控制器压根没有以host的方式运行。USB PHY节点也一样敏感。usb2phy0下面通常有usb2phy0_host和usb2phy0_otg两个子节点分别对应Type-C口的host和otg功能。有些RK3568的SDK里这两个子节点默认status是disabled需要你把对应功能的节点打开。还有一点容易被忽略USB接口的5V供电。很多板子会用GPIO控制的电源开关给USB口供电设备树里通过phy-supply或者vbus-supply字段引用。如果你插上鼠标之后设备完全没有检测到连dmesg都没有任何信息先检查这个电源有没有打开用万用表量一下VBUS引脚电压这是物理层最基础的一关。4.2 内核选项裁剪的坑RK3568的设备厂商SDK通常默认打开了HID和USB HID的配置但如果你的内核是自己裁剪的或者是从某个精简配置改过来的有几个配置项必须确认CONFIG_USBy CONFIG_USB_XHCI_HCDy CONFIG_USB_EHCI_HCDy CONFIG_USB_OHCI_HCDy CONFIG_USB_DWC3y CONFIG_USB_DWC3_DUAL_ROLEy CONFIG_USB_HIDy CONFIG_HIDy CONFIG_HID_GENERICy CONFIG_INPUTy CONFIG_INPUT_EVDEVy这里最容易被遗漏的是CONFIG_HID_GENERIC。很多人在裁剪内核时保留了CONFIG_USB_HID觉得就够了结果发现鼠标插上后usbcore枚举成功、usbhid也认领了但就是没有input设备生成。原因是新内核4.2之后把HID设备到具体驱动的绑定交给了hid-generic模块没有它hid-core不知道把设备丢给谁来解析。确认内核配置的方式很简单在板子上执行zcat /proc/config.gz | grep CONFIG_USB_HID如果/proc/config.gz不存在说明内核没有开启IKCONFIG_PROC那就去内核源码目录用make menuconfig重新确认。从实际经验来看设备树和内核配置这两关过不了后面谈驱动开发都是空中楼阁。你这会儿花十分钟把配置确认好比后面在驱动里加一堆调试打印管用得多。5. 驱动的三种实现路线内核HID、hidraw与自定义驱动5.1 默认内核驱动已经能跑什么时候要写驱动先说结论对标准USB鼠标Linux内核的usbhidhid-generic组合已经非常成熟正常情况下插上就能用完全不需要写任何驱动代码。那为什么还会有RK3568 USB鼠标驱动开发这个需求我总结了实际项目中碰到的几类场景。第一类是设备不是标准鼠标而是带特殊功能的HID设备比如带触摸板、带光学传感器数据输出、带自定义按键默认映射不符合需求需要自己解析报表描述符。第二类是鼠标上报的数据需要分发到非input子系统的通道例如你想把鼠标位移数据直接送到某个字符设备给应用程序读就需要额外写一个驱动绑定到usb接口上。第三类是系统做了深度裁剪去掉了hid和input子系统想在RTOS或者裸机环境下自己实现一个极简的USB鼠标驱动那路线完全不同不在本文讨论范围。5.2 自写驱动的推荐路线如果确实需要自己写驱动不要从零开始推荐的路线是基于hid子系统写一个自定义的hid驱动注册到HID总线。原理是usbcore完成USB枚举后把interface挂到HID总线上hid-core会通过id_table匹配设备和驱动。你写的驱动只需要关心从HID层拿到解析后的字段值而不需要关心USB协议的细节。最小驱动的大致结构是这样#include linux/hid.h #include linux/module.h static const struct hid_device_id my_mouse_id_table[] { { HID_USB_DEVICE(0x1234, 0x5678) }, { } }; static int my_mouse_probe(struct hid_device *hdev, const struct hid_device_id *id) { int ret; ret hid_parse(hdev); if (ret) return ret; ret hid_hw_start(hdev, HID_CONNECT_HIDINPUT); if (ret) return ret; return 0; } static void my_mouse_remove(struct hid_device *hdev) { hid_hw_stop(hdev); } static struct hid_driver my_mouse_driver { .name my_mouse, .id_table my_mouse_id_table, .probe my_mouse_probe, .remove my_mouse_remove, }; module_hid_driver(my_mouse_driver); MODULE_LICENSE(GPL);然后在raw_event回调里处理原始数据static int my_mouse_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { struct my_mouse_dev *priv hid_get_drvdata(hdev); priv-x (s8)data[1]; priv-y (s8)data[2]; return 0; }这样写的好处是设备的枚举、配置、端点管理全交给内核USB栈你不需要自己去提交URB或者处理复位。驱动只需要把握住产生数据之后怎么处理这一环。当然前提是你的鼠标厂商ID和产品ID在id_table里匹配上了否则内核根本不会把设备绑定到你的驱动上。如果你的需求是把鼠标数据直接喂给应用程序不想走input子系统可以调用hid_hw_start(hdev, 0x0)禁用HID连接然后用hidraw接口或者自己注册一个miscdevice来暴露数据。不过这种方式会丢到Linux标准输入体系的所有好处多进程共享、热插拔管理、evdev标准化除非有明确理由我不建议这么干。6. 实战排障鼠标插上没反应/按键错乱的排查链路6.1 第一步dmesg确认枚举每次排障都是从dmesg开始的。插上鼠标后立刻执行dmesg | tail -n 30正常的枚举日志会是这样usb 1-1: new low-speed USB device number 3 using xhci-hcd usb 1-1: New USB device found, idVendor1234, idProduct5678 usb 1-1: New USB device strings: Mfr1, Product2, SerialNumber0 usb 1-1: Product: USB Mouse input: USB Mouse as /devices/platform/usbfcc00000/... hid-generic 0003:1234:5678.0001: input,hidraw0: USB HID v1.10 Mouse如果连第一行都没出现问题出在物理层或控制器配置上。优先查顺序USB口供电 - phy配置 - 控制器status。如果停留在device not accepting address之类的错误大概率是信号完整性问题比如线材过长、Hub级联太多、或者电源波动。如果枚举成功但没有hid-generic认领的信息基本就是内核配置缺少CONFIG_HID_GENERIC或者鼠标的接口类不是HID类极少见又或者是id_table和驱动绑定没匹配上。6.2 第二步lsusb看描述符枚举没问题但没有input节点下一步就要看设备描述符是否正常lsusb lsusb -v -d 1234:5678重点关注几个字段。检查bInterfaceClass是否等于03HID。检查bEndpointAddress和bInterval确认中断端点和轮询间隔符合预期。检查HID描述符里wReportDescriptorLength是否合理鼠标一般30到100字节之间太长反而可疑。这里分享一个经验有些杂牌鼠标的HID报表描述符写得非常粗糙逻辑最小值和物理最小值的比例失当导致hid-input解析出来的位移方向和速度完全不对。lsusb -v的输出里会附带整段报表描述符的十六进制dump你可以自己解析或者把它抄出来在HID Descriptor Tool里导入可视化地看每个字段的定义。6.3 第三步evtest/hexdump看事件描述符正常但仍没有数据或者数据异常就要看input子系统有没有上报事件。用evtest非常直观evtest /dev/input/eventX移动鼠标时正常会看到type 2 (EV_REL)的REL_X和REL_Y事件按键会有type 1 (EV_KEY)的BTN_LEFT事件。如果evtest没有任何输出问题在HID层到input层之间。如果输出有但值不对比如鼠标往右移动时REL_X反而变负或者是REL_X和REL_Y反了那就是报表描述符解析后的映射问题。/proc/bus/input/devices也是排查利器它能看到系统识别到的所有input设备以及这个设备通过哪些handler暴露。如果设备在这里存在但/dev/input/eventX没有对应的节点多半是devtmpfs没挂载或者mdev/udev没配置。6.4 第四步usbmon抓包定位到了这步还没定位就需要抓USB总线上的原始数据了。usbmon是内核自带的USB协议分析工具类似网络抓包。使用方法modprobe usbmon cat /sys/kernel/debug/usb/usbmon/1u /tmp/usbmon.log然后操作鼠标若干秒CtrlC停止。抓下来的日志里每行代表一个URB格式大致是URB id... ep81 IN int interval10 typeC如果看到URB不停地提交但数据全是零说明鼠标硬件在发送空包问题在设备端或报告解析上。如果URB压根没被创建说明usbhid没有正确提交中断请求可能是HID描述符里的接口信息异常。usbmon抓出来的raw数据可以配合十六进制解析工具把每次中断传输的8字节数据和你在报表描述符里定义的字段一一比对这样就能精确定位到是哪个字节被错误解析了。6.5 一个真实的按键错乱案例之前调一块RK3568方案时遇到过这么个问题鼠标插上后移动正常但左右键反了中键变成了左键。一开始怀疑硬件问题换了个鼠标症状消失证明是报表描述符的特殊性导致的。抓出它的报表描述符逐字段解析后发现问题原因这个鼠标在Report Count和Report Size的分配上和标准鼠标不同。标准鼠标把三个按键放在同一个字节的高三位它把左右键放在一个5bit的bitfield里中键单独放在另一个字节的低位。而hid-input解析按键时完全是按报表描述符的字段定义走的所以三个按键被重新分配了usage顺序自然就乱了。解决办法有两个一是改内核驱动在hidinput_connect里针对vid/pid做query处理二是给鼠标的外围加一层专属的hid-quirks把这个鼠标的某个usage强制映射到正确的按键码上。实际项目里我采用的是后者改动范围小且不用给内核打补丁。内核源码里已经有大量类似的处理就是hid_quirks和hid-input里的map_key_clear宏。开发者手册上不会教你的经验就是HID协议只是建议不是强制外设厂商的实现在合规范围内可以有很多奇怪的操作你的驱动要做的是适配它而不是和它争论什么是对的。7. 经验总结RK3568 USB鼠标驱动开发的几条实践心得走到这里RK3568 USB鼠标驱动开发的完整路径已经清楚了。最后分享几条实操中总结出来的经验都是踩过坑才有的体会。第一设备树和内核配置的问题占了实际排查工作的一半以上。不要一上来就写驱动代码先把lsusb跑通、把evtest跑通确认默认的HID栈能用再考虑定制需求。默认驱动的兼容性比你自己写的要好一大截。第二做USB相关调试时usbmon和内核动态调试是你最顺手的工具。开发阶段尽量把CONFIG_DYNAMIC_DEBUG打开排查hid-input的行为时可以直接对hid模块开动态打印echo file drivers/hid/hid-input.c p /sys/kernel/debug/dynamic_debug/control第三驱动代码的世界里鼠标这个品类和键盘、触摸屏比算是简单的。它没有绝对坐标转换不需要multi-touch协议也无所谓校准。但正因为简单它非常适合作为RK3568上USB设备驱动开发的第一步练手。如果你能完整地跑通从设备树配置到USB枚举、从HID报表解析到input事件上报的整条链路再去做USB网卡、USB转串口甚至USB摄像头驱动都会轻松很多。第四模板代码互联网上一搜一大把但真正值钱的是排查链路。遇到问题不要急着查驱动源码先从总线层开始逐层确认哪一层断了就在哪一层深挖。这套思路适用于任何USB设备驱动开发不限于这次讲的鼠标。
返回列表