25 septiembre 2005

Sprites. Un ejemplo práctico

En la última entrega vimos un poco por encima los recursos que tiene la Gameboy Advance para manejar sprites. No obstante, la mejor forma de aprender suele ser practicando, así que vamos a poner en marcha un pequeño ejemplo en el que se dé uso a los conceptos vistos.

Vamos a hacer un programa que sea capaz de mover por pantalla un par de sprites. Los que he usado en mi ejemplo son Head y Heels, los dos protagonistas de un clásico de los 8 bits programado por Jon Ritman: Head Over Heels, y versionados por todo un crack del dibujo que es Matriax. Son dos gráficos de 32x32 pixels. Al importarlos a nuestro código sólo hemos tenido que tener la precaución de hacer que compartan la misma paleta de 256 colores.

Mediante la cruceta moveremos a uno de ellos. Si mantenemos pulsado el botón R moveremos al otro, mientras que si mantenemos pulsado L moveremos a ambos. Por último, el botón START intercambia la prioridad de los sprites, esto es, cuál se debe pintar encima del otro.

Por último, en la parte inferior mostraremos las coordenadas de Heels y Head.

El programa es muy sencillo. No obstante, vamos a comentarlo por partes.

Para gestionar los sprites podemos usar una estructura como la siguiente:

typedef struct
{
int x,y;
OAMEntry* oam;
int gfxID;
}Sprite;

Guardamos las coordenadas x e y, la entrada en la tabla OAM y un identificador del gráfico.

En la función main() simplemente inicializamos ambos sprites y, a continuación, definimos el modo de vídeo, activamos el fondo, los sprites e indicamos que el mapeado de los mismos lo haremos en modo 1D.

Borramos el fondo, activamos los sprites y entramos en un bucle sin fin:
  • Leemos los botones
  • Según las pulsaciones, moveremos los sprites convenientemente
  • Esperamos al retrazado vertical y, justo después, copiamos la información desde el buffer a la OAM (evitando así parpadeos).
El resto de funciones están comentadas en el código fuente. No obstante, si tenéis alguna duda no dudéis en consultarme.

Recordad que si vuestra instalación del compilador difiere de la mía deberéis editar el Makefile para que la compilación se efectúe correctamente.


Código fuente.
ROM.

19 septiembre 2005

Sprites. Introducción

Una de las ventajas que tiene programar para la Gameboy Advance es que incorpora control de sprites por hardware. Este hecho simplificará sobremanera nuestras rutinas, ya que únicamente tenemos que indicarle a la consola cómo dibujar nuestro sprite y cuáles son sus coordenadas, y ella se encargará de todo. Además, también nos permite aplicar transformaciones afines sobre los mismos, pero eso lo veremos más adelante.

Para empezar vamos a ver cómo se almacenan los sprites en la memoria y cómo podemos manejarlos.

Los sprites en la GBA pueden ser de tamaño 8x8 hasta 64x64 pixeles, y se pueden manejar hasta 128 de ellos. Cada sprite estará formado por bloques o tiles de 8x8. Los datos de los 128 sprites se almacenan en una zona de memoria llamada OAM (Object Attribute Memory o Memoria de Atributos de Objetos). La OAM consta de 1KB, donde se encuadran las 128 entradas de atributos de los sprites intercaladas con las 32 entradas disponibles para transformaciones afines.

En lenguaje C podríamos definir ambas estructuras de la siguiente forma:

typedef struct tipo_OAM_ENTRY
{
u16 attr0;
u16 attr1;
u16 attr2;
s16 fill;
} OAM_ENTRY;

typedef struct tipo_OAM_AFF_ENTRY
{
u16 fill0[3];
s16 pa;
u16 fill1[3];
s16 pb;
u16 fill2[3];
s16 pc;
u16 fill3[3];
s16 pd;
} OAM_AFF_ENTRY;

de forma que se entrelazarían de la siguiente manera:

mem (u16)03478BCF
OAM_ENTRY 0 1 2 0 1 2 0 1 2 0 1 2
OAM_AFF_ENTRY pa pb pc pd

