跳转至内容

映射扫描码到键码

来自 ArchWiki

本页假设您已阅读 键盘输入,该页面提供了更广泛的上下文。

扫描码 (scancodes) 映射到键码 (keycodes) 是键盘驱动程序工作的一部分 [1]。由于这是在比 Xorg、Wayland 和 Linux 控制台更低的层级实现的,因此它在所有这些环境中都有效。[2][3][4] 请注意,此方法仅适用于简单的 1:1 按键重映射;有关允许在类似低层级进行更复杂重映射的程序,请参阅 输入重映射实用程序

扫描码-键码映射表由键盘驱动程序存储在内存中,任何所做的更改都不是持久的(即,当您拔掉设备或重启时,所有更改都将丢失)。

Systemd 提供了一种标准方法使映射表的更改实际上永久生效 #使用 udev。Udev 提供了一个管理设备的基础架构,其中包括在设备连接到计算机时运行 udev 规则(微型脚本)。一些 udev 规则使用 udev 硬件数据库作为源来执行键盘重映射。通过硬件数据库,可以将扫描码-键码重映射针对特定的键盘型号。Systemd 自身就使用此功能将许多键盘型号的“未知扫描码”分配给正确的键码。这意味着只要您的键盘型号在硬件数据库中被找到,您的按键在开箱即用时就能被识别(相应的 .hwdb 文件可以在 /lib/udev/hwdb.d/60-keyboard.hwdb 下找到)。

或者,您可以在桌面会话启动时自动运行重映射工具。例如,通过 #使用 setkeycodes#使用 evmap。但这样在系统启动后插入键盘或重新连接键盘时,重映射将不会被应用。


技术背景

自 Linux 内核 2.6.37 版本以来,有 3 个 ioctl 调用可以更改映射表中的一个条目,以及 3 个相应的调用可以查询映射表中的一个条目。具体为:

  • ioctl(console_fd, KDSETKEYCODE, *kbkeycode)
  • ioctl(dev_fd, EVIOCSKEYCODE, (unsigned int[]) {scancode, keycode})
  • ioctl(dev_fd, EVIOCSKEYCODE_V2, *input_keymap_entry)

用于修改映射表条目,以及

  • ioctl(console_fd, KDGETKEYCODE, *kbkeycode)
  • ioctl(dev_fd, EVIOCGKEYCODE, (unsigned int[]) {scancode, keycode})
  • ioctl(dev_fd, EVIOCGKEYCODE_V2, *input_keymap_entry)

用于查询映射表条目。console_fd 是指向虚拟控制台的文件描述符(哪个虚拟控制台都行),dev_fd 是指向设备文件的文件描述符。

KDSETKEYCODEKDGETKEYCODE ioctl 调用是遗留调用,因为它们不可靠,且很难预测它们将作用于哪个设备。具体来说,它们将遍历满足特定规则的已连接设备(这些设备可能对应物理键盘),并只要有一个设备驱动程序在被要求重映射/查询映射条目时不返回错误,就立即返回。更糟糕的是,某些设备驱动程序即使调用没有产生任何效果也可能不返回错误,导致 ioctl 调用在没有任何更改的情况下成功返回。遍历的顺序是内核中设备初始化的顺序(对于启动后插入的设备来说是随机的)[5]

遗留的 KDSETKEYCODE ioctl 和 KDGETKEYCODE ioctl 调用被工具 setkeycodes(8)getkeycodes(8) 使用。
特定于设备的 EVIOC[SG]KEYCODE_V2 ioctl 调用被 evmap 使用。


setkeycodes 会修改用户提供的扫描码

请注意 setkeycodes 如何试图“提供帮助”,它在调用 ioctl 之前,会从每个大于或等于 0xe000 的扫描码中减去 0xdf80 (-0xe000 + 128)。但这可能根本没有帮助,并且对于想要重映射(例如 USB 键盘)的用户来说是不透明的。USB 键盘的扫描码(准确地说是 HID 协议)由一个 16 位的用法页 (Usage Page) 和一个 16 位的用法 ID (Usage ID) 组成。键盘的用法页是 7,通常导致 USB 键盘按键出现 0x0007XXXX 形式的大扫描码。如果将这样的扫描码传递给 setkeycodes,它会修改该扫描码并使其失效,从而导致错误。

setkeycodes 处理传递给它的扫描码的原因有点复杂,需要更多背景知识。

首先,“扫描码”究竟是什么定义很模糊。在本文中,扫描码是指一个对应于键盘按键的数字(即字节数组),可以传递给键盘驱动程序以查询或设置相应的键码。但这与 维基百科扫描码条目 给出的定义不符。根据维基百科,扫描码是按下或释放按键时从键盘传输的(全部或特定子集的)字节(以后称为协议扫描码 protocol scancode)。当键盘驱动程序从键盘接收到协议扫描码时,它可能会在将其替换为键码之前对其进行处理。如果驱动程序处理了它们,结果产生的扫描码将与原始的协议扫描码不同(假设驱动程序不会以同样方式处理从 ioctl 调用接收到的扫描码)。总之,协议扫描码扫描码的区别在于,协议扫描码可能比其对应的扫描码更原始(未经处理)。对于给定的键盘,扫描码由键盘驱动程序的开发人员定义,而协议扫描码由协议定义。

