开发用户空间应用程序以用于 HID 设备,使用 libhid

作者:Eoin Verling

Matrix 是一款 USB 验钞机,有时也称为纸币阅读器或纸币接收器,由 Validation Technologies International 制造。捆绑的软件是为 Microsoft Windows 开发的,但幸运的是,该设备附带了低级技术文档,其中定义了设备特定的方面,例如流控制、状态字节和本地状态 LED。

该设备是一个人机接口设备 (HID),在连接时通过枚举过程识别。Windows 设备管理器将该设备报告为此类设备,Linux 上的 usbfs 也是如此。本文专门针对这款特定的 HID 设备,因此包含其所有代码可能是不必要的,但它应该为开发其他 HID 类设备提供帮助。

在进行了一些初步研究之后,我决定使用一个名为 libhid 的正在开发中的库来开发用户空间代码,该库提供了一种跨平台的方式来访问和交互 USB HID 设备。libhid 是在 libusb 之上实现的,因此它不直接依赖于内核的 USB 支持。

驱动 Matrix 的另一种选择是直接使用 libusb,但这样做会重复发明 libhid 的轮子。第三种选择是将 Matrix 实现为内核模块,但这将产生学习内核细节的巨大开销。此选项还将使代码特定于平台。

调查

USB 设备分为设备类。调制解调器属于通信类,扬声器属于音频类。HID 类主要由人们用来控制计算机的设备组成。HID 设备的示例包括鼠标、操纵杆和力反馈游戏控制器。HID 类还包括可能不需要人工交互但提供与 HID 类设备类似格式的数据的设备,例如条形码阅读器,以及在我的情况中,Matrix 验钞机。

关于 USB 设备的信息存储在其 ROM 的段中,称为描述符。图 1 中提供了描述符结构的图表,其中可以看到层次结构的总体视图。当 USB 设备连接到 USB 总线时,会发生枚举过程,这相当于将设备上的描述符读入内存。关于 HID 类设备的信息包含在其 HID 报告描述符中。

Development of a User-Space Application for an HID Device, Using libhid

图 1. USB 设备的描述符存储在其 ROM 中,保存有关它的信息。

我将设备插入 Linux 机器,以便读取描述符并监控设备、机器和通信。我这样做是为了尽可能多地获取信息,以便更好地了解如何为该设备编写代码。

这些报告描述符的关键组成部分是使用信息,该信息在 USB HID 使用表(请参阅在线资源)中定义。使用值描述关于设备的三种基本类型的信息

  • 控件—关于设备状态的信息,例如开/关或启用/禁用。

  • 数据—在设备和主机之间传递的所有其他信息。

  • 集合—相关控件和数据的组。

使用页码和使用编号共同定义了一个唯一的常量,用于描述特定类型的设备或该设备的某一部分。例如,在通用桌面使用页(页码 0x01)上,使用编号 0x05 是游戏手柄,使用编号 0x39 是帽子开关。

因为我的设备是独一无二的——它不是鼠标、操纵杆或 HID 类设备示例中常见的其他设备——所以使用页码设置为 65,440,这是一个供应商定义的值。在比较其他 HID 类设备的 lsusb 输出时,它们都具有已定义的使用页码,例如通用桌面控件或游戏控件。由于 libhid 仍处于开发阶段,因此可供参考的先前代码示例很少。我的工作很像探索性调查。

在 Linux 上,使用标准的 Debian 2.6.9 内核和 usbutils,我能够看到 Linux 识别该设备为 USB HID 设备,bInterfaceClass = HID,并加载 hiddev 内核模块。此模块或内核代码片段是 HID 设备的通用驱动程序。它不是专门针对我们的需求的——它主要用于鼠标、操纵杆等——因此需要从设备上分离或禁用(请参阅与设备通信部分)。

该设备,像所有 USB 设备一样,在连接到 USB 总线时会被枚举。因此,查看以下命令的输出lsusb -vvv,以 root 身份运行,以获取更多信息,这有助于确定设备的功能。lsusb 将 usbfs 文件系统解析为更易读的格式

[sample lsusb -vvv]

