INT 16h is the BIOS's keyboard interface. It does not talk to the keyboard itself: that is the job of the IRQ 1 handler, INT 09h, which runs on every key press and release, turns the keyboard's scan codes into characters and stores them in a small buffer. INT 16h reads from that buffer, and from the shift flags the IRQ handler keeps up to date. DOS reads the keyboard through it too, so a key taken out with INT 16h never reaches INT 21h AH=01h, and a key stored with INT 16h AH=05h is read by DOS as if it had been typed.
| AH | Service |
|---|---|
| 00h | wait for a key and take it out of the buffer |
| 01h | is a key waiting? (ZF), and which one; the key stays |
| 02h | the shift flags: Shift, Ctrl, Alt, the Lock keys |
| 03h | set the typematic (auto-repeat) rate and delay (AT) |
| 05h | store a keystroke in the buffer (AT) |
| 10h / 11h | enhanced read and check: F11, F12 and the grey keys told apart |
| 12h | enhanced shift flags: left and right Ctrl and Alt separately |
Scan code and ASCII
Every key in the buffer is a word: the low byte is the ASCII character, the high byte the scan code, a number for the physical key. The read functions return it as it is stored, AL = ASCII and AH = scan code. "A" and "a" share scan code 1Eh and differ in AL; Ctrl+A gives AL = 01h. Keys that have no character, such as the function keys, arrows, Home or Alt+letter, are stored with AL = 0 and the scan code in AH: F1 is 3B00h, Up is 4800h, Alt+X is 2D00h. That is the whole test for an "extended" key: AL = 0.
| Key | Word (AH scan, AL ASCII) |
|---|---|
| A / a / Ctrl+A / Alt+A | 1E41h / 1E61h / 1E01h / 1E00h |
| Enter / Esc / Space | 1C0Dh / 011Bh / 3920h |
| F1 .. F10 | 3B00h .. 4400h |
| Up / Down / Left / Right (keypad) | 4800h / 5000h / 4B00h / 4D00h |
| Up on the grey arrow block (AH=10h) | 48E0h |
| F11 / F12 (AH=10h only) | 8500h / 8600h |
The buffer in the BIOS data area
The buffer is a ring of 16 words at 0040:001Eh..003Dh. Two pointers, kept as offsets from segment 0040h, chase each other round it: the head at 0040:001Ah points to the oldest key, the tail at 0040:001Ch to the slot where the next key will go. Head = tail means empty. A new key goes in at the tail and the tail moves on by 2, wrapping from the end back to the start; if that would make the tail equal to the head, the buffer counts as full and the key is dropped (the BIOS beeps). So the 16 slots hold at most 15 keys. AT-class BIOSes keep the start and end of the ring at 0040:0080h and 0040:0082h, which lets a program move the buffer elsewhere in segment 0040h.
| Address | Holds |
|---|---|
| 0040:0017h | shift flags, the byte AH=02h returns |
| 0040:0018h | more flags: left Ctrl and left Alt held, Lock keys held down, Pause active |
| 0040:001Ah | head: offset of the oldest key |
| 0040:001Ch | tail: offset where the next key goes |
| 0040:001Eh..003Dh | the buffer, 16 words |
| 0040:0080h / 0082h | start and end offset of the buffer |
| 0040:0096h | bit 4 = 1: an enhanced (101/102-key) keyboard is attached |
The example puts two keys into the buffer with INT 16h AH=05h, works out how many keys are waiting from the two pointers, and takes them out again with INT 16h AH=00h. It starts by emptying the buffer, the usual AH=01h / AH=00h loop, so the count is exact.
FLUSH1:MOV AH, 01h ; BIOS: is a key waiting?INT 16hJZ EMPTY1 ; ZF = 1: the buffer is emptyMOV AH, 00h ; take the key outINT 16hJMP FLUSH1EMPTY1:MOV AH, 05h ; store "H": scan code 23h, ASCII 48hMOV CX, 2348hINT 16hMOV AH, 05h ; store "i": scan code 17h, ASCII 69hMOV CX, 1769hINT 16hMOV AX, 40hMOV ES, AX ; ES = 0040h, the BIOS data areaMOV AX, ES:[1Ch] ; the tail: where the next key will goSUB AX, ES:[1Ah] ; minus the head: the oldest keyJNS NOWRAP1ADD AX, ES:[82h] ; the tail has wrapped round: add the ring size,SUB AX, ES:[80h] ; end minus startNOWRAP1:SHR AX, 1 ; two bytes per keyCALL PDEC ; 2MOV DX, OFFSET WAIT1MOV AH, 09h ; DOS: print the $-string at DS:DXINT 21hMOV AH, 00h ; take the oldest key outINT 16h ; AL = "H"MOV DL, ALMOV AH, 02h ; DOS: print the character in DLINT 21hMOV AH, 00h ; and the next oneINT 16h ; AL = "i"MOV DL, ALMOV AH, 02h ; DOS: print the character in DLINT 21hWAIT1 DB " keys waiting: $"PDEC PROC NEAR ; print AX in decimal; keeps every registerPUSH AXPUSH BXPUSH CXPUSH DXMOV BX, 10XOR CX, CX ; digits on the stackDECD1:XOR DX, DXDIV BX ; AX = AX / 10, DX = the last digitPUSH DXINC CXTEST AX, AXJNZ DECD1DECD2:POP DX ; the digits come back first to lastADD DL, "0"MOV AH, 02hINT 21hLOOP DECD2POP DXPOP CXPOP BXPOP AXRETPDEC ENDP
Extended keyboards
The 101-key keyboard of 1986 added F11, F12 and a second, grey block of arrows and Insert/Home/PgUp keys. To keep old programs working, the original functions 00h and 01h pretend those keys do not exist or are the old ones: a grey arrow comes back as the keypad arrow, F11 and F12 are thrown away. Programs that want them use the enhanced functions INT 16h AH=10h and 11h, which return grey keys with E0h in AL and F11/F12 as 8500h/8600h. Check bit 4 of 0040:0096h, or simply use 10h on anything from an AT onwards.
Pitfalls
Mixing BIOS and DOS keyboard input is fine as long as you remember there is one buffer: whoever reads a key first gets it. DOS checks for Ctrl+C on many of its input functions; the BIOS does not, and Ctrl+Break is handled separately, by INT 09h calling INT 1Bh and leaving a 0000h word in the buffer. A program that leaves keys in the buffer when it exits hands them to COMMAND.COM, which takes them as typing: always read what you stored.
In DOSBox-X
The keyboard BIOS is DOSBox-X's own, written in C++, and follows the IBM behaviour closely: the buffer and its pointers live where they should, AH=05h works, and the enhanced functions are there. Keys typed into the DOS screen go through the emulated keyboard controller and INT 09h like on a real PC.