IBM 的 PS/2 协议在过去几十年中是最广泛使用的键盘协议,但现在几乎不见了。使用 PS/2 的键盘由 atkbd 驱动处理,该驱动将协议扫描码处理为扫描码。在大多数情况下(准确地说,对于最流行的 set 1 和 set 2),PS/2 对某些按键使用 0xe0XX 形式的协议扫描码,然后 atkbd 减去 0xdf80 使其处于 0x80 到 0xFF 的范围内 [6]。在 setkeycodes 编写的时候,PS/2 是主导的键盘协议,人们通常使用 showkey 来获取 PS/2 协议扫描码setkeycodes 考虑到了这一点,因此会将用户提供的 PS/2 协议扫描码处理为 atkbd 驱动程序能够理解的扫描码。所以 setkeycodes 减去 0xdf80 的原因是,setkeycodes 作为 PS/2 键盘的重映射工具而成熟,并且不想改变其身份,尽管 PS/2 今天几乎不存在了;仅偶尔用于笔记本电脑的内部键盘。


识别扫描码

您需要知道想要重映射的按键的扫描码。详情请参阅 键盘输入#识别扫描码

使用 udev

您可以按照 udev#重映射特定设备 以及随后的更新/重载章节,使用 udev 的硬件数据库来覆盖默认的扫描码-到-键码映射。

使用 setkeycodes

注意 您可能想要避免使用 setkeycodes,因为它是否能重映射您的键盘有点像在赌博。即使它在一次启动会话中有效,也不能 100% 保证下次启动系统时依然有效。深入解释请参阅 #技术背景

setkeycodes(8) 是一个用于更新键盘扫描码-键码映射的工具。最好将其与 showkey 配合使用。请参阅 键盘输入#使用 showkey键盘输入#在控制台中识别键码

请注意,setkeycodes 仅在xxe0xx 格式的十六进制扫描码下按预期工作。对于使用较大扫描码的 USB 键盘,您可以通过在将扫描码数字传递给 setkeycodes 之前为其加上 0xdf80 来进行简单的变通处理(原因请参阅 #setkeycodes 会修改用户提供的扫描码)。

setkeycodes 的用法是

# setkeycodes scancode1 keycode1 scancode2 keycode2 ...

扫描码以十六进制给出(不带前导 0x 前缀),键码以十进制给出。

如前所述,仅执行此命令将导致非持久性更改。可以通过创建新服务使更改永久化

/etc/systemd/system/setkeycodes.service
[Unit]
Description=Change keycodes at boot

[Service]
Type=oneshot
ExecStart=/usr/bin/setkeycodes [scancode] [keycode] [scancode] [keycode] [...]

[Install]
WantedBy=multi-user.target

启用 setkeycodes.service

使用 evmap

注意 您必须自行编译 evmap

evmap 是一个与 setkeycodes 类似的工具,用于更新键盘的扫描码-键码映射,但它是针对特定键盘的。这意味着它需要您想要应用重映射的键盘的设备文件路径。您应该使用 /dev/input/by-id/ 文件夹中相应的键盘设备文件,因为这里的设备文件名是使用硬件信息生成的,这意味着名称(通常)包含与其对应的特定设备型号唯一的信息。

evmap 的基本用法是

# evmap -d /dev/input/by-id/your-device-file-here -s scancode1=keycode1 -s scancode2=keycode2 ...

扫描码以十六进制给出(不带前导 0x 前缀),键码数字以十进制或十六进制给出(对于后者,您需要在数字前加上 0x)。您需要将扫描码补全为完整的字节,例如 3 => 03, 23 => 23, 123 => 0123(否则您会收到不具描述性的错误信息 Invalid definition)。

除了键码数字,您也可以使用键码名称。您可以使用以下命令打印所有识别的键码名称列表

gcc -E -dM -x c - <<< '#include <linux/input-event-codes.h>' | perl -ne 'if (/^\#define (KEY_(\w+))\s+\S+/) { print "$2\n" }'

如前所述,仅执行 evmap 将导致非持久性更改。可以通过创建新服务使更改永久化

/etc/systemd/system/evmap.service
[Unit]
Description=Change keycodes at boot

[Service]
Type=oneshot
ExecStart=/usr/bin/evmap -d /dev/input/by-id/your-device-file-here -s scancode1=keycode1 -s scancode2=keycode2 ...

[Install]
WantedBy=multi-user.target

启用 evmap.service

© . This site is unofficial and not affiliated with Arch Linux.

Content is available under GNU Free Documentation License 1.3 or later unless otherwise noted.