Bus 001 Device 004: ID 0ce5:0003
Device Descriptor:
...
 idVendor           0x0ce5
 idProduct          0x0003
  ...
  Configuration Descriptor:
  ...
   Interface Descriptor:
   ...
     bNumEndpoints        1
     bInterfaceClass      3 Human Interface Devices
     bInterfaceSubClass   0 No Subclass
     bInterfaceProtocol   0 None
     ...
     HID Device Descriptor:
     ...
      Report Descriptor: (length is 32)
       Item(Global):Usage Page,data=[0xa0 0xff]65440
                            (null)
       Item(Local ):Usage, data= [ 0x01 ] 1
                            (null)
       Item(Main  ):Collection, data= [ 0x01 ] 1
                            Application

       Item(Local ):Usage, data= [ 0x03 ] 3
                            (null)
       Item(Global):Logical Minimum,data=[ 0x00 ] 0
       Item(Global):Logical Maximum,data=[ 0xff ]255
       Item(Global): Report Size, data= [ 0x08 ] 8
       Item(Global): Report Count, data= [ 0x05 ] 5
       Item(Main  ): Input, data= [ 0x02 ] 2
        Data Variable Absolute No_Wrap Linear
        Preferred_State No_Null_Position
        Non_Volatile Bitfield

       Item(Local ): Usage, data= [ 0x05 ] 5
                            (null)
       Item(Global):Logical Minimum,data=[ 0x00 ]0
       Item(Global):Logical Maximum,data=[ 0xff ]255
       Item(Global): Report Size, data= [ 0x08 ] 8
       Item(Global): Report Count, data= [ 0x05 ] 5
       Item(Main  ): Output, data= [ 0x02 ] 2
        Data Variable Absolute No_Wrap Linear
        Preferred_State No_Null_Position
        Non_Volatile Bitfield
       Item(Main  ): End Collection, data=none

以上输出——已省略部分信息——遵循图 1 中描述的层次结构。一些值得注意的值是

  • idVendor 和 idProduct—所有 USB 设备的唯一标识符,用于在代码中识别和访问设备。

  • bNumEndpoints—列出设备中可用的端点数量。此值实际上是指除了每个 USB 设备中都可用的默认端点端点 0 之外的端点数量。

  • bInterfaceClass—确定设备为 HID 类设备的值。

  • bInterfaceSubClass—设备的子类,在本例中为 HID。例如,设备的启动接口子类必须是可启动的或可供 BIOS 使用的,例如鼠标或键盘。

  • bInterfaceProtocol—使用的协议。可能的值为 0 表示无,1 表示键盘或 2 表示鼠标;其他信息可在 HID 规范中找到。

与设备通信

图 2 显示了描述数据控制流的框图。它可能有助于想象您的代码在库和设备方面的位置。从我的调查中,我知道控制消息通过控制管道定期写入,中断读取通过端点 0 进行。

控制管道用于三项任务:接收和响应 USB 控制和类数据的请求;在使用 Get_Report 请求时,通过 HID 类驱动程序轮询时传输数据;以及从主机接收数据。中断管道用于两项任务:从设备接收异步或未经请求的数据,以及向设备传输低延迟数据。

Development of a User-Space Application for an HID Device, Using libhid

图 2. 新驱动程序使用 libhid,它依赖于 libusb。

内核具有 DEBUG 功能,可以激活该功能,以便记录有关与设备通信时正在发生的事情的额外信息。为此,需要修改内核源代码中的一个文件。在 /usr/src/linux/drivers/usr/input/hid-core.c 文件中,需要将这两行从

#undef DEBUG
#undef DEBUG_DATA

更改为

#define DEBUG
#define DEBUG_DATA

需要重新编译并安装该模块。完成此操作后,这些模块应有助于确定您的代码是否正在工作以及是否正在执行您期望的操作。

libhid 附带了包含一些有用的注释的示例代码。libhid/test 目录中的文件 test_libhid.c 是开始为设备编写代码的好地方。下面是该代码的片段,以及对函数的一些更多解释;为了简洁起见,省略了详细信息

HIDInterface* hid;
hid_return ret;

