使用输入子系统,第二部分

作者:Brad Hards

在上个月的文章中,我们了解了 Linux 输入子系统如何在内核内部工作,最后简要提到了事件处理程序。每个处理程序本质上都提供了一个不同的用户空间 API,将输入事件转换为构成该 API 的特定格式。

输入子系统集成到 Linux 中的关键方面之一是事件接口的可用性。这基本上通过一系列字符设备节点将原始事件暴露给用户空间——每个逻辑输入设备一个字符设备节点。事件接口是一种非常强大的技术,因为它允许在用户空间中操作事件而不会丢失信息。例如,传统的鼠标接口仅支持两个相对轴和最多五个按钮。这些通常映射到两个真实轴和三个真实按钮,第四个和第五个按钮逻辑上映射到滚轮向上和滚轮向下事件。

然而,当尝试使用带有滚轮和三个以上按钮的鼠标时,这种映射就成了一个问题,因为任何额外的按钮只能映射到一个现有按钮。传统的 API 也阻碍了高级输入设备的使用,例如太空球和其他具有多个轴的设备。相比之下,事件 API 提供了对设备功能的完全访问权限,甚至包括对这些功能和其他设备特性的设备描述。

本月的文章重点介绍事件接口的各种 ioctl 功能,以及正常的读取和写入调用。

查找事件接口的版本

事件接口支持使用 EVIOCGVERSION ioctl 函数确定事件设备代码的版本。该参数是一个 int(32 位),旨在解释为主版本(高两位字节)、次版本(第三个字节)和补丁级别(低字节)。从机器上的每个事件设备返回的值都相同。

清单 1 显示了 EVIOCGVERSION 的示例。ioctl 函数的第一个参数是事件设备节点(例如,/dev/input/event0)的打开文件描述符。请注意,您必须将指向整数变量的指针,而不是变量本身,作为 ioctl 调用的第三个参数传递。

清单 1. EVIOCGVERSION 函数示例

查找有关设备身份的信息

事件接口支持使用 EVIOCGID ioctl 检索与底层设备关联的信息。该参数是指向 input_id 结构的指针;input_id 结构的定义如清单 2 所示。__u16 数据类型是 Linux 特有的无符号 16 位整数。您可以安全地将其强制转换为代码中的标准 uint16_t。

清单 2. iput_id 结构定义

总线类型是唯一包含准确数据的字段。您应该将其视为不透明的枚举类型,并将其与 <linux/input.h> 中提供的各种 BUS_x 类型定义进行比较。vendor、product 和 version 字段是与设备身份相关的总线类型特定信息。现代设备(通常使用 PCI 或 USB)确实具有可用的信息,但传统设备(例如串行鼠标、PS/2 键盘和 ISA 声卡上的游戏端口)则没有。因此,对于某些总线类型的值,这些数字没有意义。

清单 3 显示了 EVIOCGID ioctl 的示例。此示例调用 ioctl,然后打印出结果。案例逻辑显示了所有当前总线类型。以下是运行该代码的示例:vendor 045e product 001d version 0111 位于通用串行总线上

清单 3. EVIOCGID ioctl 示例

除了总线类型以及 vendor、product 和 version 信息之外,某些设备还可以提供构成有意义名称的字符串。可以使用 EVIOCGNAME ioctl 从事件接口获取此信息。此 ioctl 提供一个字符串并返回字符串的长度(或负错误值)。如果字符串太长而无法放入参数中,则会被截断。清单 4 中提供了一个示例。如果参数不是 &name 看起来很奇怪,请记住数组的名称与指向第一个元素的指针相同。因此,&name 将是指向指向第一个元素的指针的指针,这不是我们想要的。如果您真的想使用解引用,请使用 &(name[0])。

清单 4. 截断字符串示例

以下是运行该事件代码的示例

The device on /dev/input/event0 says its name
    is Logitech USB-PS/2 Optical Mouse

然而,并非所有设备都包含有意义的名称,因此内核输入驱动程序会尝试提供一些有意义的名称。例如,没有制造商或产品字符串的 USB 设备会将 vendor 和 product ID 信息连接起来。

尽管设备身份和名称信息通常很有用,但它可能不足以告诉您拥有哪个设备。例如,如果您有两个相同的操纵杆,您可能需要根据它们使用的端口来识别它们。这通常被称为拓扑信息,您可以使用 EVIOCGPHYS ioctl 从事件接口获取此信息。与 EVIOCGNAME 类似,它提供一个字符串并返回字符串的长度(或负错误号)。清单 5 中显示了一个示例;运行该示例将生成类似以下的内容

The device on /dev/input/event0 says its path
    is usb-00:01.2-2.1/input0

清单 5. 使用 EVIOCGPHYS 获取拓扑信息

要理解此字符串显示的内容,您需要将其分解为几个部分。usb 部分表示这是来自 USB 系统的物理拓扑。00:01.2 是 USB 主机控制器的 PCI 总线信息(在本例中,总线 0,插槽 1,功能 2)。2.1 显示了从根集线器到设备的路径。在本例中,上游集线器插入到根集线器的第二个端口,并且该设备插入到上游集线器的第一个端口。input0 表示这是设备上的第一个事件设备。大多数设备只有一个,但多媒体键盘可能会在一个接口上显示普通键盘,而在第二个接口上显示多媒体功能键。图 1 显示了此拓扑示例。