Cada entrada de la OAM consta de 3 atributos de 16 bits, en los que se almacena la siguiente información:

ATRIBUTO 0

bits 0..7: Coordenada Y de la esquina superior izquierda del sprite.
bits 8..9: usados para configurar las transformaciones afines u ocultar el sprite.

00 - dibujado normal
01 - el sprite se dibuja según la transformación afin descrita por el atributo 1, bits 9..13
10 - desactiva el dibujado (oculta el sprite)
11 - transformación afín usando el doble del área de dibujado (lo veremos más adelante)

bits 10..11: modo gráfico (efectos especiales)

00 - dibujado normal
01 - habilita el canal alpha
10 - el sprite hace de máscara
11 - prohibido

bit 12: habilita/deshabilita el efecto mosaico
bit 13: modo de color, 16 colores por pixel si vale 0 y 256 si vale 1
bits 14..15: forma del sprite, junto con el tamaño del sprite (atributo 1, bits 14..15) conforma el tamaño real del mismo

ATRIBUTO 1

bits 0..8: coordenada X de la esquina superior izquierda del sprite.
bits 9..13: índice de la transformación afín, sólo se usa si el bit 8 del atributo 0 vale 1
bit 12: volteado horizontal, sólo si el bit 8 del atributo 0 vale 0
bit 13: volteado vertical, sólo si el bit 8 del atributo 0 vale 1
bits 14..15: tamaño, junto con la forma indicada en el atributo 0 conforma el tamaño real del sprite, según la siguiente tabla:

forma\tamaño 00 01 10 11
00 8x 8 16x16 32x32 64x64
0116x 8 32x 8 32x16 64x32
108 x16 8x32 16x32 32x64
Tamaño del sprite.

ATRIBUTO 2

bits 0..9: índice del primer bloque de los que componen el sprite. Si estamos trabajando en modo bitmap, el índice debe valer 512 o más
bits 10..11: indican la prioridad, cuanto más alta, se dibuja primero (y quedará por debajo si luego se dibujan más sprites). Un sprite se dibuja por encima de un fondo de la misma prioridad y, por otro lado, para dos sprites de la misma prioridad, se dibuja primero aquél que tenga la entrada en la tabla OAM con el índice más alto.
bits 12..15: subpaleta que se usará para el sprite cuando estemos trabajando en el modo de 16 colores, sólo si el bit 12 del atributo 0 vale 1

No hay un cuarto atributo, pero para completar el alineamiento en memoria a una doble palabra de 32 bits se suele declarar un relleno (que se usa en las tablas de transformaciones afines, que estudiaremos más adelante).


DOBLE BÚFER

Cuando trabajemos con sprites, se suele usar con la OAM una técnica de doble búfer. Esto es, trabajamos sobre una copia de la OAM y cuando se produzca el retrazado vertical en el redibujado de la pantalla aprovechamos para volcar la copia en la OAM real. De esta forma evitaremos modificar los valores de los sprites justo mientras están siendo dibujados por el hardware de la Gameboy Advance.

En la próxima entrega veremos un ejemplo práctico. Vamos a crear dos sprites de 256 colores (por tanto, compartirán paleta) y podremos moverlos con el keypad y ver cómo se superpone el uno sobre el otro.

21 julio 2005

(Aviso, no me he ido)

Ni me he ido ni he abandonado la programación de la GBA.

Simplemente, se han juntado 2 semanas de vacaciones con una vuelta al trabajo muy ajetreada y, la verdad, llego a casa con poco tiempo y ganas de seguir investigando. Pero tengo claros cuáles serán los próximos pasos que seguiré:
  1. Investigar el tema de sprites. Ya estuve mirando algo por encima antes de marchar de vacaciones. Seguramente que le dedique varios artículos, porque el tema es bastante extenso.
  2. Instalarme un buen entorno de desarrollo, haciendo uso de la herramienta Make y de algún software de control de versiones como Subversion.
Igual permuto el orden, pero acometeré esas dos etapas próximamente (espero que no tardando demasiado).

Para los que vayan de vacaciones, buen viaje y a disfrutar.

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

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.

27 mayo 2005