HIDInterfaceMatcher matcher =
	 { 0x0ce5, 0x0003, NULL, NULL, 0 };
ret = hid_force_open(hid, 0, &matcher, 3);

int const PATH_LEN = 2;
int const PATH_IN[2] = { 0xffa00001, 0xffa00003 };

int const WRITE_PACKET_LEN = 8;
char write_packet[8] =
	 { 0x04,0x7f,0x7f,0x00,0x02,0x00,0x00,0x00 };

int const READ_PACKET_LEN = 5;
char read_packet[5];

ret = hid_set_output_report(hid,
 PATH_IN,
 PATH_LEN,
 write_packet,
 WRITE_PACKET_LEN);

ret = hid_interrupt_read(hid,
 USB_ENDPOINT_IN+1,
 read_packet,
 READ_PACKET_LEN,
 0);

首先要做的是识别我们要与之通信的特定设备。这可以通过 HIDInterfaceMatcher 调用来完成,只需输入供应商 ID 和产品 ID 即可。这两个标识符是识别任何 USB 设备所需的全部。如果您有多个相同的设备,则可以通过序列号来区分它们,也就是说,两个 matrix 验钞机将具有相同的供应商 ID 和产品 ID,但序列号不同。HIDInterfaceMatcher 调用可以做到这一点;请参阅 test_libhid.c 文件中的注释。

在进行一些变量设置之后,下一步是将内核驱动程序从 HID 设备分离。插入 HID 设备后,内核通常会加载 usbhid 模块,这不是我们想要的。但是,我们有几个选项可以卸载它或首先不加载它。一种方法是输入此命令

root@localhost #> modprobe -r usbhid

当 hid_force_open 函数运行时,它会尝试 n 次分离设备,然后才会失败。设备现在不受任何控制,因此我们的代码现在“打开”了设备。与任何 USB 设备一样,有必要向设备发送控制信息以激活它。必须定期发送此信息,以使设备保持活动状态。如果控制轮询停止,设备会在一定超时后停用。

写入设备需要 HID 使用路径及其长度,以及数据包及其长度。为了找到这一点,我们需要解析使用树——以下命令的输出lsusb -vvv—并获取我们想要的接口的路径。与所有其他内容一样,有多种方法可以确定路径。在这个阶段,花费了大量时间来确定要写入的路径,并且许多工具在这里很有用,例如

  1. test_libhid.c 代码:当在代码中输入正确的供应商和产品 ID 时,函数 hid_dump_tree,它使用 MGE hidparser(请参阅资源),后者解析 HID 使用树并将可用的使用放置在其叶子上,输出可用的路径。

  2. Arnaud(libhid 作者之一)提供的 Windows 应用程序也解析使用树,并生成漂亮的 GUI 输出,如图 3 所示。

  3. 通过解析以下命令的输出lsusb -vvv,以 root 身份运行,可以手动解析树以确定路径。test_libhid.c 代码的注释中解释了此过程。

Development of a User-Space Application for an HID Device, Using libhid

图 3. 了解设备:浏览 HID 树的可用节点的一种方法是使用 SystemSoft HID Browser。

从以上方法中,我们现在有了一个可以用于 hid_set_output_report 的路径值。一旦我们知道要写入的位置,接下来就是发送什么。此信息应在设备附带的技术文档中,并且可以使用 USB 嗅探工具进行验证。与我使用的特定设备一样,验证数据包的格式(使用嗅探工具)被证明很重要,因为文档中的信息与嗅探日志报告的内容不符(请参阅嗅探部分)。

一旦发送了控制消息或输出报告,我们就可以开始从读取管道端点 0 读取。所需的函数是中断读取函数。它已存在于 libusb 中,但相应的 libhid 函数不存在。libhid 的开发人员只是还没有遇到需要它的设备,因此我研究了其他函数的格式并适当地实现了它。我还向现有列表添加了一个新的错误代码。这些添加正在考虑包含在最新版本的 libhid 中。

在这个阶段,一旦存储了中断读取值,我就会按照 Matrix 文档解析此值,以向用户显示结果。对于此设备,这相当于诸如“已插入十欧元纸币”或“现金盒已断开连接”以及其他此类设备特定信息。这些细节对于本文的目的来说是不必要的,但是如果有人需要这些细节,请随时与我联系。

