StellatorOS: Hardware I/O


Históricamente la CPU se comunicaba directamente con los controladores de dispositivos, pero esto implicaba que la CPU tuviese que bajar su velocidad a la del dispositivo mas lento del sistema. Para evitar este problema actualmente la CPU se comunica con el bus controller, este a su vez se comunicará con el bus controller de esa tecnología conformando de esta manera una jerarquía de buses, esta jerarquía es necesaria para no forzar al bus controller superior a trabajar a la velocidad del dispositivo mas lento. En este artículo aprenderemos como comunicarnos con los dispositivos mediante I/O ports.

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

A diferencia de la memoria de video, los registros de los controladores son mapeados a una memoria distinta de la principal, esta memoria especial es llamada I/O address space, cada uno de estos mapeos se le llama port I/O y las instrucciones de lectura/escritura se realizan mediante unas instrucciones especiales.

El ejemplo típico es el de la disquetera 82077AA, para poner a funcionar el motor hay que seguir los siguientes pasos:

  • Almacenamos la dirección donde está mapeado el port que acciona el motor en el registro dx
  • Seteamos el registro al a 00001000
  • Seteamos la dirección de memoria apuntada por dx al valor al

Que en ASM serían:

mov dx , 0x3f2 ; Must use DX to store port address
or al , 00001000b ; Switch on the motor bit
out dx , al ; Update DOR of the device.

En sistemas antiguos donde se utilizaba ISA las direcciones de memoria de los ports eran asignadas de forma estática a cada dispositivo, pero con los buses plug-and-play y PCI la BIOS puede asignar de forma dinámica las direcciones a los dispositivos antes de arrancar el SO. Para realizar tal tarea los dispositivos deben comunicar su configuración a través de un bus que describe el número de I/O ports que necesita, cuanta memoria y un ID de hardware.

Un inconveniente de utilizar ports I/O es que no se pueden configurar desde C, debemos inyectar partes de ASM en nuestro código, además la sintaxis seguida en C es GAS, el orden de los operandos está invertido y los carácteres % deben ser escapados con % dejando un código realmente asqueroso a la vista.

Un ejemplo de lectura sería este:

unsigned char port_byte_in(unsigned short port) {
    unsigned char result ;
    __asm__("in %% dx , %% al" : "=a" (result) : "d" (port));
    return result ;
}

Como parámetro de entrada debemos indicar el port(dirección de memoria) y la función nos devolverá el valor del port en la variable result.

Vamos a programar algunas funciones sobre lectura y escritura de ports que nos resultarán de utilidad en un futuro próximo:

vi drivers/ports.c
/**
 * Read a byte from the specified port
 */
unsigned char port_byte_in(unsigned short port) {
    unsigned char result;
    __asm__("in %%dx, %%al" : "=a" (result) : "d" (port));
    return result;
}

/**
 * Write a byte to the specified port
 */
void port_byte_out(unsigned short port, unsigned char data) {
    __asm__("out %%al, %%dx" : : "a" (data), "d" (port));
}

/**
 * Read a word from the specified port
 */
unsigned short port_word_in(unsigned short port) {
    unsigned short result;
    __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port));
    return result;
}

/**
 * Write a word to the specified port
 */
void port_word_out(unsigned short port, unsigned short data) {
    __asm__("out %%ax, %%dx" : : "a" (data), "d" (port));
}

En el fichero de cabecera definiremos la interface de las funciones.

vi drivers/ports.h
unsigned char port_byte_in(unsigned short port);
void port_byte_out(unsigned short port, unsigned char data);
unsigned short port_word_in(unsigned short port);
void port_word_out(unsigned short port, unsigned short data);

Para probar el acceso a los I/O ports vamos a mover el cursor por la pantalla, pero primero debemos tener algunas ideas claras:

  • En modo texto el monitor tiene 80x25: 2000 carácteres en total
  • El registro 3D5 es el registro de datos con él controlaremos la posición del cursor, se le debe indicar la posición de carácter donde se quiere situar(0-1999), además la posición debe asignarse en dos fases
    • Primero el byte mas significativo: Movemos el número binario 8 posiciones a la derecha, obteniendo los primeros 8bits del número.
    • Luego el menos significativo: Realizamos una máscara bitwise AND con el valor 0xff: 11111111, lo que nos permitirá obtener los últimos 8bits del número.
  • El registro 3D4 es el registro de control con él indicaremos si vamos a escribir el primer byte en el registro 3D5 o el segundo byte, si enviamos el comando 14 vamos a escribir el primer byte si enviamos el comando 15 el segundo

Nuestro kernel quedaría del siguiente modo:

vi kernel/kernel.c
#include "../drivers/ports.h"

void main() {
    int cursor_x = 2;
    int cursor_y = 5;
    int char_pos = cursor_y * 80 + cursor_x;
    port_byte_out(0x3D4,14); // We indicate that we are going to write first byte in 0x3D5
    port_byte_out(0x3D5,char_pos>>8); // Write first byte
    port_byte_out(0x3D4,15); // We indicate that we are going to write second byte in 0x3D5
    port_byte_out(0x3D5,(char_pos & 0xff)); // Write second byte
}

Recompilamos el kernel mediante make tal como explicamos en artículos anteriores:

gmake run

Como podemos ver el cursor se ha movido a la posición (2,5).

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