Introduction to Keyboard Handlers
by Alioth / O.M.D
Any problems, mail me at karalioth@geocities.com
The keyboard system is a relatively complex beast to wield. It may look simple
in that you are probably used to using things such as "getch()", but the lower
you look, the more confusing it gets.
You may have needed a keyboard handler for your game or something else before,
and had one placed right in front of you - all you had to do was reuse the
code. Here however, I'm going to tell you how and why this keyboard handler
works. The purpose of the keyboard handler I will build is as follows: to
find out when keys are pressed and released at any time, allowing to
detect multiple key presses. Also, to get rid of that annoying keyboard buffer
which keeps beeping everytime it's full and to stop the typematic rate and
delay from interfering.
The above keyboard handler has enough functionality to be used in a very high
quality game. Sure, there are some nuances such has the fabled detection of
the Pause/Break key but that's not really needed anyway (if you're prepared
to ignore formalities). So, all you keyboard gurus out there, don't complain
about the absence of the Ex scancodes!
So, how do we set about doing this? Well, to begin with, you need to know what
happens AS SOON as a key is pressed on the keyboard. If you press the key
'A' on your keyboard the 8031 controller inside the keyboard sends a KScan
code value of 1Ch over the keyboard serial link. The 8042 controller on the
motherboard receives the value and translates it into the system scan code
value 1Eh. This value is then placed in its output buffer. The motherboard
controller then issues an interrupt request indicating that data is available.
The interrupt request calls the interrupt 9 handler, the keyboard BIOS and the
scan code is further converted into it's ASCII counterpart, 61h. Note that
this is the code for 'a', the lowercase value - this is because we are
assuming that no shift key is being pressed at the same time. What happens
after this is not important to us as we want to perform our own actions.
But hang on, don't we want to detect a key release too? Well, yes, but the
actions taken by the keyboard are not much different. Instead of sending
the KScan code of 'A', 1Ch over the serial link, it will this time send the
release code F0h, followed then by 1Ch. The 8042 receives the two bytes and
translates them to 1Eh like above, but this time, it sets bit 7 high to
indicate that the key was released. So this time, the system scan code will
be 9Eh.
Now, after the above in both processes have been carried out, the BIOS takes
control and provides you with the services you've been probably taking for
granted (I know I did). It's even so kind as to offer you the 16 byte FIFO
buffer that beeps EVERYTIME it can't accept any more keys!!! Well, we can
tackle that problem right now and prepare for the later problems. How? Well,
we make our own keyboard Interrupt Service Routine that is called everytime
a key is pressed. In other words, we replace the BIOS function for handling
key presses with our own! Sounds difficult but it's actually quite easy.
First, we have to save the location of the BIOS routine which handles the
key presses. The location of this is provided by the C library function
"_dos_getvect" (without the leading underscore in some compilers cases and
sometimes without the "_dos_" as evidenced with TC++). Create a pointer to
the old BIOS function like so...
void (_interrupt _far *OldKeyboard)(); /* C's notation for function pointers */
...then, using the getvect function, point to the BIOS's original function...
OldKeyboard = _dos_getvect(9); /* get the address of the BIOS keyboard
interrupt function, 9 */
So, we have saved the location of the original (soon to be old) BIOS keyboard
interrupt function. Now we need to replace it. But before we get carried
away, we need a function to replace it with! Our function (which does nothing
as yet) should look like this...
void _interrupt _far MyKeyboard(void)
{
}
Now that we have saved the original BIOS keyboard function, we can replace
it with our one. To do so, we must use the C library function "_dos_setvect"
which varies in name from compiler to compiler, like so...
_dos_setvect(9, MyKeyboard); /* Replace the old one with your one */
Now, to gather our code, we can make a function which sets up the keyboard
and then releases the keyboard afterwards (so that we don't have the BIOS
jumping to a non-existant function whenever a key is pressed after the program
shuts down)...
void InitKeyboard(void)
{
OldKeyboard = _dos_getvect(9); /* Save the old keyboard function */
_dos_setvect(9, MyKeyboard); /* Replace old function with our new one */
}
void RestoreKeyboard(void)
{
_dos_setvect(9, OldKeyboard); /* Replace our function with the original
BIOS function */
}
Now everytime the 8042 controller issues an interrupt 9 (i.e. a key is pressed
or released), the function "MyKeyboard" is called. However, seeming as there
is no code in the function to do things with the keyboard, nothing happens!
That's one task completed so far - because the FIFO buffer "which beeps" is
a BIOS creation it effectively no longer exists since we are using our own
function to manage key presses, so there is now no more beeping!!!
On a side note, all the functions get and setvect do is to either look up
the segment and offset of a function in the interrupt vector table (stored
at 0000:0000) and return a pointer to the function, or replace this value
with the segment and offset of the given function. When an interrupt is
issued, this table is searched for the appropriate entry and then "vectors"
to the give address in the table and begins to execute the code.
Now, seeming as we've got rid of the BIOS's "incarnation" of a keyboard
function, it's time to write our own so that we can control the keyboard to
a greater extent. To do this, we will make use of the port 60h. Port 60h
belongs to the keyboard controller and can be used to send commands to the
keyboard such as turn an LED on or set key attributes but this isn't important
now - what is important, is the fact that we can read from this port. The byte
read from this port is the keyboard scan code of a key being pressed or
released which is currently stored in the controller's buffer. Once the scan
code has been read, all that's left to do is issue an EOI (End of Interrupt)
to make it known that the keyboard interrupt has finished. This is done by
writing the value 20h to the port 20h which is the interrrupt controllers
control register. I was really surprised that I had to do this since the
compiler apparently should have generated the code to do so because of the
C _interrupt function type. It still puzzles me to do this day. Here's our
function so far...
void _interrupt _far MyKeyboard(void)
{
unsigned key;
key = inp(0x60); /* Retrieve the scan code of the pressed or released
key */
out(0x20, 0x20); /* Issue EOI */
}
OK, so now we can read a byte from the controller whenever a key is pressed.
The question you are probably thinking about now is, what do we do with this
byte? Well, there are still a couple of things to do yet. We have to both
get rid of the typematic rate/delay and allow multiple keypresses. These are
both difficult to solve because firstly, typematic rate/delay is controlled
through hardware and secondly, it is impossible to send multiple values
simultaneously down a serial connection! You people out there who have already
implemented a keyboard handler might be laughing at this "difficult" claim
but I think that the following solution is very clever and hasn't been given
the praise it deserves (I've no doubt anyhow that those of you laughing copied
the code and don't fully understand how and why it works). You see,
addressing the first problem; typematic rate/delay will ALWAYS be there,
there is no way you can get rid of it. You can speed it up, yes, but this
still is not good enough for proper games. To understand the solution you
will have to "change your frame of reference" (doing this usually solves a
lot of problems, simplifying them in the case). Instead of thinking about
how many times the keyboard simulates a press when you hold it down for a
period of time, think of when the key is initially pressed down, and when
the key is finally released. For an example; have a variable "keypress" which
is initially 0. When say, the ENTER key is pressed, the value in "keypress"
will change to 1 and will stay at 1 until the ENTER key is released. In which
case, a 0 will be written to it. Now, instead of checking whether the key is
pressed, check to see whether the value in "keypress" is 1. If it is, then
the ENTER is held down and how many times the 1 has been written to "keypress"
doesn't matter at all because it will ALWAYS be one as long as the ENTER key
isn't released! Note that from now on, I will refer to the pressed code being
the "make" code, and the released code, the "break" code.
"_" is where keypress = 1
"." is where a scancode is sent to the motherboard
____________________________________________________________
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
^ ^
| |
+--- Typematic delay |
+--- Typematic rate (defined in cps)
Here's some pseudo-code...
begin
key = getscancode;
if (key is make code of enter) then keypress = 1
if (key is break code of enter) then keypress = 0
end
main
if (keypress is equal to 1) then do stuff
end
This may all look really simple but getting to this stage probably required
a little bit of lateral thinking (although I'm sure the concept of make and
break codes helped in the derivation of it). So, we have eradicated typematic
delay and pumped up the typematic rate to almost lightspeed! :0)
Already you can probably foresee how we are going to tackle the problem of
multiple keypresses. Well, simple - just create an array which is big enough
to hold 128 bytes. Each byte will represent the scan code of the chosen key.
If, for example, the key 'A' is pressed, the scan code 1Eh will be reported.
Then, your interrupt handler which check to see if bit 7 is set - if it isn't
then your new table at position 1Eh will be filled with the value 1. When
the key 'A' is released, the scan code 9Eh will be reported and the check to
see if bit 7 is set will be made. In this case it is (releasing key) and 80h
is subtracted from the reported value to give the reference (9Eh - 80h = 1Eh)
and a 0 is written to the table at position 1Eh to signify that the key has
been released. So, finally, here's my "incarnation" of the function...
void _interrupt _far MyKeyboard(void)
{
unsigned key;
key = inp(0x60); /* Retrieve the scan code of the pressed or released
key */
out(0x20, 0x20); /* Issue EOI */
if (key & 0x80) /* Is bit 7 set? */
{
keytab[key - 0x80] = 0; /* Yes, therefore the key has been released */
}
else
{
keytab[key] = 1; /* No, therefore the key has been pressed */
}
}
That's all there is to making the keyboard handler! You will have to know
the scancode of all the keys on your keyboard to be able to use it. Here's
and example of it's use...
void main(void)
{
unsigned int value = 0;
InitKeyboard();
while (!keytab[1]) /* Loop until ESC pressed (1 is Escapes make code) */
{
if (keytab[0x48] && value != 65536) /* If the up cursor arrow is */
{ /* pressed, increase "value" and */
value++; /* display the value on-screen */
printf("%d\n", value);
}
if (keytab[0x50] && value != 0) /* If the down cursor arrow is */
{ /* pressed, decrease "value" and */
value--; /* display the value on-screen */
printf("%d\n", value);
}
}
ReleaseKeyboard();
}
To simplify things even more, you could use #definitions to replace the
keyboard scan codes, for example...
#define KEY_UPARROW 0x48
#define KEY_DOWNARROW 0x50
...and then check if a key is held down like so...
if (keytab[KEY_UPARROW]) { }
And there you have it, all of our above goals have been completed!
The above example doesn't show you the full power of this new keyboard
handler. If you want, I've provided a little example in the ZIP - along with
the text version of this tutorial which contains the full list of key codes.
There's a lot more to learn about the keyboard itself, rather than the
implementations you can come up with. A tutorial on how the keyboard works
would take a long time to compile, such is complexity of this apparently
quiet "backseater".