StellatorOS: Video driver básico


En este artículo aprenderemos como programar un driver de video básico en C, sabiendo que los carácteres a mostrar por pantalla se mapean a una zona de memoria compartida entre la tarjeta gráfica y el SO seremos capaces de imprimir toda clase de carácteres en las coordenadas deseadas. Nuestro driver soportará impresión de carácteres, impresión de strings, impresión multi línea y scrolling. 

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

Como ya explicamos en artículos anteriores cada carácter en pantalla está compuesto por un carácter ASCII mas su atributo, esto implica que cada carácter ocupa 2 bytes en memoria, además los carácteres se distribuyen en una matriz de 25x80 carácteres que conforman la totalidad de la pantalla.

La tarjeta de video es un dispositivo MMIO, para imprimir carácteres por pantalla deberemos escribir en una cierta región de memoria compartida entre la tarjeta de video y el SO, conforme se vaya avanzando en la escritura de la memoria se irán pintando carácteres en las posiciones siguientes de la matriz, la siguiente tabla muestra claramente las posiciones de la matriz, el carácter asociado, la posición de memoria y su equivalente en hexadecimal(muy útil cuando tenemos el kernel cargado en Gdb y estamos debugeando).

2

5

C

h

a

r

s

80 Chars

Char0+Attr

PosMem0: 0x0h

Char1+Attr

PosMem2: 0x2h

Char2+Attr

PosMe4: 0x4h

     

Char79+Attr

PosMem158: 0x9Eh

Char80+Attr

PosMem160: 0xA0h

Char81+Attr

PosMem162: 0xA2h

Char82+Attr

PosMem164: 0xA4h

     

Char159+Attr

PosMem318: 0x13Eh

 

 

           

 

 

           

 

 

           

Char1920+Attr

PosMem3840: 0xF00h

Char1921+Attr

PosMem3842: 0xF02h

Char1922+Attr

PosMem3844: 0xF04h

     

Char1999+Attr

PosMem3998: 0xF9Eh

 

PosMem4000: 0xFA0h            

NOTA: He incluido en la tabla la posición 4000 a pesar de estar fuera de la matriz, lo hago porque nos será muy útil conocer esta posición cuando debugeemos el scrolling.

Hay que tener en cuenta que las direcciones de memoria siempre se cuentan de byte en byte por lo tanto cualquier operación que implique posiciones de memoria como copia de memoria se debe indicar siempre la dirección en bytes, por otro lado tenemos los punteros que pueden ser del tipo que deseemos, de este modo podremos leer/escribir de la memoria de X en X pero repito las direcciones de memoria siempre en bytes.

Para programar el driver de video partiremos de los ficheros de artículos anteriores y generaremos algunos nuevos, pero dejo aquí un comprimido de todos ellos para mayor comodidad.

El scrolling requiere copiar valores de la memoria de una posición a otra, para ello programaremos la función memory_copy, definimos el prototipo y la función en si misma.

vi kernel/util.h
void memory_copy(short* source, short* dest, int no_bytes);
vi kernel/util.c
// Copy bytes from one place to another in blocks of short bytes until no_bytes is reached
void memory_copy(short* source, short* dest, int no_bytes) {
    int i;
    // copy short by short
    for (i =0; i<no_bytes; i++) {
        *(dest + i) = *(source + i);
    }
}

Ahora los prototipos y funciones de nuestro driver.

vi drivers/screen.h
#define VIDEO_ADDRESS 0xb8000
#define MAX_ROWS 25
#define MAX_COLS 80
#define WHITE_ON_BLACK 0x0f
#define RED_ON_WHITE 0xf4

/* Screen i/o ports */
#define REG_SCREEN_CTRL 0x3d4
#define REG_SCREEN_DATA 0x3d5

/* Public kernel API */
void clear_screen();
int get_offset(int row, int col);
void set_cursor_offset(int offset);
int get_cursor_offset();
int print_char(char character, int row, int col, char attribute_byte);
int get_offset_row(int offset);
int get_offset_col(int offset, int row);
void print_string(char *message, int row, int col);
int handle_scrolling(int cursor_offset);
vi drivers/screen.c
#include "screen.h"
#include "ports.h"
#include "../kernel/util.h"

// Offset depends on the type of the video_memory pointer, if it is short: 1screenChar -> 1offset

