01 junio 2005

Interrupciones. Un ejemplo práctico

En el anterior artículo vimos qué son las interrupciones y la necesidad de tener un gestor de interrupciones que detecte cuál se ha producido y ceda el control a la función que se vaya a encargar de su manejo.

Lo primero que debemos hacer es codificar ese gestor de interrupciones. Debe ser algo sumamente rápido, así que, anticipando acontecimientos, he tomado el siguiente código ensamblador del tutorial de Drunken Coders. De momento no nos preocuparemos en entenderlo. Simplemente responde al esquema que habíamos planteado y funciona. Más adelante quizás ahondemos en conceptos de ensamblador.

El ensamblador no va a ser el único concepto que vamos a avanzar en este ejemplo y que retomaremos más adelante para estudiarlo en profundidad. Por un lado, para entender el funcionamiento de las interrupciones vamos a implementar un contador, que imprimiremos en pantalla y que se incrementará en una unidad cada segundo. Para ello, haremos uso de los temporizadores con que nos provee el hardware de la Gameboy Advance.

Por otro lado, este proyecto ya consta de más de un archivo de código fuente (en este caso, uno en C y otro en ensamblador). De momento efectuaremos una compilación ad hoc. Anticipo que en breve estudiaremos el uso de la herramienta make, que nos permitirá gestionar proyectos con múltiples archivos fuente de forma sencilla.

Pues bien, ya tenemos claro lo que vamos a hacer. En primer lugar, haremos uso de nuestro sencillo-pero-efectivo juego de caracteres numéricos para imprimir en pantalla el contador, así como la función PintaCaracter() y la función ActualizaContadorPantalla(), adaptada del marcador del gbasnake y que nos permite imprimir un número en pantalla. Por otro lado, la lógica del programa es bien sencilla.
  1. Habilitar el modo 3 en la pantalla de la Gameboy Advance.
  2. Habilitar uno de los temporizadores, configurándolo para que se genere una interrupción cada segundo.
  3. Definir la función que gestionará las interrupciones generadas por el temporizador.
  4. Habilitar las interrupciones.
El paso 1 ya es conocido del ejemplo del gbasnake. Vamos con el resto de pasos.

2. Vamos a usar el temporizador 0. Primero escribiremos el valor inicial del temporizador en su registro de datos correspondiente:

REG_TM0D = 0xC000;

En el registro de control del temporizador, REG_T0CNT, tenemos que activar los bits 0 y 1 (para usar un conteo cada 1024 ciclos, lo que nos da una frecuencia de conteo de 16384Hz), el bit 6 (para que se genere una interrupción en cuando se produzca desbordamiento) y el bit 7 (que activa el temporizador).

¿Qué conseguimos con esto? Que cada 16384 (0x4000) ciclos el contador produzca desbordamiento y, por lo tanto, genere una interrupción. Como la frecuencia a la que trabajamos es precisamente de 16384Hz, el temporizador 0 generará una interrupción cada segundo. Ya tenemos la primera parte del problema resuelta.

3. Ahora tenemos que declarar e implementar una función que maneje dicha interrupción. Lo primero que haremos será definir un tipo de datos que represente a dicha función:

typedef void (*fp)(void);

Declaramos la tabla de vectores de interrupción (con ese nombre, que es la etiqueta que maneja el gestor de interrupciones que previamente hemos presentado en ensamblador):

fp IntrTable[14];

Declaramos la función que gestiona las interrupciones (que es la que hemos codificado en ensamblador). El nombre es importante, si no luego no podremos enlazar el código objeto y la generación del binario fallará:

extern void IrqHandler(void);

Por último nos construiremos, por comodidad, dos funciones. La primera nos servirá para habilitar las interrupciones y asignar los manejadores correspondientes. La segunda, nos servirá para deshabilitar las interrupciones.

void IRQ_Set(int irq, fp irq_handle)
{
int i;

for(i = 0; i < 14; i++)
{
if(irq & BIT(i))
IntrTable[i] = irq_handle;
}

REG_IRQ_HANDLER = IrqHandler;
REG_IME = 0;
REG_IE |= irq;
REG_IME = 1;
}

void IRQ_Disable(int irqs)
{
REG_IME = 0;
REG_IE &= ~irqs;
REG_IME = 1;
}

4. Activar las interrupciones es tan sencillo ahora como:

IRQ_Set(IRQ_TIMER_0,GestionaTimer);


Y ya está. Terminamos el código con un bucle infinito en el que no se haga nada, y veremos en efecto como aparece un contador en la esquina inferior derecha de la pantalla que se va incrementando cada segundo.

Código fuente.
ROM.


Gestor de interrupciones (irq_handler.asm).


@ arm-elf-as -mthumb-interwork -o irq_handler.o irq_handler.asm


.SECTION .iwram,"ax",%progbits

.EXTERN IntrTable
.GLOBAL IrqHandler
.ALIGN
.ARM



IrqHandler:
@ Multiple interrupts support
mov r2, #0x4000000 @ REG_BASE
ldr r3, [r2,#0x200]! @ r2 = IE : r3 = IF|IE
ldrh r1, [r2, #0x8] @ r1 = IME
mrs r0, spsr
stmfd sp!, {r0-r2,lr} @ {spsr, IME, REG_IE, lr} // IF|IE

mov r0, #1 @ IME = 1 (To permit multiple interrupts if
@ an interrupt occurs)
strh r0, [r2, #0x8]
and r1, r3, r3, lsr #16 @ r1 = IE & IF
ldr r12, =IntrTable

ands r0, r1, #1 @ V-blank interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #2 @ H-blank interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #4 @ V-counter interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #8 @ Timer 0 interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x10 @ Timer 1 interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x20 @ Timer 2 interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x40 @ Timer 3 interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x80 @ Serial Communication Interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x100 @ DMA 0 interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x200 @ DMA 1 interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x400 @ DMA 2 interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x800 @ DMA 3 interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x1000 @ Key interrupt
bne jump_intr
add r12,r12, #4
ands r0, r1, #0x2000 @ Cart interrupt


jump_intr:
strh r0, [r2, #2] @ Clear IF


mrs r3, cpsr
bic r3, r3, #0xdf @ \__
orr r3, r3, #0x1f @ / --> Enable IRQ & FIQ. Set CPU mode to System.
msr cpsr, r3

ldr r0, [r12]

stmfd sp!, {lr}
adr lr, IntrRet
bx r0

IntrRet:
ldmfd sp!, {lr}

mrs r3, cpsr
bic r3, r3, #0xdf @ \__
orr r3, r3, #0x92 @ / --> Disable IRQ. Enable FIQ. Set CPU mode to IRQ.
msr cpsr, r3

ldmfd sp!, {r0-r2,lr} @ {spsr, IME, REG_IE, lr} //IF|IE
strh r1, [r2, #0x8] @ restore REG_IME
msr spsr, r0 @ restore spsr
bx lr


.ALIGN
.POOL