RK3399 Linux USB HID Gadget配置实战:从内核到自定义报告描述符
1. 项目概述为什么要在RK3399上折腾HID Gadget最近在做一个嵌入式项目需要把一块RK3399开发板变成一个USB键盘或者鼠标直接通过USB线连接到电脑上就能输入。听起来是不是有点像那些“硬件按键精灵”或者自定义控制面板没错这就是USB HID Gadget的典型应用。RK3399作为一款性能不错的ARM处理器本身内置了USB OTG功能既能当主机Host接鼠标键盘也能当设备Device模拟成外设这个“设备”模式在Linux内核里就叫Gadget。我之所以选择在RK3399上配置HID Gadget而不是直接用串口或者网络通信核心需求就两个字即插即用和零驱动。USB HIDHuman Interface Device是操作系统原生支持的设备类别Windows、macOS、Linux插上就能识别不需要用户额外安装任何驱动。这对于需要快速部署、面向非技术用户的交互设备来说简直是福音。比如你可以用它做一个物理快捷键键盘、一个演示用的翻页笔或者一个将传感器数据如陀螺仪模拟成鼠标移动的智能硬件。网上关于STM32、ESP32-S3做USB HID的教程很多但在功能更强大的Linux平台如RK3399上反而资料比较零散。很多人卡在驱动配置、描述符修改这些环节。这次我就把从内核配置、设备树修改、到用户空间测试的完整流程结合我踩过的坑详细梳理一遍。目标很明确让你拿到就能用遇到问题知道去哪查。2. 核心思路与方案选型内核驱动 vs 用户空间库要在RK3399上实现HID Gadget主要有两条技术路径选择哪种取决于你的具体需求和对系统实时性的要求。2.1 方案对比ConfigFS与Libcomposite方案一基于ConfigFS的动态配置这是目前最主流、最灵活的方式。ConfigFS是一个位于/sys/kernel/config的虚拟文件系统允许你在系统运行时通过读写文件的方式动态创建和配置USB Gadget功能包括HID。你不需要重新编译内核模块只需要内核开启了相关支持就能通过Shell脚本或C程序实时配置。优点灵活可动态加载卸载无需重启。可以轻松组合多个功能如HID 大容量存储。缺点配置步骤稍显繁琐需要熟悉文件系统操作。方案二编译静态Gadget驱动模块传统方法通过修改内核驱动代码如drivers/usb/gadget/legacy/hid.c直接编译一个固定的HID Gadget驱动模块如g_hid.ko。加载模块时通过参数指定PID/VID和报告描述符。优点配置简单一劳永逸适合功能固定不变的产品。缺点不灵活每次修改都要重新编译内核或模块。难以实现多功能复合设备。方案三使用用户空间库如libusbgx这是对ConfigFS的封装提供了一套C语言或Python的API来操作Gadget配置。对于需要在应用程序中动态管理USB设备功能的场景比较友好。优点编程接口友好适合集成到复杂应用中。缺点需要额外引入库增加依赖。对于大多数开发和原型阶段我强烈推荐方案一ConfigFS。它给了我们最大的实验灵活性下面的实操也将围绕它展开。2.2 内核配置检查打好地基无论用哪种方案内核支持是前提。首先确保你的RK3399内核编译时开启了以下关键选项。你可以通过检查/proc/config.gz或内核源码的.config文件来确认。# 检查当前内核配置 zcat /proc/config.gz | grep -E USB_GADGET|USB_DWC2|CONFIGFS|HID关键配置项必须为y或mCONFIG_USB_GADGETy CONFIG_USB_GADGETFSm CONFIG_USB_LIBCOMPOSITEy CONFIG_USB_CONFIGFSy CONFIG_USB_CONFIGFS_HIDy # 这是HID功能支持必须为y或m CONFIG_USB_DWC2y # 或 CONFIG_USB_DWC3取决于RK3399的具体USB控制器 CONFIG_USB_OTGy如果你的内核是官方SDK构建的通常这些配置已经打开。如果是自己编译务必在make menuconfig中确认Device Drivers - USB support - USB Gadget Support选*或M。进入USB Gadget Support确保USB functions configurable through configfs被选中。在USB functions configurable through configfs子菜单中找到HID function并选中。注意CONFIG_USB_CONFIGFS_HID这个选项有时可能被命名为CONFIG_USB_F_HID具体名称取决于内核版本。确保在configfs相关的功能列表里能看到HID。3. 实战使用ConfigFS配置HID Gadget假设我们已经有一个运行着支持ConfigFS内核的RK3399系统。下面通过Shell脚本一步步创建一个模拟键盘的HID Gadget。3.1 创建Gadget框架首先挂载ConfigFS如果尚未挂载然后进入USB Gadget配置目录。#!/bin/bash # 挂载configfs mount -t configfs none /sys/kernel/config # 创建一个名为“g1”的Gadget cd /sys/kernel/config/usb_gadget/ mkdir g1 cd g13.2 设置USB设备标识符这里设置的是USB设备的“身份证”包括厂商IDidVendor、产品IDidProduct等。我们可以使用一些用于测试的ID比如0x1d6bLinux Foundation。# 设置厂商ID、产品ID和USB版本 echo 0x1d6b idVendor # Linux Foundation echo 0x0104 idProduct # 示例产品ID可自定义 echo 0x0200 bcdUSB # USB 2.0 echo 0x0200 bcdDevice # 设备版本 # 设置字符串描述符可选但建议设置方便识别 mkdir strings/0x409 echo 0123456789 strings/0x409/serialnumber echo My Company strings/0x409/manufacturer echo RK3399 HID Keyboard strings/0x409/product3.3 配置HID功能这是核心步骤。我们需要创建一个HID“功能”并为其提供报告描述符。报告描述符定义了HID设备的具体行为如按键、鼠标移动、滚轮。这里我们使用一个标准的键盘报告描述符。# 创建HID功能 mkdir functions/hid.usb0 # 设置协议0键盘1鼠标报告描述符长度和子类1引导接口可选 echo 0 functions/hid.usb0/protocol echo 1 functions/hid.usb0/subclass # 写入键盘报告描述符。 # 这是一个标准的键盘报告描述符8字节输入1字节输出用于LED可以用xxd或echo写入二进制。 # 这里用echo配合printf写入十六进制数据。 cat /tmp/keyboard_desc EOF 05010906a1018501050719e029e71500250175019508810295017508810195057501050819012905910295017503910195067508150026ff00050719002aff008100c0 EOF # 将十六进制字符串转换为二进制写入 xxd -r -p /tmp/keyboard_desc functions/hid.usb0/report_desc # 或者如果系统没有xxd可以用busybox或直接echo但要注意格式。 # 设置报告描述符的长度字节数 report_length$(stat -c %s functions/hid.usb0/report_desc) echo $report_length functions/hid.usb0/report_length实操心得报告描述符是HID的灵魂也是最容易出错的地方。对于简单的键盘鼠标可以直接使用内核源码中提供的示例如linux/drivers/hid/usbhid/usbkbd.c里的描述符。对于复杂设备建议使用在线HID描述符工具生成并先用hidrd-convert等工具测试解析是否正确。描述符错误会导致主机根本无法识别设备或者识别后行为异常。3.4 绑定功能到USB控制器并启用接下来将HID功能关联到一个具体的USB控制器通常是OTG端口并激活整个Gadget。# 创建配置 mkdir configs/c.1 mkdir configs/c.1/strings/0x409 echo Config 1: HID Keyboard configs/c.1/strings/0x409/configuration # 将HID功能链接到该配置 ln -s functions/hid.usb0 configs/c.1/ # 绑定到USB设备控制器。RK3399的OTG控制器通常是fe800000.dwc2或fe900000.dwc3具体看设备树。 # 使用ls /sys/class/udc/查看可用的UDCUSB Device Controller名称。 UDC_NAME$(ls /sys/class/udc/) echo $UDC_NAME UDC执行完echo $UDC_NAME UDC后如果一切正常你应该能立刻在连接到RK3399 OTG口的电脑上听到“发现新硬件”的声音设备管理器里会出现一个“USB输入设备”或“HID键盘设备”。3.5 发送按键测试设备创建成功后我们需要向主机发送按键数据。HID功能在/dev下会创建一个对应的设备节点通常是/dev/hidg0。向这个节点写入数据就相当于发送了一次HID报告。键盘的报告格式通常是8字节。例如发送一个按下的‘A’键键码4需要配合Modifier。# 键盘报告示例8字节 [modifier, reserved, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6] # 按下左Shift ‘a’ 键‘a’的键码是4左Shift的Modifier是0x02 echo -ne \\x02\\x00\\x04\\x00\\x00\\x00\\x00\\x00 /dev/hidg0 # 等待一小段时间模拟按键按下 sleep 0.1 # 发送全零报告表示释放所有按键 echo -ne \\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00 /dev/hidg0如果电脑上的光标位置出现了字母“A”大写恭喜你配置成功了4. 进阶复合设备与自定义HID报告描述符4.1 创建键盘鼠标的复合设备ConfigFS的强大之处在于可以轻松组合多个功能。比如创建一个同时是键盘和鼠标的Gadget。# 在之前的g1 Gadget基础上操作 cd /sys/kernel/config/usb_gadget/g1 # 1. 创建鼠标HID功能 mkdir functions/hid.usb1 echo 1 functions/hid.usb1/protocol # 协议鼠标 echo 0 functions/hid.usb1/subclass # 写入一个标准的鼠标报告描述符相对坐标3个按钮 cat /tmp/mouse_desc EOF 05010902a1010901a100050919012901150025019501750181020501090301091302150026ff00093500750895028102c0 EOF xxd -r -p /tmp/mouse_desc functions/hid.usb1/report_desc echo $(stat -c %s functions/hid.usb1/report_desc) functions/hid.usb1/report_length # 2. 将鼠标功能也链接到配置c.1 ln -s functions/hid.usb1 configs/c.1/ # 3. 重新绑定UDC如果已经绑定可能需要先解除绑定再绑定 echo UDC # 解除绑定 sleep 1 echo $UDC_NAME UDC # 重新绑定现在电脑会识别到一个复合设备可能显示为两个独立的HID设备一个键盘和一个鼠标。发送鼠标移动数据到/dev/hidg1即可。4.2 理解与编写自定义报告描述符标准键盘鼠标的描述符可以直接用。但如果你想做一个自定义的控制面板发送自定义的数据比如传感器读数、特定命令就需要自己编写报告描述符。一个报告描述符由多个条目组成定义了用法页定义大类别如通用桌面控制、键盘、按钮等。用法定义具体功能如X轴、Y轴、按钮1等。逻辑最小值/最大值数据值的范围。报告大小一个数据项的位数如8位。报告数量此类数据项的数量。输入/输出/特征定义数据方向设备到主机主机到设备或配置项。例如定义一个包含1个8位按钮状态和2个16位-32768 到 32767模拟量的自定义报告// HID报告描述符示例十六进制 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x05, // Usage (Game Pad) 0xA1, 0x01, // Collection (Application) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (Button 1) 0x29, 0x08, // Usage Maximum (Button 8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1 bit per button) 0x95, 0x08, // Report Count (8 buttons) 0x81, 0x02, // Input (Data,Var,Abs) - 这8个bit组成1个字节的按钮状态 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x15, 0x80, // Logical Minimum (-128, 实际是0x80即-128的补码) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8 bits per axis) 0x95, 0x02, // Report Count (2 axes) 0x81, 0x02, // Input (Data,Var,Rel) - 2个字节的X,Y相对坐标 0xC0 // End Collection编写完成后将其转换为纯十六进制字符串去掉0x和逗号像之前一样写入report_desc文件。在用户空间你需要按照描述符定义的格式构造相应长度的二进制数据写入/dev/hidgX。避坑指南自定义描述符调试非常耗时。务必使用工具辅助。推荐USBlyzer或Wireshark配合USBPcap在Windows主机端捕获USB数据包查看主机解析出的报告描述符是否和你预期一致。HID Descriptor Tool一个官方工具可以图形化编辑和验证报告描述符。Linux端的hid-recorder可以记录和回放HID报告辅助测试。5. 系统集成与开机自启动开发测试时用脚本没问题产品化需要集成到系统中。5.1 创建Systemd服务创建一个可靠的开机自启动服务来配置Gadget。# /etc/systemd/system/hid-gadget.service [Unit] DescriptionConfigure USB HID Gadget Afterlocal-fs.target Beforegettytty1.service [Service] Typeoneshot RemainAfterExityes # 注意这里假设你的报告描述符文件已放在/etc/hid-keyboard.desc ExecStart/usr/local/bin/setup-hid-gadget.sh ExecStop/usr/local/bin/teardown-hid-gadget.sh [Install] WantedBymulti-user.target对应的setup-hid-gadget.sh脚本内容就是前面章节的整合并确保在开头检查/sys/kernel/config挂载。teardown-hid-gadget.sh脚本用于优雅地拆除Gadget这在需要切换USB模式时很重要。#!/bin/bash # teardown-hid-gadget.sh GADGET_DIR/sys/kernel/config/usb_gadget/g1 if [ -d $GADGET_DIR ]; then echo $GADGET_DIR/UDC 2/dev/null || true rm -f $GADGET_DIR/configs/c.1/hid.usb0 rmdir $GADGET_DIR/configs/c.1/strings/0x409 2/dev/null || true rmdir $GADGET_DIR/configs/c.1 2/dev/null || true rmdir $GADGET_DIR/functions/hid.usb0 2/dev/null || true rmdir $GADGET_DIR/strings/0x409 2/dev/null || true rmdir $GADGET_DIR 2/dev/null || true fi5.2 处理与USB主机模式的冲突RK3399的USB OTG口通常只能在主机Host模式或设备Gadget模式中选择一种。如果你的板子设计是OTG口同时用于连接外围设备如4G模块就需要在设备树DTS中固定其模式或者设计一个硬件开关如通过GPIO控制USB ID引脚的电平。在软件层面一旦通过ConfigFS启用了Gadget并绑定了UDC该控制器就无法再作为主机使用了。确保你的应用场景是单向的或者有模式切换的机制。6. 常见问题排查与调试技巧即使按照步骤操作也可能会遇到问题。这里记录几个我踩过的坑和解决方法。6.1 问题速查表现象可能原因排查步骤执行echo $UDC UDC时报错-bash: echo: write error: Device or resource busyUDC控制器已被占用可能被其他Gadget功能或主机模式驱动占用。1. 检查ls /sys/class/udc/是否为空。如果为空说明内核驱动未正确加载或设备树未启用。2. 运行cat $UDC/stateUDC为具体路径查看状态。如果是configured或addressed先执行echo UDC清空当前Gadget。3. 检查是否有其他内核模块如g_ether,g_mass_storage占用了UDC用lsmod查看并rmmod。电脑无任何反应未发现新设备1. USB线不是数据线仅充电。2. 内核未开启HID Gadget支持。3. 报告描述符错误或长度不对。4. 设备树中USB控制器未使能或模式错误。1. 换一根确认可传数据的USB线。2. 确认内核配置CONFIG_USB_CONFIGFS_HIDy。3. 用od -tx1 functions/hid.usb0/report_desc检查写入的描述符是否正确。核对report_length。4. 在RK3399上检查设备树中usbdrd_dwc3_0节点的dr_mode属性应为peripheral或otg。用dmesg | grep dwc3查看内核启动日志。电脑识别到设备但显示“未知设备”或驱动错误报告描述符不符合规范主机解析失败。使用USB分析工具如WiresharkUSBPcap捕获枚举过程查看主机请求描述符后返回的状态。重点检查描述符的语法和逻辑范围。能识别为键盘但按键无反应1. 写入/dev/hidg0的数据格式错误。2. 写入权限不足。1. 用hexdump -C检查你写入的数据是否与报告描述符定义的格式匹配长度、字节序。2. 确保运行脚本的用户对/dev/hidg0有读写权限通常需要root。检查ls -l /dev/hidg0。设备频繁断开重连电源供电不足或USB数据传输不稳定。1. 确保RK3399板子供电充足USB口能提供至少500mA电流。2. 尝试缩短USB线长度或使用带屏蔽的优质USB线。3. 查看内核日志dmesg是否有关于USB复位或错误的提示。6.2 核心调试命令与日志dmesg这是最直接的调试工具。在执行关键操作挂载configfs、绑定UDC前后用dmesg -w实时查看内核信息关注dwc3,configfs,hid等关键词的错误或警告。ls /sys/class/udc/确认系统识别到的USB设备控制器。如果列表为空基本可以断定内核配置或设备树有问题。cat /sys/kernel/config/usb_gadget/g1/UDC查看当前Gadget绑定到了哪个控制器。hexdump -C /dev/hidg0如果你从主机向设备发送了输出报告比如键盘LED状态可以在这里读到数据。主机端设备管理器/系统信息在Windows的设备管理器或Linux的lsusb -v命令中查看识别到的设备详细信息确认厂商ID、产品ID、报告描述符是否与设置一致。6.3 性能与稳定性考量在RK3399上HID Gadget的响应延迟通常可以做到毫秒级对于大多数交互应用足够了。但如果需要极低的延迟如游戏控制器需要注意用户空间程序的优先级提高发送HID报告进程的调度优先级chrt命令。内核线程干扰避免系统负载过高。报告速率USB HID默认是中断传输有轮询间隔。可以在配置Gadget时尝试调整bInterval在configfs中对应functions/hid.usb0/protocol同级目录下可能有相关属性但并非所有驱动都暴露此接口更短的间隔意味着更高的数据速率和功耗。最后别忘了清理现场。测试完成后按照teardown脚本的步骤拆除Gadget释放USB控制器以便用于其他用途。整个流程从理解需求、内核准备、动态配置到调试集成虽然步骤不少但每一步都有其明确的目的。一旦跑通RK3399就能变成一个非常强大的、可编程的USB HID设备为你的硬件项目打开一扇新的大门。