void clear_screen() {
    int screen_size = MAX_COLS * MAX_ROWS - 1;
    int i;
    short* video_memory = (short*) VIDEO_ADDRESS;

    // Set each video memory address to '' value with WHITE_ON_BLACK attribute
    short character_and_attribute_byte = 0 | WHITE_ON_BLACK << 8;
    for (i = 0; i < screen_size; i++) {
        video_memory[i] = character_and_attribute_byte;
    }
    
    // Set cursor to first offset after clear screen
    set_cursor_offset(0);
}

// Convert input coordinates to video memory offset
int get_offset(int row, int col) {
    return (row * MAX_COLS + col);
}

// Set cursor position to specified video_memory
void set_cursor_offset(int offset) {
    port_byte_out(REG_SCREEN_CTRL, 14);
    port_byte_out(REG_SCREEN_DATA, offset >> 8); // High byte
    port_byte_out(REG_SCREEN_CTRL, 15);
    port_byte_out(REG_SCREEN_DATA, (offset & 0xff)); // Low byte
}

// Get cursor position and return video_memory offset equivalent
int get_cursor_offset() {
    port_byte_out(REG_SCREEN_CTRL, 14);
    int offset = port_byte_in(REG_SCREEN_DATA) << 8; // High byte
    port_byte_out(REG_SCREEN_CTRL, 15); // Low byte
    offset += port_byte_in(REG_SCREEN_DATA);
    return offset; /* Position * size of character cell */
}

int print_char(char character, int row, int col, char attribute_byte) {
    short* video_memory = (short*) VIDEO_ADDRESS;
    int offset;

    if (!attribute_byte) {
        attribute_byte = WHITE_ON_BLACK;
    }
    
    //short code variable improves printing chars performance, in that way we only write to video_memory one time
    //not twice with char+attr
    short character_and_attribute_byte = character | attribute_byte << 8;
    
    if (row >= 0 && col >= 0) {
        offset = get_offset(row, col);
    } else {
        offset = get_cursor_offset();
    }


    // If we see a newline character, set offset to the end of current row, so it will be advanced to the first col
    // of the next row when offet is incremented.
    if (character == '\n') {
        int row = get_offset_row(offset);
        offset = get_offset(row, MAX_COLS - 1);
    } else {
        //video_memory[offset] = character;
        //video_memory[offset+1] = attribute_byte;
        video_memory[offset] = character_and_attribute_byte;
    }

    // Update the offset to the next character cell
    offset++;
    
    // Make scrolling adjustment, we check if next char write will be done outside screen char matrix
    offset = handle_scrolling(offset);
    
    // Update the cursor position on the screen device .
    set_cursor_offset(offset);
    
    return offset;
}

void print_string(char *message, int row, int col) {
    int offset;
    
    /* Set cursor if col/row are negative */
    if (row < 0 || col < 0 ) {
        offset = get_cursor_offset();
        row = get_offset_row(offset);
        col = get_offset_col(offset, row);
    }
    
    /* Loop through message and print it */
    int i = 0;
    while (message[i] != 0) {
        offset = print_char(message[i], row, col, WHITE_ON_BLACK);
        i++;
        /* Compute row/col for next iteration */
        row = get_offset_row(offset);
        col = get_offset_col(offset, row);
    }
}

int get_offset_row(int offset) {
    return offset / MAX_COLS;
}

int get_offset_col(int offset, int row) {
    return (offset - (row * MAX_COLS));
}

