RK3399 Linux USB HID Gadget配置实战:从内核到自定义报告描述符

📅 2026/8/2 20:40:59 👁️ 阅读次数 📝 编程学习
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 HID(Human 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=m

CONFIG_USB_GADGET=y CONFIG_USB_GADGETFS=m CONFIG_USB_LIBCOMPOSITE=y CONFIG_USB_CONFIGFS=y CONFIG_USB_CONFIGFS_HID=y # 这是HID功能支持,必须为y或m CONFIG_USB_DWC2=y # 或 CONFIG_USB_DWC3,取决于RK3399的具体USB控制器 CONFIG_USB_OTG=y

如果你的内核是官方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 g1

3.2 设置USB设备标识符

这里设置的是USB设备的“身份证”,包括厂商ID(idVendor)、产品ID(idProduct)等。我们可以使用一些用于测试的ID,比如0x1d6b(Linux 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/product

3.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/`查看可用的UDC(USB 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

避坑指南:自定义描述符调试非常耗时。务必使用工具辅助。推荐:

  1. USBlyzerWireshark(配合USBPcap):在Windows主机端捕获USB数据包,查看主机解析出的报告描述符是否和你预期一致。
  2. HID Descriptor Tool:一个官方工具,可以图形化编辑和验证报告描述符。
  3. Linux端的hid-recorder:可以记录和回放HID报告,辅助测试。

5. 系统集成与开机自启动

开发测试时用脚本没问题,产品化需要集成到系统中。

5.1 创建Systemd服务

创建一个可靠的开机自启动服务来配置Gadget。

# /etc/systemd/system/hid-gadget.service [Unit] Description=Configure USB HID Gadget After=local-fs.target Before=getty@tty1.service [Service] Type=oneshot RemainAfterExit=yes # 注意:这里假设你的报告描述符文件已放在/etc/hid-keyboard.desc ExecStart=/usr/local/bin/setup-hid-gadget.sh ExecStop=/usr/local/bin/teardown-hid-gadget.sh [Install] WantedBy=multi-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 fi

5.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/state(UDC为具体路径),查看状态。如果是configuredaddressed,先执行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_HID=y
3. 用od -tx1 functions/hid.usb0/report_desc检查写入的描述符是否正确。核对report_length
4. 在RK3399上,检查设备树中&usbdrd_dwc3_0节点的dr_mode属性,应为peripheralotg。用dmesg | grep dwc3查看内核启动日志。
电脑识别到设备,但显示“未知设备”或驱动错误报告描述符不符合规范,主机解析失败。使用USB分析工具(如Wireshark+USBPcap)捕获枚举过程,查看主机请求描述符后返回的状态。重点检查描述符的语法和逻辑范围。
能识别为键盘,但按键无反应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的响应延迟通常可以做到毫秒级,对于大多数交互应用足够了。但如果需要极低的延迟(如游戏控制器),需要注意:

  1. 用户空间程序的优先级:提高发送HID报告进程的调度优先级(chrt命令)。
  2. 内核线程干扰:避免系统负载过高。
  3. 报告速率:USB HID默认是中断传输,有轮询间隔。可以在配置Gadget时尝试调整bInterval(在configfs中对应functions/hid.usb0/protocol同级目录下可能有相关属性,但并非所有驱动都暴露此接口),更短的间隔意味着更高的数据速率和功耗。

最后,别忘了清理现场。测试完成后,按照teardown脚本的步骤拆除Gadget,释放USB控制器,以便用于其他用途。整个流程从理解需求、内核准备、动态配置到调试集成,虽然步骤不少,但每一步都有其明确的目的。一旦跑通,RK3399就能变成一个非常强大的、可编程的USB HID设备,为你的硬件项目打开一扇新的大门。