Computing: DOS, OS/2 & Windows Programming

16-bit assembly keyboard programming (NASM on DOS).

In my tutorial 16-bit assembly programming using NASM, I describe how to build 16-bit real mode and protected mode programs on DOS. The program samples provided use DOS interrupt 21h to read from the keyboard, and to write to the screen. Reading from the keyboard using interrupt 21h (the so-called DOS Services), is fine if we want the user to enter some text. However, there are situations, where we want to do the user just hit a key (without hitting ENTER to terminate input), or simply to check if the user has pressed any or a given key. Also, the DOS input functions do not allow to enter special characters, such as the function keys, for example.

This tutorial is about BIOS interrupt 16h, part of the so-called BIOS Services, to get input from the keyboard. Interrupt 16h is the application-level interface to the keyboard. The tutorial shows some sample programs using interrupt 16h. All of these samples have been developed and tested on FreeDOS, using NASM 2.16.0.1 (16-bit protected mode). The tutorial should also apply to MS-DOS and other DOS operating systems. Use the following link to download the source code of the sample programs.

The first part of the tutorial includes details concerning interrupts in general, and the keyboard interrupt in particular. This may seem too much technical for some readers. Don't worry: It is not mandatory to perfectly understand this stuff in order to be able to do keyboard programming using interrupt 16h. If you want, you may skip the interrupt details, and pass to the second part (further theoretical background): The ASCII code. Or jump directly to Programming the keyboard (and going back to the ASCII details as needed).

Note: The first part of the tutorial is mostly based on the article Interrupts and Handlers Part 1, by Frederico Jerónimo, published at delorie.com. If you want to know more about interrupts, please, visit that (really well done) webpage...

Introduction to interrupts.

An interrupt is a request to the processor to suspend its current program and transfer control to a new piece of code, called an interrupt service routine (ISR). The ISR determines the cause of the interrupt, takes the appropriate action, and then returns control to the original program that was suspended.

For the 80×86 processor, there are 256 different interrupts (ranging from 0-255) available. These interrupts are part of one of the following 3 categories: Processor interrupts, hardware interrupts, software interrupts.

Processor interrupts (exceptions).

Exceptions originate in the processor itself (the interrupt number is set by the processor). They generally occur when the processor can't handle an internal error caused by system software. There are three main classes of exceptions:

The following table lists the processor interrupts (interrupts 00h – 07h):

80x86
INT
Description Notes
00hDivision by zero 
01hSingle stepAllows to follow the program execution instruction after instruction (debugging)
02hMemory error 
03hBreakpointAllows to stop the program execution at a given instruction (debugging)
04hOverflow 
05hPrint screenCalled, via keyboard interrupt 09h (thus not a processor interrupt) when the PrtSc key is pressed
06hInvalid instruction 
07hCoprocessor instructionCalled if no coprocessor is installed

Hardware interrupts.

Hardware interrupts are set by hardware components, such the timer component, or by peripheral devices, such as the keyboard. I/O devices can be serviced in two different ways: The CPU polling method and the interrupt based technique. In the first case, the processor continuously polls the device, in order to check if some "event" occurred. In the second case, it's the device that notifies the processor, if the "event" occurs. As the processor usually only has a single interrupt input but requires data exchange with several interrupt-driven devices, a special chip, the 8259A PIC has been implemented in order to manage them. The 8259A PIC acts like a bridge between the processor and the interrupt-requesting components, i.e. the interrupt requests are first transferred to the 8259A PIC, which in turn drives the interrupt line to the processor. Thus, the processor is saved the overhead of determining the source and priority of the interrupting device.

A characteristic of the 8259A PIC is its cascading capability, that is, the possibility to interconnect one master and up to eight slave PICs. A typical PC uses two PICs to provide 15 interrupt inputs (7 on the master PIC and 8 on the slave one). The following table lists the interrupt sources on the PC (sorted in descending order of priority):

Input on
8259A
80×86
INT
Device
IRQ 008hTimer Chip
IRQ 109hKeyboard
IRQ 20AhConnected to the secondary 8259A interrupt controller; may be used by EGA for vertical retrace
IRQ 971hRedirection from occurrences of IRQ 2; may be generated by EGA/VGA
IRQ 870hReal-time clock
IRQ 1072hReserved
IRQ 1173hReserved
IRQ 1274hReserved on AT systems; auxiliary (AUX) device on PS/2 systems
IRQ 1375h80x87 math coprocessor
IRQ 1476hHard disk controller
IRQ 1577hReserved
IRQ 30BhSerial Port 2 (COM2)
IRQ 40ChSerial Port 1 (COM1)
IRQ 50DhParallel port 2 (LPT2) on AT systems, reserved on PS/2 systems
IRQ 60EhDiskette drive
IRQ 70FhParallel Port 1 (LPT1)

Software interrupts.

Software interrupts are initiated with an INT instruction and, as the name implies, are triggered via software.

