Some time ago, I pulled most keys off my T480’s keyboard and rearranged them in the Dvorak layout for more ergonomic typing.

ThinkPad T480 with Dvorak Keyboard

At first I only changed the layout in the operating system and left it with that, adapting the rest didn’t fit in my tight schedule. Lately I found the time again to finish the project. The goal is to make the layout available during the boot process inside the GNU GRUB bootloader to edit boot entries and use the GRUB shell.

TL;DR This post explain in detail why switching GRUB terminal input to AT mode and setting a keymap may not work in some cases. If you do not care about the details and just want the necessary steps to solve the problem, you can skip to the end.

Starting with this, it seemed like an easy task. Everything is documented in the Arch wiki as well as several blog posts and stackoverflow answers.

Taking a look into GRUB keymaps

First the keymap must be converted into a format which GRUB understands. Keymaps for GRUB usually end with .gkb. I did not find any official nor unofficial documentation about the binary format used by GRUB for keymaps. However taking a look at the source code of GRUBs keymap command reveals some basic facts about the format.

All together this makes a total size of 2572 bytes for a .gkb file.

Converting the Dvorak keymap for GRUB

GRUB is shipped with the shell script grub-kbdcomp to convert keymaps into .gkb files. Unfortunately this script depends on ckbcomp, which is not available in the official Arch repos yet.

# grub-kbdcomp -o /boot/grub/layouts/dvorak.gkb dvorak
/usr/bin/grub-kbdcomp: line 76: ckbcomp: commant not found
ERROR: no valid keyboard layout found. Check the input.

As a workaround, ckbcomp from the AUR can be used or as ckbcomp is just a perl script it can be extracted from the package without installing it on the system:

cd /tmp
wget http://ftp.de.debian.org/debian/pool/main/c/console-setup/console-setup_1.205.tar.xz
tar -xf console-setup_1.205.tar.xz
cd console-setup-1.205
PATH=./Keyboard:$PATH grub-kbdcomp -o /boot/grub/layouts/dvorak.gkb dvorak

Loading the keymap in GRUB

GRUB supports different sources for input. The input sources are defined by the terminal_input command inside the GRUB shell or GRUB_TERMINAL_INPUT in /etc/default/grub. For example serial for serial keyboards or at_keyboard for the IBM AT compatible 8042 PS/2 controller emulation. The default value is console for the EFI_SIMPLE_TEXT_INPUT_PROTOCOL. Custom keymaps are not supported with the console input, as the EFI performs the mapping and feeds the resulting unicode characters to GRUB. Therefore the at_keyboard input is required, as this mode provides scan codes from the keyboard, that can be translated by GRUB.

The first step is to edit /etc/default/grub and change

GRUB_TERMINAL_INPUT=console

to

GRUB_TERMINAL_INPUT=at_keyboard

Next GRUB needs to actually load the Dvorak keymap. To use the keymap command, the keylayouts module needs to be loaded first.

To permanently add the aforementioned changes to the GRUB configuration, edit /etc/grub.d/40_custom and insert these two lines:

insmod keylayouts
keymap dvorak

Finally the configuration needs to be recreated with

grub-mkconfig -o /boot/grub/config.cfg

and after a reboot everything should work right away.

Except it doesn’t for me.

Debugging the keymap problem

With the above mentioned configuration almost nothing worked inside GRUB. Pressing keys on the keyboard does mostly nothing and sometimes issues weird input. I was still able to boot my system by waiting for the GRUB timeout to finish, but could not do any action - for example selecting another entry - inside Grub.

It first I reverted all my changes, restarted the ThinkPad and entered the GRUB console by pressing c (or k on my not yet configured Dvorak keyboard) inside the GRUB menu. There I tried to perform all the changes manually to see which one breaks the keyboard. It out, that terminal_input at_keyboard was responsible for the degradation right away. After some digging through the internet I found a relevant bug report at the Debian Bug tracker explaining what is going on. The keyboard sends data to the computer indicating which action where performed on it - namely which keys where pressed and released. This data is called “scan code”.

Understanding scan code sets

Same as always, there are several standards for translating keystrokes into scan codes.

xkcd 927 - STANDARDS

Randall Munroe July 20, 2011 - 927: STANDARDS

Historically it started with the IBM Personal Computer/XT in 1983. This PC introduced the XT keyboard which send keystrokes encoded in what is nowadays known as scan code set 1. Most common keys have single byte keystrokes with the most significant bit equal set if the key is pressed and cleared when the key is released. A little over a year later, the IBM Personal Computer/AT was introduced and brought a new protocol - the scan code set 2 - for communication with the keyboard. The scan code set 2 is mostly incompatible with the scan code set 1, as most scan codes have an entirely different meaning. Also releasing a key is not indicated by the byte 0xF0 followed by the scan code of the key. Furthermore around the same time the scan code set 3 was introduced, which can be best described as a superset of the second scan code set. However the scan code set 3 is not relevant for this post.

The following table list a selected variety of scan codes and their translation to set 1 and 2.

Scan code(s) Set1 Set2
0x01 Esc F9
0x02 1  
0x03 2 F5
0x04 3 F3
0x05 4 F1
0x06 5 F2
0x07 6 F12
0x08 7  
0x09 8 F10
0x0A 9 F8
0x0B 0 F6
0x0C - F4
0x0D = Tab
0x0E Backspace  `
0x0F Tab  
0x10 q  
0x11 w Left Alt
0x12 e Left Shift
0x13 r  
0x14 t Left Ctrl
0x15 y q
0x16 u 1
0x17 i  
0x18 o  
0x19 p  
0x1A [ z
0x1B ] s
0x1C Enter a
0x1D Left Ctrl w
0x1E a 2
0x1F s  
0x20 d  
0x21 f c
0x22 g x
0x23 h d
0x24 j e
0x25 k 4
0x26 l 3
0x27 ;  
0x28  
0x29  ` Spacebar
0x2A Left Shift v
0x2B \ f
0x2C z t
0x2D x r
0x2E c 5
0x2F v  
0x30 b  
0x31 n n
0x32 m b
0x33 , h
0x34 . g
0x35 / y
0x36 Right Shift 6
0x39 Spacebar  
0x3A Caps Lock m
0x3B F1 j
0x3C F2 u
0x3D F3 7
0x3E F4 8
0x3F F5  
0x40 F6  
0x41 F7 ,
0x42 F8 k
0x43 F9 i
0x44 F10 o
0x45 Num Lock 0
0x46 Scroll Lock 9
0x49 Keypad 9 .
0x4A Keypad - /
0x4B Keypad 4 l
0x4C Keypad 5 ;
0x4D Keypad 6 p
0x4E Keypad + -
0x54   [
0x55   =
0x57 F11  
0x58 F12 Caps Lock
0x5A   Enter
0x5B   ]
0x5D   \
0x66   Backspace
0x76   Esc
0xE0 0x48 Up Arrow  
0xE0 0x49 Page Up  
0xE0 0x4B Left Arrow  
0xE0 0x4D Right Arrow  
0xE0 0x6B   Left Arrow
0xE0 0x74   Right Arrow
0xE0 0x75   Up Arrow

So what is going on now

As the aforementioned bug report indicates, the keyboard sends scan codes in scan code set 2 while GRUB interprets the input as set 1. This can be verified either by trial and error - pressing the key c (in US ANSI layout). The keyboard will issue

0x21 0xF0 0x21
 │    │    └──  Key `c` in set 2, interpreted as `f` in set 1
 │    └───────  Indicating key release in set 2, ignored in set 1
 └────────────  Key `c` in set 2, interpreted as `f` in set 1

and GRUB reads it as ff. Otherwise GRUB provides the inb and outb commands to read bytes directly from specific ports. The PS/2 controller uses 0x60 as data port (where the keyboard data can be read). To read a scan code press the key on the keyboard connected to the PS/2 controller and enter inb 0x60 into the GRUB shell from another serial or USB keyboard.

How to fix the problem?

Multiple places in the internet mention setting the input method with terminal_input at_keyboard ; outb 0x64 0x60 ; outb 0x60 0x64 from GRUB. Aside from the fact, that this does not work for me, what should it even do?

The ports 0x64 and 0x60 are both used for communication with the PS/2 controller. The first port (0x64) reads the status of the controller and sends commands, the other port (0x60) reads and sends data.

The command

outb 0x64 0x60

writes the byte 0x60 into port 0x64 and tells the PS/2 controller to write the next byte from the data port into offset 0 off the controllers RAM. Then the next command

outb 0x60 0x64

sends the controller configuration byte 0x64 to the controller. The format of the configuration byte is

Bit Description
0 First port interrupt enabled
1 Second port interrupt enabled
2 Has the system passed POST?
3 ?
4 First port clock enabled
5 Second port clock enabled
6 XLAT (translation of set 2 to set 1) enabled
7 ?

This byte send to the controller has the 2nd and 5ft and 6th bit set - so it tells the system, that the Power On Self Test has been passed, enables the translation from set 2 to set 1, which is exactly what I need and also enables the second port clock? I am not sure about the third part, some sources list the 5th byte as enabling the keyboard in general, which would make more sense I guess. Since I do not exactly now the meaning of the 5th byte, I will leave it cleared.

  1. attempt. I use terminal_input at_keyboard console instead. That way a USB keyboard keeps working and I can recover the system after the internal keyboard is gone.

    grub> terminal_input at_keyboard console ; outb 0x64 0x60 ; outb 0x60 0x40
    

    After this command, my internal keyboard was still behaving weird.

  2. attempt, running the commands one after another

    grub> terminal_input at_keyboard console
    grub> outb 0x64 0x60 ; outb 0x60 0x40
    

    Still not the desired result.

Well then, are there any other ways to get the keyboard to send keystrokes in set 1. There is indeed another way. Up until now I only talked to the PS/2 controller and trick it into translating the scan codes from set 1 to set 2. The keyboard itself can also be told which scan code set it should use. Communication with the keyboard happens by writing to port 0x60. The command 0xf0 lets one set or read the current scan code set. These arguments are available for the command:

Argument Description
0x00 Read the current scan code set.
0x01 Set the scan code set 1.
0x02 Set the scan code set 2.
0x03 Set the scan code set 3.

As a first attempt, I will try to read the current scan code set:

grub> outb 0x60 0xf0 ; outb 0x60 0x00 ; inb 0x60
0x2

Well, this seems correct. Now let’s try to change the scan code set:

grub> outb 0x60 0xf0 ; outb 0x60 0x01

aaaaaand the keyboard works. Now I just enter normal to go back to the GRUB menu and select my Linux system. At that point I realized, that my keyboard was now entering gibberish in Linux.

Forcing Linux to reset the AT keyboard

After digging a bit around in the source code of the Linux AT and PS/2 keyboard driver, there is a parameter with the name reset defined:

#if defined(__i386__) || defined(__x86_64__) || defined(__hppa__)
static bool atkbd_reset;
#else
static bool atkbd_reset = true;
#endif
module_param_named(reset, atkbd_reset, bool, 0);
MODULE_PARM_DESC(reset, "Reset keyboard during initialization");

Vojtech Pavlik 1999-2002, Linux, drivers/input/keyboard/atkbd.c

This parameter is used for the following code - which is exactly what I need.

/*
 * Some systems, where the bit-twiddling when testing the io-lines of the
 * controller may confuse the keyboard need a full reset of the keyboard. On
 * these systems the BIOS also usually doesn't do it for us.
 */

if (atkbd_reset)
        if (ps2_command(ps2dev, NULL, ATKBD_CMD_RESET_BAT))
                dev_warn(&ps2dev->serio->dev,
                         "keyboard reset failed on %s\n",
                         ps2dev->serio->phys);

Vojtech Pavlik 1999-2002, Linux, drivers/input/keyboard/atkbd.c

To force the reset, add atkbd.reset=1 to the kernel command line by editing the variable GRUB_CMDLINE_LINUX_DEFAULT in the file /etc/default/grub.

Configuring GRUB with the Dvorak keyboard layout

All the steps to change the keymap inside GRUB pulled together:

  1. Convert the Dvorak keymap in a format understood by GRUB.
    cd /tmp
    wget http://ftp.de.debian.org/debian/pool/main/c/console-setup/console-setup_1.205.tar.xz
    tar -xf console-setup_1.205.tar.xz
    cd console-setup-1.205
    PATH=./Keyboard:$PATH grub-kbdcomp -o /boot/grub/layouts/dvorak.gkb dvorak
    
  2. Tell GRUB to use the AT keyboard driver for input by editing /etc/default/grub and changing
    GRUB_TERMINAL_INPUT="console"
    

    to

    GRUB_TERMINAL_INPUT="at_keyboard"
    
  3. Load the keymap inside GRUB and switch the keyboard to scan code set 1. Edit /etc/grub.d/40_custom and add
    insmod keylayouts
    keymap dvorak
    # Tell the PS/2 keyboard to switch to scan code set 1
    outb 0x60 0xf0 ; outb 0x60 0x01
    

    at the end.

  4. Force the Linux Kernel to reset the keyboard upon starting. Edit /etc/default/grub again and add
    atkbd.reset=1
    

    to GRUB_CMDLINE_LINUX_DEFAULT.

  5. Recreate the GRUB configuration. Run
    grub-mkconfig -o /boot/grub/grub.cfg