Using the Input Subsystem, Part II

图 1. 键盘拓扑

如果您交换两个相同设备上的电缆,此设置无济于事。在这种情况下,唯一可以帮助您的是设备是否具有某种形式的唯一标识符,例如序列号。您可以使用 EVIOCGUNIQ ioctl 获取此信息。清单 6 中显示了一个示例。大多数设备没有这样的标识符,您将从此 ioctl 获得一个空字符串。

清单 6. 查找唯一标识符

确定设备功能和特性

对于某些应用程序,仅了解设备身份可能就足够了,因为这将使您可以根据正在使用的设备处理任何特殊情况。但是,它不能很好地扩展;考虑这样一种情况,您只想在设备具有滚轮时才启用滚轮处理。您真的不想在代码中列出每个带有滚轮的鼠标的 vendor 和 product 信息。

为了避免这个问题,事件接口允许您确定特定设备可用的功能和特性。事件接口支持的功能类型有

  • EV_KEY:绝对二进制结果,例如按键和按钮。

  • EV_REL:相对结果,例如鼠标上的轴。

  • EV_ABS:绝对整数结果,例如操纵杆或平板电脑的轴。

  • EV_MSC:不适合任何其他地方的杂项用途。

  • EV_LED:LED 和类似指示。

  • EV_SND:声音输出,例如蜂鸣器。

  • EV_REP:在输入核心中启用按键的自动重复。

  • EV_FF:向设备发送力反馈效果。

  • EV_FF_STATUS:设备向主机报告力反馈效果。

  • EV_PWR:电源管理事件。

这些只是功能的类型;每种类型中都可以找到各种各样的单个功能。例如,EV_REL 功能类型区分 X、Y 和 Z 轴以及水平和垂直滚轮。同样,EV_KEY 功能类型包括数百种不同的按键和按钮。

可以使用 EVIOCGBIT ioctl 通过事件接口确定每个设备的功能或特性。此函数允许您确定任何特定设备支持的功能类型,例如,它是否具有按键、按钮或两者都没有。它还允许您确定支持的特定功能,例如,存在哪些按键或按钮。

EVIOCGBIT ioctl 接受四个参数。如果我们将其视为 ioctl(fd, EVIOCGBIT(ev_type, max_bytes), bitfield),则 fd 参数是一个打开的文件描述符;ev_type 是要返回的功能类型(其中 0 是一个特殊情况,表示应返回所有支持的功能类型的列表,而不是该类型的特定功能列表);max_bytes 显示应返回的字节数的上限;bitfield 是指向应复制结果的内存区域的指针。返回值是成功复制的字节数,失败时返回负错误代码。

让我们看几个 EVIOCGBIT ioctl 调用的示例。第一个示例清单 7 显示了如何确定存在的功能类型。它使用 evtype_bitmask 根据 <linux/input.h> 中的 EV_MAX 定义确定位数组所需的内存量。然后提交 ioctl,事件层填充位数组。然后,我们测试数组中的每个位,并显示设置位的位置,这表明设备确实具有至少一种此类型的功能。所有设备在 2.5 中都支持 EV_SYN 功能类型;输入核心设置此位。

清单 7. 使用 EVIOCGBIT 确定功能

运行时,以键盘为目标,清单 7 中的示例产生

Supported event types:
  Event type 0x00  (Synchronization Events)
  Event type 0x01  (Keys or Buttons)
  Event type 0x11  (LEDs)
  Event type 0x14  (Repeat)

以鼠标为目标,该示例产生

Supported event types:
  Event type 0x00  (Synchronization Events)
  Event type 0x01  (Keys or Buttons)
  Event type 0x02  (Relative Axes)
从设备检索输入(以及向设备发送输入)

在确定了特定设备具有哪些功能后,您就知道它将生成哪些类型的事件以及您可以发送哪些类型的事件。

从设备检索事件需要标准的字符设备“读取”功能。每次您从事件设备(例如,/dev/input/event0)读取时,您都会获得整数个事件,每个事件都由一个 struct input_event 组成。

清单 8. 检查繁忙点

清单 8 中显示的示例在打开的文件描述符上执行繁忙循环,尝试读取任何事件。它过滤掉任何与按键不对应的事件,然后打印出 input_event 结构中的各个字段。在我的键盘上打字时运行此程序会产生

Event: time 1033621164.003838, type 1, code 37, value 1
Event: time 1033621164.027829, type 1, code 38, value 0
Event: time 1033621164.139813, type 1, code 38, value 1
Event: time 1033621164.147807, type 1, code 37, value 0
Event: time 1033621164.259790, type 1, code 38, value 0
Event: time 1033621164.283772, type 1, code 36, value 1
Event: time 1033621164.419761, type 1, code 36, value 0
Event: time 1033621164.691710, type 1, code 14, value 1
Event: time 1033621164.795691, type 1, code 14, value 0

每次按键一次事件,每次释放按键另一次事件。