Inciso: Instalación del entorno de desarrollo

Manuel, un amable lector, me ha comentado por correo electrónico que no era capaz de instalar el compilador en su Linux. Quizás di algunas cosas por sentadas al comentar las herramientas que íbamos a utilizar, así que voy a detallar todo el proceso de descarga, instalación y compilación del gbasnake.

No voy a realizar una exposición muy detallada de momento del uso de herramientas auxiliares como Make, que ayudan en la compilación de proyectos complejos, ya que cada cual se monta el entorno de desarrollo un poco a su medida y según sus preferencias. Quizás más adelante aborde ese tema. Tras este inciso me gustaría seguir profundizando en el conocimiento del hardware de la consola y cómo manejarlo.

Entorno Windows

Entramos en la página devkit.tk y clicamos en la sección "Downloads". Aquí encontraremos un enlace al DevkitPro en Sourceforge. Descargamos el devkitARM en su versión para Windows. Una vez lo tengamos en nuestra máquina, procederemos a instalarlo. Para ello, basta con hacer doble clic en el ejecutable que acabamos de descargar. Nos solicitará un directorio donde descomprimirse (por ejemplo, c:\devkitarm\) y copiará todos los ficheros necesarios bajo ese directorio.

En la página devkit.tk, nos recomiendan un árbol de instalación, que sería algo parecido a:
  • c:\devkitPro
    • devkitARM
    • devkitPPC
    • msys
    • libgba
    • libmirko
    • libnds
    • proyectos
      • gba
He marcado en negrita lo que necesitamos mínimamente pare empezar. Msys es recomendable, pero no imprescindible. Msys es un sistema mínimo GNU para Windows, lo más parecido a trabajar con la shell de UNIX pero en el sistema de Microsoft.

Tan solo queda poner el directorio c:\devkitPro\devkitARM\bin\ en el PATH (para que el sistema pueda encontrar el compilador y ejecutarlo). Para compilar programas y generar los ficheros de ROMs (.gba) uso el siguiente script, que es el que usan en Drunken Coders en todos sus ejemplos. Pongo tanto la versión para consola de Windows como la "traducción" que he hecho a lenguaje de script.

CONSOLA WINDOWS

@echo off
REM just add .c file and .o file to the next two lines
set CFILES= gbasnake.c
set OFILES= gbasnake.o

set GAME= gbasnake
set SPECS=gba_mb.specs

set DEVDIR=C:\devkitPro\devkitARM
set DEVKITPRO=C:\devkitPro
set DEVKITARM=C:\devkitPro\devkitARM

PATH=%DEVDIR%\bin;%path%

REM set our paths so the linker knows were to look for the libs
set LIBDIR= %DEVDIR%\arm-elf\lib\interwork
set LIBDIR2= %DEVDIR%\libgba\lib

REM set our include directories so the compiler can find our include files
set INCDIR= %DEVDIR%\arm-elf\include
set INCDIR2= ..\..\..\include

REM the compiler and linker flags
set CFLAGS= -I. -I%INCDIR% -I%INCDIR2% -c -g -O2 -Wall -mcpu=arm7tdmi -mtune=arm7tdmi -fomit-frame-pointer -ffast-math -mthumb -mthumb-interwork

set LDFLAGS= -mthumb -mthumb-interwork -specs=%SPECS% -L%LIBDIR% -L%LIBDIR2%

REM Compile the cfiles
arm-elf-gcc %CFILES% %CFLAGS%

REM link the o files
arm-elf-gcc -o %GAME%.elf %OFILES% %LDFLAGS%

REM gcc produces .elf exicutables...objcopy to .gba
arm-elf-objcopy -v -O binary %game%.elf %game%.gba

gbafix %game%.gba

REM remove all the ofiles
del %OFILES%
del %game%.elf

pause


SHELL SCRIPT

#!/bin/sh

GCC="arm-elf-gcc"

DEVDIR="/c/devkitPro/devkitARM"
DEVIKITPRO=/c/devkitPro/
DEVKITARM=$DEVDIR

PATH=$PATH:$DEVDIR/bin