In the real mode address space of the i386, 1024 bytes are reserved for the interrupt vector table (IVT). This table contains an interrupt vector for each of the 256 possible interrupts. Every interrupt vector in real mode consists of four bytes and gives the jump address of the interrupt service routine (interrupt handler) for the particular interrupt in segment:offset format. On IA-32 and x86-64 architectures, the interrupt descriptor table (IDT) is the protected mode and long mode counterpart of the IVT. This table tells the CPU where a given interrupt service routine (one per interrupt vector) is located.

How all this (rather complicated stuff) exactly works is important to know, if we want to write our own custom interrupt handlers. For using the ISR in our programs, all we need to care about is the interrupt number, and the input and output parameters of the routine called. In my NASM tutorial, we saw know that to use the DOS Services, we have to call interrupt 21h. The function, that we want to call, has to be in AH (ex: 09h = print string to screen; 0ah = read string from keyboard), or sometimes in AX (ex: 4c00h = DOS exit function). The registers, to be used for other input or for output parameters, depend on the function used. Example: the DOS "read character" function (function code = 01h) has no further input parameter; the output of the function (ASCII code of the character read) will be available in the AL register.

This tutorial is about the BIOS Keyboard Services, that are accessed by calling interrupt 16h. Even more interesting for programmers are the BIOS Video Services, accessed by calling interrupt 10h. I am actually working on the first part of tutorial about that... Another interesting BIOS interrupt is the Timer I/O interrupt (interrupt 1ah).

Keyboard interrupt 09h.

Each time a keyboard key is pressed or released, the IRQ 1 input line of the 8259A PIC is activated (set to a high level). This results in the following (simplified; for details, cf. the article Interrupts and Handlers Part 1 at delorie.com):

  1. The PIC activates its output INT line (connected to the INTR input of the processor) to inform the processor about the interrupt request. This starts an interrupt acknowledge sequence.
  2. The CPU receives the INT signal, finishes the currently executing instruction and, provided that hardware interrupts are not disabled (masked within the CPU), outputs two interrupt acknowledge (INTA) pulses.
  3. The PIC, receiving the INTA, puts an 8-bit number onto the data bus. The CPU reads this number as the number of the interrupt handler to call, which is then fetched and executed, the scan code of the key pressed being passed to the handler.
  4. When the interrupt handler is finished, the PIC is informed of the "end of interrupt". Finally, the processor gives back the control to the program that has been interrupted.

Two important points to understand here:

The code of the default keyboard interrupt handler is part of the BIOS. But, it is also possible to use our own custom keyboard interrupt handler, by changing the corresponding entry in the IVT (IDT).

So, what does the BIOS code of the keyboard interrupt handler do? The most important action consists in interpreting the keystroke, storing a given value (the scan code of the key that has been pressed) into the keyboard buffer, located at address 0040:001e, and converts the scan code into an ASCII code, or an extended ASCII keystroke code.

One possibility to read-in the key pressed into our program would thus be to directly retrieve the key's scan code from the keyboard buffer. This is, however, not a good practice, because it does not ensure compatibility across the various keyboard layouts. The correct way to do is to call the BIOS keyboard services (INT 16h), that allow us to read the ASCII or Extended ASCII keystroke code instead.

Note: The ASCII code passed to the keyboard interrupt handler depends on the keyboard driver loaded (this is done in your fdauto.bat/autoexec.bat file). Thus, if you use a German keyboard (as I do), you'll have to load a German keyboard driver in order to make sure that the code returned by a call to interrupt 16h is the one corresponding to the key on the keyboard that you are actually using.

The keyboard interrupt handler includes some further actions:

The ASCII code.

The American Standard Code for Information Interchange (ASCII code) is a set of 256 arbitrary assignments of text characters, graphic symbols, and control characters. The lower 128 members of the ASCII set (00h to 7fh) are formally defined. Values above 7fH (decimal 127) are interpreted in different ways by various computers, printers, languages, etc. On a DOS operating system, they essentially depend on the DOS code page used.

Note: ASCII often refers to the 128 formally defined characters only, and the complete set is then designated by the term ANSI codes. A major difference between ASCII and ANSI is that, except for the first 128 characters, within the context of the system, ANSI character sets are not the same. Thus, there are a whole bunch of code pages, that are referred to as ANSI. They have in common the 128 ASCII characters, but the other 128 codes may display as this character with one code page, and as that other character with another code page.

The ASCII codes from 00h to 1fh (decimal 0 to 31) are so-called control characters (as opposed to text = printable characters). The most important ones for us are: 08h = BS (backspace); 09h = TAB; 0ah (decimal 10) = LF (linefeed); 0dh (decimal 13) = CR (carriage return); 1bh (decimal 27) = ESC (escape). Except for linefeed, these control characters have a corresponding key on the keyboard (CR = 0dh actually corresponds to the ENTER key). In fact, all control characters can be entered from the keyboard, using the CTRL-key in combination with an other key. Example: CR = CTRL+M.

The figure below (taken from the techhelpmanual.com website) shows a table with the 32 ASCII control characters.

ASCII control characters

The next figure (taken from Wikipedia) shows all 128 ASCII codes (the colored boxes indicate characters that have been changed or added in 1963 resp. 1965).

ASCII characters table

