Part VIII : Keyboard Driver
An overview of the hardware
In this part we are writing a driver for the PS/2 controller, which provides an interface for communicating with the keyboard. Writing for this driver includes:
- Reading bytes from the controller
- Sending bytes
- Enabling ports
- Sending commands to the keyboard
With all these things being low level. We are technically writing another driver too, this being the keyboard itself which:
- Receives scan codes
- Decodes scan codes
- Tracks Shift/Ctrl/Alt
- Converts scan codes into key events
- Converts to ASCII
With this being higher level compared to the PS/2 controller.
There's only two ports we need for the controller. 0x60 is the data port,
which we use to read data from and send data to the keyboard. 0x64 is the
status/command port. We read the controller's status from it and send controller
commands to it.
Reading from port 0x64 gives us:
Bit 0
Output buffer full
Bit 1
Input buffer full
Bit 2
System flag
Bit 3
Command/Data
Bit 4
Keyboard interface disabled
Bit 5
Mouse output
Bit 6
Timeout
Bit 7
Parity Error
Two bits here are important. These are the output and input buffer status bits. If the output buffer bit gets set, then `0x60 contains data that is waiting to be read. If the input buffer bit gets set, then the controller is still processing a command or data, so we should wait before writing to it. When keyboard data gets placed into the controller's output buffer, the controller can raise IRQ1 to notify the CPU that data is available.
When a key is pressed, the keyboard generates a scan code, the
controller receives it and then stores it whilst also raising IQR1. Then,
we can receive it and decode. The keyboard does not send ASCII, it sends
scan codes. These are numbers that symbolize interactions with the
keyboard. For example, keyboards can use different scan code sets to represent
keys. We will be using Scan Code Set 1, which is the traditional set used
by the PC/AT keyboard interface. In this set, 0x1E represents the A key
being pressed, while 0x9E represents the A key being released.
Some keys use extended scan code sequences containing multiple bytes. For
example, the right Control key uses the 0xE0 prefix. The 0xE0 byte tells us
that the following byte belongs to an extended scan code sequence.
For example the pause key uses a special multibyte
scan code sequence, but we will ignore that key for now (as well as many others).
When the 0xE0 scan code is received, it is not itself the key we want to handle. It's a
prefix telling us that another byte follows. For our simple implementation,
keyboard_extended_scancodes() reads the next byte and checks whether it is
the left GUI key. You can expand this later if you ever want more extended keys.
For our simple implementation of initialization, we enable keyboard scanning by
sending 0xF4 and initialize our keymaps that will be mapping a scan code
to a character. The 0xF4 command is sent through the
controller's data port (0x60), but, it's a command understood by the keyboard
itself. The controller simply forwards it to the keyboard. A complete PS/2 initialization would
also wait for the controller's input buffer to be clear,
check the controller's status, flush pending data, and handle the keyboard's responses to commands.
We will keep those details simple for now.
Now let's get writing, starting with the header file which is all that we need for our full implementation that we will make in this chapter:
#ifndef KEYBOARD_H
#define KEYBOARD_H
#define PS2_DATA 0x60
#define PS2_COMMAND 0x64
#define ENABLE_SCANNING 0xF4
#define DISABLE_SCANNING 0xF5
#define SET_LED 0xED
#define KEY_RELEASED 0x80
#define EXTENDED_SCANCODE 0xE0
#define SHIFT_PRESS 0x2A
#define L_CTRL_PRESS 0x1D
#include <stdint.h>
#include <stdbool.h>
void keyboard_handler(void);
void keyboard_init(void);
void keyboard_set_keymap(void);
void keyboard_set_shift_keymap(void);
bool keyboard_modifier_keys(uint8_t scancode);
void keyboard_extended_scancodes(void);
#endif
In Scan Code Set 1, the release scan code for most ordinary keys is formed by
setting the high bit of the code. We use KEY_RELEASED (0x80) to check for this.
We then have our data and
command ports and some commands, the SET_LED command is not needed. All
our functions are pretty self-explanatory when we look at the
implementation.
Let's look at said implementation:
#include "keyboard.h"
#include "interrupts.h"
#include "vga_text.h"
volatile bool shift_pressed = false;
volatile bool ctrl_pressed = false;
volatile bool alt_pressed = false;
extern vga_text terminal;
char keymap[128];
void keyboard_init() {
outb(PS2_DATA, ENABLE_SCANNING);
keyboard_set_keymap();
}
void keyboard_set_keymap(void)
{
/* Numbers */
keymap[0x02] = '1';
keymap[0x03] = '2';
keymap[0x04] = '3';
keymap[0x05] = '4';
keymap[0x06] = '5';
keymap[0x07] = '6';
keymap[0x08] = '7';
keymap[0x09] = '8';
keymap[0x0A] = '9';
keymap[0x0B] = '0';
/* Symbols */
keymap[0x0C] = '-';
keymap[0x0D] = '=';
keymap[0x1A] = '[';
keymap[0x1B] = ']';
keymap[0x27] = ';';
keymap[0x28] = '\'';
keymap[0x29] = '`';
keymap[0x2B] = '\\';
keymap[0x33] = ',';
keymap[0x34] = '.';
keymap[0x35] = '/';
/* Top row */
keymap[0x10] = 'q';
keymap[0x11] = 'w';
keymap[0x12] = 'e';
keymap[0x13] = 'r';
keymap[0x14] = 't';
keymap[0x15] = 'y';
keymap[0x16] = 'u';
keymap[0x17] = 'i';
keymap[0x18] = 'o';
keymap[0x19] = 'p';
/* Home row */
keymap[0x1E] = 'a';
keymap[0x1F] = 's';
keymap[0x20] = 'd';
keymap[0x21] = 'f';
keymap[0x22] = 'g';
keymap[0x23] = 'h';
keymap[0x24] = 'j';
keymap[0x25] = 'k';
keymap[0x26] = 'l';
/* Bottom row */
keymap[0x2C] = 'z';
keymap[0x2D] = 'x';
keymap[0x2E] = 'c';
keymap[0x2F] = 'v';
keymap[0x30] = 'b';
keymap[0x31] = 'n';
keymap[0x32] = 'm';
/* Control characters */
keymap[0x01] = 27; /* Escape */
keymap[0x0E] = '\b'; /* Backspace */
keymap[0x0F] = '\t'; /* Tab */
keymap[0x1C] = '\n'; /* Enter */
keymap[0x39] = ' ';
}
void keyboard_handler () {
uint8_t scancode = inb(PS2_DATA);
//vga_text_write_hex(&terminal, scancode);
if (scancode & KEY_RELEASED) {
return;
}
char c[2];
c[0] = keymap[scancode];
c[1] = '\0';
vga_text_write(&terminal, c);
}
We then also need to call keyboard_handler() in our IRQ handler
on case 1.
This is all we really need for our keyboard, we make an array that maps
each scan code to a respective key. Our init function is small, we just
enable scanning (which potentially may already have been done by the
BIOS) and set our keymap. In our handler we get the scan code and if it
contains the KEY_RELEASED definition we return. If not released we write
the key to the screen. Not every scan code has an entry in our keymap.
Undefined entries contain 0, so our later code should ignore scan codes
that we don't have a character for.
This keymap assumes we are using Scan Code Set 1. other scan code sets exist, but we will ignore them for now.
Really you could move on from this chapter with this basic implementation and build up your own from this template with modifier keys, handling extended key codes, etc. (which is what the added functions now included in the implementation from the header file reference)
Here is my adapted implementation:
#include "keyboard.h"
#include "interrupts.h"
#include "vga_text.h"
volatile bool shift_pressed = false;
volatile bool ctrl_pressed = false;
volatile bool alt_pressed = false;
extern vga_text terminal;
char keymap[128];
char shift_keymap[128];
void keyboard_init() {
outb(PS2_DATA, ENABLE_SCANNING);
keyboard_set_keymap();
keyboard_set_shift_keymap();
}
void keyboard_set_keymap(void)
{
/* Numbers */
keymap[0x02] = '1';
keymap[0x03] = '2';
keymap[0x04] = '3';
keymap[0x05] = '4';
keymap[0x06] = '5';
keymap[0x07] = '6';
keymap[0x08] = '7';
keymap[0x09] = '8';
keymap[0x0A] = '9';
keymap[0x0B] = '0';
/* Symbols */
keymap[0x0C] = '-';
keymap[0x0D] = '=';
keymap[0x1A] = '[';
keymap[0x1B] = ']';
keymap[0x27] = ';';
keymap[0x28] = '\'';
keymap[0x29] = '`';
keymap[0x2B] = '\\';
keymap[0x33] = ',';
keymap[0x34] = '.';
keymap[0x35] = '/';
/* Top row */
keymap[0x10] = 'q';
keymap[0x11] = 'w';
keymap[0x12] = 'e';
keymap[0x13] = 'r';
keymap[0x14] = 't';
keymap[0x15] = 'y';
keymap[0x16] = 'u';
keymap[0x17] = 'i';
keymap[0x18] = 'o';
keymap[0x19] = 'p';
/* Home row */
keymap[0x1E] = 'a';
keymap[0x1F] = 's';
keymap[0x20] = 'd';
keymap[0x21] = 'f';
keymap[0x22] = 'g';
keymap[0x23] = 'h';
keymap[0x24] = 'j';
keymap[0x25] = 'k';
keymap[0x26] = 'l';
/* Bottom row */
keymap[0x2C] = 'z';
keymap[0x2D] = 'x';
keymap[0x2E] = 'c';
keymap[0x2F] = 'v';
keymap[0x30] = 'b';
keymap[0x31] = 'n';
keymap[0x32] = 'm';
/* Control characters */
keymap[0x01] = 27; /* Escape */
keymap[0x0E] = '\b'; /* Backspace */
keymap[0x0F] = '\t'; /* Tab */
keymap[0x1C] = '\n'; /* Enter */
keymap[0x39] = ' ';
}
void keyboard_set_shift_keymap(void)
{
/* Numbers */
shift_keymap[0x02] = '!';
shift_keymap[0x03] = '@';
shift_keymap[0x04] = '#';
shift_keymap[0x05] = '$';
shift_keymap[0x06] = '%';
shift_keymap[0x07] = '^';
shift_keymap[0x08] = '&';
shift_keymap[0x09] = '*';
shift_keymap[0x0A] = '(';
shift_keymap[0x0B] = ')';
/* Symbols */
shift_keymap[0x0C] = '_';
shift_keymap[0x0D] = '+';
shift_keymap[0x1A] = '{';
shift_keymap[0x1B] = '}';
shift_keymap[0x27] = ':';
shift_keymap[0x28] = '"';
shift_keymap[0x29] = '~';
shift_keymap[0x2B] = '|';
shift_keymap[0x33] = '<';
shift_keymap[0x34] = '>';
shift_keymap[0x35] = '?';
/* Top row */
shift_keymap[0x10] = 'Q';
shift_keymap[0x11] = 'W';
shift_keymap[0x12] = 'E';
shift_keymap[0x13] = 'R';
shift_keymap[0x14] = 'T';
shift_keymap[0x15] = 'Y';
shift_keymap[0x16] = 'U';
shift_keymap[0x17] = 'I';
shift_keymap[0x18] = 'O';
shift_keymap[0x19] = 'P';
/* Home row */
shift_keymap[0x1E] = 'A';
shift_keymap[0x1F] = 'S';
shift_keymap[0x20] = 'D';
shift_keymap[0x21] = 'F';
shift_keymap[0x22] = 'G';
shift_keymap[0x23] = 'H';
shift_keymap[0x24] = 'J';
shift_keymap[0x25] = 'K';
shift_keymap[0x26] = 'L';
/* Bottom row */
shift_keymap[0x2C] = 'Z';
shift_keymap[0x2D] = 'X';
shift_keymap[0x2E] = 'C';
shift_keymap[0x2F] = 'V';
shift_keymap[0x30] = 'B';
shift_keymap[0x31] = 'N';
shift_keymap[0x32] = 'M';
/* Control characters */
shift_keymap[0x01] = 27;
shift_keymap[0x0E] = '\b';
shift_keymap[0x0F] = '\t';
shift_keymap[0x1C] = '\n';
shift_keymap[0x39] = ' ';
}
void keyboard_extended_scancodes(void) {
uint8_t scancode = inb(PS2_DATA);
vga_text_write_hex(&terminal, scancode);
/* This only handles the LGUI press for now */
if (scancode == 0x5B) {
vga_text_writeline(&terminal, "LGUI PRESSED");
}
}
bool keyboard_modifier_keys(uint8_t scancode) {
/* shift */
if (scancode == SHIFT_PRESS) {
shift_pressed = true;
return true;
} else if (scancode == (SHIFT_PRESS | KEY_RELEASED)) {
shift_pressed = false;
return true;
}
/* control */
if (scancode == L_CTRL_PRESS) {
ctrl_pressed = true;
return true;
} else if (scancode == (L_CTRL_PRESS | KEY_RELEASED)) {
ctrl_pressed = false;
return true;
}
return false;
}
void keyboard_handler(void) {
uint8_t scancode = inb(PS2_DATA);
if (keyboard_modifier_keys(scancode)) {
return;
}
if (scancode == EXTENDED_SCANCODE) {
keyboard_extended_scancodes();
return;
}
if (scancode & KEY_RELEASED) {
return;
}
if (scancode >= 128) {
return;
}
char c[2];
c[0] = keymap[scancode];
c[1] = '\0';
switch (c[0]) {
case '\n':
vga_text_writeline(&terminal, "");
break;
case 27:
vga_text_clear(&terminal);
break;
case '\b':
vga_text_backspace(&terminal);
break;
case '\t':
vga_text_write(&terminal, " ");
break;
default:
if (shift_pressed) {
c[0] = shift_keymap[scancode];
}
vga_text_write(&terminal, c);
break;
}
}
In our handler now we first call the modifier key function and this
returns true if a modifier key has been pressed and handles it
accordingly. If one is a modifier key, we return because modifier keys
are handled separately from normal character keys. I have
added one, this being LGUI, I could have added more, but I haven't as
we have no use for them. We can add more extended keys later using the same approach
With our Shift key we also have a separate keymap. This makes the code simple to understand, although it uses some extra memory. Later, we could use another approach to avoid storing a second full keymap. The keys that aren't regular printable characters are handled with simple special cases, such as Backspace, Enter, Tab, and Escape. I made some new functions for this in our VGA code. Let's take a look:
void vga_text_backspace(vga_text* terminal) {
if (terminal->row == 0 && terminal->column == 0) return;
else if (terminal->column != 0) {
terminal->column--;
} else {
terminal->row--;
terminal->column = terminal->width - 1;
while (vga_text_getchar(terminal) == ' ') {
terminal->column--;
}
}
vga_text_putchar(terminal, ' ');
}
char vga_text_getchar(vga_text* terminal) {
size_t index = terminal->row * terminal->width + terminal->column;
char c = (uint8_t)(terminal->buffer[index] & 0x00FF);
return c;
}
The backspace function moves the terminal cursor back by one character and replaces that character with a space. If we are at the start of a row, we move to the previous row and then move toward the end of that row.
This is basically everything to do with keyboard management, some things aren't implemented (like alt) but we don't really have much use for these keys right about now. We can easily add them later.
We will now move on to memory management.