CFILES=gbasnake
OFILES=gbasnake

GAME=gbasnake
SPECS="$DEVDIR/arm-elf/lib/gba_mb.specs"

LIBDIR=$DEVDIR/arm-elf/lib/interwork
LIBDIR2=$DEVDIR/lib

INCDIR=$DEVDIR/arm-elf/include
INCDIR2=$DEVDIR/include

CFLAGS="-I. -I$INCDIR -I$INCDIR2 -c -g -O2 -Wall -mcpu=arm7tdmi -mtune=arm7tdmi -fomit-frame-pointer -ffast-math -mthumb -mthumb-interwork "

LDFLAGS="-mthumb -mthumb-interwork -specs=$SPECS -L$LIBDIR -L$LIBDIR2"

# Compilar el fuente
$GCC $CFILES $CFLAGS

# Enlazar el objeto
$GCC -o $GAME.elf $OFILES $LDFLAGS

# Generar el .gba
arm-elf-objcopy -v -O binary $GAME.elf $GAME.gba

gbafix $GAME.gba

# Limpiar los archivos intermedios
rm $OFILES
rm $GAME.elf


Obviamente, el resultado de ambas soluciones es el mismo, ya que el compilador es el mismo, sólo cambiaría el entorno de desarrollo en el que estamos trabajando.

Ahora ya podemos descargar los fuentes completos de GBASnake (incluyendo algunos archivos de cabecera extra) y probar que nuestra instalación del compilador funciona.

Entorno Linux

El procedimiento es exacto al descrito anteriormente (salvo la parte de consola de Windows, que no es aplicable en este caso).

Entramos en la página devkit.tk y clicamos en la sección "Downloads". Aquí encontraremos un enlace al DevkitPro en Sourceforge. Descargamos el devkitARM en su versión para Linux. Una vez lo tengamos en nuestra máquina, procederemos a instalarlo. Para ello, basta descomprimir el fichero que nos hemos bajado. En mi caso ha sido algo como:

p2:/c/devkitPro# wget http://kent.dl.sourceforge.net/sourceforge/devkitpro/devkitARM_r12-linux.tar.bz2

p2:/c/devkitPro# tar xjvf devkitARM_r12-linux.tar.bz2


He respetado la misma estructura de directorios que se usa en Windows con Msys

Nos solicitará un directorio donde descomprimirse (por ejemplo, c:\devkitarm\) y copiará todos los ficheros necesarios bajo ese directorio.
  • /c/devkitPro
    • devkitARM
    • devkitPPC
    • msys
    • libgba
    • libmirko
    • libnds
    • proyectos
      • gba
Lo he hecho por comodidad, para no tener que escribir el script de compilación dos veces. En cualquier caso, las rutas de instalación son totalmente modificables. Probamos el script y vemos que todo funciona correctamente.

No olvidéis añadir la ruta de los binarios del compilador al PATH, en nuestro caso:

export PATH=PATH:/c/devkitPro/devkitARM/bin

NOTA IMPORTANTE:

La versión actual del compilador para Linux tiene un BUG (que explican en este enlace y que me ha hecho volverme loco durante un par de noches). Básicamente, hay que parchear el fichero arm-elf/lib/gba_crt0.s añadiendo la línea que falta en la función SkipEWRAMClear (la marco en negrita)

@---------------------------------------------------------------------------------
SkipEWRAMClear: @ Clear Internal WRAM to 0x00
@---------------------------------------------------------------------------------
mov r0, #3
lsl r0, #24 @ r0 = 0x3000000
ldr r1, =__iwram_end
sub r1, r0
bl ClearMem


y recompilando esa parte con:

make CRT=gba

¡¡¡Felices compilaciones!!!

18 mayo 2005

Leyendo los botones

Cuando hicimos el port del juego Snake, ya tuvimos que implementar la lectura de los botones para poder manejar la serpiente y reiniciar la partida. Ahora vamos a entrar un poco más en profundidad en este tema.

La Gameboy Advance posee 10 botones, como ya se ha comentado. El manejo de estas entradas se hace mediante dos registros de Entrada/Salida, que son los siguientes:

