Herramientas de usuario

Herramientas del sitio


tutorial_de_ensamblador

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:

hola-ventana.gb

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

tutorial_de_ensamblador.txt · Última modificación: 2023/04/19 16:59 por 127.0.0.1