As I said above, the characters corresponding to the codes above 7fh depend on the DOS code page, actually loaded (this is done in your fdconfig.sys/config.sys file). If you live in Western Europe, the Americas, or Australia, your DOS system probably uses codepage 437 (Latin_US), or codepage 850 (Latin International), that as a difference with Latin-US contains more accented letters (less text graphics symbols).

The screenshots below (output of a FASM program, that I wrote on my Windows 11), show the ANSI tables for code page 437 (on the left) and for code page 850 (on the right).

ANSI characters table (code page 437)
ANSI characters table (code page 850)

Note: In 1998, they introduced code page 858, identical to code page 850, except that the "dotless i" (character d5h) has been replaced by the euro symbol (€).

Extended ASCII keystroke codes.

If you look at the ASCII (ANSI) tables above, you notice there aren't any codes for special keys like the function keys, or the arrow and other cursor positioning related keys, nor for special key combinations, i.e. "normal" keys pressed together with the ALT key ("normal" keys pressed together with SHIFT correspond to the second text character for this key, and appear in the table; so do "normal" keys pressed together with CTRL, that correspond to the control characters).

Whereas the code of the "normal" keys is described in the ASCII (ANSI) table, the code for the "special" keys and "special key combinations" are described in another table: the table of extended ASCII keystroke codes. The figure below (taken from the techhelpmanual.com website) shows this table.

Extended ASCII keystroke codes table

The table above concerns the original AT keyboard. For the 101-keys keyboard (additional F11 and F12 function keys, numeric keypad), the BIOS supports so-called 101-key keyboard extensions, i.e. a whole series of additional extended ASCII keystroke codes. They are shown in the table below.

101-keys keyboard extended ASCII keystroke codes table extension

Programming the keyboard.

After all this theoratical stuff, we are finally arrived at the programming part of the tutorial. In the following paragraphs, I'll show some simple NASM programs, that call interrupt 16h of the BIOS Services to read characters from the keyboard; there is also an example that shows how you can simulate keyboard input from within your NASM code.

Calling interrupt 16h is similar to calling interrupt 21h. We put the code of the function to be called in register AH, the function return can be retrieved in register AL, or AX.

Program sample 1: Terminate program with ESC key.

The read (wait for) next keystroke function code is 00h. The table shows the arguments of this function.

 RegisterValueDescription
InputAH00hBIOS Keyboard Services function code
OutputALASCII code, or 0AL = 0 indicates that some special key has been pressed
 AHscan codeif AL = 0, this corresponds to the extended ASCII keystroke code

Important:

The program sample waitkey.asm waits for a key to be pressed on the keyboard. If the key pressed is the ESC key, the program terminates.

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display text
        mov     ah, 09h
        mov     dx, stext
        int     21h
doloop:
        ; Wait for key pressed
        mov     ah, 00h
        int     16h
        ; Check if key pressed is ESC key
        cmp     al, esckey
        jne     doloop
        ; Terminate program
exit:
        mov     ah, 09h
        mov     dx, newline
        int     21h
        mov     ax, 4c00h
        int     21h
segment data
esckey  equ     1bh
stext   db      'This program pauses until you hit the ESC key... ', '$'
newline db      13, 10, '$'
segment stack   stack
        resb    64
stacktop:

The screenshot shows the build of the assembly source (using my custom script nasm.bat), and the execution of the binary created.

NASM on FreeDOS: Assembly program example - Terminating a program with the ESC key

Program sample 2: Waiting for a number key.

The program sample waitkey2.asm is just a little extension of the previous one. It waits for a key to be pressed on the keyboard. If the key pressed is a number key, the number is printed (otherwise a message is displayed). As before, the ESC key terminates the program.

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display text
        mov     ah, 09h
        mov     dx, stext
        int     21h
doloop:
        ; Wait for key pressed
        mov     ah, 00h
        int     16h
        ; Exit if key pressed is ESC key
        cmp     al, keyesc
        je      exit
        ; Check if key pressed is a number key
        cmp     al, key0
        jl      nonumber
        cmp     al, key9
        jg      nonumber
        mov     [number], al
        mov     ah, 09h
        mov     dx, snumber
        int     21h
        jmp     doloop
nonumber:
        mov     ah, 09h
        mov     dx, snonum
        int     21h
        jmp     doloop
        ; Terminate program
exit:
        mov     ax, 4c00h
        int     21h
segment data
keyesc  equ     1bh
key0    equ     30h
key9    equ     39h
stext   db      'Hit a number key or the ESC key to exit... ', 13, 10, '$'
snumber db      'You hit the number key: '
number  resb    1
        db      13, 10, '$'
snonum  db      'This is not a number key!', 13, 10, '$'
segment stack   stack
        resb    64
stacktop:

The screenshot shows a program execution.

NASM on FreeDOS: Assembly program example - Waiting for a number key

Program sample 3: Checking if a key has been pressed.

The query keyboard status (preview key) function code is 01h. The table shows the arguments of this function.

 RegisterValueDescription
InputAH01hBIOS Keyboard Services function code
OutputFlags (ZF)clear: indicates that there is no key in the buffer 
  set: indicates that a key is ready 
 ALif key ready: ASCII code, or 0AL = 0 indicates that some special key has been pressed
 AHif key ready: scan codeif AL = 0, this corresponds to the extended ASCII keystroke code