REG_KEYS (0x0400:0130)

Cada bit del 0 al 9 hace referencia a cada uno de los botones:

REG_KEYS[0] -> A
REG_KEYS[1] -> B
REG_KEYS[2] -> Select
REG_KEYS[3] -> Start
REG_KEYS[4] -> Derecha
REG_KEYS[5] -> Izquierda
REG_KEYS[6] -> Arriba
REG_KEYS[7] -> Abajo
REG_KEYS[8] -> R
REG_KEYS[9] -> L

El valor de cada bit es 1 si el botón no está pulsado y 0 si lo está, de forma que REG_KEYS = 0x03FF en estado de "reposo".

REG_KEYCNT (0x0400:0132)

Este registro se usa para controlar las "interrupciones del teclado". Tiene la misma configuración que REG_KEYS pero añadiendo dos bits:

REG_KEYCNT[0] -> A
REG_KEYCNT[1] -> B
REG_KEYCNT[2] -> Select
REG_KEYCNT[3] -> Start
REG_KEYCNT[4] -> Derecha
REG_KEYCNT[5] -> Izquierda
REG_KEYCNT[6] -> Arriba
REG_KEYCNT[7] -> Abajo
REG_KEYCNT[8] -> R
REG_KEYCNT[9] -> L
REG_KEYCNT[E] -> I
REG_KEYCNT[F] -> Op

I activa o desactiva las interrupciones del teclado. Op determina la forma de calcular si lanzar la interrupción o no. Si vale 0, se aplicará una operación OR a los botones indicados, mientras que si vale 1, aplicará una operación AND.

Ejemplo: si cargamos en este registro el valor binario 1100000000001111 (0xC00F), conseguiremos que se genere una interrupción cuando los botones A, B, Select y Start se presionen simultáneamente.

Bien, ya tenemos todo para leer las pulsaciones de los botones, pero nos falta algo muy importante. Hasta ahora sabemos cuándo un botón está pulsado o no. Realmente esta no es la información que necesitamos, sino que lo lógico es saber si un botón ha cambiado de estado. ¿Por qué? Imaginemos el botón Start con su función típica de activar la pausa del juego. Leemos el estado de los botones y detectamos que Start fue pulsado. Entramos en el estado "pausa", del que saldremos cuando detectemos nuevamente que Start está pulsado. El problema es que el microprocesador de la Gameboy Advance ejecuta código mucho más rápido de lo que imaginamos. Por tanto, una pequeña pulsación de Start por nuestra parte puede implicar muchas pausas/continuaciones. De hecho, podemos considerar que el estado en el que quedará el juego tras soltar el botón es totalmente aleatorio.

Resumiendo, lo que nos interesan son los cambios de estado de los botones. Y podemos conseguir esa información mediante estas funciones:
void LeerBotones()
{ bot_ant= bot_act; bot_act= REG_KEYS; }

u16 BotonCambio(u16 boton)
{ return (bot_act^bot_ant) & boton; }

u16 BotonMantiene(u16 boton)
{ return ~(bot_act|bot_ant) & boton; }

u16 BotonPulsado(u16 boton)
{ return (~bot_act&bot_ant) & boton; }

u16 BotonSoltado(u16 key)
{ return (bot_act&~bot_ant) & boton; }

Lo que haremos será llamar a LeerBotones() cada ciclo de nuestro programa, y de esa forma podremos saber si un botón en concreto ha cambiado de estado.

Por ejemplo, BotonPulsado(KEY_START) devolverá 1 si se pulsó el botón Start, y 0 en caso contrario. Este valor lo devolverá una única vez. Hasta que no soltemos y volvamos a pulsar el botón, la función no volverá a devolver 1.

Con estos conocimientos podemos hacer prácticamente todo en cuanto a lectura de botones. Hemos aprovechado para añadir a nuestra serpiente el clásico botón de pausa (Start).

Código fuente.
ROM.

Un acercamiento superficial al hardware

