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:
- Faults issue an exception prior to completing the instruction. The saved IP value then points to the same instruction that created the exception. Thus, it is possible to reload the IP and the processor will be able to re-execute the instruction, hopefully without another exception.
- Traps issue an exception after completing the instruction execution. The saved IP points to the instruction immediately following the one that gave rise to the exception. The instruction is therefore not re-executed again. Traps are useful when, despite the fact the instruction was processed without errors, program execution should be stopped (ex: debugger breakpoints).
- Aborts usually translate very serious failures, such as hardware failures or invalid system tables. Because of this, it may happen that the address of the error cannot be found, and recovering program execution after an abort may not be possible.
The following table lists the processor interrupts (interrupts 00h – 07h):
| 80x86 INT |
Description | Notes |
|---|---|---|
| 00h | Division by zero | |
| 01h | Single step | Allows to follow the program execution instruction after instruction (debugging) |
| 02h | Memory error | |
| 03h | Breakpoint | Allows to stop the program execution at a given instruction (debugging) |
| 04h | Overflow | |
| 05h | Print screen | Called, via keyboard interrupt 09h (thus not a processor interrupt) when the PrtSc key is pressed |
| 06h | Invalid instruction | |
| 07h | Coprocessor instruction | Called 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 0 | 08h | Timer Chip |
| IRQ 1 | 09h | Keyboard |
| IRQ 2 | 0Ah | Connected to the secondary 8259A interrupt controller; may be used by EGA for vertical retrace |
| IRQ 9 | 71h | Redirection from occurrences of IRQ 2; may be generated by EGA/VGA |
| IRQ 8 | 70h | Real-time clock |
| IRQ 10 | 72h | Reserved |
| IRQ 11 | 73h | Reserved |
| IRQ 12 | 74h | Reserved on AT systems; auxiliary (AUX) device on PS/2 systems |
| IRQ 13 | 75h | 80x87 math coprocessor |
| IRQ 14 | 76h | Hard disk controller |
| IRQ 15 | 77h | Reserved |
| IRQ 3 | 0Bh | Serial Port 2 (COM2) |
| IRQ 4 | 0Ch | Serial Port 1 (COM1) |
| IRQ 5 | 0Dh | Parallel port 2 (LPT2) on AT systems, reserved on PS/2 systems |
| IRQ 6 | 0Eh | Diskette drive |
| IRQ 7 | 0Fh | Parallel 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):
- 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.
- 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.
- 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.
- 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:
- INT 09h is a hardware interrupt triggered by the keyboard. We can not call this interrupt from a program.
- What happens from a software point of view when a key is pressed on the keyboard, depends on the code of the keyboard interrupt handler.
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:
- Handling of the special cases of the PrtSc, SysReq, Ctrl+Alt+Del, Ctrl+NumLock (or Pause) keys.
- Tracking of the CapsLock, NumLock, ScrollLock and INS keys.
- Setting the status of the keyboard modifiers keys (SHIFT, CTRL, and ALT).
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.
|
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).
|
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).
|
|
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.
|
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.
|
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.
| Register | Value | Description | |
|---|---|---|---|
| Input | AH | 00h | BIOS Keyboard Services function code |
| Output | AL | ASCII code, or 0 | AL = 0 indicates that some special key has been pressed |
| AH | scan code | if AL = 0, this corresponds to the extended ASCII keystroke code |
Important:
- If no key is available in the keyboard buffer, this function continues to wait for a keystroke.
- If a key is available in the keyboard buffer, this function removes the key from the buffer.
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.
|
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.
|
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.
| Register | Value | Description | |
|---|---|---|---|
| Input | AH | 01h | BIOS Keyboard Services function code |
| Output | Flags (ZF) | clear: indicates that there is no key in the buffer | |
| set: indicates that a key is ready | |||
| AL | if key ready: ASCII code, or 0 | AL = 0 indicates that some special key has been pressed | |
| AH | if key ready: scan code | if AL = 0, this corresponds to the extended ASCII keystroke code |
Important:
- If no key is available in the keyboard buffer, this function returns without waiting for a keystroke.
- This function does not remove the (available) key from the buffer. This means, that if a key is ready, we'll have to read it, calling interrupt 16h with function code 00h.
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.
|
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.
|
|
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!
|
|
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.
|
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.
| Register | Value | Description | |
|---|---|---|---|
| Input | AH | 05h | BIOS Keyboard Services function code |
| CL | ASCII code, or 0 | use CL = 0 to indicate that we store data concerning a special key | |
| CH | scan code | with CL = 0, store the extended ASCII keystroke code here | |
| Output | AL | 0, or 1 | 0 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.
|
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:
- The cursor movement is implemented using the following ANSI escape sequences (n = number of lines/columns, set to 1):
- ESC [nA : move cursor up
- ESC [nB : move cursor down
- ESC [nC : move cursor forward (right)
- ESC [nD : move cursor back (left)
- The display of the heart consists of two steps: First, removing the "old" heart at its actual position; second displaying the heart at its new position. My implementation of the heart movement works fine, except for the case where the heart is at screen column 80. To simplify things, I chose to consider column 79 (instead of column 80) as right border. If you want that the heart can be moved to the real right border at column 80, additional code is needed to deal with this special situation.
- The "move cursor" ANSI escape sequences are said to have no effect (don't move the cursor), if the cursor already is at a border of the screen (first/last line or column). Thus, keeping track of the heart position, and testing if its actual position is a border wouldn't normally be necessary. This is true in practice for vertical movement (and you may remove the related code for arrow key = UP and DOWN). Removing the border checking for horizontal movement, however, will not work correctly with my display implementation (in fact, the actual position of the heart being column 80, the cursor actually is at column 1 of the next line). Thus, right border checking is required, and so we have to keep track of the horizontal position of the heart.
- It's obvious that in this kind of program the display of the cursor is not wanted. That's why I hide it during program execution (and display it again when the program terminates). Hiding/showing the cursor can easily be done, calling function 01h of the BIOS Video Services (interrupt 10h). For details, please, have a look at the article INT 10,3 - Set cursor position at stanislavs.org.
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.