Important:

The program sample readkey.asm continuously displays the letters 'A' to 'Z' (one after the other, a with a delay between two displays), until a key (any key) has been pressed on the keyboard.

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display text
        mov     ah, 09h
        mov     dx, stext
        int     21h
        ; Start with letter "A"
        mov     byte [letter], codeA
doloop:
        ; Display the letter
        mov     ah, 02h
        mov     dl, [letter]
        int     21h
        ; Check if a key has been pressed
        mov     ah, 01h
        int     16h
        jnz     exit
        ; Delay
        mov     bp, 43690
        mov     si, 4369
delayloop:
        dec     bp
        nop
        jnz     delayloop
        dec     si
        cmp     si, 0
        jnz     delayloop
        ; Next letter
        mov     al, [letter]
        inc     al
        cmp     al, codeZ
        jle     continue
        mov     al, codeA
continue:
        mov     [letter], al
        jmp     doloop
exit:
        ; Read the key (empty the buffer)
        mov     ah, 00h
        int     16h
        ; Terminate program
        mov     ah, 09h
        mov     dx, newline
        int     21h
        mov     ax, 4c00h
        int     21h
segment data
codeA   equ     41h
codeZ   equ     5ah
stext   db      'This program displays the alphabet until you hit a key... ', 13, 10, '$'
newline db      13, 10, '$'
letter  resb    1
segment stack   stack
        resb    64
stacktop:

Note: Concerning the delay between the display of two subsequent letters, note that the function 86h of BIOS interrupt 15h (AT Extended Services) may be used to pause execution for a given number of microseconds. However, on my VMware Workstation 16 virtual machine, running FreeDOS, calling this interrupt resulted in a fatal error of JEMMEX.EXE. The code, that I have finally used to implement the delay is based on the post by Jer Yango as answer to the question How to set 1 second time delay at assembly language 8086?, asked at stackoverflow.com.

The screenshot shows the program output.

NASM on FreeDOS: Assembly program example - Checking if a key has been pressed

Program sample 4: Display keystroke code.

The program sample keycode1.asm waits for a key pressed on the keyboard (loop, terminated by the ESC key), and displays the keystroke code corresponding to this key. In the case of a "normal" key, the code displayed corresponds to the ANSI code in the code page actually loaded (code page 850 in my case; cf. table further up in the text). In the case of a special key, or the combination of a "normal" key with ALT, this code is two bytes: 00h, followed by the extended ASCII keystroke code (as shown in the corr. table further up in the text).

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display text
        mov     dx, stext
        mov     ah, 09h
        int     21h
doloop:
        ; Wait for key pressed
        mov     ah, 00h
        int     16h
        ; Check if key pressed is ESC key
        cmp     al, keyesc
        je      exit
        ; Check if return is ASCII code or extended ASCII keystroke code
        cmp     al, 0
        je      extended
        ; Display ASCII code
        call    convert
        mov     [ascii], ax
        mov     dx, ascii
        mov     ah, 09h
        int     21h
        jmp     doloop
        ; Display extended ASCII keystroke code
extended:
        mov     al, ah
        call    convert
        mov     [ascii], ax
        mov     dx, extcode
        mov     ah, 09h
        int     21h
        jmp     doloop
        ; Terminate program
exit:
        mov     ax, 4c00h
        int     21h

; Convert positive integer (byte) to hexadecimal ASCII code
; Input: AL = byte; output: AX = byte ASCII codes
convert:
        push    bx
        push    cx
        mov     bl, al
        and     bl, 0fh                ; byte 4 LSB digits
        cmp     bl, 10
        jl      convert1
        sub     bl, 10
        add     bl, 'A'
        jmp     convert2
convert1:
        add     bl, '0'
convert2:
        mov     ch, bl
        mov     bl, al
        shr     bl, 4                  ; byte 4 MSB digits
        cmp     bl, 10
        jl      convert3
        sub     bl, 10
        add     bl, 'A'
        jmp     convert4
convert3:
        add     bl, '0'
convert4:
        mov     cl, bl
        mov     ax, cx
        pop     cx
        pop     bx
        ret

segment data
keyesc  equ     1bh
stext   db      'Hit a key (ESC to terminate) ', 13, 10, '$'
extcode db      '00 + '
ascii   resb    2
        db      13, 10, '$'

segment stack   stack
        resb    64
stacktop:

If the key pressed was a "normal" key (and the return of the function is an ANSI code), or a special key (and the return of the function is an extended ASCII keystroke code) depends on the value of AL: if AL = 0, it was a special key, and AH contains the extended ASCII keystroke code; otherwise it was a "normal" key, and AL contains the ANSI code.

Note: My implementation of the byte to ASCII conversion routine is (probably) a rather poor way to do so...

The screenshots below show executions of the program. Refer to the tables shown further up in the text, if you want to preview/check the codes that are actually displayed.

