Driver VGA (Pantalla)
Cuando Sierra Kernel es cargado por GRUB, es cargado en modo texto. Por esto el kernel tiene disponible un framebuffer que es una parte de la memoria RAM que controla una pantalla de caracteres de 80 x 25. A esta parte de la memoria le llamamos memoria de video o video memory en inglés, y podemos acceder a ella de la misma forma en la que podemos acceder a cualquier parte de la memoria RAM, en la dirección 0xB8000. Es importante resaltar que en realidad el framebuffer no es memoria RAM y que tampoco funciona como ella. Más bien es una parte del controlador VGA dedicado a la memoria de video. En este modo, el kernel no puede mostrar imágenes en la pantalla debido a que solo tiene acceso a una pantalla de caracteres y no de pixeles. El framebuffer es un conjunto de 16 bits, por lo tanto cada valor de 16 bits representa un carácter diferente. En ASCII, cada carácter solo ocupa 8 bits, por lo cual eso nos deja 8 bits libres los cuales usamos para especificar el color de fondo y de la letra (Cada color ocupa 4 bits). Hay 15 colores que podemos usar, cada uno de ellos es representado por un valor numérico; pero para facilitar la programación un poco, cada color también tiene un string que puede ser usado para representarlo:
Color | Valor Numérico | String en Sierra Kernel |
---|---|---|
Negro | 0 | COLOR_BLACK |
Azul | 1 | COLOR_BLUE |
Verde | 2 | COLOR_GREEN |
Cyan | 3 | COLOR_CYAN |
Rojo | 4 | COLOR_RED |
Magenta | 5 | COLOR_MAGENTA |
Café | 6 | COLOR_BROWN |
Gris Claro | 7 | COLOR_LIGHT_GREY |
Gris Oscuro | 8 | COLOR_DARK_GREY |
Azul Claro | 9 | COLOR_LIGHT_BLUE |
Verde Claro | 10 | COLOR_LIGHT_GREEN |
Cyan Claro | 11 | COLOR_LIGHT_CYAN |
Rojo Claro | 12 | COLOR_LIGHT_RED |
Magenta Claro | 13 | COLOR_LIGHT_MAGENTA |
Café Claro | 14 | COLOR_LIGHT_BROWN |
Blanco | 15 | COLOR_WHITE |
En el apartado pasado explique las funciones outb, inb e inw. El funcionamiento de estas funciones es posible porque el controlador VGA tiene puertos en el i/o bus principal, y estos son los puertos que usamos para mandar bytes a otros puertos.
Ahora que ya hemos explicado la teoría, podemos pasar a explicar como funciona el driver en si. El driver está dividido en varios archivos diferentes. En el primer archivo vga.c, la primer función que es definida es move_cursor(). Esta función es la que el kernel usa para posicionar y mover el cursor en la pantalla. Lo primero que se determina para hacer esto es la coordenada xy de inicio. Esto se hace con la siguiente ecuación:
cursor_y * 80 + cursor_x; // EL cursor_x puede posicionarse en 80 caracteres a lo largo
Ya que hicimos esto estamos listos para mandar el cursor. Este debe de ser mandado en dos bytes diferentes; para hacer esto primero mandamos el comando 14 al puerto 0xD4 (el registro de control) para avisar que vamos a mandar la primera byte del cursor (high cursor byte), y luego la mandamos al puerto 0x3D5 (el puerto de datos) el byte. Luego repetimos el mismo proceso con el otro byte (low cursor byte) mandando el comando 15 al puerto 0xD4. La siguiente función que es definida en este archivo es scroll() la cual es usada para hacer espacio en la pantalla cuando esta se llena, de la misma manera que una terminal. Lo primero que se hace es definir cómo va a ser el espacio. Para hacer esto primero se definen los colores que se van a usar en el attributeByte la cual esta compuesta de los colores de fondo y de texto. Luego ya definimos que el espacio (con el código 0x20 en ASCII) debe de usar esos atributos. Luego de eso hacemos una condicional usando if, la cual hace que cada vez que la posición de cursor_y sea mayor o igual a 25, primero se va a mover la primera linea de texto en la pantalla hacia atras. Ahora la ultima línea debería de estar vacía, para asegurarnos la llenamos con 80 espacios. Finalmente posicionamos cursor_y en la línea 24. En el archivo clear_screen.c _podemos encontrar la función _clear_screen(), la cual se usa para limpiar la pantalla al 100%. En realidad esto es muy fácil de hacer, simplemente llenamos la pantalla de espacios. Esto lo hacemos por medio de una ecuación que cuando determina que la pantalla no está llena, añade una línea de espacios.
i = 0; i < 80*25; i++;
Finalmente regresamos el cursor al principio de la pantalla:
cursor_x = 0;
cursor_y = 0;
move_cursor();
El siguiente archivo que vamos a ver, kputc.c, en mi opinion tiene una de las funciones más importantes de todo el driver: kputc(). Esta función es la que se usa para imprimir un carácter a la pantalla y es la base de todas las otras funciones de impresión a la pantalla. Como en todos los demás archivos, lo primero que hacemos es declarar los colores que vamos a usar de fondo y de texto, con los cuales luego formamos el attributeByte y la mandamos al tablero VGA.
u8int backColor = COLOR_BLACK; // Definimos que el color de fondo es negro.
u8int foreColor = COLOR_WHITE; // Definimos que el color del texto es blanco.
u8int attributeByte = (backColour << 4) | (foreColour & 0x0F); // Formamos el attribute byte.
u16int attribute = attributeByte << 8; // Definimos el attribute byte como los primeros 8 bits que vamos a mandar al tablero VGA.
Me voy a detener un poco a explicar como funciona el attributeByte. Como había dicho antes, en el estándar ASCII, cada carácter sólo ocupa 8 bytes; pero sin embargo, el framebuffer recibe 16 bits. Esto nos deja con 8 bytes de sobra, los cuales usamos para establecer los colores de fondo y de texto usando el attributeByte. Para hacer esto primero definimos los colores de fondo y de texto y ya que hacemos esto, le decimos a la máquina que el valor de attributeByte es igual a la suma del color de fondo (el cual ocupa los primeros 4 bits) y el color de texto (ocupa los últimos 4 bits). Finalmente establecemos al attributeByte como los primeros 8 bits de los 16 que se van a mandar. Ahora vamos a definir como se deben de manejar ciertas acciones que son comunes al trabajar con texto. La manera de definirlas es la siguiente:
if (c == /*ASCII code */) {
// Acción a realizar.
}
Se usa una condicional para que cada vez que el siguiente carácter coincida con cierta condicional, se realice una acción. Entonces si queremos definir un backspace para borrar un carácter, el código es el siguiente:
if (c == 0x08 /* el código ASCII de un backspace es este */ && cursor_x) {
cursor_x--; // Se le resta una unidad al valor de la posición de cursor_x
}
// La expresión en pseudocódigo es la siguiente: Si el siguiente carácter que se va a poner en la
// pantalla usa el ASCII code 0x08, hay que restarle una unidad al valor de cursor_x.
Las otras acciones definidas son las siguientes:
- tab : Se le sube el valor a la variable cursor_x pero solo hasta un punto donde el valor sea divisible entre 8.
- retorno de carrete (carriage return): El valor de cursor_x se reinicia (es igual a 0).
- añadir una nueva línea: El valor de cursor_x se reinicia y se le incrementa una unidad a cursor_y.
- imprimir cualquier otro carácter: Se establece que el valor de la variable 'location' es la posición del cursor. Finalmente decimos que el valor de la variable 'c' es el valor de la variable 'location' y se define que 'c' conforma los primeros 8 bits de attribute.
else if(c >= ' ') {
location = video_memory + (cursor_y*80 + cursor_x);
*location = c | attribute;
cursor_x++;
}
Finalmente tenemos la función kprintf() en el archivo kprintf.c, la cual se usa para imprimir un string completo a la pantalla.