Esta pagina se ve mejor con JavaScript habilitado

MercuryOS Driver de teclado y driver gráfico mejorado

 ·  🎃 kr0m

Uno de los aspectos mas básicos de un SO es leer la entrada del usuario de algún modo, por norma general el usuario introducirá las órdenes mediante el teclado, para leer las pulsaciones haremos uso de las interrupciones ya explicadas en artículos anteriores y de un driver de teclado básico.

Antes de comenzar es recomendable que leas estos artículos anteriores:

En este artículo el driver gráfico se ha mejorado para soportar funciones avanzadas como navegación por el scroll de la consola, teclas inicio/fin, edición de comandos con las flehas y supr/del, función clear de la pantalla, también se ha añadido un histórico de comandos y empezamos a programar una pequeña shell empotrada dentro del propio kernel donde iremos añadiendo los comandos del sistema.


PULSACIONES

La lectura de las pulsaciones es algo compleja ya que para diferenciar entre mayúsculas y minúsculas será necesario el uso de dos tablas cada una con los carácteres asociados al scancode pero teniendo en cuenta si estaba el Shift o BloqMayus  habilitado.

Las dos tablas son las siguientes:

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', '<', '>', '?',
};

Primero detectamos si se ha pulsado Shift o BloqMayus, los scancodes asociados son:

vi keyboard.h

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

En este case detectamos las mayúsculas y asignamos valores de variables en consecuencia:

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;

Luego según sea el caso aplicamos una tabla u otra, para ello utilizamos una codificación binaria:

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;
}

Finalmente imprimimos el carácter por pantalla, como se han realizado cambios en el screen actual llamamos a save_current_screen_to_screen_buffer_next_avpag para que la próxima vez que se pulse avpag para hacer scrolling por el histórico de la pantalla antes se guarde la pantalla actual:

vi keyboard.c

save_current_screen_to_screen_buffer_next_avpag();

La función de rehabilitado del guardado de la pantalla actual es:

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);
    }
}

Realizamos algunas comprobaciones adicionales para asegurarnos de que el comando no supera MAX_COLS -1 de longitud, de este modo todo es mas sencillo:

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;
}

Si vemos que todo está en orden calculamos la posición donde insertar el carácter en key_buffer y lo imprimimos.

Una vez imprimido se obtiene el offset y se compara con el máximo, esto se hace para saber hasta donde podemos mover el cursor con las flechas laterales y para controlar la edición del comando.

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);
}

HISTÓRICO

Para que el histórico de los comandos funcione los comandos ejecutados deben ser almacenados en un array bidimensional llamado command_history.

Cuando se presiona ENTER se ejecuta el siguiente código donde si se supera el máximo de comandos almacenados HISTORY_BUFFER, se descarta el último.

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;
}

El salvado normal del comando en el histórico es:

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;

Cuando se pulsa la tecla arriba, la idea es limpiar el buffer del comando actual, utilizar la variable commands_history_shift para saber las veces que se ha pulsado la flecha arriba, la variable command_to_show actuará a modo de índice del histórico de comandos.

vi keyboard.c

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

Realizamos algunas comprobaciones para asegurarnos de que no estamos fuera de los valores normales al acceder al histórico:

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;
}

Finalmente cargamos el comando del histórico a key_buffer, eliminamos la línea actual, pintamos el prompt y el key_buffer, actualizamos command_max_offset variable que nos sirve para saber el máximo offset cuando editamos el comando recuperado del histórico, esta variable se utilizará en todos los comandos de edición como flechas izq/der, del/spr, inicio/fin. Finalmente se llama a la función save_current_screen_to_screen_buffer_next_avpag ya que se ha modificado el contenido del screen actual.

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();

El código de flecha abajo es muy similar pero decrementando la variable commands_history_shift.


EDICIÓN

Otro aspecto importante es la edición de comandos mediante las teclas supr/del, inicio/fin y las flechas. Estas son detectadas mediante el siguiente código:

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;

En todos los casos primero comprobamos que no se esté en modo scrolling, luego dependiendo de la tecla presionada se realizará una acción u otra, pero todas finalizan habilitando el guardado de la pantalla en la próxima pulsación de la tecla avpag ya que todas las funciones han modificado el estado del screen actual.

Según la tecla se ejecutarán unas acciones u otras.

CURSORRIGHT/CURSORLEFT
Llaman a una función que mueve el cursor en la dirección indicada:

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;
        }
    }
}

La función string_delete_char() parte el string en varios trozos, la primera parte antes del char a eliminar y la segunda parte después del char, finalmente concatena los dos strings con string_concat()

HOME
Sacamos el offset actual para averiguar el row actual, seteamos el col a 3 para respetar el prompt, obtenemos el offset de la posición del row actual, col 3 y movemos el cursor a dicho 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
Conociendo el max_offset del comando es fácil mover el cursor a dicha posición

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
Se comporta exactamente igual que BACKSPACE pero la posición del carácter a eliminar se calcula de forma distinta, en este caso 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;
        }
    }
}

Para poder navegar por el scrolling debemos guardar las líneas que desaparecen de la pantalla en una matriz bidimensional, de este modo podremos recuperar el histórico, para ello debemos modificar la función handle_scrolling, la línea se guarda al ejecutar la función save_row_to_buffer(MAX_COLS).

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 ;
}

La función de guardado de la línea al buffer de scrolling:

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;
}

NOTA: Las teclas elegidas para la navegación son avpag/repag cuando estamos en modo navegación la edición de comandos queda deshabilitada para evitar problemas.

El scrolling se realiza mediante la siguiente función:

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;
    }
}

Cabe destacar que cuando se hace scroll también se guarda la pantalla actual para no perderla y poder recuperarla posteriormente.

Con el siguiente dibujo veremos mas claro como funciona el scrolling, la tabla representa las líneas guardadas al hacer scroll al imprimir nuevas líneas y perder las viejas:

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

screen_buffer contiene en la posición screen_buffer_counter el contenido de la pantalla actual, en screen_buffer_counter-24 la pantalla anterior y así sucesivamente.


SHELL

La shell es muy sencilla y tan solo soporta un par de comandos, HALT y 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;
}

Dejo un pequeño video donde podemos ver todas las funcionalidades nuevas en acción:

Podéis descargar la nueva versión de código desde aquí .

No me he parado a explicar todas las funciones nuevas de util.c ya que el artículo quedaría demasiado extenso y aburrido, pero el código está ahí para ser analizado por quien lo precise necesario ;)

Si te ha gustado el artículo puedes invitarme a un RedBull aquí