此读取接口具有字符设备的所有正常特性,这意味着您不需要使用繁忙循环。您可以简单地等到您的程序需要来自设备的某些输入,然后执行读取调用。此外,如果您对来自多个设备的输入感兴趣,则可以使用 poll 和 select 函数同时等待多个打开的设备。

向设备发送信息的过程与接收信息的过程类似,只是您使用标准的写入函数而不是读取函数。重要的是要记住,写入调用中使用的数据必须是 struct input_event。

清单 9 中显示了一个写入数据的简单示例。此示例打开 Caps Lock LED,等待 200 毫秒,然后关闭 Caps Lock LED。然后,它打开 Num Lock LED,等待 200 毫秒,然后关闭 Num Lock LED。然后循环重复(在无限繁忙循环中),因此您会看到两个键盘 LED 交替闪烁。

清单 9. 数据写入函数示例

到现在为止,应该很清楚,只有当某些东西发生变化时,您才会收到事件——按下或释放按键,移动鼠标等等。对于某些应用程序,您需要能够确定设备的全局状态。例如,管理键盘的程序可能需要确定当前哪些 LED 亮起以及当前键盘上按下哪些按键,即使某些按键可能在应用程序启动之前就被按下了。

EVIOCGKEY ioctl 用于确定设备的全局按键和按钮状态。清单 10 中显示了一个示例。此 ioctl 在某些方面类似于 EVIOCGBIT(...,EV_KEY,...) 函数;EVIOCGKEY 不是为设备可以发送的每个按键或按钮设置位数组中的位,而是为每个被按下的按键或按钮设置位数组中的位。

清单 10. 确定设备的全局按键和按钮状态

EVIOCGLED 和 EVIOCGSND 函数类似于 EVIOCGKEY,只是它们分别返回当前哪些 LED 亮起以及当前哪些声音开启。清单 11 中显示了如何使用 EVIOCGLED 的示例。同样,每个位的解释方式与 EVIOCGBIT 填充位数组中的位的方式相同。

清单 11. 使用 EVIOCGLED

您可以使用 EVIOCGREP ioctl 确定键盘的重复率设置。清单 12 中显示了一个示例,数组中有两个元素。第一个元素指定键盘开始重复之前的延迟,第二个元素指定后续重复之间的延迟。因此,如果您按住一个按键,您会立即获得一个字符,rep[0] 毫秒后获得第二个字符,rep[1] 毫秒后获得第三个字符,然后在您释放按键之前,每 rep[1] 毫秒获得另一个字符。

清单 12. 检查重复率设置

您还可以使用 EVIOCSREP 设置按键重复率。这使用与您用于获取设置的相同的双元素数组,如清单 13 所示;它将初始延迟设置为 2.5 秒,将重复率设置为每秒 1 次。

清单 13. 设置重复率

某些输入驱动程序支持按住的按键(由键盘扫描解释并报告为扫描码)与发送到输入层的事件之间的可变映射。您可以使用 EVIOCGKEYCODE ioctl 确定每个扫描码关联的按键。清单 14 中显示了一个示例,该示例循环遍历前 100 个扫描码。扫描码的值(函数的输入)是整数数组中的第一个元素,生成的输入事件按键编号(keycode)是数组中的第二个元素。您也可以使用 EVIOCSKEYCODE ioctl 修改映射。清单 15 中显示了一个示例;此 ioctl 将我的 M 键映射为始终生成字母 N。请注意,按键代码 ioctl 函数可能并非在每个键盘上都有效——USB 键盘是不支持可变映射的驱动程序的示例。

清单 14. 循环遍历扫描码

清单 15. 映射按键

EVIOCGABS 函数还提供状态信息。但是,它不是填充表示布尔值全局状态的位数组,而是为一个绝对轴提供一个 struct input_absinfo(参见清单 16)。如果您想要设备的全局状态,则必须为设备上的每个轴调用该函数。清单 17 中显示了一个示例。数组中的元素是有符号的 32 位量,您可以安全地将其视为等效于 int32_t。第一个元素显示轴的当前值,第二个和第三个元素显示轴的当前限制,第四个元素显示响应的“平坦”部分(如果有)的大小,最后一个元素显示可能存在的误差大小。

清单 16. 绝对轴的 input_absinfo

清单 17. 按轴检查全局状态

力反馈

三个额外的 ioctl 函数可用于控制力反馈设备:EVIOCSFF、EVIOCRMFF 和 EVIOCGEFFECT。这些函数目前分别发送力反馈效果、移除力反馈效果和确定可以同时使用的效果数量。由于力反馈支持仍在兴起,并且仍有大量工作要做,因此现在完全记录 API 有点为时过早。本文“资源”部分列出的网站可能会在您阅读本文时提供更新的信息。

资源

电子邮件:bhards@bigpond.net.au

Brad Hards 是 Sigma Bravo 的技术总监,Sigma Bravo 是一家位于澳大利亚堪培拉的小型专业服务公司。除了 Linux 之外,他的技术重点还包括飞机系统集成和认证、GPS 和电子战。有关本文的评论可以发送至 bradh@frogmouth.net

加载 Disqus 评论