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.