The screenshot on the left shows the keystroke codes, when pressing some common character and control keys: 1, 2, A, B, a, b, /, ?, ENTER, SPACE, BS, INS, DEL, TAB, SHIFT+TAB. HOME, END, LEFT, RIGHT, UP, DOWN. Except for ENTER, BS, and TAB, control keys are extended ASCII keystroke keys. The screenshot on the right shows the usage of the modifier keys for 1, 2, a, b. Lines 5-8 = SHIFT key (keys on the German keyboard: !, ", A, B). Lines 9-12 = ALT key. Lines 13-15 = CTRL key (no display for CTRL+1; this is also the case for CTRL+0). To note that CTRL+ALT + these keys is interpreted as ALT + these keys. Pressing a modifier key without pressing another key at the same time, doesn't store any data into the keyboard buffer.

NASM on FreeDOS: Assembly program example - Keystroke codes [1]
NASM on FreeDOS: Assembly program example - Keystroke codes [2]

The screenshot on the left shows the keystroke codes, when pressing the function keys F1, F2, F10 (no display for F11 and F12; these keys are not present on the AT keyboard). The first three lines show the keystroke codes, when I pressed the function key without any modifier key. Lines 4-6 show the keystroke codes with modifier key = SHIFT; lines 7-9 show the keystroke codes with modifier key = ALT; lines 10-12 show the keystroke codes with modifier key = CTRL. Note that ALT+SHIFT and CTRL+SHIFT used with the function keys results in the same display as ALT resp. CTRL. CTRL+ALT used with the function keys results in no display. The screenshot on the right shows the keystroke codes in relationship with the German keyboard. The keys, that I entered were the following: ä, Ä, ü, Ö, \, ß, µ, €, ´, `, ^, é, à, ô. The extended ASCII keystroke codes for \, µ, and € are due to the fact, that these keys require pressing the AltGr key. Note, that é, à and ô return 2 codes (the code for ´ followed by the code for e, the code for ` followed by the code for a, the codes for ^ followed by the code for o respectively); this is due to the fact, that you effectively have to press these two keys on the keyboard. However (except for é, à, and ô), the codes obtained are totally different from what we would have expected!

NASM on FreeDOS: Assembly program example - Keystroke codes [3]
NASM on FreeDOS: Assembly program example - Keystroke codes [4]

Getting unfiltered key codes.

The problem with the return of invalid key codes with keys like ä, Ä, \, ß, etc is due to the fact that function 00h of the BIOS keyboard services applies what is called extended key filtering (this is also the case for function 01h): For compatibility with older 83-key keyboards (as the AT keyboard), this converts duplicated keys into their older equivalent keys, what results in useless key codes for keys not present on the older keyboards.

The problem can be easily resolved by using function 10h instead of function 00h, and using function 11h instead of function 01h. These functions work exactly the same way as 00h resp. 01h, with the difference that no keyfiltering is done.

