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:
- Boot Sector
- Interrupciones
- Memoria
- Pila
- IF-ELSE
- Funciones
- Segmentación de memoria
- Lectura de datos desde disco
- Entrando a modo protegido 32bits
- Compilación, linkado, gestión de la pila y variables en C
- Punteros
- Kernel
- EntryPoint
- Gmake
- Gmake wildcards
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:
/**
* 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.
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:
#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
:
Como podemos ver el cursor se ha movido a la posición (2,5).