ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RK3568 USB鼠标驱动开发实战:从枚举到自定义按键映射

RK3568 USB鼠标驱动开发实战:从枚举到自定义按键映射 做嵌入式Linux开发最容易被低估的一件事就是USB鼠标驱动。很多人拿到RK3568开发板插上鼠标能亮能用就觉得这事已经结束了。直到某天换了一个带侧键的鼠标、或者设备在客户现场偶发不识别、又或者需要把鼠标键值重新映射成自定义快捷键时才发现自己连USB枚举流程都讲不清楚。这篇文章就用一个实际项目来聊一聊RK3568平台上的USB鼠标驱动开发该从哪里入手不仅说原理也给出一套能直接落地的排查和修改方法。适合正在做瑞芯微方案BSP、Linux系统定制、Android底层适配甚至OpenBMC管理的工程师参考。1. 先搞清楚需求你到底要“写”一个什么样的鼠标驱动1.1 RK3568的USB控制器与鼠标接入路径写RK3568的USB鼠标驱动第一件事不是打开编辑器改代码而是先弄清楚鼠标到底挂在哪条USB总线上。RK3568这颗SoC的USB控制器分配很典型一个USB 3.0 OTG/DWC3控制器、一个USB 2.0 Host EHCI控制器有的还带OHCI用来兼容低速设备。鼠标是低速或者全速设备通常接在USB 2.0 Host口上但如果你的板子把鼠标接口设计在Type-C口上那还要多考虑一层OTG、PD、CC引脚的逻辑。RK3568的Type-C口默认可能工作在device模式鼠标插上去没有任何反应不是驱动问题是USB角色没有切换过来。所以在项目启动时先看一眼硬件原理图确认这个USB口是直接连到EHCI Host还是通过Type-C控制器走DWC3。如果是前者问题基本限定在设备树节点、内核HID配置、线缆供电这几块如果是后者就要额外确认CC引脚的下拉电阻、PD协商以及内核里role切换的配置。很多人在RK3568上调USB鼠标花了一整天看内核日志最后发现是Type-C口一直停在device mode根本没有进入主机模式这种方向性错误会浪费大量时间。1.2 “写驱动”的三种常见需求接着要定义这个“驱动开发”到底要干什么。我接触过的RK3568项目里关于鼠标的需求基本可以分成三类。第一类标准鼠标能亮灯但光标不动或者插入后完全没反应。这类问题通常不是“写驱动”能解决的而是USB Host控制器没跑起来、HID驱动没编进内核、设备树里USB节点被禁用了属于基础配置问题。第二类鼠标左右键、滚轮都正常但侧键、多媒体键没有事件上报或者上报的键值和预期不一致。这是典型的HID映射问题需要在驱动层把设备的usage映射到Linux input子系统的键值上。第三类鼠标本身是私有协议比如某些电竞鼠标在未切换到“标准HID模式”前上报的数据格式不是标准HID report这时候就需要抓包分析然后写一个专门针对该VID/PID的USB驱动。判断属于哪一种有个很直接的验证方法把同一只鼠标插到PC Linux或者Windows上观察现象。如果PC上一切都好RK3568上不行大概率是RK3568系统侧的配置或驱动问题如果PC上同样按键也无效那就要怀疑鼠标本身是否工作在私有协议模式往往需要厂商工具切换固件状态。这个先手动作能帮你筛掉一大半无关问题。1.3 开发顺序与验收标准我建议的开发顺序是先启用内核默认的usbhid和hid_generic保证标准鼠标能完整识别然后用evtest观察所有事件确认基础功能再做私有协议抓包、写自定义驱动最后做长时间稳定性测试。不要一上来就写驱动否则你连“正常该是什么样”都不知道写出来的代码很难定位问题。验收标准建议定成四条第一插入鼠标后dmesg无错误lsusb能看到对应的VID/PID第二/proc/bus/input/devices里出现对应input节点第三evtest能稳定读到坐标、滚轮、左右中键以及自定义侧键的事件第四长时间连接、反复热插拔、恢复休眠后鼠标依然能正常识别并工作。这四条全过这个驱动基本就算合格了。2. 环境准备与内核基础设备树、config、调试工具2.1 板级调试环境搭建RK3568上做USB驱动开发调试环境比代码本身更重要。我给自己的项目永远备三样东西一个USB转串口模块、一台装了Wireshark的PC、一根质量靠谱的USB线。USB转串口模块最早用的是cp2104和ft231x这两个芯片在Linux下都有现成驱动插上就能识别用来接RK3568的调试串口实时看内核日志。不要只用printk打日志然后靠重启抓log很多枚举失败问题就发生在开机那几秒只有串口能看到完整时序。PC上的usbmon抓包工具主要用于对比分析。当鼠标在RK3568上表现异常时我会先在PC上用usbmon把鼠标正常枚举和上报数据的URB抓下来再去RK3568上抓同一份操作对比两份数据差异。这个方法在调试私有协议鼠标时几乎是必须的。USB转TTL线也要常备有时候目标板串口电平不对要靠转接线救急。2.2 设备树USB节点与PHY配置RK3568的设备树里USB相关节点不算复杂但很多人栽在status字段上。以常见Linux 5.10内核为例如果用USB 2.0 Host口接鼠标需要确认这几个节点是okay状态usb2phy0 { status okay; }; usb2phy0_host { phy-supply vcc5v0_host; status okay; }; usb_host0_ehci { status okay; }; usb_host0_ohci { status okay; };如果只是接USB 2.0鼠标EHCI和OHCI都要打开。OHCI负责低速设备我的经验是有一些低速鼠标在OHCI关闭时会出现“能识别但光标偶尔卡顿”的情况。如果鼠标是USB 3.0接口或者插在USB 3.0 Type-A口上还要检查usb3、usbdrd3以及对应PHY节点。这里有个容易忽略的地方phy-supply必须给对否则USB口的VBUS没电鼠标灯都不亮后面所有调试无从谈起。对于Type-C口还要在设备树里确认dr_mode。如果要作为Host口接鼠标就设成dr_mode host如果打算兼容host/device切换则需要额外的角色切换驱动。设备树里这些字段看似简单但一旦配错系统启动后USB控制器可能根本没初始化dmesg里反而没有明显报错。2.3 内核Kconfig关键项设备树配好之后确认内核配置。标准USB鼠标依赖一串配置项任何一个漏掉都可能导致鼠标不工作。配置项作用缺失时的典型现象CONFIG_USB_XHCI_HCDUSB 3.0控制器驱动USB 3.0口无法识别设备CONFIG_USB_EHCI_HCDUSB 2.0高速/全速控制器驱动USB 2.0口不识别设备CONFIG_USB_OHCI_HCDUSB低速控制器驱动低速鼠标不识别或枚举失败CONFIG_HIDHID核心层所有HID设备无法工作CONFIG_USB_HIDUSB HID传输层能枚举但找不到驱动CONFIG_HID_GENERIC通用HID驱动HID设备找不到匹配驱动CONFIG_INPUT_EVDEVinput事件设备节点evtest无法读取事件这里特别说一下CONFIG_HID_GENERIC。RK3568的软件方案很多基于Rockchip BSP部分BSP默认配置可能把CONFIG_HID_GENERIC关掉了只保留CONFIG_USB_HID。这时候鼠标能被lsusb看到但dmesg里会有一句“no hid driver”之类的提示input节点不会出现。检查配置时不要只看CONFIG_USB_HIDCONFIG_HID_GENERIC和CONFIG_INPUT_EVDEV同样关键。2.4 快速验证鼠标是否被内核识别环境配好后插入鼠标按顺序执行这几条命令dmesg | grep -i usb lsusb cat /proc/bus/input/devices正常的日志应该出现类似USB HID v1.11 Mouse [XXXX] on usb-xxxx-xxx的内容lsusb里能看到Bus 001 Device 002: ID 046d:c077 Logitech, Inc. Mouse。/proc/bus/input/devices中会出现名为Logitech USB Optical Mouse的input设备事件名类似/dev/input/event2。如果lsusb能看到设备但input节点没有问题多半在内核HID驱动层如果lsusb都看不到就要回头查设备树、控制器时钟和PHY配置。另外养成用lsusb -v看描述符的习惯它能直接输出接口类、端点地址、HID报告描述符长度这些信息在下一步分析协议时非常有用。3. USB协议与HID报告描述符驱动开发躲不开的基础课3.1 枚举过程与描述符鼠标驱动能工作依赖的是USB枚举阶段主机和设备的一次“自我介绍”。设备插入后主机首先给设备复位并分配地址然后读取设备描述符再获取配置描述符配置描述符里面包含了接口描述符、端点描述符以及HID描述符。鼠标属于HID类设备接口描述符中的bInterfaceClass为0x03这个字段就是内核决定加载usbhid驱动的重要依据。很多做应用层的人觉得USB枚举太底层、没必要看但调试鼠标驱动时枚举失败是最常见的问题。如果设备在枚举阶段某个描述符读取出错内核会尝试多少次复位最终可能放弃并报device descriptor read/64, error -110。这时候Windows可能还勉强能靠重试机制把设备驱动起来Linux通常直接拒绝所以在RK3568上遇到鼠标不识别先看枚举日志往往能找到直接原因。3.2 标准鼠标的Report Descriptor到底长什么样HID协议的核心是报告描述符。鼠标能上报什么信息完全由Report Descriptor定义。一个标准的两键加滚轮鼠标报告描述符简化后大约长这样05 01 Usage Page (Generic Desktop) 09 02 Usage (Mouse) A1 01 Collection (Application) 09 01 Usage (Pointer) A1 00 Collection (Physical) 05 09 Usage Page (Button) 19 01 Usage Minimum (Button 1) 29 03 Usage Maximum (Button 3) 15 00 Logical Minimum (0) 25 01 Logical Maximum (1) 95 03 Report Count (3) 75 01 Report Size (1) 81 02 Input (Data, Variable, Absolute) 95 01 Report Count (1) 75 05 Report Size (5) 81 01 Input (Constant) 05 01 Usage Page (Generic Desktop) 09 30 Usage (X) 09 31 Usage (Y) 16 00 00 Logical Minimum (0) 26 FF 7F Logical Maximum (32767) 75 10 Report Size (16) 95 02 Report Count (2) 81 06 Input (Data, Variable, Relative) 09 38 Usage (Wheel) 15 81 Logical Minimum (-127) 25 7F Logical Maximum (127) 75 08 Report Size (8) 95 01 Report Count (1) 81 06 Input (Data, Variable, Relative) C0 End Collection C0 End Collection这段描述符看起来枯燥但它是解码鼠标数据的“地图”。第一个字节块决定了按钮数量第二个字节块是X/Y坐标最后一个字节块是滚轮。我们调试侧键问题通常就是看Button的Usage Minimum和Usage Maximum是否涵盖了侧键对应的Button 4/5。如果厂商把侧键定义成Button 6/7内核通用驱动未必会映射成BTN_SIDE或BTN_EXTRA就会出现“数据上来但事件丢失”的现象。3.3 usbhid、hid-core、hid-input三层各负责什么Linux内核里鼠标驱动其实被拆成了三层。usbhid负责USB传输层它拿到的是URB里的原始数据把这些数据整理成HID reporthid-core是HID协议的核心分发层它根据设备报告描述符建立hid字段和report结构hid-input则负责把HID的usage转换成input子系统的键值和坐标事件。用生活化的比喻来说usbhid像是一个翻译官负责把USB线上的“外语”翻译成HID的“普通话”hid-core是快递分拣中心把翻译出来的数据按报告ID分门别类hid-input是最后的业务员把分拣好的包裹送到Linux input子系统这个“用户前台”。标准鼠标所以不需要自己写驱动就是因为这三层已经覆盖了绝大多数HID设备。只有在设备协议不正常或者需要特殊键值映射时才需要在hid-input这一层动手脚。3.4 什么时候必须自己写驱动如果标准鼠标功能正常只是需要映射侧键为某个组合快捷键建议先在用户态解决用udev规则配合evdev读取事件或者直接在应用层监听input节点根本不需要碰内核。但如果遇到以下情况就必须写驱动了。第一种设备本身不是标准HID协议比如游戏鼠标带宏命令、RGB灯效这类设备通常使用厂商私有vendor pageusbhid拿到数据后无法正确解析。第二种HID报告描述符不完整或有错误设备数据能上报但内核无法识别usage。第三种需求本身要求内核直接控制鼠标状态而不是经过用户态转发。写驱动不一定是要从头写一个完整的input驱动更常规的做法是写一个hid_driver只处理特定VID/PID注册input_mapping或者raw_event回调对特定usage做转换。这种方式改动量小还能复用usbhid的传输层稳定性也更好。4. 实操记录从现象到解决一个自定义按键鼠标4.1 现场现象描述下面用一个我实际调过的案例串起来。某款基于RK3568的设备系统跑的是Linux 5.10客户反馈说标配的鼠标左右键、滚轮都正常但是侧键没有反应。一开始我怀疑是鼠标本身坏了换到PC上测试两个侧键在浏览器中能前进后退说明鼠标硬件正常。于是把鼠标插回RK3568设备执行evtest事件列表里只有BTN_LEFT、BTN_MIDDLE、BTN_RIGHT果然没有BTN_SIDE和BTN_EXTRA。这个现象说明什么说明usbhid已经识别到了设备input子系统也建立了节点但hid-input并没有把侧键对应的usage映射成任何按键事件。可能原因有两个一是报告描述符里侧键的usage不是标准Button 4/5二是内核hid-input的映射表没覆盖这个usage。接下来要用抓包确认。4.2 用usbmon和Wireshark抓包定位抓包步骤很简单先加载usbmon模块然后确定鼠标所在总线。modprobe usbmon lsusb假设鼠标输出是Bus 001 Device 003: ID 1234:5678可以读取usbmon的文本接口也可以直接在Wireshark里选择usbmon1接口抓包。我个人习惯先在Wireshark里给usb总线抓一份完整数据过滤条件直接看usb.idProduct 0x5678或者usb.bus_id 1。抓包时操作鼠标按下左右键、滚轮、侧键每个动作都要分开按并停顿一秒方便后面切片分析。抓完以后看URB数据。标准鼠标通常每毫秒或者每8毫秒上报一次数据数据长度一般在4到8字节。左右键和滚轮对应前几个字节侧键按下时多字节里会出现一个只变化1到3的bit。如果抓包能看到侧键数据变化说明USB传输没问题问题在HID映射层如果抓包根本看不到数据变化那就要怀疑鼠标固件是否处于私有协议模式需要先用厂商工具切到标准HID模式。4.3 根据抓包结果写一个自定义hid_driver我这次抓包的结果是按下第一个侧键后最后一个字节从0x00变成了0x01第二个侧键变成0x02。用lsusb -v查看报告描述符发现它把侧键定义在Button Page的Usage 4和5但Logical Maximum只有3。这属于描述符不太规范的设备内核通用映射没能正确处理。解决思路不是改usbhid而是写一个针对该VID/PID的hid_driver在input_mapping回调里把对应的usage映射到BTN_SIDE和BTN_EXTRA。示例代码如下#include linux/hid.h #include linux/module.h static int demo_input_mapping(struct hid_device *hdev, struct hid_input *hi, struct hid_field *field, struct hid_usage *usage, unsigned long **bit, int *max) { if ((usage-hid HID_USAGE_PAGE) ! HID_UP_BUTTON) return 0; switch (usage-hid HID_USAGE_ID) { case 0x04: hid_map_usage_clear(hi, usage, bit, max, EV_KEY, BTN_SIDE); return 1; case 0x05: hid_map_usage_clear(hi, usage, bit, max, EV_KEY, BTN_EXTRA); return 1; default: return 0; } } static const struct hid_device_id demo_devices[] { { HID_USB_DEVICE(0x1234, 0x5678) }, { } }; static struct hid_driver demo_mouse_driver { .name demo-mouse, .id_table demo_devices, .input_mapping demo_input_mapping, }; module_hid_driver(demo_mouse_driver); MODULE_LICENSE(GPL);编译方式可以挂到内核源码树里用make Mdrivers/hid也可以单独做外部模块。独立ko的好处是不用重新编译整个内核适合现场调试。编译完成后先卸载原来绑定的usbhid驱动再加载这个模块modprobe -r usbhid insmod demo_mouse.ko这里要注意如果usbhid已经绑定了设备不加-r卸载的话我们的新驱动不会有绑定机会。这也是新手最容易卡住的地方。4.4 验证与回归加载完成后重新插入鼠标dmesg里应该看到类似demo-mouse: probe的日志/proc/bus/input/devices里会出现一个新的input节点。用evtest打开这个节点按下侧键能看到BTN_SIDE和BTN_EXTRA事件。这就算成功了。但千万不能只测一次就完事。我会把鼠标拔插三次确认每次都能自动绑定再重启系统确认模块能正常加载最后让鼠标连续跑几个小时看有没有偶发断连。如果模块是临时加载的还要把它固化到rootfs的模块目录里或者直接编进内核。这一步常常被忽略很多RK3568项目在开发机上没问题一到量产镜像就没功能就是没固化模块。4.5 不同系统的差异提醒同样的需求跑Android和跑纯Linux又有一点区别。Android系统下的HID映射规则、权限控制、input protocol和Linux主线不完全一样有些客户还要求用OpenBMC做设备管理这时rootfs精简程度很高经常连usbhid都没打进去。不要想当然认为上一个项目能用换一个系统就要重新验证一遍。OpenBMC/RK3568这类场景尤其容易出问题。它的内核和rootfs都是定制过的默认可能只保留管理功能没有桌面相关的input子系统。鼠标插上去lsusb能看到但/dev/input/eventX不存在原因往往是CONFIG_INPUT_EVDEV没开或者rootfs里缺模块。所以在跨系统适配时第一优先级永远是确认内核配置和rootfs模块列表而不是动驱动代码。5. 常见问题复盘与排查技巧5.1 插上鼠标之后完全没反应先不要怀疑驱动从物理链路查起。看鼠标灯有没有亮没亮说明VBUS供电或者线缆有问题亮了但无反应再执行dmesg | grep -i usb、lsusb。内核日志里最常见的错误是device not accepting address和unable to enumerate USB device前者往往指向设备树节点没配好后者要查PHY供电和时钟。我在RK3568上遇到过一例USB Host控制器电压域没配置好鼠标偶发不识别问题到晚上温度降低时变得特别明显最后查出来是PMIC某个LDO压差不足。这种硬件问题靠软件日志很难直接看出来所以要尽早让硬件工程师参与不要一个人闷头看代码。5.2 设备枚举失败或反复断开如果鼠标插入后一直反复disconnect/enumerate大概率不是驱动而是信号完整性问题。常见原因包括USB线缆过长或阻抗不合格、地线接触不良、D/D-走线太长、PCB的ESD/TVS器件寄生电容过大。有些RK3568工业设备会做EFT测试USB口在打静电或群脉冲时掉线整改方向一般是加共模电感、调整TVS管位置、优化USB座子接地。软件侧能做的有限但可以尝试在设备树里调整USB PHY的.dis_u2_susphy_quirk之类的quirk或者降低USB速率看是否稳定。注意这些quirk不是通用写法需要参考具体内核以及PHY驱动代码不要随便抄网上的配置。5.3 鼠标能动但按键错乱右键生效但左键没反应或者按下一个侧键却触发了两个按键这种问题的根源几乎都在报告描述符和HID映射。先用lsusb -v把报告描述符完整导出来对照HID usage table逐个分析。如果是描述符里Logical Maximum/Report Count定义有误有些数据位会被截断导致键值错乱。如果是通用映射表不支持按照第4章的方式写自己的hid_driver即可。这里有一个经验不要把所有鼠标都强制绑定到自定义驱动上一定要用VID/PID精确匹配。否则会把正常鼠标也带进你的映射逻辑导致其他鼠标PCI出问题。5.4 rootfs精简导致HID模块缺失很多RK3568量产设备用Buildroot或者OpenBMCrootfs会精简到仅有必要模块。如果发现dmesg提示找不到usbhid或者hid_generic检查rootfs里/lib/modules/$(uname -r)/kernel/drivers/hid/目录。我的习惯是把usbhid.ko、hid_generic.ko、hid.ko全部编成built-in而不是模块。虽然内核镜像大几十KB但能从根上避免模块依赖和加载顺序问题对于调试USB鼠标这种佩戴频率很高的设备非常划算。5.5 问题速查表症状可能原因第一排查命令解决方案鼠标灯不亮VBUS供电、线缆dmesg、测量电压检查设备树phy-supply、PMIC能识别但无input节点HID驱动未加载dmesg、lsmod打开CONFIG_HID/USB_HID/HID_GENERICevtest无事件设备树input节点错误cat /proc/bus/input/devices检查EVDEV配置和rootfs模块侧键无反应HID usage映射缺失lsusb -v、usbmon写自定义hid_driver反复断开信号完整性/供电dmesg、更换线缆硬件整改、检查线缆Android/OpenBMC下无功能系统差异/模块缺失lsmod、检查rootfs确认该系统的内核配置5.6 独家经验总结做RK3568鼠标驱动这几年我总结出几个值得说的经验。第一条一切以report descriptor为准不要猜。很多鼠标的按键行为和标称不一致只有导出的描述符不会骗人。第二条usbmon抓包务必养成习惯它不仅能看设备上报还能看主机发送的Set Report、Get Report等控制请求很多私有协议鼠标的初始化流程就在这些控制请求里。第三条调试阶段在驱动里多留pr_info上线前再删掉。我自己就吃过亏没打日志的时候只在某个特定鼠标上触发按键映射错误加了日志才发现是同一家厂商不同批次固件的报告描述符有细微差异。最后说几句体己话做了几年平台开发我慢慢有个体会USB鼠标驱动是个小功能但它几乎能把USB协议栈、HID协议、设备树、内核模块、input子系统全串起来。把它吃透再去看USB摄像头、触摸屏、自定义HID设备会发现思路都是通的。最后分享一个习惯每次拿到新的USB外设先在PC上用usbmon抓一份枚举数据存下来再拿到RK3568板子上对比。很多一眼看上去像驱动bug的问题实际只是设备端描述符不标准。这个习惯让我省过不少加班时间。
返回列表