Tabla de Contenidos
Introducción
NINTENDO ______ ____ / ____/___ _____ ___ ___ / __ )____ __ __ / / __/ __ `/ __ `__ \/ _ \/ __ / __ \/ / / / / /_/ / /_/ / / / / / / __/ /_/ / /_/ / /_/ / \____/\__,_/_/ /_/ /_/\___/_____/\____/\__, / /____/ Tutorial de programación Por David Pello (ladecadence.net) _n_________________ |_|_______________|_| | ,-------------. | | | .---------. | | | | | | | | | | | | | | | | | | | | | | | | | | | | `---------' | | | `---------------' | | _ GAME BOY | | _| |_ ,-. | ||_ O _| ,-. "._,"| | |_| "._," A | | _ _ B | | // // | | // // \\\\\\ | | ` ` \\\\\\ , |________...______,"
Nintendo Gameboy (DMG)
Para empezar este tutorial de desarrollo para la gameboy, vamos a repasar un poco las caracteristicas técnicas de nuestra maravillosa consola:
- CPU: 8-bit Sharp LR35902 (Similar al Z80) a 4.19 MHz
- 8K de RAM principal
- 8K de Video RAM
- 2.6“ LCD 160×144, 4 Tonos
- Sonido estéreo, 4 Canales
- Puerto serie (ext)
- 6V, 0.7A
- 90 mm x 148 x 32 mm
Bien, en el corazón de la GameBoy, tenemos una cpu propia fabricada por Sharp para Nintendo, con el nombre técnico de LR35902, tenemos un microprocesador a caballo entre el 8080 y el Z80, ya que aunque no tiene los juegos extra de registros del Z80 o los índice, si incorpora la mayoría de su juego extendido de instrucciones, como las de manipulación de bits. Además incorpora circuitería extra para el control del LCD, del joypad, del puerto serie y la generación de audio.
Este tutorial no pretende ser una guía de programación completa del z80 o en este caso del gbz80 como se le suele llamar, sino que se centra en el hardware de la GameBoy y como usarlo. Para aprender ensamblador de z80 recomiendo la documentación existente como por ejemplo la lista de instrucciones de la CPU de la GameBoy en http://gbdev.gg8.se/wiki/articles/CPU_Instruction_Set y el curso de ensamblador de z80 de spectrum de https://wiki.speccy.org/cursos/ensamblador/indice . Tened en cuenta que el z80 tiene algunas instrucciones que la CPU de la GameBoy no, pero por lo demás aprender ensamblador de z80 nos viene perfecto para la GameBoy (además de para Spectrum, Amstrad CPC, MSX, Sega Master System…).
GBz80
La CPU, vamos a llamarla gbz80, es una cpu de 8bit, con un bus de direcciones de 16 bit. Esto es, los datos internamente y en la memoria externa, se organizan en bytes, y se pueden direccionar 2^16 = 64KB de memoria externa.
Registros
El gbz80, tiene varios registros internos, en los que podemos almacenar datos mientras operamos con ellos y los movemos a o desde la memoria externa a la CPU. Estos registros de 8 bit, son: a, b, c, d, e, h y l.
Flags
Además está el registro especial 'f', que guarda el estado de los flags del procesador. El procesador al realizar ciertas operaciones, puede activar o desactivar algunos de estos flags, que nos serán muy útiles a la hora de programar. Por ejemplo, un bit dentro de ese registro es el flag Zero, que nos indica si el resultado de la operación anterior, ha sido cero o no. No todas las operaciones modifican los flags, pero muchas de ellas si lo hacen. Para conocer a fondo todas las operaciones que puede realizar el gbz80, podeis echar un vistazo a la sección correspondiente de los Pan Docs: http://gbdev.gg8.se/wiki/articles/CPU_Instruction_Set
Los flags del registro f, son los siguientes:
Bit Nombre (1) (0) Explicación 7 zf Z NZ Flag Zero 6 n - - Flag de Suma/Resta (BCD) 5 h - - Flag de medio acarreo (BCD) 4 cy C NC Flag de Acarreo 3-0 - - - Sin uso (siempre a cero)
Los flags mas usados son:
Flag Zero (Z)
Este bit se pone a 1 si el resultado de una operación ha sido cero (0). Muy usado en saltos condicionales.
Flag de acarreo (C, ó Cy)
Se pone a 1 cuando el resultado de una suma es mayor que $FF (8bit) o $FFFF (16bit), o cuando el resultado de una resta o comparación es menor que cero. Además se pone a uno cuando en una instrucción de rotación o desplazamiento, la operación saca fuera un 1. Usado en saltos condicionales o en instrucciones como ADC, SBC, RL, RLA, etc.
Registros de 16 bit
Algunos de estos registros se pueden combinar, para formar registros de 16 bits, muy útiles para manejar direcciones de memoria o números más grandes si los necesitamos. Los registros de 16 bits son: af, bc, de, y hl.
Además tenemos otros dos registros especiales, el pc, o program counter, de 16 bits, que guarda la dirección de memoria de la siguiente instrucción que se ejecutará, y el sp, ó stack pointer, también de 16 bits, que guarda el puntero de la pila.
Mapa de memoria
La memoria principal de la gameboy, mapeada en un espacio de 16 bit, nos permite direccionar directamente 64K (2^16 = 65536). En este espacio de direcciones, tenemos que direccionar todos los bloques de memoria a los que la gameboy necesita acceder, esto es, la RAM, la ROM del cartucho, la RAM interna del cartucho para los juegos que graban partidas, la memoria de vídeo, etc. Para la ello los diseñadores de la GameBoy mapearon la memoria en diferentes bloques necesarios, como la RAM interna o la memoria de video, dejando dos bloques de 16K para el acceso a la ROM de los juegos, y un bloque de 8K para el acceso a la RAM de los juegos (partidas guardadas). Como muchos juegos empezaron a requerir mas de 32K de ROM o de 8K de RAM de guardado, se empezó a emplear una técnica denominada “Banking”, en la que la ROM del juego se divide en diversos bloques que se puedan independizar (los gráficos o sonidos de cada pantalla por ejemplo), que se van mapeando en el bloque de acceso a la memoria según sean necesarios. En la GameBoy, esto se diseñó de la siguiente manera; tenemos un bloque fijo de 16K(donde programamos la lógica principal del juego), y luego, mediante ciertas instrucciones (dependiendo del chip de mapping que usemos en nuestro cartucho), podemos ir intercambiando bancos de 16K en el otro bloque disponible. Parece complicado, pero hablaremos de banking más adelante. Todo esto queda reflejado en el siguiente mapa de memoria con todos los bloques disponibles en el espacio de direcciones de la GameBoy.
Mapa general de memoria* Registros de escritura de bancos ------------------------- ---------------------------------- Registro de activacion de Interrupciones ---------------------------------- $FFFF RAM Alta Interna ---------------------------------- $FF80 No usable ---------------------------------- $FF4C Puertos de E/S ---------------------------------- $FF00 No usable ---------------------------------- $FEA0 Atributos de sprite (OAM) ---------------------------------- $FE00 Espejo de los 8kB de RAM interna ---------------------------------- $E000 8kB de RAM interna ---------------------------------- $C000 ------------------------------ 8kB banco intercambiable de RAM / Selector MBC1 ROM/RAM ---------------------------------- $A000 / ----------------------------- 8kB Video RAM / / Selector banco RAM ---------------------------------- $8000 --/ / ---------------------------- 16kB banco intercambiable de ROM $6000 ----/ / Selector banco ROM ---------------------------------- $4000 ------/ --------------------------- 16kB Banco #0 de ROM $2000 --------/ Activación de Banco RAM ---------------------------------- $0000 ------------------------------------
* NOTA: b = bit, B = byte
* NOTA: Usando la convención del RGBDS, el ensamblador que usaremos en el tutorial, los números en notación hexadecimal, los escribiré con un “$” delante, en binario con un ”%“ y en decimal sin prefijo.
Organización de una ROM de GameBoy
Las ROMs de GameBoy tienen que guardar cierta estructura para que la GameBoy las acepte, especialmente en el tema de la cabecera. A continuación detallo las partes de una ROM y su función:
ESQUEMA DE UNA IMAGEN ROM DE GAMEBOY ------------------------------------ $0 - $100: Vectores de interrupción. Aunque también se pueden usar estas direcciones para meter código propio $100 - $103: Punto de entrada a la ejecución del programa, normalmente se suele poner un nop, seguido de un salto a nuestro propio punto de inicio. $104 - $14E: Cabecera del cartucho. Contiene el logo de nintendo, (104h-133h) que es comparado con el existente en la rom justo al arranque de la consola, si no coiciden, se detiene la ejecución. Luego tenemos los siguientes datos: $134 - Nombre del cartucho - 15bytes $143 - Soporte de gameboy color $80 = GBColor, $00 u otro = B/N $144 - Codigo de licencia, 2 bytes (no importante) $146 - Soporte de supergameboy (SGB) 00 = GameBoy, 03 = Super GameBoy $147 - Tipo de cartucho (Sólo ROM, MBC1, MBC1+RAM.. etc) 0 - SOLO ROM 12 - ROM+MBC3+RAM 1 - ROM+MBC1 13 - ROM+MBC3+RAM+BATT 2 - ROM+MBC1+RAM 19 - ROM+MBC5 3 - ROM+MBC1+RAM+BATT 1A - ROM+MBC5+RAM 5 - ROM+MBC 1B - ROM+MBC5+RAM+BATT 6 - ROM+MBC2+BATT 1C - ROM+MBC5+RUMBLE 8 - ROM+RAM 1D - ROM+MBC5+RUMBLE+SRAM 9 - ROM+RAM+BATT 1E - ROM+MBC5+RUMBLE+SRAM+BATT B - ROM+MMM01 1F - GBCamera C - ROM+MMM01+SRAM FD - Bandai TAMA5 D - ROM+MMM01+SRAM+BATT FE - Hudson HuC-3 F - ROM+MBC3+TIMER+BATT FF - Hudson HuC-1 10 - ROM+MBC3+TIMER+RAM+BATT 11 - ROM+MBC3 $148 - Tamaño ROM 0 - 256Kbit = 32KByte = 2 bancos 1 - 512Kbit = 64KByte = 4 bancos 2 - 1Mbit = 128KByte = 8 bancos 3 - 2Mbit = 256KByte = 16 bancos 4 - 4Mbit = 512KByte = 32 bancos 5 - 8Mbit = 1MByte = 64 bancos 6 - 16Mbit = 2MByte = 128 bancos 7 - 32Mbit = 4MByte = 256 bancos $52 - 9Mbit = 1.1MByte = 72 bancos $53 - 10Mbit = 1.2MByte = 80 bancos $54 - 12Mbit = 1.5MByte = 96 bancos $149 - Tamaño RAM 0 - Ninguna 1 - 16kBit = 2kB = 1 banco 2 - 64kBit = 8kB = 1 banco 3 - 256kBit = 32kB = 4 bancos 4 - 1MBit = 128kB = 16 bancos $14A - Código de zona 0 - Japonés 1 - No Japonés $14B - Codigo de licencia antiguo, 2 bytes $33 - Buscar el ćodigo en $0144/$0145 (Las funciones de SGB no funcionan si no es $33) $14C - Versión de ROM (Normalmente $00) $14D - Prueba de complemento (importante) $14E - Checksum (no importante) $14F - $3FFF: Nuestro código. Este es el banco 0 de 16K, y es fijo. $4000 - $7FFF: Segundo banco de 16K. En el arranque de la gameboy este se corresponde con el banco 1 (hasta 32K de la rom), pero podría ser intercambiado por bancos superiores de 16K hasta el total de ROM que tengamos disponible usando un mapper como el MBC1.
Sistema de vídeo
El sistema de video de la GameBoy, no es un sistema de acceso directo pixel a pixel, como en un PC, sino que es un sistema de bloques o “tiles”. Consta de un buffer de 256×256 pixeles (32×32 tiles) del que se pueden mostrar 160×144 en cada momento (tamaño real de la pantalla, 20×18 tiles). Tenemos un par de registros SCROLLX y SCROLLY, que nos permiten mover el buffer por el área visualizable. Además el buffer es circular, cuando se hace scroll más allá de un extremo del buffer, empezamos a visualizar los datos del lado opuesto.
^ | v +------------------------------------+ |(scrollx, scrolly) | | +----------------+ | | | | | | | | | | | | | | | 20x18 | | | | área visible | | | | | | | | | | <-> | | | | <-> | +----------------+ | | | | 32x32 | | mapa de fondo | | | | | | | | | | | +------------------------------------+ ^ | v
Los registros que controlan el sistema de vídeo, son muy importantes, ya que nos permiten controlar lo que mostramos por pantalla, y cómo. Los más importantes para empezar son:
$FF40 - LCDC - Control del LCD (R/W)
Este registro de control, nos permite ajustar la pantalla de la GameBoy, con lo que deberemos manejarlo para cualquier operación que involucre mostrar algo en ella.
Cada bit de este registro tiene un siginificado especial, que pasaré a detallar:
Bit 7 - Control del Display (0=Off, 1=On) Bit 6 - Selección del Window Tile Map (0=9800-9BFF, 1=9C00-9FFF) Bit 5 - Control de la Ventana (0=Off, 1=On) Bit 4 - Selección del Tile Data de fondo y ventana (0=8800-97FF, 1=8000-8FFF) Bit 3 - Selección del Tile Map de fondo (0=9800-9BFF, 1=9C00-9FFF) Bit 2 - Tamaño de los OBJ (Sprites) (0=8x8, 1=8x16) Bit 1 - Control de los OBJ (Sprites) (0=Off, 1=On) Bit 0 - Control del fondo (0=Off, 1=On)
Vamos a detallar las funciones de algún bit, porque esto es muy importante:
Bit 7 - Control del display
Activa o desactiva el LCD. El display ha de activarse antes de que podamos mostrar algo en él, y a veces lo querremos desactivar si tenemos que escribir muchos datos en pantalla, como una imagen de prensentación o fin completa, para que de tiempo a meter todos los gráficos en memoria antes de que la pantalla empieze a dibujarlos y veamos cosas raras y gráficos a medias.
ATENCIÓN - El display solo puede activarse y desactivarse cuando estamos en el periodo de intervalo vertical (V-Blank). Activarlo o desactivarlo fuera de este periodo puede dañar el hardware. Mucho cuidado con esto, ya que en los emuladores no pasa nada, pero puedes estropear tu gameboy si lo haces en el hardware real. Asi que para activar o desactivar el LCD, primero espera al intervalo vertical (veremos como hacer esto).
Bit 0 - Control del fondo
Si este bit es cero, el fondo no se dibuja, se queda en blanco (con lo que para dibujar el fondo tenemos que poner este bit a uno, claro). Hemos visto esto en muchos juegos para hacer efectos de parpadeos y cosas asi.
El resto de bits se comportan de manera similar y creo que quedan explicados en la tabla.
$FF41 - Status - status del LCD (R/W)
Este registro es también muy importante, ya que nos permite conocer que está haciendo el LCD en cada momento. Deberemos consultarlo para numerosas operaciones, asi que atentos. Además nos permite activar las interrupciones del LCD, asi que como veis, tenemos mucha funcionalidad en este registro.
Una tabla explicativa y luego lo detallamos:
Bit 5 - Interrupción Modo 2 OAM (1=Activada) (Lectura/Escritura) Bit 4 - Interrupción Modo 1 V-Blank (1=Activada) (Lectura/Escritura) Bit 3 - Interrupción Modo 0 H-Blank (1=Activada) (Lectura/Escritura) Bit 2 - Flag de coincidencia (0:LYC<>LY, 1:LYC=LY) (Sólo Lectura) Bit 1-0 - Flag de Modo (Modo 0-3, ver abajo) (Sólo Lectura) 0: Estamos en H-Blank 1: Estamos en V-Blank 2: Buscando OAM-RAM 3: Transfiriendo datos al LCD
Los bytes del 3 al 5, nos permiten activar las interrupciones del LCD, muy útiles para ciertos procesos que requieran redibujados rápidos de la pantalla, ya que escribiendo nuestro código de dibujado en ellas, estamos seguros de que el dibujado se producirá correctamente (sobretodo en los periodos de intervalo).
El byte 2, sirve para comparar dos registros especiales, el LY ($FF44), que es la coordenada Y donde el LCD está dibujando en este momento, y el registro LCY ($FF45), que podemos definir nosotros.
Los bytes 1 y 0, nos indican el modo en que está el LCD, ya sea en los dos periodos de intervalo, o accediendo a la RAM o escribiendo en el LCD. Como dijimos antes, esto es muy útil para saber si estamos en el periodo de VBLank por ejemplo; sólo tendriamos que ver si en los bits 1-0 de este registro, tenemos “01”, esto en ensamblador de la gameboy es tan sencillo como hacer lo siguiente:
ld a, [$FF41] and 1 cp 1
Esto es, cargamos en el registro a, el valor que tenemos en el registro de status del LCD, y hacemos un AND con 1. Cualquier valor que tenga el registro, al hacerle and con 1, si en las ultimas posiciones tenia 01, el resultado será 01, asi que lo comparamos con 1 (cp 1). Si ahora el resultado de la comparación es 0 (iguales), es que estamos en el periodo de VBlank.
Aunque hay otra manera más rápida de saber si estamos en V-Blank, y es usando el registro LY que comenté antes. Como dije, el registro LY nos indica en que línea horizontal se encuentra “dibujando” el LCD. A partir de la linea 144, el LCD está fuera de pantalla y por lo tanto estamos en el periodo de intervalo vertical. Asi que haciendo esto…
ld a, [$FF44] cp 145
Cargamos en a, el valor contenido en el registro LY, y lo comparamos con 145, si son iguales, acabamos de entrar en VBlank… como vemos, una instrucción menos, y en ensamblador, esto es muy importante.
Asi por ejemplo, para que vayais viendo un poco el ensamblador de la gameboy, si queremos esperar hasta que estemos en VBlank (para activar o desactivar el LCD por ejemplo, como vimos antes), podriamos hacer algo asi:
.espera_vblank: ld a, [$FF44] cp 145 jr nz, .espera_vblank
Lo mismo que antes, pero después de la comparación, si el resultado no es cero (el LCD no está en la linea 145), saltamos a la etiqueta inicial con lo que volvemos a hacer la comprobación… este bucle se ejecutaria hasta que el LCD llegue a la linea 145, con lo que continuará con las instrucciones a continuación. Tranquilos con el ensamblador, lo iremos explicando más adelante.
$FF42 - SCY - Scroll Y (R/W), $FF43 - SCX - Scroll X (R/W)
Registros de control del scroll del display, como expliqué anteriormente, nos permiten mover la ventana visible sobre el mapa de fondo escribiendo en ellos. Para posicionar el área visible en la coordenada (0,0) del fondo, simplemente escribimos cero en ambos registros.
$FF4A - WY - Posición Y de la ventana (R/W), $FF4B - WX - Posición X de la ventana menos 7 (R/W)
Controlan la posición x,y de la ventana. La ventana es un fondo alternativo, que puede ser dibujado encima del fondo normal, para efectos como la pantalla de estatus del zelda. Esta ventana no tiene scroll, pero puede ser posicionada en cualquier posición moviendola usando estos registros. Si la colocamos en WY=0, WX=7, la ventana cubrirá todo el fondo visible. Los sprites quedan también por encima de la ventana.
Fondos
Tenemos entonces un área en la VRAM, conocida como “Background Tile Map”, (mapa de tiles de fondo) de 32×32 = 1024 bytes. Cada uno de estos bytes, contiene un número que referencia al tile a mostrar de la “Tile Data Table” (tabla de datos de tiles). En realidad tenemos dos mapas de fondo, uno en $9800-9BFF y el otro en $9C00-9FFF, pudiendo seleccionar uno u otro a través del registro LCDC ($FF40).
Además tenemos una “ventana” que flota encima del fondo, controlada por los registros WNDPOSX y WNDPOSY.
Tenemos también dos Tile Data Tables, una que va desde $8000 a 8FFF, sin signo (0-255) y que puede ser usada para el fondo, sprites y ventana, y otra que va de $8800 a $97FF, con signo (-128, 127) y solo puede ser usada para los fondos.
Los dibujos de los tiles 8×8 pixéles, se guardan en las Tile Data Tables usando 16 bits por línea del sprite, esto es, dos bits por pixel para guardar los datos del color; ya que en la GameBoy tenemos 4 posibles colores: blanco, gris claro, gris oscuro y negro, en binario: 00, 01, 10 y 11
Se guardan en memoria de la siguiente manera:
- Byte 0-1 - Primera línea (8 pixeles)
- Byte 2-3 - Segunda línea
- …
- Byte 14-15 - Octava línea.
De los dos bytes de cada línea (esto se complica un poco), el primer byte, recibe los bits menos significativos del color de cada pixel, y el segundo byte los más significativos, siendo el bit 7 el pixel de más a la izquierda, y el 0 el pixel de más a la derecha; eso es, si el primer pixel es gris oscuro (10) el bit 7 del primer byte será 1 y el bit 7 del segundo byte será cero.
Con lo que para un sprite que dibuje una “A” multicolor, tenemos algo asi:
Tile: Datos: .33333.. %01111100 %01111100 -- $7C $7C (Hex) 22...22. %11000110 %00000000 -- $C6 $00 11...11. %00000000 %11000110 -- $00 $C6 2222222. <-- los digitos %11111110 %00000000 -- $FE $00 33...33. representan %11000110 %11000110 -- $C6 $C6 22...22. los colores %11000110 %00000000 -- $C6 $00 11...11. %00000000 %11000110 -- $00 $C6 ........ %00000000 %00000000 -- $00 $00
Asi que si escribimos los 16 bytes $7C, $7C, $C6, $00, $00, $C6, $FE, $00, $C6, $C6, $C6, $00, $00, $C6, $00, $00 a partir de la posición de memoria $8000, el tile número 0, será nuestra “A” multicolor.
Entonces ahora tenemos el mapa de tiles de fondo, que como dijimos guarda en una tabla de 32×32 (1024 bytes) los números de los tiles a mostrar en nuestra pantalla virtual de 32×32 tiles (32 columnas x 32 lineas) 20×18 visibles). Asi, si ahora, en la posición $9800 de memoria, escribimos $00, le estamos diciendo que en la posición de tiles de fondo de pantalla (0,0), dibuje el tile 0, Si activamos el LCD, y activamos el fondo, ahora en la esquina superior izquierda de la pantalla, tendriamos nuestra “A” multicolor.
IMPORTANTE:
Hay que tener en cuenta que no se debe escribir en la memoria de video mientras no estemos en un periodo de intervalo (ya sea horizontal o vertical) o desactivando el LCD. Esto es porque podrian suceder cosas muy raras si accedemos a la memoria de video para escribir en ella, mientras el LCD está intentando leer de ella para dibujar… tendremos parpadeos y otros efectos no deseados. Lo mismo se aplica para los sprites.
Sprites
Además de los fondos, por supuesto tenemos los sprites. Los sprites son objetos gráficos que se mueven por encima del fondo, tienen un color transparente, y propiedades que les podemos aplicar como reflejo horizontal y vertical. Normalmente los sprites son usados para dibujar nuestros personajes, naves, enemigos, y cualquier objeto gráfico que queramos manejar independientemente del fondo.
El controlador de video de la GameBoy, puede dibujar 40 sprites en modos 8×8 px o 8×16 px, aunque por una limitación de hardware, sólo 10 sprites pueden dibujarse simultáneamente en una linea horizontal (solo 10 sprites pueden compartir la misma coordenada Y). Los gráficos de los sprites, se guardan en memoria en el mismo formato que los tiles de fondo, en la tabla conocida como Sprite Pattern Table (Tabla de patrones de sprites), entre las posiciones de memoria $8000-8FFF, con lo que podemos definir hasta 256 sprites (4096 bytes / 16 bytes por sprite). Como vimos antes, la dirección $8000 está también usada por la tabla de datos de tiles de fondo, asi que si, los gráficos para tiles y sprites se comparten, hay que tener esto en cuenta cuando diseñemos nuestros gráficos.
Los atributos de los sprites, la tabla que define que sprites vamos a usar y cómo los usaremos, residen en la Sprite Atributte Table, entre las posiciones de memoria $FE00-FE9F. Cada una de estas entradas consta de 4 bytes, con lo que tenemos 160/4 = 40 entradas, como dijimos antes.
Cada entrada está formada de la siguiente manera:
Byte 0 - Coordenada Y
Especifica la posición Y del sprite menos 8. ¿Cómo?, que el primer pixel vertical visible en pantalla es el 8, esto es así para que podamos poner el sprite un poco o totalmente fuera de la pantalla, dibujandolo entre las posiciones 0-7. Asi mismo podemos ir sacándolo de la pantalla por abajo dibujándolo en posiciones mayores a 140.
Byte 1 - Coordenada X
Especifica la posición X del sprite menos 8. Se aplica lo mismo que la posición Y salvo que tenemos 160 pixeles visibles en la coordenada X.
Byte 2 - Número de sprite
Especifica el número de sprite a usar para esta entrada de los sprites definidos en la tabla $8000-8FFF.
Byte 3 - Atributos
Especifica los atributos especiales que se pueden aplicar a un sprite. Cada bit de este byte, es un flag que aplica diferentes modificaciones al sprite, según la siguiente tabla:
Bit7 Prioridad (0= Sprite por encima del fondo, 1=Objeto por debajo de los colores de fondo 1-3) (Válido para el fondo y la ventana. El color 0 de fondo, está siempre detrás del sprite (transparente)) Bit6 Reflejo Y (0=Normal, 1=Espejado vertical) Bit5 Reflejo X (0=Normal, 1=Espejado horizontal) Bit4 Número de paleta **Solo en GB no Color** (0=OBP0, 1=OBP1) Bit3 Banco de VRAM **Solo en GB Color** (0=Banco 0, 1=Banco 1) Bit2-0 Número de paleta **Solo en GB Color** (OBP0-7)
Como vemos, varios bits son sólo aplicables a la GB Color, asi que de momento lo ignoramos, y nos centramos en la prioridad, espejados y paleta.
Herramientas
Ensamblador
Para programar en ensablador para la GameBoy, lo primero que necesitamos, ¿qué es? Pues un ensamblador, el programa que convertirá nuestro código escrito en ensamblador, a código máquina de la gameboy, en forma de una ROM que podremos ejecutar en nuestro emulador favorito o cargar directamente en nuestra Gameboy con un FlashCart.
Personalmente, utilizo el RGBDS, un sistema de desarrollo en ensamblador específico para la GameBoy, asi que todo es muy sencillo con él. La podeis descargar de aqui:
Windows: Hay builds para windows en https://github.com/rednex/rgbds/releases
Lo descargais y copiais los .exe a c:\windows o similar dentro del PATH
Linux: Desde hace un tiempo, el RGBDS para Unix está alojado en GitHUB: https://github.com/bentley/rgbds
En linux puede que tengais que compilarlo (Por ejemplo en Arch hay un paquete en el AUR), pero vamos, es simplemente ejecutar como root un “make install” en el directorio después de haberlo descargado con git, en el github teneis las instrucciones de instalación.
Teneis la documentación del RGBDS en: https://rednex.github.io/rgbds/
Bien, RGBDS viene con cuatro herramientas, rgbasm, el ensamblador en si mismo, rgblink, el enlazador que creará nuestras roms, el rgblib, para crear librerias, y el rgbfix, para modificar la cabecera de la rom, para que cumpla los requerimientos necesarios para que la rom se ejecute. Como vimos en la introducción, la cabecera de una rom de GameBoy, lleva varios checksums y demás, pues esta herramienta los ajusta para que la rom sea correcta.
Además necesitaremos algunos archivos que nos harán más sencillo el tema de ensamblar y generar nuestras roms, os los dejo a continuación:
Windows: assemble.bat
Linux: assemble.sh
NOTA - El código fuente, deberá llevar la extension .agb (assembler gameboy) para que estos scripts funcionen.
Y por último, una ayuda más que importante, un fichero que podeis incluir en vuestro código fuente, que define nombres para todos los registros y direcciones de memoria de la gameboy, así como algunas ayudas como la cabecera de la rom, etc.
Podeis descargarlo aqui: gbhw.inc
Editor
Necesitareis además un editor para vuestro código fuente. Yo personalmente utilizo VIM, y tengo definido un archivo de sintaxis para los ficheros de ensamblador de gameboy .agb, si alguien lo quiere puede descargarlo aqui: agb.vim, e instalarlo en su ~/.vim/syntax/. Recordad añadir la siguiente línea a vuestro .vimrc para que os reconozca esta extension:
au BufRead,BufNewFile *.agb set filetype=agb
También tengo un archivo de sintaxis para los archivos .agb para el Gedit: agb.lang, podeis instalarlo en ~/.local/share/gtksourceview-3.0/language-specs (cread el directorio si no existe)
Si preferis Atom, podeis instalar el paquete “language-gb” desde el gestor de paquetes del editor para tener resaltado de sintaxis. Si quereis que os reconozca automáticamente los archivos .agb podeis editar el código del paquete desde las preferencias, seleccionando el paquete, settings → view code, y editando el archivo gb-assembly.cson en grammars, añadir 'agb' a la lista de “Filetypes” (Recordad la coma).
Emulador
Además, para ir probando vuestros programas, lo mejor es un emulador. Hay muchos emuladores de GameBoy disponibles, aunque yo os recomiendo el BGB, funciona tanto en Windows como en Linux usando el Wine (en Options → Graphics, seleccionar en output DirectDraw u OpenGL si teneis problemas con la imagen), y es realmente fiel a la máquina original, asi no os llevareis sorpresas al ejecutar vuestras roms en una gameboy real, y que funcionen cosas diferentes al emulador. Además tiene un debugger integrado, visualizadores de las tablas de tiles y sprites, desensamblador… la caña. Podeis descargarlo aqui: http://bgb.bircd.org/
También el VisualBoyAdvance funciona muy bien emulando a la GameBoy, y las últimas versiones incorporan GUI y algunas herramientas como poder desactivar capas gráficas, canales de sonido, etc.
Hola mundo
Vamos a hacer un pequeño programa de ejemplo, un pequeño hola mundo, que podremos ejecutar en nuestra gameboy, y mostrará una pequeña cara sonriente en pantalla. Después, paso a paso, explicaré las instrucciones del programa. Vamos a ello:
Primero vamos a dibujar nuestro sprite como aprendimos anteriormente:
.33333.. %01111100 %01111100 -- $7C $7C 3111113. %10000010 %11111110 -- $82 $FE 31.1.13. %10000010 %11010110 -- $82 $D6 31.1.13. %10000010 %11010110 -- $82 $D6 3111113. %10000010 %11111110 -- $82 $FE 3.111.3. %10000010 %10111010 -- $82 $BA 31...13. %10000010 %11000110 -- $82 $C6 .33333.. %01111100 %01111100 -- $7C $7C
Código
Teniendo estos datos para dibujar nuestra cara sonriente, empezamos con el programa:
- holamundo.asm
; Hola mundo ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom sin mapper, de 32K y sin RAM, lo más básico ; (como por ejemplo la del tetris) ROM_HEADER ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. call apaga_LCD ; llamamos a la rutina que apaga el LCD ; cargamos el tile en la memoria de tiles ld hl, TileCara ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld b, 16 ; b = 16, numero de bytes a copiar .bucle_carga: ld a,[hl] ; cargamos en A el dato apuntado por HL ld [de], a ; y lo metemos en la dirección apuntada en DE dec b ; decrementamos b, b=b-1 jr z, .fin_bucle_carga ; si b = 0, terminamos, no queda nada por copiar inc hl ; incrementamos la dirección a leer de inc de ; incrementamos la dirección a escribir en jr .bucle_carga ; seguimos .fin_bucle_carga: ; escribimos nuestro tile, en el mapa de tiles ld hl, _SCRN0 ; en HL la dirección del mapa de fondo ld [hl], $00 ; $00 = el tile 0, nuestro tile. ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8|LCDCF_OBJOFF ld [rLCDC], a ; bucle infinito bucle: halt nop jr bucle ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento .espera_VBlank ld a, [rLY] cp 145 jr nz, .espera_VBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos ; Datos de nuestro tile TileCara: DB $7C, $7C, $82, $FE, $82, $D6, $82, $D6 DB $82, $FE, $82, $BA, $82, $C6, $7C, $7C EndTileCara:
Bien, ahi está nuestro hola mundo. Guardadlo como hola.agb, y lo compilais usando el bat o sh que os descargasteis de la siguiente manera:
assemble.sh (o assemble.bat) hola
El mismo buscará el archivo hola.agb y ensablará y creará la rom. Ahora deberiais tener un hola.gb que si ejecutais en vuestro emulador o gameboy, vereis una cara sonriente en la esquina superior izquierda de la pantalla. Bueno, realmente, si el emulador es bueno, o lo ejecutais en la gameboy real, es probable que la pantalla esté llena de estos smileys quizá salvo el logo de nintendo. ¿Por qué?, porque no hemos limpiado la memoria de video y otras cosillas con las que iremos mejorando el programa de ejemplo.
Explicación
Bien, vamos por partes…
; Hola mundo ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador
Bien, en ensamblador, los comentarios del código empiezan por ”;“. Como sabeis, los comentarios sirven para añadir explicaciones y documentación a nuestro código, para que otras personas o nosotros después de un tiempo, sepamos por que programamos tal o cual rutina, etc. Asi que simplemente, el ensamblador ignora el texto de una linea a partir de un punto y coma y hasta el final de la misma.
INCLUDE "gbhw.inc" ; importamos el archivo de definiciones
INCLUDE, es un comando del ensamblador, que lo que hace es importar el código del archivo que le decimos, a partir del momento en que aparece este comando; esto es, es como si cogiéramos el código en “gbhw.inc” y lo pegáramos a continuación. Esto es útil, porque podemos tener código que usamos amenudo en archivos aparte, e importarlos desde todos nuestros programas, ahorrándonos teclearlos o copiarlos en cada uno. Como vemos, más adelante en la linea, aparece un comentario, asi que a partir del ”;“ y hasta el final de la linea, ese texto es ignorado.
; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio
SECTION define una sección de código, como su propio nombre indica, un bloque de código que queremos definir y colocar en una posición específica de la memoria. El ensamblador del RGBDS nos permite definirle un nombre “start” y una dirección de inicio, la dirección a partir de la cual, colocamos ese código en memoria. En la GameBoy, hemos visto en el mapa de memoria, que la ROM del cartucho, donde debe ir nuestro propio código, comienza en $0000, pero los 256 primeros bytes ($100), están reservados para las rutinas de interrupción, por lo que debemos colocar nuestro código a partir de la dirección, $0100, por eso colocamos la sección “start” a partir de ROM0 ($0000) + $100. Entonces ejecutamos un nop, esta instrucción de gbz80, no hace nada, literalmente (No OPeration). Sirve para perder tiempo (4 ciclos) o para rellenar bloques de datos que necesitemos vacios (1 byte). Después ejecutamos la instrucción jp, que sirve para hacer saltos de un lugar a otro del programa, pudiendo definir condiciones para realizar el salto o no. En este caso, no hay condiciones, y le decimos que simplemente salte hasta la etiqueta inicio. ¿Por qué hacemos esto? Bien, si miramos la documentación sobre la organizazión de una ROM de GameBoy, veremos que la cabecera de la ROM debe empezar en la dirección $0104, con lo que si colocamos un nop(1 byte), y luego la instrucción jp <direccion> (3 bytes), estamos llenando 4 bytes, justo lo que necesitamos para empezar a definir la cabecera, que será lo siguiente que introduzcamos. Pero necesitamos ese jp, para que salte desde aqui, hasta el código que empezaremos a ejecutar, ya que en la cabecera van datos, no código ejecutable.
; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom sin mapper, de 32K y sin RAM, lo más básico ; (como por ejemplo la del tetris) ROM_HEADER ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE
Este ROM_HEADER es un macro definido dentro del gbhw.inc. Los macros son unas construcciones especiales del RGBDS que nos permiten definir ciertos bloques de código y modificarlos mediante parámetros. Podemos asumir que con esto, el RGBDS nos introducirá aqui los bytes necesarios de la cabecera del cartucho, sin tener que preocuparnos de definirlos a mano. Podeis echar un vistazo al final del gbhw.inc si teneis curiosidad.
; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram
Bien, empezamos con el tema. Primero un nop, que ya conocemos, luego con la instrucción di, deshabilitamos las interrupciones, no las vamos a usar en este ejemplo, y solo nos molestarian. Luego cargamos en el registro SP el puntero de pila, la dirección $ffff que es el tope de la ram de la gameboy. La pila se usa para guardar datos temporales que necesitemos, como las direcciones de retorno cuando llamamos a una subrutina. Además, la pila crece en direcciones descendentes. Asi ahora, si guardamos un byte en la pila, SP se decrementa, con lo que vale $fffe y el byte se guarda ahi. Si ahora sacamos un byte de la pila, nos devuelve el dato en $fffe e incrementa SP, que vuelve a apuntar a $ffff.
inicializacion: ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta
Definimos una etiqueta “inicializacion” y empezamos a escribir el código que pondra nuestra gameboy a funcionar correctamente. Primero la paleta. Como sabemos la GameBoy clásica tiene 4 colores, blanco, negro y dos tonos de gris. Podemos definir dos paletas, vamos, ordenar estos cuatro colores como queramos, para que uno sea el primero, otro el segundo, etc. Además tenemos dos paletas posibles. Esto está bien, porque asi podemos tener por ejemplo una paleta invertida para efectos o cosas asi. Pero de momento vamos a usar sólo una paleta muy normalita, los colores del mas oscuro (11), al más claro (00). Entonces con la instrucción ld, una de las más importantes y que más usareis en ensamblador de gameboy (básicamente porque sirve para mover datos de un lado a otro de muchas maneras posibles), cargamos en el registro “a”, los datos de la paleta (%11100100 en binario). La instrucción ld, se construye básicamente de la siguiente manera, ld <destino> <origen>, donde destino y origen pueden ser registros, direcciones de memoria o números directos.
Ahora con otro ld, cargamos en la dirección de memoria apuntada por rBGP (si miramos gbhw.ic, veremos que rBGP es una dirección de memoria normal), el contenido del registro “a”; rBGP va entre corchetes porque lo que queremos decir es que guarde en esa dirección de memoria.
Vamos a explicar esto: Si tenemos que el registro “a”, contiene el numero 200, y hacemos “ld a, 5”, ahora “a” contiene 5. Pero si por el contrario hacemos “ld [a], 5” lo que estamos haciendo es grabar en la posición de memoria apuntada por “a”, el valor 5, con lo que escribiriamos 5 en la posición de memoria 200 (que era el valor que tenia “a” al inicio).
ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo.
Bien, sencillo ¿verdad?, como hemos visto, simplemente cargamos 0 en el registro a (a=0) y después escribimos el contenido de a, en los registros de scroll vertical y horizontal, con lo que ahora valen 0, posicionando la pantalla arriba a la izquierda del mapa de fondo. Fijaros que usamos los corchetes, para referirnos a, escribe este dato en la posición de memoria apuntada por rSCX y rSCY.
call apaga_LCD ; llamamos a la rutina que apaga el LCD
Aqui con call, llamamos a una subrutina. Las subrutinas en ensamblador, son como las funciones en C u otro lenguaje de programación. Son bloques de código que ejecutan ciertas operaciones y cuando terminan, regresan al lugar del que fueron llamadas. El ensamblador buscará una etiqueta llamada apagaLCD, saltará ahi, y ejecutará el código que se encuentre a continuación, hasta que encuentre la instrucción ret, que vuelve al punto en que la subrutina fue llamada. Al contrario que en C u otros lenguajes, si deseamos pasar parámetros a las subrutinas, tenemos que hacerlo nosotros mismos, preocupandonos de guardar ciertos datos en registros o en la pila antes de llamar a la subrutina, y de tener cuidado de no modificar indeseadamente registros que estemos usando fuera de la subrutina, un error común que nos puede dar buenos dolores de cabeza.
; cargamos el tile en la memoria de tiles ld hl, TileCara ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld b, 16 ; b = 16, numero de bytes a copiar .bucle_carga: ld a,[hl] ; cargamos en A el dato apuntado por HL ld [de], a ; y lo metemos en la dirección apuntada en DE dec b ; decrementamos b, b=b-1 jr z, .fin_bucle_carga ; si b = 0, terminamos, no queda nada por copiar inc hl ; incrementamos la dirección a leer de inc de ; incrementamos la dirección a escribir en jr .bucle_carga ; seguimos .fin_bucle_carga:
Bueno, vamos con algo interesante, ya que nos va a enseñar como se hacen las cosas en ensamblador, especialmente los bucles. Con este código vamos a cargar en la memoria de tiles, los 16 bytes que definen nuestra cara sonriente. Podriamos hacerlo simplemente con instrucciones ld cargando byte a byte… cargariamos un byte en a por ejemplo, y luego cargariamos el contenido de a, en una direccion de memoria a partir del comienzo de la memoria de tiles, luego el siguiente, etc… pero imaginaros que tenemos que cargar 50 tiles… como que no. Para eso están los bucles, como en cualquier lenguaje de programación. Veamos como lo hacemos.
Primero, empezamos inicializando ciertos registros con datos. Cargamos en hl(registro de 16 bit), la dirección donde están los bytes de nuestro tile. Si usamos una etiqueta en una instrucción ld, el ensamblador la substituye por la dirección de memoria donde empieza el código o datos después de la etiqueta. Tenemos que usar un registro de 16 bit, porque como hemos visto, las direcciones de memoria del gbz80, son de 16 bits.
Luego cargamos en el registro de, la dirección de memoria de video, que empieza en $8000 y que como hemos visto, es la direccion de inicio de la memoria de tiles.
Y ahora cargamos en el registro b, el número 16, que son los bytes que tenemos que copiar.
Ahora empezamos con el bucle. Ponemos una etiqueta al inicio del bucle, porque tendremos que ir volviendo a ella en cada “vuelta” del bucle. Bien, cargamos en a, el dato apuntado por hl, como sabemos en hl está la dirección donde comienzan los datos de nuestro sprite. Asi que con esta instrucción, cargamos en a, el primer byte de nuestro tile. Ahora metemos en la dirección apuntada por de, el contenido de a, con lo que ya tenemos el primer byte de nuestro tile, copiado a la primera dirección de memoria de tiles. Ahora decrementamos b, usando la instrucción dec, que simplemente resta una unidad al registro que le digamos. Como ya hemos copiado un byte, pues ya solo quedan 15 por copiar, asi que restamos uno a b, para reflejar esto.
Y ahora viene la lógica principal del bucle. Con jr z, .fin_bucle_carga, lo que hacemos es decir, “salta a .fin_bucle_carga si el resultado de la operación anterior ha sido cero”. Como vimos en la sección de flags, z significaba que el flag zero habia sido activado. Asi que si el resultado de la operación anterior, dec b, es cero, saltamos fuera del bucle, porque ya hemos terminado de copiar. Es sencillo, en cada vuelta del bucle, vamos restando uno a b, asi que cuando hayamos copiado los 16 bytes, b valdrá cero, se activará el flag y el jz z, saltará fuera.
Si el flag de zero no está activado, porque b todavia no es cero, pues no se produce el salto, y la ejecución sigue a continuacion. Ahora con inc, incrementamos (sumamos uno) a hl y de. Hacemos esto, porque como ya habíamos copiado el primer dato a la primera dirección de memoria de tiles, ahora tenemos que seguir con el segundo, tercero, etc, asi que en cada vuelta del bucle, incrementamos estas dos direcciones, que irán apuntando a las siguientes direcciones de nuestros datos y de las posiciones de memorias de tiles respectivamente. Y para finalizar, saltamos a .bucle_carga, para copiar los siguientes datos, hasta que b sea cero, con lo que saltará hasta .fin_bloque_carga, terminando nuestro bucle.
; escribimos nuestro tile, en el mapa de tiles ld hl, _SCRN0 ; en HL la dirección del mapa de fondo ld [hl], $00 ; $00 = el tile 0, nuestro tile.
Esto es muy sencillo comparado con lo anterior. Simplemente cargamos en hl la dirección del mapa de tiles de fondo, y luego escribimos en esta dirección, el byte cero, para indicarle, que el tile 0,0 del mapa de fondo, sea el tile cero, el primer tile de la tabla, que como sabeis, acabamos de escribir en la tabla de tiles.
; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8|LCDCF_OBJOFF ld [rLCDC], a
Pues nada, ya hemos metido los datos de nuestro tile al inicio de la memoria de tiles, por lo que es el tile cero, y hemos escrito un cero en la primera posición del mapa de fondo, con lo que el primer tile del fondo (0,0 esquina superior izquierda), será nuestro sprite. Entonces ahora podemos activar el LCD para ver nuestra creación. Lo que hacemos es cargar en a, ciertas configuraciones del lcd, que están definidas en el gbhw.inc. haciendo un OR (|) entre esas configuraciones, las seleccionamos todas, LCDCF_ON (lcd encendido), LCDCF_BG8000 (tile data en $8000), LCDCF_BG9800 (tile map en $9800), LCDCF_BGON (fondo activado), LCDCF_OBJ8 (sprites de 8×8), LCDCF_OBJOFF (sprites desactivados). Y entonces escribimos a en la posición de memoria del registro de control del LCD, que aplicará esa configuración. A partir de aqui, tendriamos nuestro tile en pantalla.
; bucle infinito bucle: halt nop jr bucle
Bien, en ensamblador, cuando llegamos a un punto muerto, tenemos que hacer un bucle infinito. ¿Por qué?, porque si no, el procesador seguiria ejecutando código que no hemos escrito, vamos, las siguientes posiciones de memoria después de nuestro código, que no sabemos que contienen (normalmente valores aleatorios), asi que podria pasar cualquier cosa. Asi que ahora, que nuestro programa ha terminado, y no vamos a hacer nada más, creamos este bucle infinito, que se ejecutará hasta que apaguemos la consola o se acaben las pilas
Es muy sencillo, hacemos un halt, que pone el procesador en modo de bajo consumo y detiene la ejecución hasta que ocurra una interrupción. Como podrian llegar interrupciones, pues seguidamente hacemos un nop (nada) y saltamos al inicio del bucle de nuevo, con lo que nos quedariamos aqui por siempre.
; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento .espera_VBlank ld a, [rLY] cp 145 jr nz, .espera_VBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos
Bien, nuestro programa ha terminado, pero a continuación podemos escribir datos o subrutinas que podrán ser llamadas o usadas por nuestro código si el programa principal hace referecia a ellas. Este es el caso de la rutina apaga_LCD, que habíamos llamado desde el programa principal con el comando call.
Vamos a analizarla. Lo primero que hacemos, es cargar en a, el contenido del registro de control del lcd, rLCDC. ahora ejecutamos la orden rlca. Es una orden bastante peculiar, que lo hace es poner el bit 7 del registro a, en el flag de acarreo. ¿Para que vale esto? pues para hacer comprobaciones, claro, que es lo que hacemos a continuación. Como habiamos comentado, la instrucción ret, sale de la subrutina y devuelve la ejecución a donde se dejó con call, pero ret. también puede aceptar condiciones para retornar o no, dependiendo de un flag. En este caso comprobamos el flag de acarreo, exactamente si está a cero. ret nc, significa, “retorna si el flag de acarreo es cero”. En el flag de acarreo habiamos puesto el bit 7 de rLCDC, y si miramos que significa ese bit en la sección de video, vemos que significa si el LCD está encendido o apagado. Lo vais viendo, podriamos entonces traducirlo como: “retorna si el lcd está apagado”, vamos, que no tiene sentido apagar el LCD si ya está apagado, por lo tanto, si ya lo está, volvemos y nuestra subrutina no hace nada.
Si no estuviera apagado, el bit 7 de rLCDC valdria 1, y por lo tanto el ret no se ejecutaria, con lo que la ejecución continuaria. Vale, ahora como vimos, era muy importante no apagar el LCD mientras no estuviera en el intervalo vertical, porque se podria dañar. Entonces es lo que hacemos ahora. Comprobamos el registro rLY, para ver en que linea se encuentra dibujando el LCD, si es 145, significa que ya está fuera de la pantalla, en el intervalo vertical, y podemos continuar. Si no, seguimos esperando hasta que rLY sea 145.
Y ahora apagamos el LCD, cargamos el contenido del rLCDC en a, y con la instrución res, que resetea (pone a cero) el bit que le digamos del registro a, pone a cero el bit 7, que como sabemos enciende o apaga el LCD. Ahora volvemos a escribir el contenido de a en rLCDC, con lo que hemos cambiado el bit 7 sin afectar al resto del contenido de rLCDC.
El LCD está apagado, con lo que podemos volver con ret.
; Datos de nuestro tile TileCara: DB $7C, $7C, $82, $FE, $82, $D6, $82, $D6 DB $82, $FE, $82, $BA, $82, $C6, $7C, $7C EndTileCara:
Y aqui, a continuación escribimos los datos de nuestro sprite. Es común poner los datos entre etiquetas significativas, y además añadir una etiqueta al final. Ahora sabemos que tenemos 16 tiles, pero si tuvieramos muchos, en vez de contarlos, podriamos decirle al ensamblador, que cargue EndTileCara-TileCara bytes, con lo que el ensamblador contaria cuantos bytes hay entre esas direcciones y substituiria ese valor en nuestro programa. Asi que por costumbre está bien hacer esto.
Como veis, ponemos los datos de nuestro sprite tal cual, separados por comas, y predecidos por la directiva DB, de Define Byte, que simplemente inserta esos valores en la memoria de nuestro programa tal cual, para usarlos como datos.
Por cosas como estas, os decia que era importante el bucle infinito al final de nuestro programa, porque si no, el programa seguiria ejecutando cualquier cosa que pudiera haber a continuacion, y podria interpretar datos de gráficos, música, o textos, como si fueran instrucciones, con lo que podria pasar cualquier cosa
Y hasta aqui nuestro programa de ejemplo, que como habeis visto tiene sus pequeños errores (a propósito), para que veais que hay que inicializar bien las memorias.
Mejoras
Entonces ahora, vamos a aplicar un par de mejoras a nuestro programa, os explico lo que voy a hacer, y luego os dejo el código bien comentado para que lo estudieis y trateis de entenderlo. Primero, voy a definir otro tile, el tile cero, que será un tile totalmente blanco, y con él, voy a llenar todo el mapa de fondo antes de hacer nada. Con lo que, como supondreis, borraré toda la pantalla en blanco. Después simplemente cambiaré el tile que voy a dibujar en 0,0 por el tile 1, ya que ahora nuestra cara feliz será el segundo tile, y lo dibujaré, por lo demás el programa será el mismo.
Os dejo el código:
- holamundo2.asm
; Hola mundo mejorado ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom sin mapper, de 32K y sin RAM, lo más básico ; (como por ejemplo la del tetris) ROM_HEADER ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. call apaga_LCD ; llamamos a la rutina que apaga el LCD ; cargamos los tiles en la memoria de tiles ld hl, Tiles ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld b, 32 ; b = 32, numero de bytes a copiar (2 tiles) .bucle_carga: ld a,[hl] ; cargamos en A el dato apuntado por HL ld [de], a ; y lo metemos en la dirección apuntada en DE dec b ; decrementamos b, b=b-1 jr z, .fin_bucle_carga ; si b = 0, terminamos, no queda nada por copiar inc hl ; incrementamos la dirección a leer de inc de ; incrementamos la dirección a escribir en jr .bucle_carga ; seguimos .fin_bucle_carga: ; ahora limpiamos la pantalla (llenamos todo el mapa de fondo), con el tile 0 ld hl, _SCRN0 ld de, 32*32 ; numero de tiles en el mapa de fondo .bucle_limpieza: ld a, 0 ; el tile 0 es nuestro tile vacio ld [hl], a dec de ; ahora tengo que comprobar si 'de' es cero, para ver si tengo que ; terminar de copiar. dec de no modifica ningun flag, asi que no puedo ; comprobar el flag zero directamente, pero para que 'de' sea cero, d y e ; tienen que ser cero los dos, asi que puedo hacer un or entre ellos, ; y si el resultado es cero, ambos son cero. ld a, d ; cargamos d en a or e ; y hacemos un or con e jp z, .fin_bucle_limpieza ; si d OR e es cero, de es cero. Terminamos. inc hl ; incrementamos la dirección a escribir en jp .bucle_limpieza .fin_bucle_limpieza ; bien, tenemos todo el mapa de tiles lleno con el tile 0, ;ahora podemos pintar el nuestro ; escribimos nuestro tile, en el mapa de tiles ld hl, _SCRN0 ; en HL la dirección del mapa de fondo ld [hl], $01 ; $01 = el tile 1, nuestro tile. ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8|LCDCF_OBJOFF ld [rLCDC], a ; bucle infinito bucle: halt nop jr bucle ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento .espera_VBlank ld a, [rLY] cp 145 jr nz, .espera_VBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos ; Datos de nuestros tiles Tiles: DB $00, $00, $00, $00, $00, $00, $00, $00 DB $00, $00, $00, $00, $00, $00, $00, $00 DB $7C, $7C, $82, $FE, $82, $D6, $82, $D6 DB $82, $FE, $82, $BA, $82, $C6, $7C, $7C EndTiles:
Y el resultado, esta vez si:
Hola sprites
Bien, ahora vamos a modificar el hola mundo, para crear y manejar un sprite. Lo primero, voy a crear otro tile diferente al blanco, va a ser un tile de fondo suave, para que podais ver como el sprite se mueve por encima sin problemas. Entonces haré esto, iniciaré la gameboy, como en el hola mundo, pero añadiendo una paleta también para los sprites (usaré la misma), llenaré el fondo con un tile suave, y luego crearé un sprite, y lo iré moviendo rebotado por la pantalla. Usaré una pequeña rutina de retardo para que todo vaya más lento. Vamos a ver el código:
- holasprite.asm
; Hola sprite ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; definimos unas constantes para trabajar con nuestro sprite _SPR0_Y EQU _OAMRAM ; la Y del sprite 0, es el inicio de la mem de sprites _SPR0_X EQU _OAMRAM+1 _SPR0_NUM EQU _OAMRAM+2 _SPR0_ATT EQU _OAMRAM+3 ; creamos un par de variables para ver hacia donde tenemos que mover el sprite _MOVX EQU _RAM ; inicio de la ram dispobible para datos _MOVY EQU _RAM+1 ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom sin mapper, de 32K y sin RAM, lo más básico ; (como por ejemplo la del tetris) ROM_HEADER ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta de fondo ld [rOBP0], a ; y en la paleta 0 de sprites ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. call apaga_LCD ; llamamos a la rutina que apaga el LCD ; cargamos los tiles en la memoria de tiles ld hl, Tiles ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld b, 32 ; b = 32, numero de bytes a copiar (2 tiles) .bucle_carga: ld a,[hl] ; cargamos en A el dato apuntado por HL ld [de], a ; y lo metemos en la dirección apuntada en DE dec b ; decrementamos b, b=b-1 jr z, .fin_bucle_carga ; si b = 0, terminamos, no queda nada por copiar inc hl ; incrementamos la dirección a leer de inc de ; incrementamos la dirección a escribir en jr .bucle_carga ; seguimos .fin_bucle_carga: ; ahora limpiamos la pantalla (llenamos todo el mapa de fondo), con el tile 0 ld hl, _SCRN0 ld de, 32*32 ; numero de tiles en el mapa de fondo .bucle_limpieza: ld a, 0 ; el tile 0 es nuestro tile vacio ld [hl], a dec de ; ahora tengo que comprobar si de es cero, para ver si tengo que ; terminar de copiar. dec de no modifica ningñun flag, asi que no puedo ; comprobar el flag zero directamente, pero para que de sea cero, d y e ; tienen que ser cero los dos, asi que puedo hacer un or entre ellos, ; y si el resultado es cero, ambos son cero. ld a, d ; cargamos d en a or e ; y hacemos un or con e jp z, .fin_bucle_limpieza ; si d OR e es cero, de es cero. Terminamos. inc hl ; incrementamos la dirección a escribir en jp .bucle_limpieza .fin_bucle_limpieza ; bien, tenemos todo el mapa de tiles lleno con el tile 0, ; ahora vamos a crear el sprite. ld a, 30 ld [_SPR0_Y], a ; posición Y del sprite ld a, 30 ld [_SPR0_X], a ; posición X del sprite ld a, 1 ld [_SPR0_NUM], a ; número de tile en la tabla de tiles que usaremos ld a, 0 ld [_SPR0_ATT], a ; atributos especiales, de momento nada. ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8|LCDCF_OBJON ld [rLCDC], a ; preparamos las variables de la animacion ld a, 1 ld [_MOVX], a ld [_MOVY], a ; bucle infinito animacion: ; lo primero, esperamos por el VBlank, ya que no podemos modificar ; la VRAM fuera de él, o pasarán cosas raras .wait: ld a, [rLY] cp 145 jr nz, .wait ; incrementamos las y ld a, [_SPR0_Y] ; cargamos la posición Y actual del sprite ld hl, _MOVY ; en hl, la dirección del incremento Y add a, [hl] ; sumamos ld hl, _SPR0_Y ld [hl], a ; guardamos ; comparamos para ver si hay que cambiar el sentido cp 152 ; para que no se salga de la pantalla (max Y) jr z, .dec_y cp 16 jr z, .inc_y ; lo mismo, minima coord Y=16 ; no hay que cambiar jr .end_y .dec_y: ld a, -1 ; ahora hay que decrementar las Y ld [_MOVY], a jr .end_y .inc_y: ld a, 1 ; ahora hay que incrementar las Y ld [_MOVY], a .end_y: ; vamos con las X, lo mismo pero cambiando los márgenes ld a, [_SPR0_X] ; cargamos la posición X actual del sprite ld hl, _MOVX ; en hl, la dirección del incremento X add a, [hl] ; sumamos ld hl, _SPR0_X ld [hl], a ; guardamos ; comparamos para ver si hay que cambiar el sentido cp 160 ; para que no se salga de la pantalla (max X) jr z, .dec_x cp 8 ; lo mismo, minima coord izq = 8 jr z, .inc_x ; no hay que cambiar jr .end_x .dec_x: ld a, -1 ; ahora hay que decrementar las X ld [_MOVX], a jr .end_x .inc_x: ld a, 1 ; ahora hay que incrementar las X ld [_MOVX], a .end_x: ; un pequeño retardo call retardo ; volvemos a empezar jr animacion ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento .espera_VBlank ld a, [rLY] cp 145 jr nz, .espera_VBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos ; rutina de retardo retardo: ld de, 2000 ; numero de veces a ejecutar el bucle .delay: dec de ; decrementamos ld a, d ; vemos si es cero or e jr z, .fin_delay nop jr .delay .fin_delay: ret ; Datos de nuestros tiles Tiles: DB $AA, $00, $44, $00, $AA, $00, $11, $00 DB $AA, $00, $44, $00, $AA, $00, $11, $00 DB $3E, $3E, $41, $7F, $41, $6B, $41, $7F DB $41, $63, $41, $7F, $3E, $3E, $00, $00 EndTiles:
El resultado:
Hola JoyPad
Bien, vamos a crear un ejemplo para leer el Pad y los botones. Si vamos a los estupendos PanDocs y su sección sobre el pad, (http://gbdev.gg8.se/wiki/articles/Joypad_Input), vemos que tenemos un registro mapeado en memoria, en $FF00, que vamos a llamar rP1 (de Player 1), que podemos leer para conocer el estado del pad y los botones. Bien, supongo que para ahorrar pines en la CPU de la gameboy, los diseñadores decidieron implementar un diseño tipo matriz para los botones de la GameBoy, con lo que el pad direccional (4 botones), y los botones A, B, Select, y Start, comparten las lineas de entrada. ¿Entonces cómo podemos saber cual está pulsado?. Pues como vemos en los PanDocs, hay dos bits en ese registro, que nos permiten “activar” o el pad o los botones, y entonces leer de ellos. Vamos a ver el registro:
Bit 7 - Sin uso Bit 6 - Sin uso Bit 5 - P15 Selecciona los botones (0=Selecciona) Bit 4 - P14 Selecciona el PAD direccional (0=Selecciona) Bit 3 - P13 Abajo o Start (0=Pulsado) (Sólo lectura) Bit 2 - P12 Arriba o Select (0=Pulsado) (Sólo lectura) Bit 1 - P11 Izquierda o B (0=Pulsado) (Sólo lectura) Bit 0 - P10 Derecha o A (0=Pulsado) (Sólo lectura)
Entonces, ¿cómo vamos a hacer para leer todos los botones? Bien, sencillo, vamos a crear una rutina que lo que hará es:
- Activar el pad y desactivar los botones
- Leer los cuatro ultimos bits del registro del pad en A (con lo que tenemos en A el estado del PAD direccional)
- Poner a cero los 4 bits superiores de A (sólo nos importan los 4 bajos)
- Intercambiar los 4 bits bajos de A por los altos (para que los bajos nos queden libres de nuevo y el PAD direccional quede guardado en los altos)
- Mover A a otro registro temporal (B por ejemplo)
- Activar los botones y desactivar el PAD
- Leer los cuatro últimos bits del registro del pad en A (con lo que tenemos en A el estado de los botones)
- Poner a cero los 4 bits superiores de A (sólo nos importan los bajos)
- Hacer un OR con el registro temporal (B), para asi, tener en A el estado de todos los botones (el pad en los 4 bits altos, y los botones en los 4 bajos)
- Obtener el complemento de A, lo que cambia los 0's por 1's y los 1's por ceros, asi tendremos unos en los botones pulsados, en vez de al revés
- Guardar A en una variable en memoria.
Luego comprobaremos esa variable para saber que botones están pulsados (los bits que estén a 0) y moveremos el sprite según los botones. También vamos a alternar entre las dos posibles paletas de sprites, cuando se pulse A.
Una cosa a tener en cuenta, es el “bouncing”. Los contactos eléctricos de los pulsadores de cada botón, pueden sufrir del efecto conocido como “bouncing” esto es, a nivel microscópico cuando pulsamos un botón, los contactos rebotan produciendo pequeños saltos en la señal, con lo que por un pequeño espacio de tiempo, la señal no es real, y puedes leer que un botón no está pulsado, cuando si lo está, o que el botón está pulsado cuando acabas de soltarlo. Los aparatos modernos, suelen llevar circuiteria que controla este bouncing, pero la GameBoy no. La solución es sencilla, realizar unas cuantas lecturas seguidas, para intentar minimizar este efecto.
Además he añadido código para limpiar la memoria de atributos de sprites, para poner los 40 sprites, todo a cero, asi, los sprites no usados, quedarán fuera de la pantalla. Quizá en los emuladores no pase nada, pero en la GameBoy real, que he probado, un par de sprites con “basura” aparecian por el medio de la pantalla. Nunca se sabe lo que pueden contener los chips de RAM al iniciar (estática y demás), asi que recordad, inicializar siempre.
Pues vamos con el programa:
- holajoypad.asm
; Hola joypad ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; definimos unas constantes para trabajar con nuestro sprite _SPR0_Y EQU _OAMRAM ; la Y del sprite 0, es el inicio de la mem de sprites _SPR0_X EQU _OAMRAM+1 _SPR0_NUM EQU _OAMRAM+2 _SPR0_ATT EQU _OAMRAM+3 ; variable donde guardar el estado del pad _PAD EQU _RAM ; al inicio de la ram interna ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom sin mapper, de 32K y sin RAM, lo más básico ; (como por ejemplo la del tetris) ROM_HEADER ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta de fondo ld [rOBP0], a ; y en la paleta 0 de sprites ; creamos otra paleta para la paleta 2 de sprites, inversa a la normal ld a, %00011011 ld [rOBP1], a ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. call apaga_LCD ; llamamos a la rutina que apaga el LCD ; cargamos los tiles en la memoria de tiles ld hl, Tiles ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld b, 32 ; b = 32, numero de bytes a copiar (2 tiles) .bucle_carga: ld a,[hl] ; cargamos en A el dato apuntado por HL ld [de], a ; y lo metemos en la dirección apuntada en DE dec b ; decrementamos b, b=b-1 jr z, .fin_bucle_carga ; si b = 0, terminamos, no queda nada por copiar inc hl ; incrementamos la dirección a leer de inc de ; incrementamos la dirección a escribir en jr .bucle_carga ; seguimos .fin_bucle_carga: ; ahora limpiamos la pantalla (llenamos todo el mapa de fondo), con el tile 0 ld hl, _SCRN0 ld de, 32*32 ; numero de tiles en el mapa de fondo .bucle_limpieza_fondo: ld a, 0 ; el tile 0 es nuestro tile vacio ld [hl], a dec de ; ahora tengo que comprobar si de es cero, para ver si tengo que ; terminar de copiar. dec de no modifica ningún flag, asi que no puedo ; comprobar el flag zero directamente, pero para que de sea cero, d y e ; tienen que ser cero los dos, asi que puedo hacer un or entre ellos, ; y si el resultado es cero, ambos son cero. ld a, d ; cargamos d en a or e ; y hacemos un or con e jp z, .fin_bucle_limpieza_fondo ; si d OR e es cero, de es cero. ; Terminamos. inc hl ; incrementamos la dirección a escribir en jp .bucle_limpieza_fondo .fin_bucle_limpieza_fondo ; bien, tenemos todo el mapa de tiles lleno con el tile 0 ; ahora limpiamos la memoria de sprites ld hl, _OAMRAM ; memoria de atributos de sprites ld de, 40*4 ; 40 sprites x 4 bytes cada uno .bucle_limpieza_sprites ld a, 0 ; lo vamos a poner todo a cero, asi los sprites ld [hl], a ; sin uso, quedarán fuera de pantalla dec de ; lo mismo que en bucle anterior ld a, d ; cargamos d en a or e ; y hacemos un or con e jp z, .fin_bucle_limpieza_sprites ; si d OR e es cero, de es cero. inc hl ; incrementamos la dirección a escribir en jp .bucle_limpieza_sprites .fin_bucle_limpieza_sprites ; ahora vamos a crear el sprite. ld a, 74 ld [_SPR0_Y], a ; posición Y del sprite ld a, 90 ld [_SPR0_X], a ; posición X del sprite ld a, 1 ld [_SPR0_NUM], a ; número de tile en la tabla de tiles que usaremos ld a, 0 ld [_SPR0_ATT], a ; atributos especiales, de momento nada. ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8|LCDCF_OBJON ld [rLCDC], a ; bucle principal movimiento: ; leemos el pad call lee_pad ; lo primero, esperamos por el VBlank, ya que no podemos modificar ; la VRAM fuera de él, o pasarán cosas raras .wait: ld a, [rLY] cp 145 jr nz, .wait ; Ahora movemos el sprite dependiendo de los botones ld a, [_PAD] ; cargamos el estado del pad and %00010000 ; derecha call nz, mueve_derecha ; si el resultado no es cero, habia un 1 ahi ; entonces llamamos a la subrutina ld a, [_PAD] and %00100000 ; izquierda call nz, mueve_izquierda ld a, [_PAD] and %01000000 ; arriba call nz, mueve_arriba ld a, [_PAD] and %10000000 ; abajo call nz, mueve_abajo ld a, [_PAD] and %00000001 ; Boton A call nz, cambia_paleta ; un pequeño retardo call retardo ; volvemos a empezar jr movimiento ; Rutinas de movimiento mueve_derecha: ld a, [_SPR0_X] ; obtenemos la posición actual cp 160 ; estamos en la esquina? ret z ; si estamos en la esquina, volvemos inc a ; avanzamos ld [_SPR0_X], a ; guardamos la posicion ret ; volvemos mueve_izquierda: ld a, [_SPR0_X] ; obtenemos la posición actual cp 8 ; estamos en la esquina? ret z ; si estamos en la esquina, volvemos dec a ; retrocedemos ld [_SPR0_X], a ; guardamos la posicion ret mueve_arriba: ld a, [_SPR0_Y] ; obtenemos la posición actual cp 16 ; estamos en la esquina? ret z ; si estamos en la esquina, volvemos dec a ; retrocedemos ld [_SPR0_Y], a ; guardamos la posicion ret mueve_abajo: ld a, [_SPR0_Y] ; obtenemos la posición actual cp 152 ; estamos en la esquina? ret z ; si estamos en la esquina, volvemos inc a ; avanzamos ld [_SPR0_Y], a ; guardamos la posicion ret cambia_paleta: ld a, [_SPR0_ATT] and %00010000 ; en el bit 4, está el numero de paleta jr z, .paleta0 ; si es cero, estaba seleccionada la paleta 0 ; si no, estaba seleccionada la paleta 1 ld a, [_SPR0_ATT] res 4, a ; ponemos a cero el bit 4, seleccionando la paleta 0 ld [_SPR0_ATT], a ; guardamos los atributos call retardo ; el cambio es muy rapido, vamos a esperar un poco ret ; volvemos .paleta0: ld a, [_SPR0_ATT] set 4, a ; ponemos a uno el bit 4, seleccionando la paleta 1 ld [_SPR0_ATT], a ; guardamos los atributos call retardo ret ; volvemos ; Rutina de lectura del pad lee_pad: ; vamos a leer la cruzeta: ld a, %00100000 ; bit 4 a 0, bit 5 a 1 (cruzeta activada, botones no) ld [rP1], a ; ahora leemos el estado de la cruzeta, para evitar el bouncing ; hacemos varias lecturas ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] and $0F ; solo nos importan los 4 bits de abajo. swap a ; intercambiamos parte baja y alta. ld b, a ; guardamos el estado de la cruzeta en b ; vamos a por los botones ld a, %00010000 ; bit 4 a 1, bit 5 a 0 (botones activados, cruzeta no) ld [rP1], a ; leemos varias veces para evitar el bouncing ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] ; tenemos en A, el estado de los botones and $0F ; solo nos importan los 4 bits de abajo. or b ; hacemos un or con b, para "meter" en la parte ; superior de A, el estado de la cruzeta. ; ahora tenemos en A, el estado de todo, hacemos el complemento y ; lo guardamos en la variable cpl ld [_PAD], a ; volvemos ret ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento .espera_VBlank ld a, [rLY] cp 145 jr nz, .espera_VBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos ; rutina de retardo retardo: ld de, 2000 ; numero de veces a ejecutar el bucle .delay: dec de ; decrementamos ld a, d ; vemos si es cero or e jr z, .fin_delay nop jr .delay .fin_delay: ret ; Datos de nuestros tiles Tiles: DB $AA, $00, $44, $00, $AA, $00, $11, $00 DB $AA, $00, $44, $00, $AA, $00, $11, $00 DB $3E, $3E, $41, $7F, $41, $6B, $41, $7F DB $41, $63, $41, $7F, $3E, $3E, $00, $00 EndTiles:
Hola Ventana
Ahora en el siguiente ejemplo vamos a hacer uso de la ventana, a modo de las típicas pantallas de “start” con datos que aparecen en algunos juegos. Juegos como el Zelda usan esta técnica. Simplemente, cargamos el otro mapa de tiles (recordad que habia dos mapas, uno en $9800-$9BFF, y otro en $9C00-$9FFF), con los tiles que queremos mostrar en la ventana, y le decimos a la ventana, que use este mapa para la ventana al inciciar el rLCDC. Pero además, para este ejemplo he añadido bastantes cosas. Para empezar he creado un mapa de fondo con varios tiles como si fuera una pantalla de un juego, y nos podremos mover a través de lo ancho del mapa usando los registros de scroll. Asi, cuando el sprite llegue a cierta posición no le dejaremos avanzar más, pero moveremos el scroll para que parezca que sigue avanzando más allá de la pantalla. Además voy a usar un sprite de 16×16. ¿Cómo? Pues sencillo, usando 4 sprites de 8×8 juntos y moviéndolos a la vez cuando sea necesario. Además haré uso de los atributos de los sprites haciéndoles un espejado dependiendo si se mueven a izquierda o derecha, asi puedo usar los mismos tiles para ambas direcciones. Otro truco que he usado es ir cambiando entre tres juegos de sprites para el personaje cuando se está moviendo. Así da la impresión de que se mueven sus piernas. Las subrutinas responsables son num_spr_mario, y camina_mario. Además la rutina sin_pulsaciones vuelve al sprite del personaje quieto cuando no se pulsa nada. En el código, podeis ver que he añadido por ejemplo las subrutinas CopiaMemoria y RellenaMemoria. Como estaba haciendo varias copias para rellenar los tiles en memoria, los mapas, limpiar los sprites, etc, pues las he dejado como subrutinas que esperan los datos en ciertos registros, asi podemos reutilizarlas. También he dejado los sprites en archivos aparte, asi puedo hacer modificaciones más fácilmente. Al final, veis que uso los includes entre dos etiquetas para delimitar los datos dentro de cada archivo.
Y nada, todo esto junto, a continuación:
- holaventana.asm
; Hola ventana ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; definimos unas constantes para trabajar con nuestros sprites _SPR0_Y EQU _OAMRAM ; la Y del sprite 0, es el inicio de la mem de sprites _SPR0_X EQU _OAMRAM+1 _SPR0_NUM EQU _OAMRAM+2 _SPR0_ATT EQU _OAMRAM+3 _SPR1_Y EQU _OAMRAM+4 _SPR1_X EQU _OAMRAM+5 _SPR1_NUM EQU _OAMRAM+6 _SPR1_ATT EQU _OAMRAM+7 _SPR2_Y EQU _OAMRAM+8 _SPR2_X EQU _OAMRAM+9 _SPR2_NUM EQU _OAMRAM+10 _SPR2_ATT EQU _OAMRAM+11 _SPR3_Y EQU _OAMRAM+12 _SPR3_X EQU _OAMRAM+13 _SPR3_NUM EQU _OAMRAM+14 _SPR3_ATT EQU _OAMRAM+15 ; VARIABLES ; variable donde guardar el estado del pad _PAD EQU _RAM ; al inicio de la ram interna ; variables de control de los sprites _POS_MAR_2 EQU _RAM+1 ; posición donde colocar los segundos sprites _SPR_MAR_SUM EQU _RAM+2 ; numero a sumar a los sprites para alternarlos ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom sin mapper, de 32K y sin RAM, lo más básico ; (como por ejemplo la del tetris) ROM_HEADER ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: ; iniciamos las variables ld hl, _POS_MAR_2 ; sprites mirando a la dcha. ld [hl], -8 ld hl, _SPR_MAR_SUM ; empezamos con el 0 ld [hl], 0 ; paletas ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta de fondo ld [rOBP0], a ; y en la paleta 0 de sprites ; creamos otra paleta para la paleta 2 de sprites, para mario ld a, %11010000 ld [rOBP1], a ; scroll ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. ; video call apaga_LCD ; llamamos a la rutina que apaga el LCD ; cargamos los tiles en la memoria de tiles ld hl, Tiles ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld bc, FinTiles-Tiles ; número de bytes a copiar call CopiaMemoria ; cargamos el mapa ld hl, Mapa ld de, _SCRN0 ; mapa 0 ld bc, 32*32 call CopiaMemoria ; cargamos el mapa para la ventana ld hl, Ventana ld de, _SCRN1 ; mapa 1 ld bc, 32*32 call CopiaMemoria ; bien, tenemos todo el mapa de tiles cargado ; ahora limpiamos la memoria de sprites ld de, _OAMRAM ; memoria de atributos de sprites ld bc, 40*4 ; 40 sprites x 4 bytes cada uno ld l, 0 ; lo vamos a poner todo a cero, asi los sprites call RellenaMemoria ; no usados quedan fuera de pantalla ; ahora vamos a crear los sprites. ld a, 136 ld [_SPR0_Y], a ; posición Y del sprite ld a, 80 ld [_SPR0_X], a ; posición X del sprite ld a, 0 ld [_SPR0_NUM], a ; número de tile en la tabla de tiles que usaremos ld a, 16 | 32 ld [_SPR0_ATT], a ; atributos especiales, paleta 1 ld a, 136+8 ld [_SPR1_Y], a ; posición Y del sprite ld a, 80 ld [_SPR1_X], a ; posición X del sprite ld a, 1 ld [_SPR1_NUM], a ; número de tile en la tabla de tiles que usaremos ld a, 16 | 32 ld [_SPR1_ATT], a ; atributos especiales, paleta 1 ld a, 136 ld [_SPR2_Y], a ; posición Y del sprite ld a, [_POS_MAR_2] add 80 ld [_SPR2_X], a ; posición X del sprite ld a, 2 ld [_SPR2_NUM], a ; número de tile en la tabla de tiles que usaremos ld a, 16 | 32 ld [_SPR2_ATT], a ; atributos especiales, paleta 1 ld a, 136+8 ld [_SPR3_Y], a ; posición Y del sprite ld a, [_POS_MAR_2] add a, 80 ld [_SPR3_X], a ; posición X del sprite ld a, 3 ld [_SPR3_NUM], a ; número de tile en la tabla de tiles que usaremos ld a, 16 | 32 ld [_SPR3_ATT], a ; atributos especiales, paleta 1 ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8|LCDCF_OBJON|LCDCF_WIN9C00 ld [rLCDC], a ; bucle principal movimiento: ; leemos el pad call lee_pad ; lo primero, esperamos por el VBlank, ya que no podemos modificar ; la VRAM fuera de él, o pasarán cosas raras .wait: ld a, [rLY] cp 145 jr nz, .wait ; Ahora movemos el sprite dependiendo de los botones ld a, [_PAD] ; cargamos el estado del pad and %00010000 ; derecha call nz, mueve_derecha ; si el resultado no es cero, habia un 1 ahi ; entonces llamamos a la subrutina ld a, [_PAD] and %00100000 ; izquierda call nz, mueve_izquierda ld a, [_PAD] and %01000000 ; arriba ;call nz, mueve_arriba ld a, [_PAD] and %10000000 ; abajo ;call nz, mueve_abajo ld a, [_PAD] and %00001000 ; Boton START call nz, muestra_ventana ld a, [_PAD] and %11111111 call z, sin_pulsaciones ; un pequeño retardo ld bc, 2000 call retardo ; volvemos a empezar jr movimiento ; Rutinas de movimiento mueve_derecha: ld a, [_SPR0_X] ; obtenemos la posición actual cp 120 ; estamos en la esquina? jp nz, .ad ; si no estamos en la esquina, avanzamos ld a, [rSCX] ; si estamos al borde, movemos el scroll inc a ld [rSCX], a ; modificamos los sprites call num_spr_mario call camina_mario ret .ad: ; los segundos sprites deben estar detras de los primeros push af ld a, -8 ld [_POS_MAR_2], a pop af ; movimiento inc a ; avanzamos ld [_SPR0_X], a ; guardamos la posicion ld [_SPR1_X], a ld hl, _POS_MAR_2 ; desplazamiento a los primeros add a, [hl] ; sumamos ld [_SPR2_X], a ; guardamos ld [_SPR3_X], a ; derecha, por lo tanto, los sprites deben estar reflejados horizontalmente ld a, [_SPR0_ATT] set 5, a ld [_SPR0_ATT], a ld [_SPR1_ATT], a ld [_SPR2_ATT], a ld [_SPR3_ATT], a ; modificamos los prites call num_spr_mario call camina_mario ret ; volvemos mueve_izquierda: ld a, [_SPR0_X] ; obtenemos la posición actual cp 16 ; estamos en la esquina? jp nz, .ai ; si no estamos en la esquina, avanzamos ld a, [rSCX] ; si estamos, scroll dec a ld [rSCX], a ; modificamos los sprites call num_spr_mario call camina_mario ret .ai: ; los segundos sprites deben estar delante de los primeros push af ld a, 8 ld [_POS_MAR_2], a pop af ; movimiento dec a ; retrocedemos ld [_SPR0_X], a ; guardamos la posicion ld [_SPR1_X], a ld hl, _POS_MAR_2 ; desplazamiento a los primeros add a, [hl] ; sumamos ld [_SPR2_X], a ; guardamos ld [_SPR3_X], a ; izquierda, por lo tanto, los sprites deben estar reflejados horizontalmente ld a, [_SPR0_ATT] res 5, a ld [_SPR0_ATT], a ld [_SPR1_ATT], a ld [_SPR2_ATT], a ld [_SPR3_ATT], a ; modificamos los sprites call num_spr_mario call camina_mario ret ; volvemos mueve_arriba: ld a, [_SPR0_Y] ; obtenemos la posición actual cp 16 ; estamos en la esquina? ret z ; si estamos en la esquina, volvemos dec a ; retrocedemos ld [_SPR0_Y], a ; guardamos la posicion ret mueve_abajo: ld a, [_SPR0_Y] ; obtenemos la posición actual cp 152 ; estamos en la esquina? ret z ; si estamos en la esquina, volvemos inc a ; avanzamos ld [_SPR0_Y], a ; guardamos la posicion ret muestra_ventana: ld a, 8 ld [rWX], a ld a, 144 ld [rWY], a ;activamos la ventana y desactivamos los sprites ld a, [rLCDC] or LCDCF_WINON res 1, a ld [rLCDC], a ; animacion ld a, 144 .anim_most_vent: push af ld bc, 1000 call retardo pop af dec a ld [rWY], a jr nz, .anim_most_vent ; esperamos a que pulsen select para salir .espera_salir: call lee_pad and %00001000 ; Boton START jr z, .espera_salir; .anim_ocul_vent: push af ld bc, 1000 call retardo pop af inc a ld [rWY], a cp 144 jr nz, .anim_ocul_vent ;desactivamos la ventana y activamos los sprites ld a, [rLCDC] res 5, a or LCDCF_OBJON ld [rLCDC], a ret ; volvemos ; si no se pulsa nada, se viene aqui para modificar el sprite por el mario ; estático sin_pulsaciones: ld hl, _SPR_MAR_SUM; empezamos con el 0 ld [hl], 0 ld a, 0 ld [_SPR0_NUM], a inc a ld [_SPR1_NUM], a inc a ld [_SPR2_NUM], a inc a ld [_SPR3_NUM], a ret ; modifica la variable para cambiar los sprites de mario caminando num_spr_mario: ld a, [_SPR_MAR_SUM] add 4 ; sumamos 4 ld [_SPR_MAR_SUM], a ; lo guardamos cp 12 ; menor que 12? ret nz ; volvemos ld a, 4 ; 12? entonces volvemos al 4 ld [_SPR_MAR_SUM], a ret camina_mario: ld a, [_SPR_MAR_SUM] ld [_SPR0_NUM], a inc a ld [_SPR1_NUM], a inc a ld [_SPR2_NUM], a inc a ld [_SPR3_NUM], a ret ; Rutina de lectura del pad lee_pad: ; vamos a leer la cruzeta: ld a, %00100000 ; bit 4 a 0, bit 5 a 1 (cruzeta activada, botones no) ld [rP1], a ; ahora leemos el estado de la cruzeta, para evitar el bouncing ; hacemos varias lecturas ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] and $0F ; solo nos importan los 4 bits de abajo. swap a ; intercambiamos parte baja y alta. ld b, a ; guardamos el estado de la cruzeta en b ; vamos a por los botones ld a, %00010000 ; bit 4 a 1, bit 5 a 0 (botones activados, cruzeta no) ld [rP1], a ; leemos varias veces para evitar el bouncing ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] ; tenemos en A, el estado de los botones and $0F ; solo nos importan los 4 bits de abajo. or b ; hacemos un or con b, para "meter" en la parte ; superior de A, el estado de la cruzeta. ; ahora tenemos en A, el estado de todo, hacemos el complemento y ; lo guardamos en la variable cpl ld [_PAD], a ; volvemos ret ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento .espera_VBlank ld a, [rLY] cp 145 jr nz, .espera_VBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos ; rutina de retardo ; parámetros ; bc - numero de iteraciones retardo: .delay: dec bc ; decrementamos ld a, b ; vemos si es cero or c jr z, .fin_delay nop jr .delay .fin_delay: ret ; rutina de copia a memoria ; copia un numero de bytes de una direccion a otra ; espera los parámetros: ; hl - dirección de datos a copiar ; de - dirección de destino ; bc - numero de datos a copiar ; destruye el contenido de A CopiaMemoria: ld a, [hl] ; cargamos el dato en A ld [de], a ; copiamos el dato al destino dec bc ; uno menos por copiar ; comprobamos si bc es cero ld a, c or b ret z ; si es cero, volvemos ; si no, seguimos inc hl inc de jr CopiaMemoria ; rutina de relleno de memoria ; rellena un numero de bytes de memoria con un dato ; espera los parámetros: ; de - direccion de destino ; bc - número de datos a rellenar ; l - dato a rellenar RellenaMemoria: ld a, l ld [de], a ; mete el dato en el destino dec bc ; uno menos a rellenar ld a, c or b ; comprobamos si bc es cero ret z ; si es cero volvemos inc de ; si no, seguimos jr RellenaMemoria Tiles: INCLUDE "mario_sprites.z80" FinTiles: Mapa: INCLUDE "mapa_mario.z80" FinMapa: Ventana: INCLUDE "ventana.z80" FinVentana:
Y los datos,
mario_sprites.z80:
- mario_sprites.z80
DB $00,$00,$0F,$0F,$1B,$14,$7F,$7F DB $1B,$14,$1B,$1E,$31,$2F,$3F,$3F DB $10,$1F,$1F,$1F,$3F,$2A,$1F,$15 DB $1F,$1F,$07,$07,$09,$0F,$0F,$0F DB $00,$00,$00,$00,$E0,$E0,$F0,$10 DB $F0,$F0,$30,$F0,$68,$F8,$88,$F8 DB $70,$F0,$F8,$C8,$F8,$68,$B8,$D8 DB $98,$F8,$F0,$F0,$10,$F0,$F0,$F0 DB $00,$00,$0F,$0F,$1B,$14,$7F,$7F DB $1B,$14,$1B,$1E,$31,$2F,$3F,$3F DB $10,$1F,$0F,$0F,$1E,$1B,$1E,$17 DB $1F,$1F,$0F,$0F,$11,$1F,$1F,$1F DB $00,$00,$00,$00,$E0,$E0,$F0,$10 DB $F0,$F0,$30,$F0,$68,$F8,$88,$F8 DB $70,$F0,$F0,$90,$F8,$48,$74,$FC DB $F4,$FC,$F4,$FC,$0C,$0C,$00,$00 DB $00,$00,$0F,$0F,$1B,$14,$7F,$7F DB $1B,$14,$1B,$1E,$31,$2F,$3F,$3F DB $10,$1F,$6F,$6F,$5F,$7A,$5F,$75 DB $5F,$7F,$7F,$7F,$00,$00,$00,$00 DB $00,$00,$00,$00,$E0,$E0,$F0,$10 DB $F0,$F0,$30,$F0,$68,$F8,$88,$F8 DB $70,$F0,$F8,$C8,$F8,$68,$B8,$D8 DB $98,$F8,$E8,$F8,$90,$F0,$E0,$E0 DB $00,$00,$00,$00,$00,$00,$00,$00 DB $00,$00,$00,$00,$00,$00,$00,$00 DB $FF,$00,$FF,$00,$FF,$00,$FF,$00 DB $FF,$00,$FF,$00,$FF,$00,$FF,$00 DB $FF,$FF,$FF,$00,$FF,$88,$FF,$00 DB $FF,$20,$FF,$02,$FF,$80,$FF,$22 DB $FF,$00,$C3,$3C,$81,$7E,$B9,$46 DB $81,$7E,$81,$7E,$9D,$62,$81,$7E DB $FF,$00,$FE,$01,$F8,$07,$F0,$0F DB $E0,$1F,$C0,$3F,$80,$7F,$00,$FF DB $FF,$00,$00,$FF,$00,$FF,$00,$FF DB $00,$FF,$00,$FF,$00,$FF,$00,$FF DB $FF,$00,$1F,$E0,$0F,$F0,$07,$F8 DB $03,$FC,$01,$FE,$01,$FE,$01,$FE DB $F8,$00,$97,$00,$6E,$00,$7D,$00 DB $7F,$00,$7F,$00,$BB,$00,$C4,$00 DB $33,$00,$CD,$00,$FE,$00,$FE,$00 DB $FA,$00,$F6,$00,$FD,$00,$03,$00 DB $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF DB $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF DB $FF,$FF,$FF,$FF,$FF,$FF,$E0,$E0 DB $EF,$EF,$EF,$EF,$EF,$EF,$EF,$EF DB $FF,$FF,$FF,$FF,$FF,$FF,$00,$00 DB $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF DB $FF,$FF,$FF,$FF,$FF,$FF,$07,$07 DB $F7,$F7,$F7,$F7,$F7,$F7,$F7,$F7 DB $F7,$F7,$F7,$F7,$F7,$F7,$F7,$F7 DB $F7,$F7,$F7,$F7,$F7,$F7,$F7,$F7 DB $F7,$F7,$F7,$F7,$F7,$F7,$F7,$F7 DB $07,$07,$FF,$FF,$FF,$FF,$FF,$FF DB $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF DB $00,$00,$FF,$FF,$FF,$FF,$FF,$FF DB $EF,$EF,$EF,$EF,$EF,$EF,$EF,$EF DB $E0,$E0,$FF,$FF,$FF,$FF,$FF,$FF DB $EF,$EF,$EF,$EF,$EF,$EF,$EF,$EF DB $EF,$EF,$EF,$EF,$EF,$EF,$EF,$EF DB $FF,$FF,$BD,$BD,$BD,$BD,$DB,$DB DB $DB,$DB,$E7,$E7,$E7,$E7,$FF,$FF DB $FF,$FF,$83,$83,$BF,$BF,$8F,$8F DB $BF,$BF,$BF,$BF,$83,$83,$FF,$FF DB $FF,$FF,$9D,$9D,$AD,$AD,$AD,$AD DB $B5,$B5,$B5,$B5,$B9,$B9,$FF,$FF DB $FF,$FF,$83,$83,$EF,$EF,$EF,$EF DB $EF,$EF,$EF,$EF,$EF,$EF,$FF,$FF DB $FF,$FF,$E7,$E7,$DB,$DB,$DB,$DB DB $81,$81,$BD,$BD,$BD,$BD,$FF,$FF
mapa_mario.z80
- mapa_mario.z80
DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$13,$14,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$13,$14,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$13,$14,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$13,$14 DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$13,$14,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0F,$0D,$10,$11 DB $11,$12,$0D,$0F,$0D,$0D,$0D,$10,$11,$12 DB $0D,$0D,$0D,$0D,$0F,$0D,$0F,$0D,$0D,$10 DB $11,$11,$12,$0D,$0E,$0E,$0E,$0E,$0E,$0E DB $0E,$0E,$0E,$0E,$0E,$0E,$0E,$0E,$0E,$0E DB $0E,$0E,$0E,$0E,$0E,$0E,$0E,$0E,$0E,$0E DB $0E,$0E,$0E,$0E,$0E,$0E,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D,$0D DB $0D,$0D,$0D,$0D
y ventana.z80
- ventana.z80
DB $16,$17,$17,$17,$17,$17,$17,$17,$17,$17 DB $17,$17,$17,$17,$17,$17,$17,$17,$17,$18 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$1D,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$19,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$1D,$15,$16,$17,$17,$17 DB $17,$17,$17,$17,$17,$17,$17,$17,$17,$17 DB $17,$18,$15,$19,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$1D,$15,$1D,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$19,$15,$19,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$1D,$15 DB $1D,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$19,$15,$19,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $1D,$15,$1D,$15,$15,$15,$1E,$1F,$20,$21 DB $22,$20,$22,$15,$15,$15,$15,$19,$15,$19 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$1D,$15,$1D,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$19 DB $15,$19,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$1D,$15,$1D,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$19,$15,$19,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$1D,$15,$1D,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$19,$15,$19,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$1D,$15 DB $1D,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$19,$15,$19,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $1D,$15,$1D,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$19,$15,$19 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$1D,$15,$1D,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$19 DB $15,$19,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$1D,$15,$1D,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$19,$15,$19,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$1D,$15,$1D,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$19,$15,$19,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$1D,$15 DB $1D,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$19,$15,$19,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $1D,$15,$1C,$1B,$1B,$1B,$1B,$1B,$1B,$1B DB $1B,$1B,$1B,$1B,$1B,$1B,$1B,$1A,$15,$19 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$1D,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$19,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$1C,$1B,$1B,$1B,$1B,$1B DB $1B,$1B,$1B,$1B,$1B,$1B,$1B,$1B,$1B,$1B DB $1B,$1B,$1B,$1A,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15,$15,$15,$15,$15,$15,$15 DB $15,$15,$15,$15
El resultado:
Para generar los gráficos de este ejemplo. he usado el GBTD (Gameboy Tile Designer) y el GBMB (Gameboy Map Builder), dos estupendas herramientas que nos permiten diseñar tiles y mapas de forma visual, y luego generar los datos listos para ser importados en nuestros programas asm de RGBDS, C de GBDK, etc. Además funcionan perfectamente en Wine, por si quereis usarlos desde Linux (yo mismo los uso asi). Podeis descargarlos de:
http://www.devrs.com/gb/hmgd/gbtd.html
http://www.devrs.com/gb/hmgd/gbmb.html
El GBTD, es muy sencillo de usar, vais seleccionando tiles a la derecha, y los vais dibujando en la rejilla, con algunas herramientas típicas como pincel, cubo de relleno, etc, y otras no tan típicas como desplazamientos, etc. Además podeis seleccionar modos más grnades (por ej 16×16), que al ponerlo luego de nuevo en modo 8×8, vereis como se alinean los tiles en memoria, para usarlos. Una vez diseñado vuestro juego de tiles, lo grabais, y le dais a “Export To” en el menu “File”. Ahi es sencillo, poneis un nombre de archivo, el tipo “RGBDS Assembly File”, un nombre de Etiqueta (Label), y una sección, y en From y To, poneis los rangos de sprites a exportar, si habeis dibujado 10 sprites, pues poneis del 0 al 9, y activais la checkbox de “Export Tiles as one unit”. Le dais a exportar, y ya teneis vuestro fichero .z80 con los datosa de los tiles. Yo normalmente limpio todo el fichero y sólo dejo los DB con los datos, ya que los suelo incluir entre mis propias etiquetas y demás, y a veces las definiciones de bancos y extras que añade el GBTD no hacen más que estorbar.
Luego el GBMB, es también muy sencillo de usar. Lo primero al abrirlo es ir a “Map Properties” en el menú “File”, y ahi seleccionar el alto y ancho del mapa, para estos ejemplos, yo siempre uso 32×32. El tema es que aunque podria crear un mapa de por ejemplo, 20×18 para la parte visible de la pantalla, a la hora de cargarlo en memoria, tendria que ir haciendo los “saltos”, quiero decir, tendria que llenar 20 bytes de la memoria de mapa, luego saltarme 12 (para completar los 32, llenar otros 20, saltarme 12, etc. Esto no es difícil, pero de momento en los demos no necesitamos ahorrar memoria, y asi podemos usar las subrutinas que ya hemos definido. Luego hay que seleccionar un archivo de tiles creado con el GBTD. Le dais a Browse y seleccionais el que habeis creado. Le dais a “OK” y ahora tendreis vuestro mapa editable, y a la derecha la lista de tiles que habeis creado. Pues nada, ahora es cuestión de ir creando el mapa, rellenándolo con los tiles que teneis disponibles. Una vez que tengais el mapa listo, vamos a “Export To” del menu “File”. Como anteriormente, seleccionais un nommbre de archivo, tipo “RGBDS Assembly File”, un nombre de etiqueta, una sección (0 por ej), y entonces vais a la segunda pestaña la de “Location Format”. Ahi definiremos como se guarda en memoria el mapa. Primero en el cuadro grande, seleccionais la primera propiedad (sale con un 1), y seleccionais ”[Tile Number], y la poneis de 8 bits. Luego a la derecha, seleccionais, “Map Layout”: “Rows”, “Plane count”: “1 plane, 8 bits” y el resto como está, “Tiles are continues” y Offset 0. Le dais a exportar y ya teneis los datos listos. Como en el archivo de tiles, es mejor limpiar y dejar sólo los DB's correspondientes.
Hola Timer
Para este ejemplo, vamos a usar el Timer interno de la GameBoy, que nos ayudará a controlar el tiempo de manera precisa. Además haciendo uso de las interrupciones, no tendremos que preocuparnos de llevar nosotros el control, dejaremos que cada vez que se produzca una interrupción del timer, una subrutina se preocupe de controlar el tiempo.
El timer consta de 3 registros. rTAC, rTMA y rTIMA.
rTAC, es el registro de control del timer, nos permite activarlo o desactivarlo, y también ajustar la frecuencia de funcionamiento. El bit 2 de este registro, activa o desactiva el timer (1=activado), y los bits 0 y 1, nos permiten ajustar su frecuencia, según la siguiente tabla:
00: 4096 Hz (~4194 Hz SGB) 01: 262144 Hz (~268400 Hz SGB) 10: 65536 Hz (~67110 Hz SGB) 11: 16384 Hz (~16780 Hz SGB)
Vamos a usar la frecuencia de 4096Hz, con lo que tendremos que el timer se activa cada 1/4096 = 0,0002414 segundos.
Cada vez que el timer se activa, incrementa el contenido del registro rTIMA en una unidad. rTIMA es un registro de 8 bits, asi que puede contar de 0 a 255. Cuando el registro rTIMA se desborda (pasa de 255 a 0 de nuevo), se genera una interrupción del timer, que si está activada, la CPU pasará el control del programa a la subrutina de manejo de interrupción del timer en la dirección $0050.
El tercer registro, es rTMA, y es el valor inicial que se carga en rTIMA después de un desbordamiento, asi cuando rTIMA llegue a 255, al desbordarse, genera la interrupción, y entonces empieza a contar de nuevo desde el valor contenido en rTMA, esto nos permite ajustar cada cuanto tiempo se genera la interrupción más facilmente.
Asi, para este programa, un cronómetro, lo ideal sería poder contar de 1 en 1 segundos. Entonces, si tenemos que la frecuencia es 4096Hz, sabemos que rTIMA se incrementa cada 1/4096 = 0,0002414 segundos. Ahora si ponemos el valor de inicio de rTIMA en 51, tenemos, (1/4096)*(255-51) = (1/4096) * 204 = 0,049 s. Entonces sabemos que se generará una interrupción cada aproximadamente 0.05 segundos. Si ahora en la rutina de interrupción, le hacemos contar las veces que se le llama, y esperar hasta que se la llame 20 veces, tenemos 0.05 * 20 = 1 Segundo.
Además para este programa, vamos a usar otra interrupción, la interrupción de vBlank, que se genera siempre que el LCD entre en el periodo de intervalo vertical. Llamando desde ahi, a la rutina de dibujado, estaremos siempre seguros de que el dibujado se producirá mientras estemos en vBlank
Os dejo con el código, podeis poner en marcha o para el cronómetro con el botón A, y resetearlo a cero con B:
- holatimer.asm
; Hola Timer ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; Variables _CONTROL_TIEMPO EQU _RAM ; controla los milisegundos _ACTIVADO EQU _RAM+1 ; cronómetro activado o no _SEGUNDOS EQU _RAM+2 _MINUTOS EQU _RAM+3 _HORAS EQU _RAM+4 _PAD EQU _RAM+5 ; Constantes _POS_CRONOM EQU _SCRN0+32*4+6 ; posición en pantalla ; interrupción de vBlank SECTION "Vblank",ROM0[$0040] call DibujaCronometro reti ; interrupción de desbordamiento del timer SECTION "Timer_Overflow",ROM0[$0050] ; cuando hay una interrupción del timer, llamamos a esta subrutina call ControlTimer reti ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom sin mapper, de 32K y sin RAM, lo más básico ; (como por ejemplo la del tetris) ROM_HEADER ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: ; iniciamos el timer ld a, 0 ld [rTAC], a ; timer apagado, divider a 00 (4096 Hz) ld a, 51 ld [rTMA], a ; cuando TIMA se desborde, este es ; su valor de reinicio, (1/4096)*(255-51) = 0.049 s ld [rTIMA], a ; valor inicial del timer. ; iniciamos las variables ld a, 0 ld [_CONTROL_TIEMPO], a ld [_ACTIVADO], a ld [_SEGUNDOS], a ld [_MINUTOS], a ld [_HORAS], a ; paletas ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta de fondo ld [rOBP0], a ; y en la paleta 0 de sprites ; scroll ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. ; video call apaga_LCD ; llamamos a la rutina que apaga el LCD ; cargamos los tiles en la memoria de tiles ld hl, Tiles ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld bc, FinTiles-Tiles ; número de bytes a copiar call CopiaMemoria ; limpiamos el mapa ld de, _SCRN0 ; mapa 0 ld bc, 32*32 ld l, 11 ; tile vacio call RellenaMemoria ; bien, tenemos todo el mapa de tiles cargado ; ahora limpiamos la memoria de sprites ld de, _OAMRAM ; memoria de atributos de sprites ld bc, 40*4 ; 40 sprites x 4 bytes cada uno ld l, 0 ; lo vamos a poner todo a cero, asi los sprites call RellenaMemoria ; no usados quedan fuera de pantalla ; dibujamos el cronómetro call DibujaCronometro ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8 ld [rLCDC], a ; bucle principal control: ; leemos el pad call lee_pad ; Ahora activamos o desactivamos el cronometro ld a, [_PAD] and %00000001 ; Boton A call nz, Activa ; Resetear ld a, [_PAD] and %00000010 ; Boton B call nz, Resetea ld bc, 15000 call retardo ; volvemos a empezar jr control ; Rutina de control del cronometro Activa: ld a, [_ACTIVADO] cp 1 jp z, .desactiva ; si esta activado, desactivamos ; si no, activamos ld a, 1 ld [_ACTIVADO], a ld a, %00000100 ; timer activado ld [rTAC], a ld a, %00000101 ; interrupciones del timer y vBlank ld [rIE], a ei ; activamos las interrupciones ret ; volvemos .desactiva ld a, 0 ld [_ACTIVADO], a ld a, %00000000 ; timer desactivado ld [rTAC], a ld a, %00000101 ; interrupciones del timer y vBlank ld [rIE], a di ; desactivamos las interrupciones ret ; Resetea el cronometro Resetea: ld a, 0 ld [_SEGUNDOS], a ld [_MINUTOS], a ld [_HORAS], a ld a, 51 ; valor inicial del timer ld [rTIMA], a ; miramos si está activado ld a, [_ACTIVADO] ret z ; si no lo está, redibujamos call EsperaVBlank call DibujaCronometro ret DibujaCronometro: ; decenas de horas ld a, [_HORAS] and $F0 swap a ld [_POS_CRONOM], a ; horas ld a, [_HORAS] and $0F ld [_POS_CRONOM+1], a ; : ld a, 10 ld [_POS_CRONOM+2], a ; decenas de minutos ld a, [_MINUTOS] and $F0 swap a ld [_POS_CRONOM+3], a ; minutos ld a, [_MINUTOS] and $0F ld [_POS_CRONOM+4], a ; : ld a, 10 ld [_POS_CRONOM+5], a ; decenas de segundos ld a, [_SEGUNDOS] and $F0 swap a ld [_POS_CRONOM+6], a ; segundos ld a, [_SEGUNDOS] and $0F ld [_POS_CRONOM+7], a ret ; Controla el tiempo ControlTimer: ld a, [_CONTROL_TIEMPO] cp 20 ; cada 20 interrupciones, pasa 1 seg jr z, .incrementa inc a ; si no, incrementamos y volvemos ld [_CONTROL_TIEMPO], a ret .incrementa ; reseteamos el contador ld a, 0 ld [_CONTROL_TIEMPO], a ; incrementamos los segundos ld a, [_SEGUNDOS] inc a daa cp 96 ; han pasado 60 segundos? (96 porque usamos BCD) jr z, .minutos ; si, a controlar los minutos ld [_SEGUNDOS], a ; no, guardamos y volvemos ret .minutos ld a, 0 ld [_SEGUNDOS], a ; incrementar el minuto, segundos a 0 ld a, [_MINUTOS] inc a daa cp 96 ; han pasado 60 minutos? jr z, .horas ; si, a controlar las horas ld [_MINUTOS], a ; no, guardamos y volvemos ret .horas ld a, 0 ld [_MINUTOS], a ; incrementar el minuto, segundos a 0 ld a, [_HORAS] inc a daa cp 36 ; han pasado 24 horas? (36 equivale a 24 en BCD) jr z, .reset ; si, a volver a empezar ld [_HORAS], a ; no, guardamos y volvemos ret .reset call Resetea ret ; Rutina de lectura del pad lee_pad: ; a cero ld a, 0 ld [_PAD], a ; vamos a leer la cruzeta: ld a, %00100000 ; bit 4 a 0, bit 5 a 1 (cruzeta activada, botones no) ld [rP1], a ; ahora leemos el estado de la cruzeta, para evitar el bouncing ; hacemos varias lecturas ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] and $0F ; solo nos importan los 4 bits de abajo. swap a ; intercambiamos parte baja y alta. ld b, a ; guardamos el estado de la cruzeta en b ; vamos a por los botones ld a, %00010000 ; bit 4 a 1, bit 5 a 0 (botones activados, cruzeta no) ld [rP1], a ; leemos varias veces para evitar el bouncing ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] ; tenemos en A, el estado de los botones and $0F ; solo nos importan los 4 bits de abajo. or b ; hacemos un or con b, para "meter" en la parte ; superior de A, el estado de la cruzeta. ; ahora tenemos en A, el estado de todo, hacemos el complemento y ; lo guardamos en la variable cpl ld [_PAD], a ; reseteamos el pad ld a,$30 ld [rP1], a ; volvemos ret ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento call EsperaVBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos EsperaVBlank: ld a, [rLY] cp 145 jr nz, EsperaVBlank ret ; rutina de retardo ; parámetros ; bc - numero de iteraciones retardo: .delay: dec bc ; decrementamos ld a, b ; vemos si es cero or c jr z, .fin_delay nop jr .delay .fin_delay: ret ; rutina de copia a memoria ; copia un numero de bytes de una direccion a otra ; espera los parámetros: ; hl - dirección de datos a copiar ; de - dirección de destino ; bc - numero de datos a copiar ; destruye el contenido de A CopiaMemoria: ld a, [hl] ; cargamos el dato en A ld [de], a ; copiamos el dato al destino dec bc ; uno menos por copiar ; comprobamos si bc es cero ld a, c or b ret z ; si es cero, volvemos ; si no, seguimos inc hl inc de jr CopiaMemoria ; rutina de relleno de memoria ; rellena un numero de bytes de memoria con un dato ; espera los parámetros: ; de - direccion de destino ; bc - número de datos a rellenar ; l - dato a rellenar RellenaMemoria: ld a, l ld [de], a ; mete el dato en el destino dec bc ; uno menos a rellenar ld a, c or b ; comprobamos si bc es cero ret z ; si es cero volvemos inc de ; si no, seguimos jr RellenaMemoria Tiles: ; números de 0 al 9, formato de tiles del RGBDS ; 0 DW `00000000 DW `00333300 DW `03000330 DW `03003030 DW `03030030 DW `03300030 DW `00333300 DW `00000000 ; 1 DW `00000000 DW `00003000 DW `00033000 DW `00003000 DW `00003000 DW `00003000 DW `00333300 DW `00000000 ; 2 DW `00000000 DW `00333300 DW `03000030 DW `00003300 DW `00030000 DW `00300000 DW `03333330 DW `00000000 ; 3 DW `00000000 DW `00333300 DW `03000030 DW `00003300 DW `00000030 DW `03000030 DW `00333300 DW `00000000 ; 4 DW `00000000 DW `00000300 DW `00003300 DW `00030300 DW `00333300 DW `00000300 DW `00000300 DW `00000000 ; 5 DW `00000000 DW `03333330 DW `03000000 DW `00333300 DW `00000030 DW `03000030 DW `00333300 DW `00000000 ; 6 DW `00000000 DW `00003000 DW `00030000 DW `00300000 DW `03333300 DW `03000030 DW `00333300 DW `00000000 ; 7 DW `00000000 DW `03333330 DW `00000300 DW `00003000 DW `00030000 DW `00300000 DW `00300000 DW `00000000 ; 8 DW `00000000 DW `00333300 DW `03000030 DW `00333300 DW `03000030 DW `03000030 DW `00333300 DW `00000000 ; 9 DW `00000000 DW `00333300 DW `03000030 DW `03000030 DW `00333300 DW `00003000 DW `00330000 DW `00000000 ; : DW `00000000 DW `00033000 DW `00033000 DW `00000000 DW `00033000 DW `00033000 DW `00000000 DW `00000000 ; tile vacio DW `00000000 DW `00000000 DW `00000000 DW `00000000 DW `00000000 DW `00000000 DW `00000000 DW `00000000 FinTiles:
ROM: hola-timer.gb
Resultado:
Explicación
Me han comentado que estaría bien que explicara un par de funciones, asi que vamos a ello:
Primero voy a explicar ControlTimer, porque nos ayudará a explicar mejor la otra.
; Controla el tiempo ControlTimer: ld a, [_CONTROL_TIEMPO] cp 20 ; cada 20 interrupciones, pasa 1 seg jr z, .incrementa inc a ; si no, incrementamos y volvemos ld [_CONTROL_TIEMPO], a ret
Como explico más arriba, tenemos que con el timer a 4096Hz y con el timer con valor inicial 51, se llama a la interrupción del timer cada aprox 0.05 segundos, asi que para contar de 1 en 1 segundos para el cronómetro, lo que hacemos aqui es contar las veces que se llama a esta subrutina, si se la ha llamado 20 veces, han pasado 0.05 * 20 = 1 segundo, asi que vamos a modificar el tiempo, si no, incrementamos el contador y volvemos.
.incrementa ; reseteamos el contador ld a, 0 ld [_CONTROL_TIEMPO], a ; incrementamos los segundos ld a, [_SEGUNDOS] inc a daa cp 96 ; han pasado 60 segundos? (96 porque usamos BCD) jr z, .minutos ; si, a controlar los minutos ld [_SEGUNDOS], a ; no, guardamos y volvemos ret
Bien, ahora vamos a incrementar el tiempo ya que ha pasado 1 segundo, primero reseteamos el contador de veces que se llama a la función, porque como han pasado 20, tenemos que volver a contar desde cero.
Luego vamos a aumentar los segundos. Cargamos en A los segundos, y le sumamos uno, entonces tenemos que ver si el resultado es mayor que 60, porque si es asi, tenemos que poner los segundos a cero, y sumar un minuto. Pero veis que uso daa. daa es una instrucción, que lo que hace es convertir el contenido de A a BCD (Binary Coded Decimal), si conoceis BCD, los numeros binarios en lugar de guardarse tal cual, se guardan en un byte de manera que cada 4 bits representan un numero del 0 al 9, y no del 0 al 15 como podria ser en base 2 normal. Esto es muy útil porque aunque con un byte sólo podemos guardar de 0 a 99, en cada medio byte tengo unidades y decenas respectivamente del número de segundos/minutos/horas, y esto nos será muy útil para luego dibujarlos. Asi que convierto el contenido de A en BCD y lo comparo con 60 (que en BCD es 0110 (6) y 0000 (0), 01100000 y eso es 96 representado en binario normal, que es lo que entiende el procesador al comparar, que el contenido en binario sea igual, por eso comparo con 96), si tenemos mas de 60 segundos, salto a aumentar los minutos, si no, guardo el resultado en los segundos, y vuelvo.
.minutos ld a, 0 ld [_SEGUNDOS], a ; incrementar el minuto, segundos a 0 ld a, [_MINUTOS] inc a daa cp 96 ; han pasado 60 minutos? jr z, .horas ; si, a controlar las horas ld [_MINUTOS], a ; no, guardamos y volvemos ret .horas ld a, 0 ld [_MINUTOS], a ; incrementar el minuto, segundos a 0 ld a, [_HORAS] inc a daa cp 36 ; han pasado 24 horas? (36 equivale a 24 en BCD) jr z, .reset ; si, a volver a empezar ld [_HORAS], a ; no, guardamos y volvemos ret .reset call Resetea ret
Y aqui es casi lo mismo para los minutos y las horas. Para los minutos, lo primero que hago es poner los segundos a cero (59–>0) y luego incremento los minutos y hago lo mismo que con los segundos, comprobar si han pasado 60 y en caso afrimativo ir a aumentar las horas, y si no volver. Y con las horas igual, sólo que si llegan a pasar 24 horas, reseteo, pongo todo a cero, y ale, a volver a empezar.
Y ahora explico la rutina que dibuja el cronómetro, que es muy sencilla. Al inicio he definido una constante llamada _POS_CRONOM, tal que: _SCRN0+32*4+6. ¿Que significa eso? _SCRN0 era la dirección en memoria del mapa de fondo, donde se guarda que tile pintar en cada posición de los 32*32 tiles de fondo. Si a eso le sumo 32*4, obtengo, 32 bytes más adelante * 4 = La cuarta linea, y si le sumo 6, pues la cuarta linea de la pantalla, más 6 posiciones, el punto donde empezaré a dibujar el cronómetro. Y a partir de aqui es muy sencillo. Tengo que empezar a dibujar por las decenas de horas, luego las horas, luego los dos puntos, luego decenas de minutos… etc. Pues viendo el principio de la rutina:
; decenas de horas ld a, [_HORAS] and $F0 swap a ld [_POS_CRONOM], a ; horas ld a, [_HORAS] and $0F ld [_POS_CRONOM+1], a ; : ld a, 10 ld [_POS_CRONOM+2], a
Cargo en el registro A, el contenido de la variable horas, como está guardada en BCD, sé que en el medio byte superior tengo las decenas de horas y en el inferior las unidades. Asi que para dibujar las decenas, hago un and de su contenido con $F0, que es %11110000, asi que esto borra el contenido de la media parte baja, dejando igual la media parte alta, y entonces haciendo swap, cambio la media parte alta por la media baja, con lo que ahora tengo en A, sólo el número de decenas de horas. Entonces simplemente cargo ese número en la posición _POS_CRONOM, que me meterá ese número en esa posición del mapa de pantalla. Sabemos que la gameboy, dibuja el fondo cogiendo los tiles de la lista de tiles segun el número que le digamos, entonces meterá en esa posición de fondo el tile número igual al número de decenas de horas. Como nuestros tiles del 0 al 9 son precisamente los dibujos de los números del 0 al 9, la gameboy dibujará en esa posición el número correspondiente a las decenas de horas. Luego para las unidades de horas, hacemos algo similar, pero lo que hacemos es el and contrario, con $0F, porque sólo nos interesa quedarnos con la parte baja (las unidades) y entonces no necesitamos el swap y dibujamos directamente el tile correspondiente al número, pero ahora en_POS_CRONOM+1, porque queremos dibujarlo una posición más a la derecha. Luego dibujamos los dos puntos (tile 10), y hacemos lo mismo para decenas de minuto, unidades de minuto, etc, simplemente añadiendo posiciones a la dirección inicial para dibujar el resto de números.
Y hasta aqui este ejemplo. Siguiente, bancos.
Hola Bancos
Como habia comentado, la Gameboy sólo puede manejar directamente un espacio de direcciones de 64K, y en ellos tiene que manejar la memoria ram, la memoria de video, los registros de entrada/salida, etc, por lo que para los datos del cartucho, solamente tenemos disponibles 32K, organizados en dos bloques de 16K, uno de $0000 a $3FFF y otro de $4000 a $7FFF.
Entonces, cuando necesitamos más memoria para nuestros programas, tenemos que usar los conocidos mappers. Un mapper es un chip que reside en el cartucho, y que escucha determinados “comandos” que le podemos mandar, para dejarnos dispobible más memoria en los cartuchos a nuestra petición. Esto se consigue mediante banking. Vamos a asumir, que el primer bloque de 16K el de $0000 a $3FFF, es un bloque fijo, que siempre está disponible al inicio de la memoria del cartucho en esa dirección. Pero en el segundo bloque, de $4000 a $7FFF, podemos mapear otras zonas de la memoria del cartucho, denominados bancos. En la GameBoy, estos bancos son siempre de 16K. Entonces por ejemplo: Si tenemos un cartucho con una memoria de 64K, lo dividimos en 4 bloques de 16K, y tenemos, que el bloque 0 de $0000 a $3FFF en la memoria del cartucho, siempre estará disponible en la memoria de la gameboy de $0000 a $3FFF, pero los otros 3 bloques, el 1, 2 y 3, podremos intercambiarlos en el rango de memoria de la gameboy de $4000 a $7FFF, asi pudiendo acceder en ese bloque a los tres bancos superiores de memoria del cartucho, uno por uno cuando los necesitemos.
Para hacerlo, como dije, necesitamos un mapper. A lo largo de la existencia de la GameBoy, se desarrollaron varioas de estos chips según iban creciendo las necesidades. Los mappers más conocidos son el MBC1, el MBC2, el MBC3 y el MBC5 que apareció ya con la GameBoy Color. Lo bueno es que la mayoría de estos mappers son muy parecidos entre si, sólo que cada vez permiten más memoria, pero el acceso básico suele ser compatible. Además estos mappers, introducian una característica muy importante. El permitirnos el uso de memoria RAM externa en el cartucho, normalmente protegida por una pila para que no pierda su contenido al apagar la consola, con lo que tenemos guardado de partidas o datos importantes.
Entonces vamos con el ejemplo. Lo primero que notareis es que he modificado la cabecera del cartucho, diciendo que ahora tenemos un cartucho con mapper MBC1, memoria ram externa y pila, que el cartucho tiene 64K de ROM y 8K de RAM. Necesitamos definir la cabecera de manera correcta en estos aspectos, aunque la GameBoy real los ignora y nos permite hacer cosas, normalmente los emuladores le hacen caso y solo permiten usar los bancos y el guardado si estos datos son correctos.
Meto todo el código posible en el banco cero, al iniciar mi programa con la dirección inicial como siempre, el enlazador me lo colocará todo en el banco 0 a no ser que le diga lo contrario.
Luego al final de programa, podeis ver que defino dos bancos y las directivas de como definirlos. Como os comenté los bancos superiores al 0, siempre tendrñan que ser mapeados en el segundo bloque de direcciones de $4000 a $7FFF, por eso les pongo esa dirección de inicio, pero además le digo al RGBDS que en la realidad me coloque esos datos en el banco X en la rom generada. Asi en la memoria ROM del cartucho, realmente el banco 0 estará de $0000 a $3FFF, el banco 1 de $4000 a $7FFF, el banco 2 de $8000 a $BFFF, etc, pero cuando usemos el mapper para acceder a esos bancos, los “colocará” en la memoria de la gameboy de $4000 a $7FFF.
Asi el programa arranca y como es normal, por defecto al iniciar la consola, el mapper coloca el banco 1 a continuación del 0, con lo que si imprimimos el mensaje contenido en $4000, nos dice que está en el banco 1. Ahora pulsando A o B, podemos enviar los comandos al mapper que le indican cambiar el banco, asi si ahora imprimimos el mensaje en $4000, podemos estar imprimiendo el contenido del inicio del banco 1 o 2, por lo que el mensaje cambia. Ademñas, si pulsamos Select, nos guarda el número del banco actual en la memoria externa, asi cuando encendais la GameBoy (o el emulador) de nuevo, vereis que “recuerda” el ultimo número de banco que teniais seleccionado cuando le disteis a “grabar” con Select.
Seleccionar un banco con el MBC1 es sencillo, simplemente escribimos el número de banco que queremos seleccionar, en la dirección de memoria $2000. Como es tontería escribir en esa dirección, porque en teoria estamos escribiendo en la ROM del cartucho, cosa imposible ya que es de sólo lectura, el mapper escucha este intento de escritura y selecciona un banco u otro en función de ese número que intentemos escribir. Muy sencillo.
Luego para la memoria, hay una diferencia, y es que tenemos que activarla. Se hace esto para evitar posibles accesos innecesarios a la memoria, o posibles fallos de alimentación(las pilas) que podrian ocasionar pérdida de datos, asi que cuando queramos acceder a la memoria externa, la activamos, leemos o escribimos, y desactivamos. Esto se consigue “escribiendo” un $0A en $0000 para activar la memoria externa, y un $00 en $0000 para desactivarla, y luego simplemente leemos o escribimos datos de $A000 a $BFFF. También tenemos bancos de RAM si necesitamos más, y funcionan igual que los bancos de ROM pero en bloques de 8K, usando la dirección $4000 para seleccionarlos (escribiendo ahi el número de banco que queremos).
Además he añadido alguna cosilla a este ejemplo, como las rutinas para imprimir cadenas de texto, o una fuente completa con los caracteres ascii desde el 32 (espacio) hasta el 127. Lo he hecho muy sencillo, uso el segundo bloque de memoria de tiles, de $8800 a $97FF que como sabemos usa números de -128 a 127, asi que el cero está en $9000. Si colo el espacio (ascii 32), 32 tiles arriba (cada tile tiene 2 bytes por linea, y 8 lineas, 32*2*8), asi que en $9200, puedo usar texto normal en mis datos para escribir en el fondo, ya que el número de carácter ascii, coincidirá con el número de tile a dibujar. Lo veis en las etiquetas con texto que uso, que escribo el texto tal cual entre comillas. El RGBDS se encarga de convertir estos caracteres en sus valores ascii para meterlos como si metiera bytes con DB a mano. Las cadenas las acabo en 0 (usando la coma como para añadir un byte mas), para que las rutinas de impresión, sepan donde terminar de imprimir.
Para generar la fuente, he creado un programilla en java, que toma una imagen de 768×8 pixels (96 tiles de 8×8) en escala de grises, y nos genera los datos de los tiles en formato del RGBDS. Lo publicaré enseguida.
Os dejo el código del ejemplo:
- holabancos.asm
; Hola Bancos ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; Constantes _TILES_FUENTE EQU $9200 ; la segunda tabla de tiles va de $8800 ; a $97FF y los tiles van numerados ; de -128 a 127, con lo que el tile 0 ; está en $9000. Si le sumo 32 tiles ; (32*2bytes*8lineas) y coloco ahi ; el primer caracter (espacio), me ; coinciden con el codigo ASCII ; y puedo usar cadenas de texto tal cual ; Variables _BANCO EQU _RAM ; guardaremos el banco actual _PAD EQU _RAM+1 ; estado del pad ; interrupción de vBlank SECTION "Vblank",ROM0[$0040] reti ; no hacemos nada, volvemos ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom con mapper MBC1, de 64K y con RAM ROM_HEADER ROM_MBC1_RAM_BAT, ROM_SIZE_64KBYTE, RAM_SIZE_8KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: ; iniciamos las variables ld a, 1 ld [_BANCO], a ; paletas ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta de fondo ld [rOBP0], a ; y en la paleta 0 de sprites ; scroll ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. ; video call apaga_LCD ; llamamos a la rutina que apaga el LCD ; cargamos los tiles en la segunda tabla de la memoria de tiles ld hl, Fuente1 ; cargamos en HL la dirección de nuestro tile ld de, _TILES_FUENTE ; en DE dirección de la tabla ld bc, EndFuente1-Fuente1 ; número de bytes a copiar call CopiaMemoria ; limpiamos el mapa ld de, _SCRN0 ; mapa 0 ld bc, 32*32 ld l, 0 ; tile vacio (espacio) call RellenaMemoria ; bien, tenemos todo el mapa de tiles cargado ; ahora limpiamos la memoria de sprites ld de, _OAMRAM ; memoria de atributos de sprites ld bc, 40*4 ; 40 sprites x 4 bytes cada uno ld l, 0 ; lo vamos a poner todo a cero, asi los sprites call RellenaMemoria ; no usados quedan fuera de pantalla ld b, 1 ld c, 1 ld hl, $4000 call ImprimeCadena ld b, 1 ld c, 16 ld hl, Info call ImprimeCadena ; vamos a leer el dato guardado ld a, $0A ld [$0000], a ; activamos la RAM externa ld a, [$A000] ; cargamos en a el dato ld b, a ; lo guardamos ld a, $00 ld [$0000], a ; desactivamos la RAM externa ; lo imprimimos ld l, b ld b, 18 ld c, 16 call ImprimeNumero ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8800|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8 ld [rLCDC], a ; bucle principal control: ; leemos el pad call lee_pad ; Ahora cambiamos al banco 1 ld a, [_PAD] and %00000001 ; Boton A call nz, Banco1 ; O al banco 2 ld a, [_PAD] and %00000010 ; Boton B call nz, Banco2 ; Guarda en la RAM externa ld a, [_PAD] and %000000100 ; Boton Select call nz, Guarda ld bc, 15000 call Retardo ; volvemos a empezar jr control ; Cambia al banco 1 y muestra el mensaje Banco1: ; escribimos el banco que queremos en la dirección de selección de bancos ld hl, $2000 ld [hl], 01 ; banco 1 ; guardamos en la variable ld a, 01 ld [_BANCO], a ; mostramos el mensaje que reside al inicio del banco superior call EsperaVBlank ld b, 1 ld c, 1 ld hl, $4000 call ImprimeCadena ; hemos cambiado, borrar el mensaje de guardado si lo hay ld b, 1 ld c, 12 ld hl, Limpia call ImprimeCadena ret ; Cambia al banco 2 y muestra el mensaje Banco2: ; lo mismo pero seleccionamos el banco 2 ld hl, $2000 ld [hl], 02 ld a, 02 ld [_BANCO], a call EsperaVBlank ld b, 1 ld c, 1 ld hl, $4000 call ImprimeCadena ; hemos cambiado, borrar el mensaje de guardado si lo hay ld b, 1 ld c, 12 ld hl, Limpia call ImprimeCadena ret ; Guarda el número de banco actual en la memoria del cartucho Guarda: ; primero activamos la SRAM externa ld a, $0A ; $0A, activar ld [$0000], a ; ;escribimos el dato en el primer byte de la ram externa ld a, [_BANCO] ld [$A000], a ; desactivamos la SRAM ld a, $00 ; $00, desactivar ld [$0000], a ; ; lo imprimimos ld a, [_BANCO] ld l, a ld b, 18 ld c, 16 call ImprimeNumero ; mostramos mensaje ld b, 1 ld c, 12 ld hl, Guardado call ImprimeCadena ret ; Imprime una cadena de texto en el fondo (cadena acabada en 0) ; Parámetros: ; b - coordenada x ; c - coordenada y ; hl - dirección de la cadena ImprimeCadena: push hl ; guardamos hl pa luego ; vamos a usar hl ahora para los cálculos del destino ld hl, _SCRN0 ; vamos a la posición y ld a, c cp 0 jr z, .fin_y ; si es cero, vamos a por las x .avz_y: ld de, 32 add hl, de ; avanzamos en las Y por lo tanto 32 tiles dec a jr nz, .avz_y .fin_y: ; vamos a por las x ld a, b cp 0 jr z, .fin_x ; si es cero, terminamos .avz_x: inc hl dec a jr nz, .avz_x .fin_x: push hl pop de ; de = hl ; bien, tenemos en 'de' la posición de memoria donde escribir la cadena ; vamos a ello pop hl ; rescatamos hl de la pila .imprime: ld a, [hl] ; cargamos un carácter cp 0 ret z ; si es cero, volvemos ld [de], a ; si no, imprimimos inc de ; siguiente inc hl jr .imprime ret ; Imprime un numero (unidad) ; Parámetros ; b - posicion x ; c - posición y ; l - numero a imprimir (0-9) ImprimeNumero: push hl ; guardamos hl pa luego ; vamos a usar hl ahora para los cálculos del destino ld hl, _SCRN0 ; vamos a la posición y ld a, c cp 0 jr z, .fin_y ; si es cero, vamos a por las x .avz_y: ld de, 32 add hl, de ; avanzamos en las Y por lo tanto 32 tiles dec a jr nz, .avz_y .fin_y: ; vamos a por las x ld a, b cp 0 jr z, .fin_x ; si es cero, terminamos .avz_x: inc hl dec a jr nz, .avz_x .fin_x: push hl pop de ; de = hl ; bien, tenemos en 'de' la posición de memoria donde escribir el numero ; vamos a ello pop hl ; rescatamos el número ld a, l and $0F ; solo nos interesa la parte baja add a, 48 ; el primer caracter es 32 el espacio, el cero está a +16 ld [de], a ret ; Rutina de lectura del pad lee_pad: ; a cero ld a, 0 ld [_PAD], a ; vamos a leer la cruzeta: ld a, %00100000 ; bit 4 a 0, bit 5 a 1 (cruzeta activada, botones no) ld [rP1], a ; ahora leemos el estado de la cruzeta, para evitar el bouncing ; hacemos varias lecturas ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] and $0F ; solo nos importan los 4 bits de abajo. swap a ; intercambiamos parte baja y alta. ld b, a ; guardamos el estado de la cruzeta en b ; vamos a por los botones ld a, %00010000 ; bit 4 a 1, bit 5 a 0 (botones activados, cruzeta no) ld [rP1], a ; leemos varias veces para evitar el bouncing ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] ld a, [rP1] ; tenemos en A, el estado de los botones and $0F ; solo nos importan los 4 bits de abajo. or b ; hacemos un or con b, para "meter" en la parte ; superior de A, el estado de la cruzeta. ; ahora tenemos en A, el estado de todo, hacemos el complemento y ; lo guardamos en la variable cpl ld [_PAD], a ; reseteamos el pad ld a,$30 ld [rP1], a ; volvemos ret ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento call EsperaVBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos EsperaVBlank: ld a, [rLY] cp 145 jr nz, EsperaVBlank ret ; rutina de retardo ; parámetros ; bc - numero de iteraciones Retardo: .delay: dec bc ; decrementamos ld a, b ; vemos si es cero or c jr z, .fin_delay nop jr .delay .fin_delay: ret ; rutina de copia a memoria ; copia un numero de bytes de una direccion a otra ; espera los parámetros: ; hl - dirección de datos a copiar ; de - dirección de destino ; bc - numero de datos a copiar ; destruye el contenido de A CopiaMemoria: ld a, [hl] ; cargamos el dato en A ld [de], a ; copiamos el dato al destino dec bc ; uno menos por copiar ; comprobamos si bc es cero ld a, c or b ret z ; si es cero, volvemos ; si no, seguimos inc hl inc de jr CopiaMemoria ; rutina de relleno de memoria ; rellena un numero de bytes de memoria con un dato ; espera los parámetros: ; de - direccion de destino ; bc - número de datos a rellenar ; l - dato a rellenar RellenaMemoria: ld a, l ld [de], a ; mete el dato en el destino dec bc ; uno menos a rellenar ld a, c or b ; comprobamos si bc es cero ret z ; si es cero volvemos inc de ; si no, seguimos jr RellenaMemoria Limpia: DB " ",0 Info: DB "Banco guardado: ", 0 Guardado: DB "Guardado correcto", 0 ; Fuente ; ======================================================================== INCLUDE "fuente1.agb" ; Datos del primer Banco ; ======================================================================== SECTION "Banco1",CODE[$4000],BANK[1] DB "Soy el banco 1",0 ; Datos del segundo Banco ; ======================================================================== SECTION "Banco2",CODE[$4000],BANK[2] DB "Soy el banco 2",0
Y el archivo con la fuente: fuente1.agb
La captura obligada:
Hola Sonido
El sonido de la GameBoy es un tema complejo si vienes de programar en un sistema moderno donde simplemente reproduces sonido digital, mezclando fácilmente música y efectos. La GameBoy por el contrario, tiene un hardware de sonido bastante sencillo, formado por cuatro canales independientes y diferentes entre si. Podemos controlar estos canales a nuestro antojo, que la GameBoy se encargará de mezclarlos. Estos cuatro canales se manejan escribiendo valores en registros mapeados en memoria (cómo no), y cada canal cuenta con varios registros para cada uno de sus parámetros, asi que el tema lleva un rato explicarlo. Vamos a conocer los canales y luego veremos como se programan.
- Canal 1: El canal 1, es un canal de onda cuadrada con ciclo de trabajo modificable, con envolvente y portamento.
- Canal 2: Es un canal de onda cuadrada con ciclo de trabajo modificable, con envolvente.
- Canal 3: Canal de onda programable con una tabla RAM de 32 pasos.
- Canal 4: Canal de ruido blanco con envolvente.
Bien, para empezar, voy a explicar un poco de teoria musical, bueno, sólo lo que nos interesa, el ciclo de trabajo, la envolvente y el portamento.
El ciclo de trabajo, es algo muy sencillo, si tenemos que la onda es cuadrada, es simplemente cuanto del tiempo de la duración de la nota, tenemos el nivel alto, y cuanto tiempo el nivel bajo. Esto se expresa en tanto por ciento. Asi en la GamebBoy podemos elegir ciclos de trabajo del 12,5%, 25%, 50%, y 75%. Vamos a explicarlo con una imagen robada de por ahi:
Como veis, las ondas tienen la misma frecuencia (todas empiezan en el mismo momento), pero dependiendo del ciclo de trabajo, unas se pasan más tiempo arriba y otras más tiempo abajo.
La envolvente de una nota, es una función que se aplica a la amplitud de esta, para modificarla y que varie a lo largo de la duración de la misma. En síntesis, se suele usar una envolvente conocida como ADSR, de Attack, Decay, Sustain y Release, es decir, ataque, decaimiento, sostenimiento y relajación. Vamos a poner un gráfico… wikipedia al rescate. Si tenemos que una nota normal, tendria cierta amplitud (volumen) fija, la gráfica de su amplitud, sería una linea recta. Ahora si le aplicamos una envolvente ADSR modelo, tendriamos algo como esto:
Con lo que viendo esto, podemos decir, que, el ataque, es el tiempo que tarda la nota en llegar hasta su máxima amplitud, el decaimiento, es el tiempo que tarda en pasar desde el fin del ataque, hasta el valor de sostenimiento, que es el valor de amplitud que mantenemos hasta el fin de la nota, y la relajación, el tiempo que tarda la nota en “desaparecer” desde que la acabamos de tocar. Con todos estos parámetros de envolvente, podemos modelar las notas de manera que cambien bastante aunque estemos tocando la misma frecuencia.
El portamento, es cuando el paso de una nota a otra, no es directo, sino que la frecuencia va cambiando progresivamente tocando todos los sonidos intermedios. Esto por ejemplo en una guitarra se hace cuando se arrastra el dedo sobre todos los trastes entre dos notas que se quieran tocar. En la GameBoy, este hardware de portamento, nos permite variar una nota con el tiempo, pudiendo definir un incremento o decremento de la frecuencia base con el tiempo, y cuanto cambia esta frecuencia.
Vamos a ver a continuación los registros del sistema de sonido y sus funciones:
Canal 1 - Tono y portamento
$FF10 - NR10 - Registro de portamento del canal 1(R/W)
Bit 6-4 - Duración del portamento Bit 3 - Incremento/Decremento del portamento 0: Adicción (la frecuencia se incrementa) 1: Sustracción (la frecuencia se decrementa) Bit 2-0 - Paso actual del desplazamiento (n: 0-7)
Duración del portamento:
000: Apagado - sin cambios de frecuencia 001: 7.8 ms (1/128Hz) 010: 15.6 ms (2/128Hz) 011: 23.4 ms (3/128Hz) 100: 31.3 ms (4/128Hz) 101: 39.1 ms (5/128Hz) 110: 46.9 ms (6/128Hz) 111: 54.7 ms (7/128Hz)
El cambio de la frecuencia original (definido en NR13,NR14) en cada paso, se calcula con la siguiente fórmula, donde X(0) es la frecuencia inicial y X(t-1) es la última frecuencia:
X(t) = X(t-1) +/- X(t-1)/2^n
$FF11 - NR11 - Canal 1 duración/ciclo de trabajo (R/W)
Bit 7-6 - Ciclo de trabajo (R/W) Bit 5-0 - Longitud (R) (t1: 0-63)
Ciclo de trabajo:
00: 12.5% ( _-------_-------_------- ) 01: 25% ( __------__------__------ ) 10: 50% ( ____----____----____---- ) (normal) 11: 75% ( ______--______--______-- )
Longitud = (64-t1)*(1/256) segundos La longitud solo se utiliza si el Bit 6 en NR14 está a uno.
$FF12 - NR12 - Canal 1 - Envolvente (R/W)
Bit 7-4 - Volumen inicial de la envolvente (0-0Fh) (0=Sin sonido) Bit 3 - Dirección de la envolvente (0=Decrece, 1=Crece) Bit 2-0 - Periodo (n: 0-7) (si es cero, la envolvente no actua.)
Longitud de un paso = n*(1/64) segundos
$FF13 - NR13 - Canal 1 - Frecuencia (low) (W)
8 bits bajos de los 11 bit de la frecuencia (x). Los siguientes 3 bits están en NR14 ($FF14)
$FF14 - NR14 - Canal 1 - Frecuencia (hi) (R/W)
Bit 7 - Disparador (1=Reinicia el sonido) (W) Bit 6 - Activa o desactiva la longitud (R/W) (1=Detiene el sonido cuando alcanza la longitud en NR11) Bit 2-0 - 3 bits altos de la frecuencia (x) (W)
Frecuencia = 131072/(2048-x) Hz
Canal 2 - Tono
Este canal funciona exactamente igual que el 1, pero no tiene un registro de portamento.
$FF16 - NR21 - Canal 2 Longitud/Ciclo de trabajo (R/W)
Bit 7-6 - Ciclo de trabajo (R/W) Bit 5-0 - Longitud (R) (t1: 0-63)
Ciclo de trabajo:
00: 12.5% ( _-------_-------_------- ) 01: 25% ( __------__------__------ ) 10: 50% ( ____----____----____---- ) (normal) 11: 75% ( ______--______--______-- )
Longitud = (64-t1)*(1/256) segundos La longitud solo se utiliza si el Bit 6 en NR14 está a uno.
$FF17 - NR22 - Canal 2 Envolvente (R/W)
Bit 7-4 - Volumen inicial de la envolvente (0-0Fh) (0=Sin sonido) Bit 3 - Dirección de la envolvente (0=Decrece, 1=Crece) Bit 2-0 - Periodo (n: 0-7) (si es cero, la envolvente no actua.)
Longitud de un paso = n*(1/64) segundos
$FF18 - NR23 - Canal 2 - Frecuencia (lo) (W)
8 bits bajos de los 11 bit de la frecuencia (x). Los siguientes 3 bits están en NR24 ($FF19)
$FF19 - NR24 - Canal 2 - Frecuencia (hi) (R/W)
Bit 7 - Disparador (1=Reinicia el sonido) (W) Bit 6 - Activa o desactiva la longitud (R/W) (1=Detiene el sonido cuando alcanza la longitud en NR21) Bit 2-0 - 3 bits altos de la frecuencia (x) (W)
Frecuencia = 131072/(2048-x) Hz
Canal 3 - Onda programable
Este canal se puede usar para reproducir sonido digital, pero la longitud del buffer de samples (RAM de onda) Está limitada a 32 valores de 4 Bits. Se podria usar este canal para sacar notas normales si inicicalizamos la memoria de onda con los valores de una onda cuadrada. No dispone de control de envolvente.
$FF1A - NR30 - Canal 3 on/off (R/W)
Bit 7 - Apaga o enciende el canal (0=Detenido, 1=Reproduce) (R/W)
$FF1B - NR31 - Canal 3 - Longitud
Bit 7-0 - Longitud (t1: 0 - 255)
Longitud = (256-t1)*(1/256) segundos. Este valor sólo se usa si el bit 6 de NR34 está a uno.
$FF1C - NR32 - Canal 3 - Nivel de salida (R/W)
Bit 6-5 - Seleccionar volumen de la salida (R/W)
Posibles valores son:
00: Apagado (sin sonido) 01: 100% Volumen (Datos de la RAM de onda tal cual) 10: 50% Volumen (Datos de la RAM de onda desplazados una vez a la derecha) 11: 25% Volumen (Datos de la RAM de onda desplazados dos veces a la derecha)
$FF1D - NR33 - Canal 3 - Frecuencia (lo) (W)
8 bits bajos de la frecuencia del canal(x).
$FF1E - NR34 - Canal 3 - Frecuencia (hi) (R/W)
Bit 7 - Disparador (1=Reinicia el sonido) (W) Bit 6 - Activa o desactiva la longitud (R/W) (1=Detiene el sonido cuando alcanza la longitud en NR11) Bit 2-0 - 3 bits altos de la frecuencia (x) (W)
Frecuencia = 131072/(2048-x) Hz
$FF30-$FF3F - Memoria de onda
Contiene los valores para la generación de la onda.
Esta memoria se organiza en 32 valores de 4 bit que se reproducen con los 4 bits altos en primer lugar.
Canal 4 - Ruido
Este canal se usa para reproducir ruido blanco. Esto se logra variando la amplitud aleatoriamente dada una frecuencia. Dependiendo de la frecuencia, el ruido parecerá más “duro” o “blando”.
También se puede influenciar la salida del generador de números aleatorios, permitiendo menos variación y asi una salida de tono casi normal.
$FF20 - NR41 - Canal 4 - Longitud (R/W)
Bit 5-0 - Longitud (R) (t1: 0-63)
Longitud = (64-t1)*(1/256) segundos La longitud solo se utiliza si el Bit 6 en NR44 está a uno.
$FF21 - NR42 - Canal 4 - Envolvente (R/W)
Bit 7-4 - Volumen inicial de la envolvente (0-0Fh) (0=Sin sonido) Bit 3 - Dirección de la envolvente (0=Decrece, 1=Crece) Bit 2-0 - Periodo (n: 0-7) (si es cero, la envolvente no actua.)
Longitud de un paso = n*(1/64) segundos
$FF22 - NR43 - Canal 4 - Contador polinómico (R/W)
La amplitud se cambia aleatoriamente entre alto y bajo a la frecuencia dada. Una frecuencia alta hará el ruido más “suave”. Cuando el bit 3 está a uno, la salida es más regular, y algunas frecuencias suenan más como tono que como ruido.
Bit 7-4 - Frecuencia del reloj de desplazamiento (s) Bit 3 - Longitud del contador (0=15 bits, 1=7 bits) Bit 2-0 - Radio de división de las frecuencias (r)
Frecuencia = 524288 Hz / r / 2^(s+1) ; Para r=0, se comporta como si r=0.5
$FF23 - NR44 - Channel 4 - Contador; Disparador (R/W)
Bit 7 - Disparador (1=Reinicia el sonido) (W) Bit 6 - Activa o desactiva la longitud (R/W) (1=Detiene el sonido cuando alcanza la longitud en NR41)
Registros de control generales del sonido
$FF24 - NR50 - Control de volumen / 5º Canal (R/W)
Los bits de volumen controlan el Volumen principal de las salidas izquierda y derecha.
Bit 7 - Vin sale por SO2 (1=activado) Bit 6-4 - Volumen de SO2 (0-7) Bit 3 - Vin sale por SO1 (1=Activado) Bit 2-0 - Volumen de SO1 (0-7)
La señal Vin viene del bus del cartucho, permitiendo a hardware externo en este, añadir un quinto canal a los cuatro internos de la GameBoy.
$FF25 - NR51 - Selección de salida de cada canal (R/W)
Nos permite enviar cada canal a la salida izquierda o derecha (o a ambas o ninguna).
Bit 7 - Canal 4 a la salida SO2 Bit 6 - Canal 3 a la salida SO2 Bit 5 - Canal 2 a la salida SO2 Bit 4 - Canal 1 a la salida SO2 Bit 3 - Canal 4 a la salida SO1 Bit 2 - Canal 3 a la salida SO1 Bit 1 - Canal 2 a la salida SO1 Bit 0 - Canal 1 a la salida SO1
$FF26 - NR52 - Sonido Encendido/Apagado
Si tus programas de GameBoy no usan sonido, escribir 00h en este registro, te ahorrará un 16% o más de consumo eléctrico. Deshabilitar el sistema de sonido poniendo a cero el Bit 7 destruye el contenido de todos los registros de sonido. Además, deja inaccesibles todos estos registros (excepto el mismo $FF26) mientras el sistema de sonido esté apagado.
Bit 7 - Activa o desactiva todo el sonido (0: Detener todos los circuitos de sonido) (R/W) Bit 3 - Canal 4 ON (R) Bit 2 - Canal 3 ON (R) Bit 1 - Canal 2 ON (R) Bit 0 - Canal 1 ON (R)
Los Bits 0-3 de este registro, son sólamente bits de estátus, escribir en ellos no activa o desactiva el sonido de esos canales. Estos flags se ponen a uno cuando se activa el flag “Disparador” del canal (Bit 7 en NR14-NR44), el flag permanece a uno mientras dure la longitud del sonido (si está activa). Una envolvente que ha hecho bajar la amplitud a cero, no desactivaria este flag.
Ejemplo
Bien, vamos con un ejemplo, rescatamos el programa del sprite, y le añadimos música, bueno, si a esto se le puede llamar música
Lo que hago es ir tocando las notas de la escala (Do, Re, Mi, Fa, Sol, La, Si), de la 5ª octava cada cierto tiempo. Tengo una linea de datos con los valores bajos de la frecuencia de estas notas, y el valor alto, que es siempre el mismo en las notas de la 5ª octava, lo meto fijo en el registro correspondiente.
Para todo esto uso el canal 2. Atentos a las funciones iniciar_sonido, donde activo el sistema de sonido y preparo la salida por ambos altavoces, y preparo el canal 2, longitud, ciclo de trabajo, envolvente y meto el valor alto de la frecuencia y activo la longitud, y a la función cambia_nota que comprueba si tenemos que tocar la nota (dependiendo de la constante TEMPO) y toca la nota y prepara la siguente.
- holasonido.agb
; Hola sonido ; David Pello 2010 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; Velocidad de la musica, no es un tempo real en el sentido musical _TEMPO EQU 20 ; definimos unas constantes para trabajar con nuestro sprite _SPR0_Y EQU _OAMRAM ; la Y del sprite 0, es el inicio de la mem de sprites _SPR0_X EQU _OAMRAM+1 _SPR0_NUM EQU _OAMRAM+2 _SPR0_ATT EQU _OAMRAM+3 ; creamos un par de variables para ver hacia donde tenemos que mover el sprite _MOVX EQU _RAM ; inicio de la ram dispobible para datos _MOVY EQU _RAM+1 ; guarda la nota actual _NOTA EQU _RAM+2 ; contador para el tempo _CONT_MUS EQU _RAM+3 ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom sin mapper, de 32K y sin RAM, lo más básico ; (como por ejemplo la del tetris) ROM_HEADER ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta de fondo ld [rOBP0], a ; y en la paleta 0 de sprites ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. call inicia_sonido ; iniciamos el sistema de sonido call apaga_LCD ; llamamos a la rutina que apaga el LCD ; cargamos los tiles en la memoria de tiles ld hl, Tiles ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld b, 32 ; b = 32, numero de bytes a copiar (2 tiles) .bucle_carga: ld a,[hl] ; cargamos en A el dato apuntado por HL ld [de], a ; y lo metemos en la dirección apuntada en DE dec b ; decrementamos b, b=b-1 jr z, .fin_bucle_carga ; si b = 0, terminamos, no queda nada por copiar inc hl ; incrementamos la dirección a leer de inc de ; incrementamos la dirección a escribir en jr .bucle_carga ; seguimos .fin_bucle_carga: ; ahora limpiamos la pantalla (llenamos todo el mapa de fondo), con el tile 0 ld hl, _SCRN0 ld de, 32*32 ; numero de tiles en el mapa de fondo .bucle_limpieza: ld a, 0 ; el tile 0 es nuestro tile vacio ld [hl], a dec de ; ahora tengo que comprobar si de es cero, para ver si tengo que ; terminar de copiar. dec de no modifica ningñun flag, asi que no puedo ; comprobar el flag zero directamente, pero para que de sea cero, d y e ; tienen que ser cero los dos, asi que puedo hacer un or entre ellos, ; y si el resultado es cero, ambos son cero. ld a, d ; cargamos d en a or e ; y hacemos un or con e jp z, .fin_bucle_limpieza ; si d OR e es cero, de es cero. Terminamos. inc hl ; incrementamos la dirección a escribir en jp .bucle_limpieza .fin_bucle_limpieza ; bien, tenemos todo el mapa de tiles lleno con el tile 0, ; ahora vamos a crear el sprite. ld a, 30 ld [_SPR0_Y], a ; posición Y del sprite ld a, 30 ld [_SPR0_X], a ; posición X del sprite ld a, 1 ld [_SPR0_NUM], a ; número de tile en la tabla de tiles que usaremos ld a, 0 ld [_SPR0_ATT], a ; atributos especiales, de momento nada. ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8|LCDCF_OBJON ld [rLCDC], a ; preparamos las variables de la animacion ld a, 1 ld [_MOVX], a ld [_MOVY], a ;preparamos las variables para el sonido ; nota actual ld a, 0 ld [_NOTA], a ; contador retardo musical ld [_CONT_MUS], a ; bucle infinito animacion: ; lo primero, esperamos por el VBlank, ya que no podemos modificar ; la VRAM fuera de él, o pasarán cosas raras .wait: ld a, [rLY] cp 145 jr nz, .wait ; incrementamos las y ld a, [_SPR0_Y] ; cargamos la posición Y actual del sprite ld hl, _MOVY ; en hl, la dirección del incremento Y add a, [hl] ; sumamos ld hl, _SPR0_Y ld [hl], a ; guardamos ; comparamos para ver si hay que cambiar el sentido cp 152 ; para que no se salga de la pantalla (max Y) jr z, .dec_y cp 16 jr z, .inc_y ; lo mismo, minima coord (min Y = 16) ; no hay que cambiar jr .end_y .dec_y: ld a, -1 ; ahora hay que decrementar las Y ld [_MOVY], a jr .end_y .inc_y: ld a, 1 ; ahora hay que incrementar las Y ld [_MOVY], a .end_y: ; vamos con las X, lo mismo pero cambiando los márgenes ld a, [_SPR0_X] ; cargamos la posición X actual del sprite ld hl, _MOVX ; en hl, la dirección del incremento X add a, [hl] ; sumamos ld hl, _SPR0_X ld [hl], a ; guardamos ; comparamos para ver si hay que cambiar el sentido cp 160 ; para que no se salga de la pantalla (max X) jr z, .dec_x cp 8 ; lo mismo, minima coord izq = 8 jr z, .inc_x ; no hay que cambiar jr .end_x .dec_x: ld a, -1 ; ahora hay que decrementar las X ld [_MOVX], a jr .end_x .inc_x: ld a, 1 ; ahora hay que incrementar las X ld [_MOVX], a .end_x: ; un pequeño retardo call retardo ; tocamos la nota siguiente call cambia_nota ; volvemos a empezar jr animacion ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento .espera_VBlank ld a, [rLY] cp 145 jr nz, .espera_VBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos ; rutina de retardo retardo: ld de, 2000 ; numero de veces a ejecutar el bucle .delay: dec de ; decrementamos ld a, d ; vemos si es cero or e jr z, .fin_delay nop jr .delay .fin_delay: ret ; rutina para iniciar el sistema de sonido inicia_sonido: ; activamos sistema de sonido ld a, %10000000 ld [rNR52], a ; iniciamos los volumenes, etc ld a, %01110111 ; SO1 y S02 a tope de volumen ld [rNR50], a ld a, %00000010 ; Canal 2, sale por SO1 y S02 ld [rNR51], a ; canal 2, longitud 63, ciclo 50% ld a, %10111111 ld [rNR21], a ; canal 2, envolvente, volumen inicial maximo, decreciente ld a, %11110111 ld [rNR22], a ; canal 2, longitud activada y valor de la frecuencia alta ld a, %01000110 ; 1 en el bit 6, longitud activa, y ld [rNR24], a ; escribimos %110 en los tres bits altos de la frecuencia. ret cambia_nota: ld a, [_CONT_MUS] ; vemos si hay que tocar la nota o esperar cp a, _TEMPO jr z, .toca_nota inc a ld [_CONT_MUS], a ret .toca_nota: ; reiniciamos el contador ld a, 0 ld [_CONT_MUS], a ; pasamos a tocar la nota ld a, [_NOTA] ; obtenemos el numero de nota a tocar ld c, a ; lo guardamos ld b, 0 ld hl, Notas ; en hl la dirección de las notas add hl, bc ; ahora tenemos la dirección de la nota a tocar ld a, [hl] ; cargamos la Nota ld [rNR23], a ; la escribimos en el registro de frecuencia del canal 2 ; reiniciamos la nota ld a, [rNR24] set 7,a ld [rNR24], a ; pasamos a la siguiente nota y comprobamos si tenemos que reiniciar ld a, c inc a cp a, EndNotas - Notas ; hemos llegado al final? jr z, .resetea_notas ; no, guardamos y volvemos ld [_NOTA], a ret .resetea_notas: ;si, reiniciarmos, guardamos y volvemos ld a, 0 ld [_NOTA], a ret ; Datos de nuestros tiles Tiles: DB $AA, $00, $44, $00, $AA, $00, $11, $00 DB $AA, $00, $44, $00, $AA, $00, $11, $00 DB $3E, $3E, $41, $7F, $41, $6B, $41, $7F DB $41, $63, $41, $7F, $3E, $3E, $00, $00 EndTiles: ; Datos de la musica ; Vamos a usar la quinta octava, porque sus valores comparten los mismos ; tres bits superiores, %110, asi no tendremos que variarlos. Notas: ; 5ª Octava, Do, Re, Mi, Fa, Sol, La, Si (poniendo $6 en freq hi) DB $0A, $42, $72, $89, $B2, $D6, $F7 EndNotas:
Y aqui la ROM por si quereis escucharlo: hola-sonido1.gb
Hola Color
Aunque este tutorial está pensando principalmente para la Gameboy Original (DMG y GBP), la Gameboy Color incorporó numerosas mejoras para satisfacer la demanda de recursos que implica añadir color a la arquitectura de la GameBoy. Principalmente tenemos por supuesto el color, una pantalla de la misma resolución que la GameBoy, 160×144 pixeles, pero con 32.768 colores posibles, con capacidad de mostrar 56 de ellos a la vez. Además duplica la velocidad del procesador hasta los 8.388Mhz (por defecto usa un modo compatible con la GB original a 4.194MHz), se incrementa la RAM interna hasta los 32K (en 8 bancos) y la memoria de vídeo hasta los 16K (en 2 bancos), y añade varios registros y zonas mapeadas en memoria para los datos de las paletas en color y algunas mejoras para el manejo de sprites.
Para empezar, en la definición de cabecera del juego deberemos de poner el byte $143 a $80 para especificar que la GameBoy arranque en modo Color. En el archivo gbhw.inc tenemos para ello una definición extra del macro que inserta la cabecera, llamado ROM_HEADER_COLOR. Después podemos comprobar si realmente estamos en una GameBoy Color. Para hacerlo podemos comprobar el registro a nada más empezar la ejecución del programa, antes de hacer nada. Si este registro contiene el valor $11 en hexadecimal, estamos ante una GameBoy Color. Además podría ser que estuvieramos usando una GameBoy Advance en modo GameBoy. Para saber si esto es asi, podemos comprobar además el registro b, si el bit 0 está a 1, es una GBA. Saber si es una GBA es importante porque los colores no son exactamente iguales que en la GBC habiendo cambios de brillo y saturación, por ello algunos juegos de GBC se ven horribles en una GBA. Si lo detectamos podemos usar paletas diferentes al ejecutar el juego en una GBA. También hay juegos que pueden correr tanto en GameBoy normal como en GameBoy Color, simplemente cargando o no las paletas de color y haciendo un buen uso de las tablas de paletas, con lo que podríamos diseñar nuestro juego de esa manera (un gran ejemplo es el Link's Awakening DX).
También debemos decidir si queremos usar el modo de doble velocidad de la GameBoy Color, para lo que debemos usar el registro rKEY1 ($FF4D), que nos permite ver el modo en que estamos actualmente, y en caso de requerirlo, pedir el cambio de modo. El modo de doble velocidad de 8.4MHz también consume más energía de nuestras pilas, asi que recomiendo no usarlo a no ser que sea necesario. Por defecto la GBC se inicia en el modo de velocidad normal. Para cambiar entre modos, da igual si es de uno a otro o de otro a uno, debemos de pedir el cambio escribiendo un 1 en el registro rKEY1 (previamente deberíamos comprobar en que modo estamos), y después usar una instrucción stop para que se produzca el cambio.
Luego deberemos cargar las paletas que vamos a usar. Al igual que en la GameBoy original, los objetos (tiles y sprites), pueden tener cuatro colores, pero estos cuatro colores se pueden elegir de entre 32768 colores (16 bit). Estos colores se definen en paletas de 4 colores, cada color formado por 2 bytes, de la siguiente manera:
XBBBBBGG GGGRRRRR : Blue Green Red
Los colores en la GBC no se mezclan de manera similar a como los veríamos en una pantalla moderna de ordenador, hay que tenerlo en cuenta a la hora de diseñar nuestras paletas. Afortunadamente, herramientas como el GBTD nos hacen una traducción de colores bastante fiel a la GBC real.
En total podemos tener 8 paletas para fondos y otras 8 para sprites. Para definir nuestras paletas, tenemos dos registros que nos ofrecen punteros a la tabla de paletas, rBGPI y rOBPI, uno a la tabla de paletas de fondos (tiles) y otro a la tabla de paletas de objetos (sprites). Estos registros junto con los registros de datos de paleta de fondo y de sprites, rBGPD y rOBPD, nos permiten escribir los datos de paleta de una manera sencilla empezando desde 0, y pudiendo definir que vayan haciendo autoincremento al escribir los datos en los registros de datos. Para ello ponemos a 1 el bit 7 de los registros rBGPI o rOBPI para que hagan autoincremento, y vamos escribiendo los datos de paleta en los registros rBGPD o rOBPD. En el programa de ejemplo a continuación, esto se hace en los bucles .bucle_paleta_fondo y .bucle_paleta_sprites. Defino una paleta para el fondo, al ser la única definida, los tiles de fondo que por defecto tendrán sus atributos de paleta a 0, usarán esta. Para los sprites, defino 8 paletas. Luego cuando se producen las “colisiones” con los lados de la pantalla (copiado del ejemplo “holasprite” visto anteriormente, lo que hago es cambiar la paleta a usar por el sprite. Habíamos visto que los sprites tenian una tabla de atributos, la Sprite Atributte Table, que definia las coordenadas X e Y, el número de sprite a usar, y un byte de atributos, cuyos bits 0 a 2 no se usaban en la GameBoy original. Pues son estos bits los que nos permiten seleccionar una paleta de las 8 disponibles. Para los fondos, que en la GameBoy original no tenian una entrada de atributos, en la GameBoy Color si la tienen, pero está en el banco extra de VRAM, asi que deberíamos cambiar al banco 1 de VRAM usando el registro rVBK (usando el bit 0, a 0 o a 1 para seleccionar banco 0 o 1), y escribir entonces en la dirección que estaría el mapa de fondos ($9800) definiremos atributos para cada uno de los tiles en la tabla de tiles (mapa) del banco 1. En el ejemplo sólo usamos una paleta de fondo, asi que no nos hace falta hacer esto. La tabla de atributos de fondo consistiria en bytes cada uno organizado de la siguiente manera:
Bit 0-2 Numero de paleta de fondo (BGP0-7) Bit 3 Banco de RAM del tile (0=Banco 0, 1=Baco 1) Bit 4 No usado Bit 5 Flip Horizontal (0=Normal, 1=Espejar horizontalmente) Bit 6 Flip Vertical (0=Normal, 1=Espejar verticalmente) Bit 7 Prioridad BG-to-OAM (0=Usar el bit de prioridad OAM, 1=Prioridad de Fondos)
Ahora con el programa de ejemplo. En el bucle principal, uso una variable _SPR0_PAL para guardar el número de paleta a usar en cada momento por el sprite0, al principio de bucle aplico la paleta al sprite modificando los bits de sus atributos en la tabla, y cada vez que se produce una colisión incremento ese número teniendo en cuenta de resetearlo a 0 si llega a 8. Con esto tenemos nuestra cara sonriente cambiando de color cada vez que “rebota” en los bordes de la pantalla.
- holacolor.asm
; Hola color ; David Pello 2018 ; ladecadence.net ; Para el tutorial en: ; http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador INCLUDE "gbhw.inc" ; importamos el archivo de definiciones ; definimos unas constantes para trabajar con nuestro sprite _SPR0_Y EQU _OAMRAM ; la Y del sprite 0, es el inicio de la mem de sprites _SPR0_X EQU _OAMRAM+1 _SPR0_NUM EQU _OAMRAM+2 _SPR0_ATT EQU _OAMRAM+3 ; creamos un par de variables para ver hacia donde tenemos que mover el sprite _MOVX EQU _RAM ; inicio de la ram dispobible para datos _MOVY EQU _RAM+1 ; y otra para guardar su paleta _SPR0_PAL EQU _RAM+2 ; El programa comienza aqui: SECTION "start",ROM0[$0100] nop jp inicio ; Cabecera de la ROM (Macro definido en gbhw.inc) ; define una rom de GBColor, sin mapper, de 32K y sin RAM, lo más básico ROM_HEADER_COLOR ROM_NOMBC, ROM_SIZE_32KBYTE, RAM_SIZE_0KBYTE ; aqui empieza nuestro programa inicio: nop di ; deshabilita las interrupciones ld sp, $ffff ; apuntamos la pila al tope de la ram inicializacion: call apaga_LCD ; llamamos a la rutina que apaga el LCD ; Escribir datos de paleta ; Fondos ld a, %10000000 ; Primera dirección del indice, con autoincremento ld [rBGPI], a ld hl, PaletaFondo ; Dirección de los datos de paleta de fondo ld b, 4 ; Cuatro colores (8 bytes) .bucle_paleta_fondo: ld a, [hl] ld [rBGPD], a inc hl ld a, [hl] ld [rBGPD], a dec b jr z, .fin_bucle_paleta_fondo inc hl jp .bucle_paleta_fondo .fin_bucle_paleta_fondo: ; Sprites ld a, %10000000 ; Primera dirección del indice, con autoincremento ld [rOBPI], a ld hl, PaletaSprites ld b, 4*8 ; 4 colores por 8 paletas .bucle_paleta_sprites: ld a, [hl] ld [rOBPD], a inc hl ld a, [hl] ld [rOBPD], a dec b jr z, .fin_bucle_paleta_sprites inc hl jp .bucle_paleta_sprites .fin_bucle_paleta_sprites: ld a, %11100100 ; Colores de paleta desde el mas oscuro al ; más claro, 11 10 01 00 ld [rBGP], a ; escribimos esto en el registro de paleta de fondo ld a, %01101100 ld [rOBP0], a ; y en la paleta 0 de sprites ld a, 0 ; escribimos 0 en los registros de scroll X e Y ld [rSCX], a ; con lo que posicionamos la pantalla visible ld [rSCY], a ; al inicio (arriba a la izq) del fondo. ; cargamos los tiles en la memoria de tiles ld hl, Tiles ; cargamos en HL la dirección de nuestro tile ld de, _VRAM ; en DE dirección de la memoria de video ld b, 32 ; b = 32, numero de bytes a copiar (2 tiles) .bucle_carga: ld a,[hl] ; cargamos en A el dato apuntado por HL ld [de], a ; y lo metemos en la dirección apuntada en DE dec b ; decrementamos b, b=b-1 jr z, .fin_bucle_carga ; si b = 0, terminamos, no queda nada por copiar inc hl ; incrementamos la dirección a leer de inc de ; incrementamos la dirección a escribir en jr .bucle_carga ; seguimos .fin_bucle_carga: ; ahora limpiamos la pantalla (llenamos todo el mapa de fondo), con el tile 0 ld hl, _SCRN0 ld de, 32*32 ; numero de tiles en el mapa de fondo .bucle_limpieza: ld a, 0 ; el tile 0 es nuestro tile vacio ld [hl], a dec de ; ahora tengo que comprobar si 'de' es cero, para ver si tengo que ; terminar de copiar. 'dec de' no modifica ningún flag, asi que no puedo ; comprobar el flag zero directamente, pero para que 'de' sea cero, d y e ; tienen que ser cero los dos, asi que puedo hacer un or entre ellos, ; y si el resultado es cero, ambos son cero. ld a, d ; cargamos d en a or e ; y hacemos un or con e jp z, .fin_bucle_limpieza ; si d OR e es cero, de es cero. Terminamos. inc hl ; incrementamos la dirección a escribir en jp .bucle_limpieza .fin_bucle_limpieza ; bien, tenemos todo el mapa de tiles lleno con el tile 0, ; ahora vamos a crear el sprite. ld a, 30 ld [_SPR0_Y], a ; posición Y del sprite ld a, 30 ld [_SPR0_X], a ; posición X del sprite ld a, 1 ld [_SPR0_NUM], a ; número de tile en la tabla de tiles que usaremos ld a, 0 ld [_SPR0_ATT], a ; atributos especiales, de momento nada. ; configuramos y activamos el display ld a, LCDCF_ON|LCDCF_BG8000|LCDCF_BG9800|LCDCF_BGON|LCDCF_OBJ8|LCDCF_OBJON ld [rLCDC], a ; preparamos las variables de la animacion ld a, 1 ld [_MOVX], a ld [_MOVY], a ld a, 0 ld [_SPR0_PAL], a ; bucle infinito animacion: ; lo primero, esperamos por el VBlank, ya que no podemos modificar ; la VRAM fuera de él, o pasarán cosas raras .wait: ld a, [rLY] cp 145 jr nz, .wait ; cambiamos la paleta del objeto 0 ld a, [_SPR0_ATT] ; cargamos los atributos del objeto 0 and a, %11111000 ; borramos los tres primeros bits (paleta) ld hl, _SPR0_PAL ; cargamos en b la paleta actual ld b, [hl] or b ; hacemos un OR con a para añadir la paleta ld [_SPR0_ATT], a ; lo escribimos ; incrementamos las y ld a, [_SPR0_Y] ; cargamos la posición Y actual del sprite ld hl, _MOVY ; en hl, la dirección del incremento Y add a, [hl] ; sumamos ld hl, _SPR0_Y ld [hl], a ; guardamos ; comparamos para ver si hay que cambiar el sentido cp 152 ; para que no se salga de la pantalla (max Y) jr z, .dec_y cp 16 jr z, .inc_y ; lo mismo, minima coord Y=16 ; no hay que cambiar jr .end_y .dec_y: ld a, -1 ; ahora hay que decrementar las Y ld [_MOVY], a ; y cambiar la paleta ld hl, _SPR0_PAL inc [hl] jr .end_y .inc_y: ld a, 1 ; ahora hay que incrementar las Y ld [_MOVY], a ld hl, _SPR0_PAL ; y cambiar la paleta inc [hl] .end_y: ; vamos con las X, lo mismo pero cambiando los márgenes ld a, [_SPR0_X] ; cargamos la posición X actual del sprite ld hl, _MOVX ; en hl, la dirección del incremento X add a, [hl] ; sumamos ld hl, _SPR0_X ld [hl], a ; guardamos ; comparamos para ver si hay que cambiar el sentido cp 160 ; para que no se salga de la pantalla (max X) jr z, .dec_x cp 8 ; lo mismo, minima coord izq = 8 jr z, .inc_x ; no hay que cambiar jr .end_x .dec_x: ld a, -1 ; ahora hay que decrementar las X ld [_MOVX], a ld hl, _SPR0_PAL ; y cambiar la paleta inc [hl] jr .end_x .inc_x: ld a, 1 ; ahora hay que incrementar las X ld [_MOVX], a ld hl, _SPR0_PAL ; y cambiar la paleta inc [hl] .end_x: ; probar que no nos hemos pasado del limite de paletas (8) ld a, [_SPR0_PAL] cp 8 jr z, .reset_spr_pal jr .end_anim .reset_spr_pal ld a, 0 ld [_SPR0_PAL], a .end_anim ; un pequeño retardo call retardo ; volvemos a empezar jr animacion ; Rutina de apagado del LCD apaga_LCD: ld a,[rLCDC] rlca ; Pone el bit alto de LCDC en el flag de acarreo ret nc ; La pantalla ya está apagada, volver. ; esperamos al VBlank, ya que no podemos apagar la pantalla ; en otro momento .espera_VBlank ld a, [rLY] cp 145 jr nz, .espera_VBlank ; estamos en VBlank, apagamos el LCD ld a,[rLCDC] ; en A, el contenido del LCDC res 7,a ; ponemos a cero el bit 7 (activado del LCD) ld [rLCDC],a ; escribimos en el registro LCDC el contenido de A ret ; volvemos ; rutina de retardo retardo: ld de, 2000 ; numero de veces a ejecutar el bucle .delay: dec de ; decrementamos ld a, d ; vemos si es cero or e jr z, .fin_delay nop jr .delay .fin_delay: ret ; Datos de nuestros tiles Tiles: DB $AA, $00, $44, $00, $AA, $00, $11, $00 DB $AA, $00, $44, $00, $AA, $00, $11, $00 DB $3E, $3E, $41, $7F, $41, $6B, $41, $7F DB $41, $63, $41, $7F, $3E, $3E, $00, $00 EndTiles: PaletaFondo: DW $76BD, $753D, $14B4, $0019 EndPaletaFondo: PaletaSprites: DW $77DF, $47EB, $7101, $14BD DW $0003, $5B63, $0204, $D671 DW $A5D4, $BBFD, $9569, $FFE3 DW $2014, $3B09, $4A8E, $934A DW $0416, $96BE, $0164, $B9DA DW $9B62, $35DF, $10B2, $D12F DW $1E09, $F91B, $0445, $48F9 DW $840E, $D972, $0A15, $AF6F EndPaletaSprites:
La ROM: holacolor.gb