The screenshot below shows the execution of the binary created by assembling the program sample keycode2.asm, identical to keycode1.asm, except that the instruction mov ah, 00h in line 15 has been replaced by mov ah, 10h. The keys, that I entered were ä, Ä, ü, Ö, \, ß, µ, €, ´, `, ^, é, à, ô. As you can see, the codes returned are well those that we expect, when looking up the key in the ANSI table for code page 850, except for € (this symbol is not defined in code page 850). Note, that é, à, and ô still result in two codes, as two keys have been pressed on the keyboard.

NASM on FreeDOS: Assembly program example - Keystroke codes [5]

Function 10h of the BIOS keyboard services also returns codes for the function keys F11 and F12; these actually are 00h + 85h resp. 00h + 86h.

So, should we forget functions 00h and 01h, and always use 10h and 11h instead? I would recommend not to use functions 10h and 11h, unless you really need them. In fact there is a problem (at least on my system): After a call to functions 10h or 11h, the keyboard layout is reset to US (and typing, for example, a "z", will display a "y"). Reason for this issue? No idea!

Program sample 5: Simulating keyboard input.

The store keystroke data function code is 05h. The table shows the arguments of this function.

 RegisterValueDescription
InputAH05hBIOS Keyboard Services function code
 CLASCII code, or 0use CL = 0 to indicate that we store data concerning a special key
 CHscan codewith CL = 0, store the extended ASCII keystroke code here
OutputAL0, or 10 indicates that the data has been successfully stored; 1 indicates that the data has not been stored (no room in buffer)

The program sample hello3b.asm is a modified version of the program hello3.asm of my tutorial 16-bit assembly programming using NASM. Instead of asking the user for their name, we simulate the keyboard input by filling the keyboard buffer with the name string ('Aly Lutgen') calling BIOS Keyboard Services function 05h.

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Ask for name
        mov     dx, qname
        mov     ah, 09h
        int     21h
        ; Instead of entering the name from the keyboard, store it into keyboard buffer here
        xor     cx, cx
        lea     esi, [sname]
        mov     bl, 11
storechar:
        mov     ah, 05h
        mov     cl, [esi]
        int     16h
        inc     esi
        dec     bl
        jnz     storechar
        ; Get name from keyboard buffer
        mov     dx, buffer
        mov     ah, 0ah
        int     021h
        ; Display "hello" message
        lea     esi, [buffer + 2]
        mov     cl, [buffer + 1]
        lea     edi, [hname]
copychar:
        mov     bl, [esi]
        mov     [edi], bl
        inc     esi
        inc     edi
        dec     cl
        test     cl, cl
        jnz     copychar
        mov     byte [edi], '!'
        mov     byte [edi + 1], 13
        mov     byte [edi + 2], 10
        mov     byte [edi + 3], '$'
        mov     dx, hello
        mov     ah, 09h
        int     021h
        ; Terminate the program
        mov     ax, 4c00h
        int     21h
segment data
maxlen  equ     20
qname   db      'What is your name? ', '$'
sname   db      'Aly Lutgen', 13
buffer  db      maxlen + 1
        resb    maxlen + 2
hello   db      13, 10, 'Hello '
hname   resb    maxlen
        resb    4
segment stack   stack
        resb    64
stacktop:

Note, how we have to store character code 0dh (decimal: 13) at the end of our string in order to simulate the pressure of the ENTER key.

The screenshot shows the build and the execution of the program. As you can see, filling the keyboard buffer, echoes the characters stored onto the screen.

NASM on FreeDOS: Assembly program example - Simulating keyboard input

An important point to know is that the number of characters that you can stuff into the keyboard buffer is limited to 16. In fact, the length of the buffer is determined by two values in the BIOS Data Area: the address 0040:001a contains the 2-bytes address of the keyboard buffer head, and the address 0040:001c contains the 2-bytes address of the keyboard buffer tail. However, you can't easily enlarge the buffer by changing these values. Instead, programs which "stuff the keyboard" and may need to stuff more than 16 keys, usually implement a timer interrupt handler.

With a maximum buffer length of 16 characters, the length of a string to be stiffed into the keyboard buffer is limited to 15. The last byte of the buffer is reserved for character 0dh (pressure of the ENTER key).

To demonstrate this, let's modify the program sample hello3b.asm, replacing the string 'Aly Lutgen' by 'Aly Baba Ben Bubu' (note that for this modified version of the program to run correctly, you'll also have to replace the instruction mov bl, 11 in line 16 by mov bl, 18).

Our new string has a length of 17 characters, thus 2 characters more than those that can fit into the buffer. What do you think will happen when we run the modified program? For the first 15 characters of the string, there is no problem; they are stored into the buffer and echoed onto the screen. The last 2 characters of the string can't be filled into the buffer, as it is full. As the keyboard buffer does not contain a terminating character 0dh (corr. to the pressure of the ENTER key), the program remains in a waiting stage. If now, we press the ENTER key, code 0dh will be stored into the last (reserved) byte of the buffer. The string in the buffer now has its terminating character 0dh, and the program continues executing, the greeting message displayed being: Hello Aly Baba Ben Bu!.

Querying the keyboard shift status.

When talking about the keyboard interrupt handler 09h, I said that besides filling the keyboard buffer, and dealing with special keys or key combinations, it also sets the state of the toggle keys (CapsLock, NumLock, ScrollLock, and INS) and the modifier keys (SHIFT, ALT, and CTRL). Two bytes stored at address 0040:0017 and 0040:0018 (BIOS Data Area) identify the status of the keyboard modifier keys and the keyboard toggles.

Instead of reading these values directly from memory, we can use the BIOS keyboard services functions 02h, or 12h, normally described as "Query the keyboard shift status" resp. "Query extended keyboard shift status". INT 16h 02h returns one byte (referred to as KbdShiftFlagsRec) in the AL register; it is exactly as found in the byte at 0040:0017 in the BIOS Data Area. INT 16h 12h returns 2 bytes (referred to as KbdShiftFlags101Rec) in the AX register; it is similar to the word at 0040:0017 in the BIOS Data Area.

After several unsuccessful trials with these functions, I gave it up. No idea, if there were some errors in my code, or if perhaps this doesn't work on my VMware Workstation 16 FreeDOS VM (?). Anyway, if you are interested in this topic, please have a look at the article Keyboard Shift Status Flags at techhelpmanual.com.

Programming in assembly is fun!

Writing a tutorial like this one (plus writing and testing the program samples) is hours and hours of (partially hard) work. But, it's something that I really like to do, and also, for me, it's fun. Concerning yourself, if you use NASM on DOS for playing around with assembly in order to learn something about this programming language, you should not forget that programming (in assembly, or whatever language) can be fun, and should be fun. And, even with such simple things like the BIOS Keyboard Services functions, you can create interesting programs. (Besides some assembly knowledge), all that you need is fantasy!

The following two sections include the NASM 16-bit protected mode code that you may use to move a character around on the screen using the arrow keys (you can consider it as the very basics of interactive real-time game programming). To note that the program samples use some ANSI escape sequences in order to clear the screen and position and advance the cursor; for help, please, have a look at my tutorial Text positioning and coloring using ANSI escape sequences on DOS.

Program sample 6: Character animation (I).

After having started the major part of the program (pressing the F1 key), the screen is cleared and a heart (ASCII code: 03h) is displayed at the center of the screen. The program then waits for the user to press a key. Use the arrow keys (LEFT, RIGHT, UP, DOWN) to move the heart in the corresponding direction; use the ESC key to terminate the program. To note, that if the heart reaches one of the borders, you cannot continue to move it in the actual direction; moving it in the other directions remaining possible (ex: if the heart has reached screen row 1, you cannot move it upwards anymore; moving it downwards, to the left or to the right being still possible).

Here is the code of the program sample arrwkey1.asm:

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display text
        mov     dx, stext
        mov     ah, 09h
        int     21h
dowait:
        ; Wait for key pressed
        mov     ah, 00h
        int     16h
        ; Check if key pressed is ESC or F1 key
        cmp     al, keyesc
        je      exit
        cmp     al, 0
        jne     dowait
        cmp     ah, keyf1
        jne     dowait
        ; If key was F1 key, display start screen
        mov     dx, cls                ; clear screen
        mov     ah, 09h
        int     21h
        mov     dx, center             ; set cursor at center of screen
        mov     ah, 09h
        int     21h
        mov     ah, 02h                ; display the heart
        mov     dl, heart
        int     21h
        call    cursor_off             ; turn cursor off
doloop:
        ; Wait for key pressed
        mov     ah, 00h
        int     16h
        ; Check if key pressed is ESC
        cmp     al, keyesc
        je      exit
        ; Check for arrow keys
        cmp     al, 00h
        jne     doloop
        cmp     ah, keyleft
        je      arrow_left
        cmp     ah, keyrght
        je      arrow_right
        cmp     ah, keyup
        je      arrow_up
        cmp     ah, keydown
        je      arrow_down
        jmp     doloop
        ; Key was left arrow
arrow_left:
        mov     bl, [x]
        cmp     bl, 1
        je      doloop
        dec     bl
        mov     [x], bl
        mov     ax, left
        call    move_heart
        jmp     doloop
        ; Key was right arrow
arrow_right:
        mov     bl, [x]
        cmp     bl, 79
        je      doloop
        inc     bl
        mov     [x], bl
        mov     ax, right
        call    move_heart
        jmp     doloop
        ; Key was up arrow
arrow_up:
        mov     bl, [y]
        cmp     bl, 1
        je      doloop
        dec     bl
        mov     [y], bl
        mov     ax, up
        call    move_heart
        jmp     doloop
        ; Key was down arrow
arrow_down:
        mov     bl, [y]
        cmp     bl, 25
        je      doloop
        inc     bl
        mov     [y], bl
        mov     ax, down
        call    move_heart
        jmp     doloop
        ; Terminate program
exit:
        call    cursor_on              ; turn cursor on again
        mov     ax, 4c00h
        int     21h

; * Subroutine: Move the heart
; *   Input:  AX: address of cursor direction escape sequence
; *   Output: AX: destroyed
move_heart:
        push    dx
        push    ax
        mov     ah, 02h
        mov     dl, bkspace
        int     21h
        mov     ah, 02h
        mov     dl, space
        int     21h
        mov     ah, 02h
        mov     dl, bkspace
        int     21h
        mov     ah, 09h
        pop     dx
        int     21h
        mov     ah, 02h
        mov     dl, heart
        int     21h
        pop     dx
        ret

; * Subroutine: Set cursor off
; *   Input:  --
; *   Output: --
cursor_off:
        push    ax
        push    cx
        mov     ah, 01h                ; interrupt 10h function 01h (set cursor type)
        mov     ch, 1                  ; cursor starting line
        mov     cl, 0                  ; cursor ending line
        int     10h
        pop     cx
        pop     ax
        ret

; * Subroutine: Set cursor on (default cursor)
; *   Input:  --
; *   Output: --
cursor_on:
        push    ax
        push    cx
        mov     ah, 01h                ; interrupt 10h function 01h (set cursor type)
        mov     ch, 6                  ; cursor starting line
        mov     cl, 7                  ; cursor ending line
        int     10h
        pop     cx
        pop     ax
        ret

segment data
space   equ     20h
bkspace equ     08h
heart   equ     03h
keyesc  equ     1bh
keyf1   equ     3bh
keyleft equ     4bh
keyrght equ     4dh
keyup   equ     48h
keydown equ     50h
stext   db      'Hit F1 to start, then use arrow keys to move the heart, or ESC to exit ', 13, 10, '$'
y       db      12
x       db      40
cls     db      1bh, '[2J', '$'
center  db      1bh, '[12;40H', '$'
left    db      1bh, '[1D', '$'
right   db      1bh, '[1C', '$'
up      db      1bh, '[1A', '$'
down    db      1bh, '[1B', '$'

segment stack   stack
        resb    64
stacktop:

Notes concerning the code of this program:

Program sample 7: Character animation (II).

The program arrwkey2.asm does the same as the one before, with 2 differences: First, after an array key has been pressed, the heart continues to move in the actual direction, until another array key changes its direction; second, when the heart reaches a border, the program terminates.

segment code
..start:
        ; Initialization
        mov     ax, data
        mov     ds, ax
        mov     ax, stack
        mov     ss, ax
        mov     sp, stacktop
        ; Display text
        mov     dx, stext
        mov     ah, 09h
        int     21h
dowait:
        ; Wait for key pressed
        mov     ah, 00h
        int     16h
        ; Check if key pressed is ESC or F1 key
        cmp     al, keyesc
        je      exit
        cmp     al, 0
        jne     dowait
        cmp     ah, keyf1
        jne     dowait
        ; If key was F1 key, display start screen
        mov     dx, cls                ; clear screen
        mov     ah, 09h
        int     21h
        mov     dx, center             ; set cursor at center of screen
        mov     ah, 09h
        int     21h
        mov     ah, 02h                ; display the heart
        mov     dl, heart
        int     21h
        call    cursor_off             ; turn cursor off
        mov     byte [keyarrw], 0
doloop:
        ; Check if a key has been pressed
        mov     ah, 01h
        int     16h
        jnz     check_keys
        ; Delay
        mov     bp, 43690
        mov     si, 4369
delayloop:
        dec     bp
        nop
        jnz     delayloop
        dec     si
        cmp     si, 0
        jnz     delayloop
        ; Set direction according to arrow key previously pressed
        mov     al, [keyarrw]
        cmp     al, keyleft
        je      arrow_left
        cmp     al, keyrght
        je      arrow_right
        cmp     al, keyup
        je      arrow_up
        cmp     al, keydown
        je      arrow_down
        jmp     doloop
check_keys:
        ; Read the key (empty buffer)
        mov     ah, 00h
        int     16h
        ; Check if key pressed is ESC
        cmp     al, keyesc
        je      exit
        ; Check for arrow keys
        cmp     al, 00h
        jne     doloop
        cmp     ah, keyleft
        je      arrow_left
        cmp     ah, keyrght
        je      arrow_right
        cmp     ah, keyup
        je      arrow_up
        cmp     ah, keydown
        je      arrow_down
        jmp     doloop
        ; Key was left arrow
arrow_left:
        mov     bl, [x]
        cmp     bl, 1
        je      exit
        dec     bl
        mov     [x], bl
        mov     ax, left
        call    move_heart
        mov     byte [keyarrw], keyleft
        jmp     doloop
        ; Key was right arrow
arrow_right:
        mov     bl, [x]
        cmp     bl, 80
        je      exit
        inc     bl
        mov     [x], bl
        mov     ax, right
        call    move_heart
        mov     byte [keyarrw], keyrght
        jmp     doloop
        ; Key was up arrow
arrow_up:
        mov     bl, [y]
        cmp     bl, 1
        je      exit
        dec     bl
        mov     [y], bl
        mov     ax, up
        call    move_heart
        mov     byte [keyarrw], keyup
        jmp     doloop
        ; Key was down arrow
arrow_down:
        mov     bl, [y]
        cmp     bl, 25
        je      exit
        inc     bl
        mov     [y], bl
        mov     ax, down
        call    move_heart
        mov     byte [keyarrw], keydown
        jmp     doloop
        ; Terminate program
exit:
        call    cursor_on              ; turn cursor on again
        mov     ax, 4c00h
        int     21h

; * Subroutine: Move the heart
; *   Input:  AX: address of cursor direction escape sequence
; *   Output: AX: destroyed
move_heart:
        push    dx
        push    ax
        mov     ah, 02h
        mov     dl, bkspace
        int     21h
        mov     ah, 02h
        mov     dl, space
        int     21h
        mov     ah, 02h
        mov     dl, bkspace
        int     21h
        mov     ah, 09h
        pop     dx
        int     21h
        mov     ah, 02h
        mov     dl, heart
        int     21h
        pop     dx
        ret

; * Subroutine: Set cursor off
; *   Input:  --
; *   Output: --
cursor_off:
        push    ax
        push    cx
        mov     ah, 01h                ; interrupt 10h function 01h (set cursor type)
        mov     ch, 1                  ; cursor starting line
        mov     cl, 0                  ; cursor ending line
        int     10h
        pop     cx
        pop     ax
        ret

; * Subroutine: Set cursor on (default cursor)
; *   Input:  --
; *   Output: --
cursor_on:
        push    ax
        push    cx
        mov     ah, 01h                ; interrupt 10h function 01h (set cursor type)
        mov     ch, 6                  ; cursor starting line
        mov     cl, 7                  ; cursor ending line
        int     10h
        pop     cx
        pop     ax
        ret

segment data
space   equ     20h
bkspace equ     08h
heart   equ     03h
keyesc  equ     1bh
keyf1   equ     3bh
keyleft equ     4bh
keyrght equ     4dh
keyup   equ     48h
keydown equ     50h
stext   db      'Hit F1 to start, then use arrow keys to move the heart, or ESC to exit ', 13, 10, '$'
y       db      12
x       db      40
cls     db      1bh, '[2J', '$'
center  db      1bh, '[12;40H', '$'
left    db      1bh, '[1D', '$'
right   db      1bh, '[1C', '$'
up      db      1bh, '[1A', '$'
down    db      1bh, '[1B', '$'
keyarrw resb    1

segment stack   stack
        resb    64
stacktop:


If you find this text helpful, please, support me and this website by signing my guestbook.