嵌入式linux利用configfs用户态配置HID Gadget模拟键鼠
嵌入式linux利用configfs用户态配置HID Gadget模拟键鼠
概述
大部分开发板如树莓派等提供了若干个USB接口,但这些接口只能用于连接从属设备(即树莓派属于USB host)。部分嵌入式linux开发板还提供了可作为USB slave模式使用的USB口,并且linux内核支持配置USB Hid gadget,若正确配置,则该开发板可作为USB设备连接主机,并模拟任意Hid设备如鼠标键盘等。
hid gadget有两种配置方式:
- 在linux内核源码直接加入相关驱动代码后编译、烧写内核;
- 在用户态通过configfs配置(但需要内核开启该功能)。
相比之下第二种方式更加灵活,且便于修改和调试,因此本文采用基于configfs的方法。
过程中使用的程序和自动化配置脚本见Github或Gitee。
linux内核与configfs
通过configfs配置usb-hid-gadget需要linux内核支持configfs和USB gadget。对于已经拿到手的开发板,已经烧录的linux内核对前两者的支持有三种可能:已经支持,通过内核模块支持,不支持。可以通过如下步骤获知实际的支持情况。
查看挂载点
|
|
如果执行上述命令得到了类似的输出,则说明内核已经支持configfs,并且configfs挂载在/sys/kernel/config
路径下。该路径可能有所不同。
查看该路径下的目录:
|
|
若存在usb_gadget,则说明已经支持配置USB gadget,可以直接进行USB Gadget配置。若不存在,则说明内核编译时不支持USB gadget,见内核编译设置
内核模块加载
如果上述步骤没有输出configfs的挂载点,则有可能需要先加载内核模块。如下命令所示,其中CONFIGFS_HOME是自定义的挂载点,选择一个合理的路径即可。
|
|
如果上述命令均正常返回,则可以重新尝试上一步查看挂载点。
如果modprobe出错,说明内核编译时并没有将该功能编译为模块,此时必须修改内核编译设置。
内核编译设置
注意:该部分内容只适用于你能找到适用于所使用的开发板的linux内核源代码的情况。
在PC上下载开发板内核源代码,在源码根目录执行make arch=arm64 menuconfig
并Load源码根目录下的.config配置文件就可以看到内核编译的默认设置。
例如对于RK3399ProX Toybrick开发板,在Device Drivers->USB support->USB Gadget Support将该选项配置为’build-in’。
在该项的子项下开启通过configfs配置USB functions,以支持在开发板系统用户态即可配置USB功能。其中configfs功能可以将内核配置映射为文件,挂载后在用户态对文件进行读写操作即可配置功能。
修改编译设置后,编译、烧录后可以回到查看挂载点。如果选择编译为模块,则烧录后要先进行内核模块加载。
USBGadget配置
进行后续步骤前,请确认如下两点:
- 你已经获得了config的挂载点
$CONFIGFS_HOME
(例如/sys/kernel/config。下面的命令中替换为你设备上实际的挂载点) - $CONFIGFS_HOME下存在usb_gadget目录。
进入configfs 的usbgadget目录,并创建一个自己的gadget目录,名称自定义:
|
|
该目录即配置USB Gaget的目录。如果你有多个配置,可以创建多个目录。
对于一个USB Gadget,主要有三部分需要配置:
- 配置字符串
- 配置functions
- 配置configs
配置字符串
字符串是当设备连接到主机上时,主机上显示的设备名称、生产商之类的信息。
以下是需要配置的部分字符串参数及其含义。其中idProduct和Vendor必须是4位十六进制数,如果想要开发的USB设备在连接主机时具有专属名称,可以向USB协会提交申请。如果只是用来测试也可以填我用的这个。而strings下的字符串可以按自己的需要填写。
|
|
各个项的值通过echo命令写入对应文件即可。如果某个文件夹不存在,则需要自己mkdir。
其中0x409是语言ID,表示英语(en-us),在strings目录下还可创建其他语言ID的目录,便于主机选择不同语言进行显示。
配置function
USB Gadget通过function配置实际的功能。每个function由protocol、subclass、report_desc、report_length这四项组成1。同一个USB Gadget可以通过一条线同时支持多个function(例如同时模拟鼠标和键盘)
其中protocol指定HID设备使用的协议,对于键盘设备其值为1,鼠标设备其值为22。subclass指定子类,本项目中取默认值零。
report_desc、report_length分别描述HID设备向主机发送报文的格式和长度,其中report_desc为二进制格式。USB-IF官网提供了HID description Tool可以编辑和生成描述符文件,并且附带了许多常见设备的示例。例如键盘和鼠标的描述符分别如下:
鼠标 | 键盘 |
---|---|
该工具支持导出描述符为.txt/.h/.asm等格式,但都是以文本形式。为了生成二进制格式描述符,可以导出为c头文件格式,并以如下简短的c程序产生二进制输出(见gadget/hid2bin)。
|
|
命令行下对于二进制文件可以使用hexdump xxx.bin
查看。
因此最终鼠标和键盘对于function的配置如下所示。
|
|
分别使用echo和cat写入functions目录下 protocol、report_desc、report_length、subclass对应文件中即可。
配置config
在configs目录下可以通过mkdir $configName
创建Gadget的一个配置目录,名称可自定义。
配置信息及其目录结构如下,其中string也是支持多语言的字符串。
|
|
然后对于每个需要启用的function创建软链接,将配置的functions链接到当前使用的config,这样才能使用其功能:
|
|
启用Gadget
查看设备实际的USB Device Controller:
|
|
选择其中一个(如果有多个的话)作为该USB Gadget的UDC,即将查看得到的UDC编号写入你的Gadget目录下的UDC文件内:
|
|
最后查看结果
|
|
如果ls指令成功输出了两个设备文件则说明配置成功,/dev/hidg0 /dev/hidg1
分别是两个模拟键盘和鼠标的HID输出设备文件,赋予666为所有用户的读写权限,便于后续操作。
注意:每个function都会对应产生一个/dev路径下的hidgX设备文件,顺序按function的链接顺序。后续操作时注意对应关系。
最终配置完成的configfs/usb_gadget目录结构类似下图所示。部分目录的名称根据你的选择可能有所不同。;另外图中一些上文未涉及的项暂时无需修改。
由于基于configfs的配置在系统重启后即自动重置,可将上述配置过程写入一个配置脚本en_gadget.sh(位于setup_script),每次启动时运行该脚本即可自动配置。
模拟键鼠测试
连接测试
此时将开发板上支持OTG的USB接口通过USB数据线连接主机,在主机的设备界面也可以看到此前设置的设备名称和制造商、序列号等各参数。例如在linux下可以使用USB viewer查看相关信息。
在windows下也可以正常识别为鼠标和键盘的复合输入设备:
模拟操作测试
在开发板系统上,向``/dev/hidg0 /dev/hidg1`这两个设备文件分别输出键盘和鼠标的二进制HID报文便可以看到主机响应了对应的键盘和鼠标操作。
为方便测试,linux kernel的官方文档提供了HID-gadget设备使用例程3,见gadget/hid_gadget_test.c。编译该文件后可以以交互形式模拟键盘和鼠标指令。例如连接电脑并模拟键盘:
|
|
在程序输出命令提示后键入--left-meta e
并回车,连接的主机便会按下meta+e
快捷键,在windows下通常会打开文件管理器。
类似地,使用下面的参数测试鼠标:
|
|
输入的格式是X Y [button]
,其中X和Y分别是相对位移(-127~127),button可为--b1/--b2/--b3
,分别表示左键右键和中键。例如输入-127 -127 --b2
然后回车可以看到鼠标向左上方移动(坐标原点为屏幕左上角),并同时点击了右键。
Python接口
为了在python中调用,本设计采用动态链接库的方式复用linux kernel的官方示例程序中的报文生成函数。
首先将示例程序编译为动态链接库。在开发板上执行:
|
|
在Python中,ctypes.cdll模块支持调用动态链接库中的函数4:
|
|
只需要调用示例程序中的生成HID report的函数,即可生成对应的HID报文。注意其中report为字符数组,函数将填写的报告存于report变量中:
|
|
最后只需将报告以二进制方式写入/dev/hidg*
设备文件,即可实现发送:
|
|
这里有三点需要注意:
- 打开设备文件时需要以二进制方式“wb”
- 默认情况在文件读写时fp.write会使用写缓冲区技术来避免频繁读写,但对于
/dev/hidg*
等虚拟的设备“文件”,写缓冲会造成写入时序错误,因此每次write后需要使用fp.flush()
语句强制清空缓冲区 - HID设备在写入后默认会保持当前状态重复发送,因此如果不是“按住”状态,发送一次报告后还要发送一次零报告
为方便调用,将上述代码封装为Gadget类,见python_wrap/gadget.py。
一个简单的调用示例如下:
|
|
这样便可实现通过python控制模拟的鼠标和键盘。
小结
基于configfs的USB gadget使得开发和调试HID设备更加方便,并且通过一些配置可以支持许多不同功能的复合设备。但相关的资料几乎只有linux kernel的文档,在开发和调试中也涉及到诸如内核编译、HID描述符、shell脚本和文件写缓冲区等扩展知识。本文总结了通过configfs配置USB Gadget的基本步骤,以供参考。
过程中使用的程序和自动化配置脚本见Github或Gitee。
USB/Linux USB Layers/Configfs Composite Gadget/Usage eq. to g hid.ko - Tizen Wiki[EB/OL]. [2022-04-17]. https://wiki.tizen.org/USB/Linux_USB_Layers/Configfs_Composite_Gadget/Usage_eq._to_g_hid.ko. ↩︎
USB-IF. Device Class Definition for HID 1.11 [EB/OL]. [2022-04-17]. https://www.usb.org/document-library/device-class-definition-hid-111. ↩︎
Linux USB HID gadget driver[EB/OL]. [2022-04-17]. https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt. ↩︎
ctypes — Python 的外部函数库 — Python 3.10.4 文档[EB/OL]. [2022-04-17]. https://docs.python.org/zh-cn/3/library/ctypes.html. ↩︎