This page looks best with JavaScript enabled

MercuryOS Keyboard Driver and Improved Graphics Driver

 ·  🎃 kr0m

One of the most basic aspects of an OS is to read user input in some way. Generally, the user will enter commands through the keyboard. To read keystrokes, we will use the interrupts already explained in previous articles and a basic keyboard driver.

Before we begin, it is recommended that you read these previous articles:

In this article, the graphics driver has been improved to support advanced functions such as console scroll navigation, home/end keys, command editing with arrows and supr/del, screen clear function, a command history has also been added, and we start programming a small shell embedded within the kernel itself where we will add system commands.


PULSES

Reading pulses is somewhat complex since to differentiate between uppercase and lowercase it will be necessary to use two tables, each with the characters associated with the scancode but taking into account whether Shift or Caps Lock was enabled.

The two tables are as follows:

vi keyboard.c

unsigned char kbd_us[] = 
{
 '\0',  ESC,  '1',  '2',  '3',  '4',  '5',  '6',  '7',  '8',  '9',  '0',  '-',  '=',  '\b',
 '\t',  'q',  'w',  'e',  'r',  't',  'y',  'u',  'i',  'o',  'p',  '[', ']', '\n',
 '\0',  'a',  's',  'd',  'f',  'g',  'h',  'j',  'k',  'l',  ';', '\'',
 '\0', '\0', '\\',  'z',  'x',  'c',  'v',  'b',  'n',  'm',  ',',  '.', '/',
 '\0', '\0', '\0',  ' ',
};

unsigned char kbd_us_shift[] = 
{
 '\0', ESC, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '\b',
 '\t', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '\n',
 '\0', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\"',
 '\0', '\0', '|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?',
};

First, we detect whether Shift or Caps Lock has been pressed, the associated scancodes are:

vi keyboard.h

#define  LSHIFT         0x2A
#define _LSHIFT         0xAA
#define  RSHIFT         0x36
#define _RSHIFT         0xB6
#define  CAPS           0x3A
#define _CAPS           0xBA

In this case, we detect uppercase letters and assign variable values accordingly:

vi keyboard.c

