Jump to content

Map scancodes to keycodes

From ArchWiki

This page assumes that you have read Keyboard input, which provides wider context.

Mapping scancodes to keycodes is part of the keyboard driver's job [1]. Since this is achieved in a layer lower than Xorg, Wayland and the Linux console, it will be effective in all. [2][3][4] Note that this method can only be used for simple 1:1 key remaps; see Input remap utilities for programs which allow more complex remaps at a similar low level.

The scancode-keycode mapping table is stored in memory by the keyboard driver and any changes made are not persistent (i.e. when you unplug the device or reboot, all changes will be lost).

Systemd has a standardized way to make changes to the mapping table effectively permanent #Using udev. Udev provides an infrastructure to manage devices. That includes matching the device's modalias (a hardware identifier) of every newly connected device against the entries in the udev hardware database. If there's a match, specific things can be applied, including scancode-keycode remappings. Systemd uses this itself to assign "unknown scancodes" of many keyboard models to the correct keycode. That means your keys are recognized out of the box when your keyboard model has been found in the hardware database (the corresponding .hwdb file can be found under /lib/udev/hwdb.d/60-keyboard.hwdb).

Alternatively, you can automatically run remapping tools like setkeycodes(8) or evmap when your desktop session starts. For example, by using a systemd service. But this way the remappings won't be applied when you plug your keyboard in after your system has booted up or when you reconnect your keyboard.


Technical background

Since linux kernel version 2.6.37 there are 3 ioctl calls that can change one entry of the mapping table and 3 corresponding calls that can query one entry of the mapping table. Namely,

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

modify a mapping table entry and

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

query a mapping table entry. console_fd is a file descriptor pointing to a virtual console (it doesn't matter which virtual console) and dev_fd is a file descriptor pointing to a device file.

The KDSETKEYCODE and KDGETKEYCODE ioctl calls are legacy calls because they are unreliable and it can be hard to predict which device they'll operate on. To be specific, they will iterate over connected devices that satisfy specific rules (which can also match devices that do not correspond to a physical keyboard) and return as soon as one device driver doesn't return an error when asked to remap/query a mapping entry. To make things more problematic, some device drivers may not return an error even though the call didn't have any effect, letting the ioctl call return successfully without any changes at all. The iteration's order is the order of initalization of the devices within the kernel (which is random for devices that were plugged in since boot) [5].

The legacy KDSETKEYCODE ioctl and KDGETKEYCODE ioctl calls are used by the tools setkeycodes(8) and getkeycodes(8).
The device-specific EVIOC[SG]KEYCODE_V2 ioctl calls are used by evmap.


setkeycodes modifies user-given scancodes

Notice how setkeycodes wants to be helpful and substracts 0xdf80 (-0xe000 + 128) from every scancode bigger than 0xe000 before calling ioctl. But this may not be very helpful at all and is intransparent to users that want to remap, for example, an USB keyboard. The scancodes of USB keyboards normally consist of a 16-Bit Usage Page and a 16-Bit Usage ID. The Usage Page for keyboard is 7, resulting in large scancodes in the form 0x0007XXXX which is above 0xe000 and are therefore modified by setkeycodes, resulting in an error.

I'm not too sure why setkeycodes subtracts 0xdf80 but I have a theory:

First of all, it's ambiguous what exactly a scancode is. Converting the raw byte stream to a keycode stream involves many operations and across different protocolls it can be hard to tell at which step we have a scancode (and what belongs to a scancode).

In this article a scancode is a number (i.e. byte array) that corresponds to a key on the keyboard and can be passed to the keyboard driver to query or set the corresponding keycode. But this doesn't match the definition the scancode wikipedia article is giving. According to the wikipedia article scancodes are the key-related bytes that are transfered when a key is pressed or released (refered to as wikipedia scancode from now on). The difference between a wikipedia scancode and an actual scancode is that a wikipedia scancode can be more raw (unprocessed) and may contain encoding artifacts. For a given keyboard, scancodes are defined by the developer of the keyboard driver and wikipedia scancodes are defined by the protocol.

IBM PS/2 was the most widely used protocol for keyboards for the last couple of decades but is currently declining. However it's still sometimes/often used for internal laptop keyboards (If your keyboard is named AT Translated Set 2 keyboard it uses the PS/2 protocol). The byte 0xe0 is used to indicate trailing bytes as part of the encoding.

My guess is that the drivers for PS/2 compatible keyboards process the wikipedia scancodes before substituting them with keycodes. All wikipedia scancodes above 0xe000 are subtracted by 0xdf80 to be within the range 0x80 to 0xFF (In the pre-2000s scancodes were typically in the range 0x00 to 0x7F so there should be no overlap). Those wikipedia scancodes under 0x80 are just used directly as scancodes . This creates a hole-less range of scancodes which is easier to implement (dense mapping table). Normally, tools that query the scancodes of keyboards should show scancodes, but my guess is that back in the pre-2000s (the time when setkeycodes was written) some tools showed the actual bytes transfered (wikipedia scancodes) which people used to pass directly to setkeycodes. Since that returned an error, setkeycodes wanted to be helpful: Because almost all keyboards used the PS/2 protocol and scancodes were typically one byte, setkeycodes itself processed too large scancodes (that are likely of type wikipedia scancode) to scancodes that PS/2 keyboard drivers understand. This paragraph is pure speculation, so take it with a grain of salt.


Identifying scancodes

You need to know the scancodes of keys you wish to remap. See Keyboard input#Identifying scancodes for details.

Using udev

You can use udev's hardware database to override the default scancodes-to-keycodes mapping by following udev#Remap specific device and the update/reload sections after.

Using setkeycodes

Note You may want to avoid setkeycodes because it's a bit of a gamble wheater it will remap your keyboard. And even if it works in one boot session that doesn't 100 % guarantee it will work the next time you boot your system. See #Technical background for an in-depth explanation.

setkeycodes is a tool to update scancode-keycode mappings of a keyboard. It is best to use it with showkey. See Keyboard input#Using showkey and Keyboard input#Identifying keycodes in console.

Do note that setkeycodes only works as expected with xx and e0xx formatted hexadecimal scancodes. For USB keyboards that use larger scancodes you can do a little workaround by adding 0xdf80 to the scancode number before passing it to setkeycodes (see #setkeycodes modifies user-given scancodes).

setkeycodes usage is:

# setkeycodes scancode1 keycode1 scancode2 keycode2 ...

Scancodes are given in hexadecimal (without a leading 0x prefix), keycodes in decimal.

As stated, just executing this command will result in non-persistent changes. The changes can be made permanent by creating a new service:

/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

and enabling setkeycodes.service.



Using evmap

Note You have to compile evmap yourself.

evmap is a tool similar to setkeycodes to update scancode-keycode mappings of a keyboard, but it's keyboard specific. That means it requires a device file path of the keyboard you want to apply the remapping on. You'd want to use the corresponding keyboard device file in the folder /dev/input/by-id/ since the device file names in here are generated using hardware information, meaning the names (usually) contain information unique to the specific device they correspond to.

evmap's basic usage is:

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

Scancodes are given in hexadecimal (without a leading 0x prefix), keycodes in decimal or hexadecimal (for which you need to prefix the number with 0x). You need to pad scancodes to full bytes, e.g. instead of 3 write 03 and instead of 123 write 0123 (otherwise you'll get the non-descriptive error message Invalid definition). Instead of keycode numbers you can also use keycode names. You can print the list of all recognized keycode names with the command:

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

As stated, just executing evmap will result in non-persistent changes. The changes can be made permanent by creating a new service:

/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

and enabling evmap.service.