嵌入式linux利用configfs用户态配置HID Gadget模拟键鼠

嵌入式linux利用configfs用户态配置HID Gadget模拟键鼠

概述

大部分开发板如树莓派等提供了若干个USB接口,但这些接口只能用于连接从属设备(即树莓派属于USB host)。部分嵌入式linux开发板还提供了可作为USB slave模式使用的USB口,并且linux内核支持配置USB Hid gadget,若正确配置,则该开发板可作为USB设备连接主机,并模拟任意Hid设备如鼠标键盘等。

hid gadget有两种配置方式:

  1. 在linux内核源码直接加入相关驱动代码后编译、烧写内核;
  2. 在用户态通过configfs配置(但需要内核开启该功能)。

相比之下第二种方式更加灵活,且便于修改和调试,因此本文采用基于configfs的方法。

过程中使用的程序和自动化配置脚本见GithubGitee

linux内核与configfs

通过configfs配置usb-hid-gadget需要linux内核支持configfs和USB gadget。对于已经拿到手的开发板,已经烧录的linux内核对前两者的支持有三种可能:已经支持,通过内核模块支持,不支持。可以通过如下步骤获知实际的支持情况。

查看挂载点

1
2
$ sudo mount -l | grep configfs
configfs on /sys/kernel/config type configfs (rw,relatime)

如果执行上述命令得到了类似的输出,则说明内核已经支持configfs,并且configfs挂载在/sys/kernel/config路径下。该路径可能有所不同。

查看该路径下的目录:

1
2
$ ls /sys/kernel/config
usb_gadget ...

若存在usb_gadget,则说明已经支持配置USB gadget,可以直接进行USB Gadget配置。若不存在,则说明内核编译时不支持USB gadget,见内核编译设置

内核模块加载

如果上述步骤没有输出configfs的挂载点,则有可能需要先加载内核模块。如下命令所示,其中CONFIGFS_HOME是自定义的挂载点,选择一个合理的路径即可。

1
2
3
sudo modprobe libcomposite
CONFIGFS_HOME=XXX
sudo mount none $CONFIGFS_HOME -t configfs

如果上述命令均正常返回,则可以重新尝试上一步查看挂载点

如果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目录,名称自定义:

1
2
$ cd $CONFIGFS_HOME/usb_gadget
$ mkdir simKM_gadget

该目录即配置USB Gaget的目录。如果你有多个配置,可以创建多个目录。

对于一个USB Gadget,主要有三部分需要配置:

  1. 配置字符串
  2. 配置functions
  3. 配置configs

配置字符串

字符串是当设备连接到主机上时,主机上显示的设备名称、生产商之类的信息。

以下是需要配置的部分字符串参数及其含义。其中idProduct和Vendor必须是4位十六进制数,如果想要开发的USB设备在连接主机时具有专属名称,可以向USB协会提交申请。如果只是用来测试也可以填我用的这个。而strings下的字符串可以按自己的需要填写。

1
2
3
4
5
6
7
8
simKM_gadget
├── idProduct=0xa4ac# 产品id
├── idVendor =0x0525# 产品厂商id
└── strings  # 用于主机显示的相关文本
	└── 0x409# 语言标识符(EN)
		├── manufacturer ="myself" #生产商名称
        ├── product      ="simKM"   #产品名称
        └── serialnumber ="0001"       #产品序列号

各个项的值通过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)。

1
2
3
4
5
6
7
8
9
#include"mouse.h"
#include<stdio.h>
int main()
{
	FILE *f = fopen("mouse.bin", "wb");
	fwrite(ReportDescriptor, sizeof(char), 50, f);
	fclose(f);
	return 0;
}

命令行下对于二进制文件可以使用hexdump xxx.bin查看。

因此最终鼠标和键盘对于function的配置如下所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
f1Type="hid"
f1Name="kbd"
f1protocol=1 # 1 for keyboard
f1subclass=0
f1reportDescBinPath="~/Desktop/kybd-descriptor.bin"
f1reportLength=8

#function 2
f2Type="hid"
f2Name="ms"
f2protocol=2 # 2 for mouse
f2subclass=0
f2reportDescBinPath="~/Desktop/mouse-descriptor.bin"
f2reportLength=3

分别使用echo和cat写入functions目录下 protocol、report_desc、report_length、subclass对应文件中即可。

配置config

在configs目录下可以通过mkdir $configName创建Gadget的一个配置目录,名称可自定义。

配置信息及其目录结构如下,其中string也是支持多语言的字符串。