/* Advance the text cursor, scrolling the video buffer if necessary. */
int handle_scrolling(int offset) {
    // 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;
    int row_length = (MAX_COLS-1) * 2;
    // While we are working with memory addresses bear in mind that offset = 1short(2bytes), memory_pos = offset * 2
    for (i = 1; 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 = 0; 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 ;
}

Una de las partes mas complejas del driver posiblemente sea la que controla las líneas nuevas(\n).

    if (character == '\n') {
        int row = get_offset_row(offset);
        offset = get_offset(row, MAX_COLS - 1);
    } else {
        video_memory[offset] = character_and_attribute_byte;
    }

    offset++;

Cuando se detecta el carácter '\n' se obtiene el número de fila y el offset(desplazamiento desde la dirección base video_memory) asociado a la última columna de esa misma fila, como unas líneas mas abajo se incrementa offset estamos justo en la siguiente línea, el próximo carácter que se escriba será en esa posición.

La otra función un poco mas compleja es la del scrolling, primero comprueba si el offset donde se va a escribir el próximo carácter será una posición dentro de la matriz de chars de la pantalla(25x80) si está dentro se retorna y no se ejecuta el scrolling.

    // Are we writting inside screen char matrix?
    if (offset < MAX_ROWS * MAX_COLS) {
        return offset ;
    }

Si necesitamos realizar el scrolling debemos copiar el contenido de la fila N a la N-1, la función memory_copy precisa de una dirección de memoria origen/destino y el número de bytes a copiar, recordemos que el offset equivale a dos bytes en RAM ya que el puntero video_memory se ha definido como short(2bytes: char+attr), cada vez que escribimos/leemos utilizando este puntero estamos esribiendo/leyendo 2 bytes, para calcular el desplazamiento de memoria desde video_memory hasta un offset en concreto aplicaremos la formula: memory = offset * 2.

El bucle que copia las filas parte de la fila número 1(la segunda en pantalla) hasta la última(MAX_ROWS - 1) y copiamos su contenido mediante la función memory_copy.

    int i;
    int row_length = (MAX_COLS-1) * 2;
    // While we are working with memory addresses bear in mind that offset = 1short(2bytes), memory_pos = offset * 2
    for (i = 1; 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);
    }
  • dirección origen: Primera posición de la fila get_offset(i, 0) * 2
  • dirección destino: Primera posición fila anterior get_offset(i-1, 0) * 2
  • número de bytes: El tamaño de una fila (MAX_COLS-1) * 2

NOTA: Como estamos trabajando con posiciones de memoria debemos multiplicar todos los resultados X2 para pasarle memory_copy el valor de la posición de memoria y no el offset.

Como estamos copiando la fila N a la N - 1 la última fila queda repetida en la última y penúltima fila, por ello debemos localizar la última fila y ponerla a 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 = 0; i < (MAX_COLS - 1); i++) {
        last_line[i] = character_and_attribute_byte;
    }

El scrolling se ha ejecutado porque se ha detectado que el offset estaba en una posición fuera de la matriz de la pantalla, ahora que ya hemos ejecutado el scrolling debemos setear el offset a la primera posición de la última fila.

    // Offset outside screen char matrix, move the offset back one row.
    offset -= MAX_COLS ;

Finalmente el código main.

vi kernel/kernel.c
#include "../drivers/screen.h"
#include "util.h"

void main() {
    clear_screen();
    print_string("Welcome to StellatorOS v0.1b by Kr0m\n", 0, 0);
    print_string("Line1\n", -1, -1);
    print_string("Line2\n", -1, -1);
    print_string("Line3\n", -1, -1);
    print_string("Line4\n", -1, -1);
    print_string("Line5\n", -1, -1);
    print_string("Line6\n", -1, -1);
    print_string("Line7\n", -1, -1);
    print_string("Line8\n", -1, -1);
    print_string("Line9\n", -1, -1);
    print_string("Line10\n", -1, -1);
    print_string("Line11\n", -1, -1);
    print_string("Line12\n", -1, -1);
    print_string("Line13\n", -1, -1);
    print_string("Line14\n", -1, -1);
    print_string("Line15\n", -1, -1);
    print_string("Line16\n", -1, -1);
    print_string("Line17\n", -1, -1);
    print_string("Line18\n", -1, -1);
    print_string("Line19\n", -1, -1);
    print_string("Line20\n", -1, -1);
    print_string("Line21\n", -1, -1);
    print_string("Line22\n", -1, -1);
    print_string("Line23\n", -1, -1);
    print_string("Line24\n", -1, -1);
    print_string("Line25\n", -1, -1);
    print_string("Line26\n", -1, -1);
}

Si compilamos y cargamos el kernel en Qemu podremos ver el scrolling:

gmake run

NOTA: El Makefile está customizado para mi cross-compiler y mis herramientas bajo FreeBSD puede ser necesario realizar algunos ajustes para su correcto funcionamiento.

Dejo un video donde avanzo la ejecución mediante Gdb para que se vea mejor como va ejecutándose el código, hay algunos glitches por el compositor gráfico pero se ve el scrolling sin problemas:

Si te ha gustado el artículo puedes invitarme a un redbull aquí.
Autor: kr0m -- 28/06/2020 05:45:42