static char shift = 0, alt = 0, ctl = 0, caps = 0, spec = 0;
switch (scancode) {
    case LSHIFT:
    case RSHIFT:
        shift = 1;
        return;
    case _LSHIFT:
    case _RSHIFT:
        shift = 0;
        return;
    case CAPS:
        caps = !caps;
        return;

Then, depending on the case, we apply one table or another, for which we use a binary encoding:

vi keyboard.c

// We shift caps to make room for shift bit, in that way we have 4 posible values
// 00 -> 0: Regular key
// 01 -> 1: Shift
// 10 -> 2: Caps
// 11 -> 3: Caps + Shift
switch ((caps << 1) | shift) {
    case 0: /* Normal */
        letter = kbd_us[scancode];
        break;
    case 1: /* Shift */
        letter = kbd_us_shift[scancode];
        break;
    case 2: /* Caps */
        letter = kbd_us[scancode];
        if (letter >= 'a' && letter <= 'z')
            letter = kbd_us_shift[scancode];
        break;
    case 3: /* Caps + Shift */
        letter = kbd_us_shift[scancode];
        if (letter >= 'A' && letter <= 'Z')
            letter = kbd_us[scancode];
        break;
}

Finally, we print the character on the screen. As changes have been made to the current screen, we call save_current_screen_to_screen_buffer_next_avpag so that the next time avpag is pressed to scroll through the screen history, the current screen is saved first:

vi keyboard.c

save_current_screen_to_screen_buffer_next_avpag();

The function to rehabilitate the saving of the current screen is:

vi keyboard.c

void save_current_screen_to_screen_buffer_next_avpag() {
    if (screen_saved == 1) {
        // If we saved current screen when avpag was pressed in first time
        // we reset it each time we modify current screen content
        screen_saved = 0;
        buffer_segment = 0;
        // Free last saved screen from screen_buffer in that way next save operation will reuse that positions
        screen_buffer_counter = screen_buffer_counter - (MAX_ROWS - 1);
    }
}

We perform some additional checks to ensure that the command does not exceed MAX_COLS -1 in length, making everything simpler:

vi keyboard.c

// Print screen key pulsations:
// Get cursor position and insert char to key_buffer, then clear line, print key_buffer content and adjust cursor position
buffer_length = string_length(key_buffer);
offset = get_cursor_offset();
row = get_offset_row(offset);
col = get_offset_col(offset, row);

if (col >= MAX_COLS -1) {
    ps("\n");
    print_prompt();
    ps("ERROR: Multi-line command not allowed\n");
    print_prompt();
    reset_key_buffer();
    return;
}

If everything is in order, we calculate the position to insert the character in key_buffer and print it.

Once printed, we obtain the offset and compare it with the maximum. This is done to know how far we can move the cursor with the arrow keys and to control command editing.

vi keyboard.c

// Position where insert new char, being aware that our prompt uses first 3 chars of the row
// The position in key_buffer array will be col - 3
key_buffer_position = col - 3;

buffer_size = sizeof(key_buffer);
return_code = 0;
return_code = string_insert(key_buffer, buffer_size, letter, key_buffer_position);
if (return_code == 0) {
    delete_current_line();
    print_prompt();
    ps(key_buffer);
    
    current_offset = get_cursor_offset();
    // We use command_max_offset to know allowed arrow keys range movement
    if (current_offset > command_max_offset) {
        command_max_offset = current_offset;
    }
    
    // Adjust cursor position, important when editing command with arrow keys or backspace
    set_cursor_offset(offset + 1);
}

HISTORY

For the command history to work, the executed commands must be stored in a two-dimensional array called command_history.

When ENTER is pressed, the following code is executed, where if the maximum number of stored commands HISTORY_BUFFER is exceeded, the last one is discarded.

vi keyboard.c

if (command_counter >= HISTORY_BUFFER) {
    // Move all command one position up, last one will be duplicated
    int i;
    int j;
    for (i=0; i < HISTORY_BUFFER-1; i++) {
        for (j=0; command_history[i+1][j] != '\0'; j++) {
            command_history[i][j] = command_history[i+1][j];
        }
        command_history[i][j] = '\0';
    }
    // We are out of HISTORY_BUFFER by 1 position, we set it to last HISTORY_BUFFER position
    command_counter = command_counter - 1;
}

The normal saving of the command in the history is:

vi keyboard.c

// Save last command in history slot
// if it was last position the duplicity produced by moving all history commands will be fixed
int k;
for (k=0; key_buffer[k] != '\0'; k++) {
    command_history[command_counter][k] = key_buffer[k];
}
// Add '\0' to end of command
command_history[command_counter][k] = '\0';

command_counter = command_counter + 1;

// reset keybuffer content and history_shift
reset_key_buffer();
commands_history_shift = 0;

When the up arrow key is pressed, the idea is to clear the buffer of the current command, use the commands_history_shift variable to know how many times the up arrow has been pressed, and the command_to_show variable will act as an index of the command history.

vi keyboard.c

reset_key_buffer();
commands_history_shift = commands_history_shift + 1;
command_to_show = command_counter - commands_history_shift;

We perform some checks to make sure we are not out of normal values when accessing the history:

vi keyboard.c

// If we are out of bounds revert shift position by one
if (command_to_show < 0) {
    commands_history_shift = commands_history_shift - 1;
    command_to_show = 0;
}

Finally, we load the command from the history into key_buffer, delete the current line, paint the prompt and key_buffer, update the command_max_offset variable that serves to know the maximum offset when editing the command recovered from the history, this variable will be used in all editing commands such as left/right arrows, del/backspace, home/end. Finally, we call the save_current_screen_to_screen_buffer_next_avpag function since the content of the current screen has been modified.

vi keyboard.c

if ( command_to_show >= 0 && command_to_show <= command_counter && command_to_show < HISTORY_BUFFER ) {
    for (int i=0; command_history[command_to_show][i] != '\0'; i++) {
        key_buffer[i] = command_history[command_to_show][i];
    }
    delete_current_line();
    print_prompt();
    ps(key_buffer);
    command_max_offset = get_cursor_offset();
}
save_current_screen_to_screen_buffer_next_avpag();

The down arrow code is very similar but decrementing the commands_history_shift variable.


EDITING

Another important aspect is editing commands using the del/backspace, home/end, and arrow keys. These are detected using the following code:

vi keyboard.c

case CURSORRIGHT:
    // Dont allow any operation while screen scrolling
    if (cursor_enabled == 0) {
        return;
    }
    move_cursor_right(command_max_offset);
    save_current_screen_to_screen_buffer_next_avpag();
    return;
case CURSORLEFT:
    // Dont allow any operation while screen scrolling
    if (cursor_enabled == 0) {
        return;
    }
    move_cursor_left(command_max_offset);
    save_current_screen_to_screen_buffer_next_avpag();
    return;
case BACKSPACE:
    // Dont allow any operation while screen scrolling
    if (cursor_enabled == 0) {
        return;
    }
    offset = get_cursor_offset();
    buffer_size = sizeof(key_buffer);
    return_code = 0;
    return_code = backspace(key_buffer, buffer_size, command_max_offset);
    if (return_code == 0) {
        delete_current_line();
        print_prompt();
        ps(key_buffer);
        // Adjust cursor position, important when editing command with arrow keys or backspace
        set_cursor_offset(offset - 1);
        command_max_offset = command_max_offset - 1;
    }
    save_current_screen_to_screen_buffer_next_avpag();
    return;
case HOME:
    // Dont allow any operation while screen scrolling
    if (cursor_enabled == 0) {
        return;
    }
    move_curso_home();
    save_current_screen_to_screen_buffer_next_avpag();
    return;
case END:
    // Dont allow any operation while screen scrolling
    if (cursor_enabled == 0) {
        return;
    }
    move_curso_end(command_max_offset);
    save_current_screen_to_screen_buffer_next_avpag();
    return;
case DEL:
    // Dont allow any operation while screen scrolling
    if (cursor_enabled == 0) {
        return;
    }
    offset = get_cursor_offset();
    buffer_size = sizeof(key_buffer);
    return_code = 0;
    return_code = delete(key_buffer, buffer_size, command_max_offset);
    if (return_code == 0) {
        delete_current_line();
        print_prompt();
        ps(key_buffer);
        // Adjust cursor position, important when editing command with arrow keys or backspace
        set_cursor_offset(offset);
        command_max_offset = command_max_offset - 1;
    }
    save_current_screen_to_screen_buffer_next_avpag();
    return;

In all cases, we first check that we are not in scrolling mode, then depending on the pressed key, one action or another will be performed, but all end up enabling the saving of the screen on the next press of the avpag key since all functions have modified the state of the current screen.

According to the key pressed, different actions will be executed.

CURSORRIGHT/CURSORLEFT
Calls a function that moves the cursor in the indicated direction:

int move_cursor_left(int max_offset) {
    // We only allow command edition when not scrolling screen history
    if (cursor_enabled == 1) {
        int offset = get_cursor_offset();
        offset = offset - 1;
        int row = get_offset_row(offset);
        int col = get_offset_col(offset, row);
        if (col >= 3 && offset <= max_offset) {
            set_cursor_offset(offset);
            return 0;
        } else {
            return 1;
        }
    }
}

int move_cursor_right(int max_offset) {
    // We only allow command edition when not scrolling screen history
    if (cursor_enabled == 1) {
        int offset = get_cursor_offset();
        offset = offset + 1;
        int row = get_offset_row(offset);
        int col = get_offset_col(offset, row);
        if (col >= 3 && offset <= max_offset) {
            set_cursor_offset(offset);
            return 0;
        } else {
            return 1;
        }
    }
}

BACKSPACE

int backspace(char key_buffer[], int buffer_size, int max_offset) {
    int offset = get_cursor_offset();
    int row = get_offset_row(offset);
    int col = get_offset_col(offset, row);
    int return_code = 0;
    
    int position = col - 4;
    // Dont allow to delete shell prompt or top lines
    if ( col >= 3 ){
        if (offset <= max_offset) {
            return_code = string_delete_char(key_buffer, buffer_size, position);
            if (return_code == 0) {
                return 0;
            } else {
                return 1;
            }
        } else {
            return 1;
        }
    }
}

The string_delete_char() function splits the string into several pieces, the first part before the char to be deleted and the second part after the char, finally concatenates the two strings with string_concat()

HOME
We get the current offset to find out the current row, set the col to 3 to respect the prompt, get the offset of the position of the current row, col 3 and move the cursor to that offset:

void move_curso_home() {
    // We only allow command edition when not scrolling screen history
    if (cursor_enabled == 1) {
        int offset = get_cursor_offset();
        int row = get_offset_row(offset);
        // Col3: Start of input after prompt
        int col = 3;
        offset = get_offset(row, col);
        set_cursor_offset(offset);
    }
}

END
Knowing the max_offset of the command, it is easy to move the cursor to that position

void move_curso_end(int max_offset) {
    // We only allow command edition when not scrolling screen history
    if (cursor_enabled == 1) {
        set_cursor_offset(max_offset);
    }
}

DEL
Behaves exactly like BACKSPACE but the position of the character to be deleted is calculated differently, in this case position = col - 3

int delete(char key_buffer[], int buffer_size, int max_offset) {
    int offset = get_cursor_offset();
    int row = get_offset_row(offset);
    int col = get_offset_col(offset, row);
    int return_code = 0;
    
    int position = col - 3;
    // Dont allow to delete shell prompt or top lines
    if ( col >= 3 ){
        if (offset < max_offset) {
            return_code = string_delete_char(key_buffer, buffer_size, position);
            if (return_code == 0) {
                return 0;
            } else {
                return 1;
            }
        } else {
            return 1;
        }
    }
}

SCROLLING NAVIGATION

To be able to navigate through scrolling, we must save the lines that disappear from the screen in a two-dimensional matrix, in this way we can recover the history, for this we must modify the handle_scrolling function, the line is saved by executing the save_row_to_buffer(MAX_COLS) function.

vi screen.c

/* Advance the text cursor, scrolling the video buffer if necessary. */
int handle_scrolling(int offset) {
    // IMPORTANT: We cant use print function inside handle_scrolling because print calls handle_scrolling and we get sticked inside infinite loop

    // Are we writting inside screen char matrix?
    if (offset < MAX_ROWS * MAX_COLS) {
        return offset ;
    }
    
    // We are outside screen char matrix so copy row to inmediately above.
    int i;
    // row_length: number of memory positions that compose a row
    int row_length = MAX_COLS * 2;
    
    save_row_to_buffer(MAX_COLS);
    
    // While we are working with memory addresses bear in mind that offset = 1short(2bytes), memory_pos = offset * 2
    // Make screen scrolling
    // We should start moving rows in position 1 to 0, but we want to preserver OS banner, so we start at 2 moving data to 1
    for (i = 2; i <= MAX_ROWS - 1; i++) {
        memory_copy((short*) (VIDEO_ADDRESS + (get_offset(i, 0) * 2)), (short*) (VIDEO_ADDRESS + (get_offset(i-1, 0) * 2)), row_length);
    }
    
    // Blank last line by setting all bytes to 0
    short* last_line = (short*) (VIDEO_ADDRESS + (get_offset(MAX_ROWS-1, 0) * 2));
    short character_and_attribute_byte = 0 | WHITE_ON_BLACK << 8;
    for (i = 1; i < (MAX_COLS - 1); i++) {
        last_line[i] = character_and_attribute_byte;
    }
    
    // Offset outside screen char matrix, move the offset back one row.
    offset -= MAX_COLS ;
    
    // Return the updated cursor position.
    return offset ;
}

The function for saving the line to the scrolling buffer:

vi screen.c

int save_row_to_buffer(int row_offset) {
    // Save row that we are going to delete to a screen buffer to be able to scroll history
    // we dont have a memory allocator so we cant reserve a portion of memory to save video memory content in that location
    // we have to save text chars+attr in our bidimensional array
    
    // If we consume SCREEN_HISTORY_BUFFER positions delete old ones
    short* video_memory = (short*) VIDEO_ADDRESS;
    if (screen_buffer_counter >= SCREEN_HISTORY_BUFFER) {
        // Move all saved rows one position up, last one will be duplicated
        int i;
        int j;
        for (i=0; i < SCREEN_HISTORY_BUFFER-1; i++) {
            for (j=0; j <= (MAX_COLS - 1); j++) {
                screen_buffer[i][j] = screen_buffer[i+1][j];
            }
        }
        // We are out of HISTORY_BUFFER by 1 position, we set it to last HISTORY_BUFFER position
        screen_buffer_counter = screen_buffer_counter - 1;
    }
    
    // Save screen row in screen history
    // i indicates screen offset: MAX_COLS to (2*MAX_COLS) - 1 would be the second line in screen
    // if it was last position the duplicity produced by moving all screen rows will be fixed
    int k = 0;
    for (int i=row_offset; i <= ((row_offset*2)-1); i++) {
        short video_char_attr = video_memory[i];
        if (k <= MAX_COLS-1) {
            screen_buffer[screen_buffer_counter][k] = video_char_attr;
            k = k + 1;
        }
    }
    screen_buffer_counter = screen_buffer_counter + 1;
}

NOTE: The keys chosen for navigation are avpag/repag when we are in navigation mode, command editing is disabled to avoid problems.

Scrolling is performed using the following function:

vi screen.c

// buffer_segment: Screen segment to show
// screen_buffer_counter: How many commands we have saved in buffer
void avrepag_scroll(int buffer_segment) {
    int row_length = MAX_COLS-1;
    
    // Save current display only first avpag key press
    if (buffer_segment == 1 && screen_saved == 0) {
        // Save current display to screen buffer, without OS banner
        for (int i=1; i<=MAX_ROWS-1; i++){
            // save_row_to_buffer function needs first row position offset as input argument
            int row_offset = MAX_COLS*i;
            save_row_to_buffer(row_offset);
        }
        // Save cursor offset
        pre_scrolling_offset = get_cursor_offset();
        screen_saved = 1;
    }
    
    // Set cursor to second screen line
    set_cursor_offset(MAX_COLS);
    
    // Calculate screen_buffer start to display in screen
    // 24 is added to buffer_segment*24 because we have saved current display to screen_buffer
    int screen_buffer_start = screen_buffer_counter-((buffer_segment * (MAX_ROWS - 1)) + (MAX_ROWS - 1));
    if (screen_buffer_start < 0) {
        screen_buffer_start = 0;
        screen_buffer_start_reached = 1;
    } else {
        screen_buffer_start_reached = 0;
    }
    
    // Calculate screen_buffer end to display in screen
    int screen_buffer_end = screen_buffer_start + (MAX_ROWS - 1);
    if (screen_buffer_end >= screen_buffer_counter) {
        screen_buffer_end = screen_buffer_counter;
        screen_buffer_end_reached = 1;
    } else {
        screen_buffer_end_reached = 0;
    }
    
    // Fill screen with start to end characters
    for (int i=screen_buffer_start; i<=screen_buffer_end; i++) {
        for (int j=0; j<=MAX_COLS-1; j++) {
            short video_char_attr = screen_buffer[i][j];
            // Get first byte of video_char_attr
            char character = (video_char_attr) & 0xFF;
            // Get second byte of video_char_attr
            char attribute_byte = (video_char_attr >> 8) & 0xFF;
            // We are scrolling, in that way if we are printing last row, last col we dont want to increment offset
            int offset = get_cursor_offset();
            if (offset != ((MAX_ROWS * MAX_COLS)-1)) {
                print_char(character, -1, -1, attribute_byte, 0);
            } else {
                print_char(character, -1, -1, attribute_byte, 1);
            }
        }
    }
    
    if (screen_buffer_end_reached == 1) {
        set_cursor_offset(pre_scrolling_offset);
        cursor_enabled = 1;
    } else {
        // Disable cursor
        set_cursor_offset(-1);
        cursor_enabled = 0;
    }
}

It should be noted that when scrolling is done, the current screen is also saved so as not to lose it and be able to recover it later.

With the following drawing we will see more clearly how scrolling works, the table represents the lines saved when scrolling by printing new lines and losing the old ones:

screen_buffer
screen_buffer_counter-52 buffer_segment=3
screen_buffer_counter-48 buffer_segment=2
screen_buffer_counter-24 buffer_segment=1
screen_buffer_counter current screen content

The screen_buffer contains the current screen content in the screen_buffer_counter position, the previous screen in screen_buffer_counter-24, and so on.


SHELL

The shell is very simple and only supports a couple of commands, HALT and clear.

int user_input(char input[]) {
    /*ps("Received command: |");
    ps(input);
    ps("|\n");
    print_prompt();*/
    // string_append_char from util.c protects shell input but we add a second check
    if ( string_length(input) >= 256) {
        return 1;
    }
    if (string_compare(input, "HALT") == 0) {
        ps("Stopping the CPU. Bye!\n");
        asm volatile("hlt");
    } else if (string_compare(input, "uptime") == 0) {
        ps("Not implemented!\n");
    } else if (string_compare(input, "help") == 0) {
        ps("Not implemented!\n");
    } else if (string_compare(input, "clear") == 0) {
        clear_screen();
        print_os_banner();
    } else {
        ps("Command not found\n");
    }
    print_prompt();
    return 0;
}

Here’s a short video showing all the new features in action:

You can download the new code version from here .

I haven’t gone into detail about all the new functions in util.c as the article would become too long and boring, but the code is there to be analyzed by whoever needs it ;)

If you liked the article, you can treat me to a RedBull here