Como todos sabréis, Gameboy Advance es un sistema portátil de videojuegos creado por Nintendo. Las características más importantes de su hardware son:
  • Procesador ARM de 32 bits, corriendo a 16,78 MHz.
  • Pantalla LCD de 240x160 pixels, capaz de visualizar 32768 colores (15 bits).
  • Memoria dividida en diferentes zonas. La Entrada/Salida está mapeada en memoria.
  • 10 botones (cruceta de 4 direcciones, A, B, L, R, Select y Start).
  • Sonido estéreo, con 6 canales disponibles, 4 de ellos ya presentes en la Gameboy original y dos conversores Digital-Analógico.
La CPU puede ejecutar dos juegos de instrucciones: el ARM, que es un juego de instrucciones de 32 bits, y el THUMB, que usa instrucciones de 16 bits. THUMB es un subconjunto de instrucciones del juego ARM. El procesador cuenta además con 16 registros de 32 bits, si bien los tres últimos no son de propósito general (r13 = SP, r14 = LR y r15 = PC). Además, en modo THUMB sólo están disponibles los 8 primeros.

Cuando trabajemos en zonas de memoria con tamaño de palabra de 32 bits, usaremos código ARM, mientras que preferiremos usar THUMB para las zonas con palabra de 16 bits.

La memoria está dividida en diferentes áreas:

ROM del sistema (0x0000:0000 - 0x0000:3FFF [16KB] palabras de 32 bits).
Es la BIOS del sistema. No se puede leer su contenido, sólo ejecutarse.

EWRAM (0x0200:0000 - 0x0203:FFFF [256KB] palabras de 16 bits).
RAM externa de trabajo (External Work RAM). Disponible para código y datos. En caso de usar un cable multiboot, aquí se almacenaría el código descargado y empezaría la ejecución.

IWRAM (0x0300:0000 - 0x0300:7FFF [32KB] palabras de 32 bits).
RAM interna de trabajo (Internal Work RAM). También disponible para código y datos. Es la memoria más rápida, de hecho se encuentra embebida dentro del procesador.

IORAM (0x0400:0000 - 0x0401:03FF [1KB] palabras de 16 bits).
Zona de Entrada/Salida mapeada en memoria. En esta zona se encuentran mapeados los registros de Entrada/Salida, que nos servirán para prácticamente controlar todo el hardware.

PALRAM (0x0500:0000 - 0x0500:03FF [1KB] palabras de 16 bits).
Zona de almacenamiento de paletas de vídeo. Cada una contiene 256 entradas de 15 bits. La primera de ellas para fondos, la segunda para sprites.

VRAM (0x0600:0000 - 0x0601:7FFF [96KB] palabras de 16 bits).
Memoria de vídeo. La estructura de los datos almacenados dependerá del modo de vídeo en que estemos trabajando.

OAM (0x0700:0000 - 0x0700:03FF [1KB] palabras de 32 bits).
Memoria de atributos de objetos (Object Attribute Memory). En esta zona se controlan los sprites.

PAKROM (0x0800:0000 - variable [variable] palabras de 16 bits).
En esta zona se mapea la ROM de los cartuchos de software. Normalmente será la zona donde comience la ejecución, salvo que hayamos arrancado mediante un cable multiboot.

CartRAM (0x0E00:0000 - variable [variable] palabras de 8 bits).
Esta es la RAM usada en los cartuchos para guardar partidas, datos de configuración, etc. Su longitud es variable. Puede ser de tipo SRAM, FlashROM o EEPROM. En cualquier caso, su comportamiento será el mismo.

La pantalla de la GBA, como hemos comentado antes, consta de 240x160 pixeles. Se refresca cada 280,896 ciclos de CPU, o sea, alrededor de los 59,73 Hz. Soporta 6 modos de funcionamiento, algunos basados en pixeles y otros en bloques (tiles).

La consola da soporte a 128 sprites simultáneos, de hasta 64x64 pixeles.

Como vemos, el hardware de la Gameboy Advance es lo suficientemente complejo como para no intentar abarcarlo todo en una primera aproximación. Iremos profundizando juntos en su conocimiento a medida que vayamos programando y vayamos necesitando afinar más para conseguir aquellos efectos que nos propongamos.



Enlaces: