I got a little carried away researching for what was just intended to be a mild elaboration on what was already said. I hope you find this interesting nonetheless:
Old Terminal Hardware
First, the ASCII section in Eric S. Raymond's "Things Every Hacker Once Knew" goes into more detail on how old terminals mapped keys, including that Shift toggles the 16 or 32 bit depending on the key, and that Ctrl would mask out the top three bits.
That's why you type Ctrl+D (Historically visualized as ^D
) to exit an interactive Python session on Linux or if you're using cat > name_of_file
as an equivalent to the old DOS COPY CON
trick. The binary representation of D
is 1000100
while 0000100
is the ASCII control character EOT
(End of Transmission).
As ESR points out, DOS and Windows use Ctrl+Z (^Z
) as their end-of-file character, which is not the ASCII-specified meaning but still derives from how old terminals would mask out the top three bits of the letter when you held Ctrl.
Here's the ASCII-to-binary chart he includes, generated using his ascii utility:
0000000 NUL 0100000 1000000 @ 1100000 `
0000001 SOH 0100001 ! 1000001 A 1100001 a
0000010 STX 0100010 " 1000010 B 1100010 b
0000011 ETX 0100011 # 1000011 C 1100011 c
0000100 EOT 0100100 $ 1000100 D 1100100 d
0000101 ENQ 0100101 % 1000101 E 1100101 e
0000110 ACK 0100110 & 1000110 F 1100110 f
0000111 BEL 0100111 ' 1000111 G 1100111 g
0001000 BS 0101000 ( 1001000 H 1101000 h
0001001 HT 0101001 ) 1001001 I 1101001 i
0001010 LF 0101010 * 1001010 J 1101010 j
0001011 VT 0101011 + 1001011 K 1101011 k
0001100 FF 0101100 , 1001100 L 1101100 l
0001101 CR 0101101 - 1001101 M 1101101 m
0001110 SO 0101110 . 1001110 N 1101110 n
0001111 SI 0101111 / 1001111 O 1101111 o
0010000 DLE 0110000 0 1010000 P 1110000 p
0010001 DC1 0110001 1 1010001 Q 1110001 q
0010010 DC2 0110010 2 1010010 R 1110010 r
0010011 DC3 0110011 3 1010011 S 1110011 s
0010100 DC4 0110100 4 1010100 T 1110100 t
0010101 NAK 0110101 5 1010101 U 1110101 u
0010110 SYN 0110110 6 1010110 V 1110110 v
0010111 ETB 0110111 7 1010111 W 1110111 w
0011000 CAN 0111000 8 1011000 X 1111000 x
0011001 EM 0111001 9 1011001 Y 1111001 y
0011010 SUB 0111010 : 1011010 Z 1111010 z
0011011 ESC 0111011 ; 1011011 [ 1111011 {
0011100 FS 0111100 < 1011100 \ 1111100 |
0011101 GS 0111101 = 1011101 ] 1111101 }
0011110 RS 0111110 > 1011110 ^ 1111110 ~
0011111 US 0111111 ? 1011111 _ 1111111 DEL
If you'd like more information on the specific standards that established the various meanings of the ASCII control characters, I recommend Aivosto Oy's Control characters in ASCII and Unicode which I can't really meaningfully excerpt because it's mostly tabular information.
Conversely, if you'd like to know more about how key-presses on UNIX and Linux terminals (and terminal emulators) get translated into what's seen by software, Linus Åkesson's The TTY demystified is the most accessible reference I've ever found.
The "Configuring the TTY device" section at the end even provides various example commands you can play with in any environment which presents a VT100-compatible terminal emulator plus POSIX-compliant commands. (UNIX, Linux, macOS, Microsoft's WSL, etc.)
IBM PC Hardware
To step forward one ecosystem, the IBM PC keyboard puts a microcontroller in the keyboard which bundles all the matrix decoding up in a binary protocol based on key presses and releases with no concern for what role each key plays. Modifiers become modifiers on the PC side.
The easiest way to play around with this is probably to boot up an X11-based desktop (eg. on Linux) and play around with the preset key reassignments offered by the setxkbmap
tool.
setxkbmap -option compose:rctrl -option ctrl:swapcaps -option shift:both_capslock_cancel
This command will...
- Remap the right control key to Compose so you can type ™ by pressing (and not holding) Composetm
- Swap the Ctrl and Caps Lock keys so you can type your emacs keyboard shortcuts with less awkwardness and risk of repetitive strain injuries.
- Set up "pressing both Shift keys together enables Caps Lock, pressing one Shift key disables it"
(A selection of behaviours I chose to demonstrate both using modifier keys as ordinary keys and vice-versa.)
For further details on playing with setxkbmap, I recommend a blog post I wrote after not finding anything better, Getting your way with setxkbmap.
If you want to explore the wire protocol that IBM established and PC clone makers followed with the XT, AT, and PS/2 keyboards, The Vintage PC Pages at seasip.info have some excellent SVG diagrams and tables explaining the key mappings used by the microcontrollers in the UK versions of various standard-setting IBM PC keyboards (XT 83-key, XT 84-key, PS/2 102-key) to report key-presses over the wire.
It links to 10. Keyboard-internal scan codes from "Keyboard scancodes"
by Andries Brouwer for a detailed explanation of how the scan codes are encoded as byte sequences. Because IBM defined three different modes to the protocol for backwards compatibility, each encoding a "break code" (key release event) differently, I won't go into detail here but the gist is:
- In Set 1 (PC XT), each key is assigned an identifier consisting of one or more bytes (usually just one) using a scheme which guarantees unique prefixes and key releases are indicated by XORing the last byte with 0x80.
- In Set 2 (PC AT), each key is again assigned a (different) byte sequence but key releases are indicated by sending a 0xf0 byte before the last byte of the key sequence.
- In Set 3 (Terminal), key releases are indicated the same way as Set 2 but it's possible to enable or disable reporting of key releases on a per-key basis and, by default, it's only enabled for modifiers and keys which didn't exist when IBM specified the protocol, such as multimedia keys.
In both Set 1 and Set 2, the Pause/Break key is unique in generating a uniquely long 6-byte (Set 1) or 8-byte sequence.
Unless the requested otherwise, the keyboard controller on the PC side will translate Set 2 or Set 3 keycodes into Set 1 for legacy compatibility.
According to Brouwer, support for Sets 1 and 3 is either buggy or missing in most non-IBM clone keyboards and it is also the only mode supported when BIOS legacy support is exposing a USB keyboard as if it were a PS/2 device.
The IBM 6110344 Keyboard page on seasip.info points out that, as it was used for the 122-key keyboard of the IBM 5271, which predates the PC AT, it is likely that Set 3 originates in the world of IBM 3270 terminals.
If you want to go deeper, Adam Chapweske wrote PS/2 Mouse/Keyboard Protocol, which goes into the electrical details you need to build your own keyboard or hardware keyboard emulator using a microcontroller.
USB Hardware
You'd think that, because dual-mode keyboards which can speak USB and PS/2 protocol are a thing, there isn't much relevant to your question that wasn't already answered, but USB HID actually re-introduces treating modifier keys differently as a form of data compression.
Details can be found in the USB HID specification but the gist is:
USB HID defines two different protocols keyboards can speak. The fact that most keyboards only implement the boot protocol is what leads people to believe that USB HID is inherently limited to 6-key rollover.
The boot protocol is specified to fit in a single USB 1.1 Low-Speed "transaction" (I think of them as akin to TCP/IP packets), which is limited to 8 bytes.
As such, when a USB keyboard is speaking the barebones boot protocol that everything must implement for compatibility with things like BIOS menus, the PC polls for status (USB HID says interrupts are supported but optional) and receives a report that looks like this[1]:
Bit 0 Bit 1 Bit 2 Bit 3 Bit 4 Bit 5 Bit 6 Bit 7
╔═══════╦════════╦═══════╦═══════╦═══════╦════════╦═══════╦═══════╗
Byte 0 ║ LCtrl ║ LShift ║ LAlt ║ LWin ║ RCtrl ║ RShift ║ RAlt ║ RWin ║
╠═══════╩════════╩═══════╩═══════╩═══════╩════════╩═══════╩═══════╣
Byte 1 ║ Reserved ║
╠═════════════════════════════════════════════════════════════════╣
Byte 2 ║ Keycode #1 ║
╠═════════════════════════════════════════════════════════════════╣
Byte 3 ║ Keycode #2 ║
╠═════════════════════════════════════════════════════════════════╣
Byte 4 ║ Keycode #3 ║
╠═════════════════════════════════════════════════════════════════╣
Byte 5 ║ Keycode #4 ║
╠═════════════════════════════════════════════════════════════════╣
Byte 6 ║ Keycode #5 ║
╠═════════════════════════════════════════════════════════════════╣
Byte 7 ║ Keycode #6 ║
╚═════════════════════════════════════════════════════════════════╝
(Thank you, tablesgenerator.com and Vim editing commands.)
Note that this does not constrain USB HID to only 256 keycodes. (Though the X11 input event protocol did bake in such a limitation)
The USB HID Usage Tables define key codes 0xA5-0xAF and 0xE8-0xFFFF in the Keyboard table as Reserved and some devices, such as the ATi Remote Wonder II, have buttons from other tables such as Channel Down/Up which get mapped to key codes above 255 by the Linux driver.
I'm unsure whether these keys require switching away from the boot protocol to access or if there's an option I missed to switch the boot protocol into a mode which supports three 16-bit key codes rather than six 8-bit key codes.