1
2
3
4
5
6
configs
└── ${configName}
	├──MaxPower=120 #最大电流,120mA
    └── strings
        └── 0x409
        	└── configuration "config1" #配置描述字符串

然后对于每个需要启用的function创建软链接,将配置的functions链接到当前使用的config,这样才能使用其功能:

1
ln -s functions/${functionDir} configs/${configName}

启用Gadget

查看设备实际的USB Device Controller:

1
2
$ ls /sys/class/udc
xxx.xxx

选择其中一个(如果有多个的话)作为该USB Gadget的UDC,即将查看得到的UDC编号写入你的Gadget目录下的UDC文件内:

1
echo  xxx.xxx >UDC #enable gadget

最后查看结果

1
2
3
4
$ ls /dev/hid* 
/dev/hidg0 /dev/hidg1
$ chmod 666 /dev/hidg0
$ chmod 666 /dev/hidg1

如果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。编译该文件后可以以交互形式模拟键盘和鼠标指令。例如连接电脑并模拟键盘:

1
./hid_gadget_test /dev/hidg0 keyboard

在程序输出命令提示后键入--left-meta e并回车,连接的主机便会按下meta+e快捷键,在windows下通常会打开文件管理器。

类似地,使用下面的参数测试鼠标:

1
./hid_gadget_test /dev/hidg1 mouse

输入的格式是X Y [button],其中X和Y分别是相对位移(-127~127),button可为--b1/--b2/--b3,分别表示左键右键和中键。例如输入-127 -127 --b2然后回车可以看到鼠标向左上方移动(坐标原点为屏幕左上角),并同时点击了右键。

Python接口

为了在python中调用,本设计采用动态链接库的方式复用linux kernel的官方示例程序中的报文生成函数。

首先将示例程序编译为动态链接库。在开发板上执行:

1
gcc ./hid_gadget_test.c -shared -fPIC -o gadget.so

在Python中,ctypes.cdll模块支持调用动态链接库中的函数4

1
2
from ctypes import *
gadget_c=cdll.LoadLibrary(os.path.dirname( __file__ )+"/gadget.so")  

只需要调用示例程序中的生成HID report的函数,即可生成对应的HID报文。注意其中report为字符数组,函数将填写的报告存于report变量中:

1
toSend=gadget_c.keyboard_fill_report((report),inputBuf,byref(hold))

最后只需将报告以二进制方式写入/dev/hidg*设备文件,即可实现发送:

1
2
3
4
5
6
7
fp=open(dev,'wb')
fp.write(report.raw[0:toSend])
fp.flush()
zbytes=b'\0\0\0\0\0\0\0\0'
if not hold:
    fp.write(zbytes[0:toSend])
    fp.flush()

这里有三点需要注意:

  1. 打开设备文件时需要以二进制方式“wb”
  2. 默认情况在文件读写时fp.write会使用写缓冲区技术来避免频繁读写,但对于/dev/hidg*等虚拟的设备“文件”,写缓冲会造成写入时序错误,因此每次write后需要使用fp.flush()语句强制清空缓冲区
  3. HID设备在写入后默认会保持当前状态重复发送,因此如果不是“按住”状态,发送一次报告后还要发送一次零报告

为方便调用,将上述代码封装为Gadget类,见python_wrap/gadget.py。

一个简单的调用示例如下:

1
2
3
4
5
6
7
import Gadget
#键盘测试
gkey=Gadget("/dev/hidg0","keyboard")
gkey.send("--left-meta e")
#鼠标测试
gms=Gadget("/dev/hidg1","mouse")
gms.send("-65 -65 --b2")

这样便可实现通过python控制模拟的鼠标和键盘。


小结

基于configfs的USB gadget使得开发和调试HID设备更加方便,并且通过一些配置可以支持许多不同功能的复合设备。但相关的资料几乎只有linux kernel的文档,在开发和调试中也涉及到诸如内核编译、HID描述符、shell脚本和文件写缓冲区等扩展知识。本文总结了通过configfs配置USB Gadget的基本步骤,以供参考。

过程中使用的程序和自动化配置脚本见GithubGitee


  1. 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↩︎

  2. 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↩︎

  3. Linux USB HID gadget driver[EB/OL]. [2022-04-17]. https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt↩︎

  4. ctypes — Python 的外部函数库 — Python 3.10.4 文档[EB/OL]. [2022-04-17]. https://docs.python.org/zh-cn/3/library/ctypes.html↩︎

0%