28 mayo 2005

Interrupciones. Primera aproximación.

No, no se trata de un nuevo inciso en nuestro proceso de aprendizaje. Es más, el tema que vamos a tratar no es sencillo, pero es crucial a la hora de programar. Se trata del manejo de interrupciones.

Una interrupción, como su propio nombre indica, es un evento que provoca que la CPU deje de ejecutar el código que estaba ejecutando y salte a una zona de código predefinida, que hará una tarea determinada (normalmente rápida) y vuelva a donde estaba en el preciso instante en que se produjo la interrupción.

Hay dos tipos de interrupciones: las generadas por la circuitería interna de la consola (interrupciones hardware) y aquéllas generadas por los programas (interrupciones software). Comenzaremos hablando de las primeras.

Para poder manejar las interrupciones, tendremos que acceder a una serie de registros. Son los siguientes:

REG_IME (0x0400:0208) es el registro maestro de interrupciones. Si queremos habilitar las interrupciones, deberemos poner este registro a 1.

REG_IE (0x400:0200) nos permite habilitar o deshabilitar cada uno de los 14 tipos de interrupción, a saber:

REG_IE[0] - INT_VBLANK (Vertical Blank - generada en la zona de retrazado vertical)
REG_IE[1] - INT_HBLANK (Horizontal Blank - generada en la zona de retrazado horizontal)
REG_IE[2] - INT_VCOUNT (Vertical Count - generada al principio de cada scanline)
REG_IE[3] - INT_TM0 (Timer 0 - temporizador 0)
REG_IE[4] - INT_TM1 (Timer 1 - temporizador 1)
REG_IE[5] - INT_TM2 (Timer 2 - temporizador 2)
REG_IE[6] - INT_TM3 (Timer 3 - temporizador 3)
REG_IE[7] - INT_COM (Communications - generada al finalizar una transmisión por el puerto de comunicaciones)
REG_IE[8] - INT_DMA0 (DMA - Acceso directo a memoria, canal 0)
REG_IE[9] - INT_DMA1 (DMA - Acceso directo a memoria, canal 1)
REG_IE[10] - INT_DMA2 (DMA - Acceso directo a memoria, canal 2)
REG_IE[11] - INT_DMA3 (DMA - Acceso directo a memoria, canal 3)
REG_IE[12] - INT_KEYS ( generada cuando alguno de los botones indicados en el registro REG_P1CNT se ha pulsado)
REG_IE[13] - INT_CART (generada cuando se extrae el cartucho de la consola)

Para habilitar un tipo de interrupción deberemos poner a 1 el bit correspondiente. Adicionalmente, algunos tipos necesitan de registros adicionales, como en el caso de INT_KEYS, o INT_VBLANK, INT_HBLANK e INT_VCOUNT, que hacen uso del registro REG_DISPSTAT.

REG_IF (0x400:0202) tiene la misma disposición que REG_IE y su misión es indicarnos qué interrupción se ha generado, poniendo a uno el bit correspondiente. Una vez que hemos tratado la interrupción, debemos escribir un 1 en este mismo registro en el bit que corresponda.

¿Cómo funciona el mecanismo de interrupciones? Una vez habilitadas, se da el siguiente proceso:
  1. Ocurre una interrupción. El procesador pasa a modo ARM (importante) y se guarda información del estado actual en la pila.
  2. La BIOS lee la dirección de memoria contenida en 0x0300:7FFC y el procesador salta a esa dirección.
  3. El código al que apuntaba el valor contenido en 0x0300:7FFC comienza a ejecutarse. Como hemos indicado anteriormente, se trata de código en modo ARM.
  4. Una vez finalizado el tratamiento, debemos indicar que la interrupción ha sido tratada, escribiendo un 1 en el bit correspondiente del registro REG_IF, y volver de la interrupción con la instrucción: bx lr
  5. Se recupera el estado que se almacenó en la pila y continúa la ejecución donde se dejó en el momento de "levantarse" la interrupción.
En principio, los pasos 1, 2 y 5 son automáticos (gestionados por la BIOS). Sólo tenemos que preocuparnos de poner en la dirección 0x0300:7FFC la dirección en la que se almacenará nuestro código gestor de interrupciones. Por comodidad, podemos definir un registro al efecto:
typedef void (*fp)(void);  //un puntero a una función de tipo void que no recibe parámetros
#define REG_IRQ_HANDLER (*(fp*)0x3007FFC)
Y una primera aproximación sería:
void gestor_interrupciones()
{
... tratamos la interrupción ...
}

REG_IRQ_HANDLER = gestor_interrupciones;

REG_IE = INT_CART;
REG_IME = 1;
De esta forma, cada vez que se genere una interrupción de las indicadas (en el ejemplo, la extracción del cartucho), se ejecutará la función 'gestor_interrupciones'.

Como vemos, en la Gameboy Advance no existe una tabla de vectores de interrupción. Por tanto, el módulo gestor de interrupciones debería discriminar qué interrupción se ha producido y llamar a la rutina de tratamiento correspondiente. También aprovecharemos para que, una vez ejecutada la rutina de control, escribiremos un 1 en el bit de REG_IF correspondiente, para dar por tratada la interrupción.

El módulo gestor de interrupciones debe ser extremadamente rápido, como todo código que se ejecute al levantarse una interrupción. Por tanto, lo escribiremos en ensamblador del ARM. Todo ello, y un ejemplo práctico de uso de los temporizadores, en la siguiente entrega.