只要驱动程序正在运行,此过程就会重复。我们必须保持轮询设备以使其保持活动状态。设备上有一个状态 LED,当设备处于活动状态时变为绿色,当设备处于非活动状态时保持橙色。很长一段时间的目标是使小灯变绿。

嗅探

可以使用许多实用程序进行嗅探。正是在这里,我了解了 Matrix 文档所说内容与实际发生情况之间的差异

[5037 ms]  <<<  URB 647 coming back  <<<
-- URB_FUNCTION_CONTROL_TRANSFER:
  PipeHandle           = 8180c814
  TransferFlags        = 00000002 (DIRECTION_OUT)
  TransferBufferLength = 00000005
  TransferBuffer       = 92a137ed
  TransferBufferMDL    = fe9876e8
  UrbLink              = 00000000
  SetupPacket          =
    00000000: 21 09 00 02 00 00 05 00

[5038 ms]  <<<  URB 645 coming back  <<<
-- URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER:
  PipeHandle           = fe9876a0 [endpoint 0x81]
  TransferFlags        = 00000003 (DIRECTION_IN)
  TransferBufferLength = 00000005
  TransferBuffer       = fefeef08
  TransferBufferMDL    = 81a18f48
    00000000: 00 20 00 00 1a
  UrbLink              = 00000000

[5038 ms]  >>>  URB 648 going down  >>>
-- URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER:
  PipeHandle           = fe9876a0 [endpoint 0x81]
  TransferFlags        = 00000003 (DIRECTION_IN)
  TransferBufferLength = 00000005
  TransferBuffer       = fefeef08
  TransferBufferMDL    = 00000000
  UrbLink              = 00000000

从嗅探日志中,我们看到在开始时发送到设备的控制消息,然后是一系列中断读取。根据文档,“主机发送 [一个] 轮询以定期请求来自 Matrix 的信息。Matrix 回答轮询并报告所有正在发生的事件。”因此,我对这的理解是向设备发送周期性控制写入消息,并从中断端点读取响应。同样根据文档,写入消息的格式为五个字节长,因此有了这些信息,我使用了 libhid 附带的 test_libhid.c 文件来查看会发生什么。我发现 libhid 中的函数在失败时会给出错误代码,并且 /var/log/messages 文件(带有来自修改后的内核文件的额外 DEBUG 信息)报告有用的错误。

在更仔细地检查嗅探日志后,我看到控制写入实际上是八个字节长。请参阅嗅探日志输出中的 SetupPacket。文档中描述的五个字节似乎代表数据包的前五个字节,最后三个字节似乎是填充。也就是说,更改最后三个字节似乎不会影响操作。随后的无错误测试(数据包设置为八个字节)证实文档具有误导性。

结论

就从哪里开始这个项目而言,我发现 libhid 的邮件列表很有帮助。libusb 邮件列表也提供了指导。Linux usbutils 在确定设备上可用的接口以及描述符的含义方面非常有用。

仍在不断开发中的 libhid 源代码也是帮助来源。由于代码不断被开发,因此最好关注 Subversion 存储库以了解更改,包括文档更改,例如代码中的有益注释。

致谢

特别感谢 libhid 的原始作者 Charles Lepple 和 Arnaud Quette,以及后来加入并领导重写的 Martin F. Krafft。他们都为我提供了很多帮助,没有他们,我肯定不会让我的小灯变绿。

还要感谢我在 WIT 的导师 Paul O'Leary 博士,感谢他的鼓励和分析技巧。总是有经验的眼睛来指导我朝着正确的方向前进是件好事。

libhid 使用 MGE 提供的 HIDParser 框架。

本文的资源: /article/8275

Eoin Verling (everling@theverlings.com) 于 1998 年获得资格,此后一直担任系统管理员。他目前正在爱尔兰沃特福德理工学院攻读并行计算研究硕士学位。他最喜欢的事情莫过于一点音乐和欢乐!

加载 Disqus 评论