Herramientas de usuario

Herramientas del sitio


assembly_tutorial

Introduction

    NINTENDO
   ______                     ____             
  / ____/___ _____ ___  ___  / __ )____  __  __
 / / __/ __ `/ __ `__ \/ _ \/ __  / __ \/ / / /
/ /_/ / /_/ / / / / / /  __/ /_/ / /_/ / /_/ / 
\____/\__,_/_/ /_/ /_/\___/_____/\____/\__, /  
                                      /____/   
			Programming tutorial
			by David Pello (ladecadence.net)


             _n_________________     
            |_|_______________|_|    
            |  ,-------------.  |    
            | |  .---------.  | |    
            | |  |         |  | |    
            | |  |         |  | |    
            | |  |         |  | |    
            | |  |         |  | |    
            | |  `---------'  | |   
            | `---------------' |  
            |   _ GAME BOY      |    
            | _| |_         ,-. |    
            ||_ O _|   ,-. "._,"|    
            |  |_|    "._,"   A | 
            |      _  _  B      |
            |     // //         |    
            |    // //  \\\\\\  |    
            |    `  `    \\\\\\ ,    
            |________...______,"     

Nintendo Gameboy (DMG)

As a starter for this tutorial, let's do a quick review of the tech specs of our loved machine:

  • CPU: 8-bit Sharp LR35902 (i8080/Z80 alike) @ 4.19 MHz
  • 8K main RAM
  • 8K Video RAM
  • 2.6“ LCD 160×144, 4 shades of grey
  • Stereo sound, 4 Channels
  • Serial port (ext)
  • 6V, 0.7A
  • 90 mm x 148 x 32 mm

So, at the GameBoy's heart lays a custom CPU made by Sharp for Nintendo, the LR35902, a microprocessor somewhere in between the intel 8080 and the Z80; it doesn't have the Z80 alternate register set nor index ones (and their associated instructions), but it has most of its extended instruction set like the bit handling ones. It also has extra circuitry for LCD control, joypad, serial port and audio.

GBz80

The CPU, let's call it gbz80, it's an 8bit CPU with a 16bit address bus. This is, internal CPU data and memory data are handled as bytes, and it can address 2^16 = 64KB external memory.

Registers

The gbz80 CPU, has several internal registers in wich we can store data while we make calculations with them and move them to/from external memory. These 8 bit registers are the same as the 8080: a, b, c, d, e, h and l.

Flags

We have also the special 'f' register, that stores the status bits of the CPU. When the cpu executes instructions, some of them can set or unset these flags that will be really useful when programming. For example, one of these bits is the Zero flag, that tells us if the result of the previous instruction has been zero. Not all instructions modify the flags, but many of them do. If you want a deep knowledge of all gbz80 instructions, you can have a look at the Pan Docs: http://gbdev.gg8.se/wiki/articles/CPU_Instruction_Set

f register flags are:

Bit  Name  (1)  (0)  Role
 7    zf    Z   NZ   Zero Flag
 6    n     -   -    Add/substract Flag (BCD)
 5    h     -   -    Half carry Flag (BCD)
 4    cy    C   NC   Carry Flag
 3-0  -     -   -    Unused (always zero)

Most used flags are:

Zero Flag (Z)

This bit becomes 1 if the output of the previous instructions has been a zero (0). Really useful in conditional jumps.

Carry Flag (C, or Cy)

Becomes 1 when the result of an addition is bigger than $FF (8bit) or $FFFF (16bit),or when the result of a substraction or comparation is less then zero. It also becomes one when after an shift or rotate instruction, a 1 goes out. Used in conditional jumps or in arithmetical instructions as ADC, SBC, RL, RLA, etc.

16 bit registers

Some of these registers can be joined together to become 16 bit registers, really useful to manipulate memory addresses or bigger numbers if we need them. 16 bit registers are: af, bc, de, y hl. We have also two special registers, pc, or program counter, 16 bits wide, that stores the memory address of the next instruction that will be executed, and sp, or stack pointer, also 16 bits wide, that stores the current stack position.

Memory Map

Gameboy's main memory is mapped in a 16 bit address space, so it lets us index 64K bytes (2^16 = 65536). In this address space we need to keep all the addresses we need to access, this is, RAM, cartridge's ROM, cartridge's RAM in games that let you save the progress, video memory, etc. In order to do that, GameBoy designers mapped the memory space in several blocks like main RAM or video memory, keeping two 16K blocks for the cartridge's ROM, and an 8K block for the cartridge's RAM (saved games). As many games started to need more than 32K of ROM or 8K of save RAM, they needed to start using a technique called “Banking”, game's ROM is divided in several independent blocks (like the graphics and sounds for each level), that can be swapped for one another as necessary. In the GameBoy, it happens this way: we have a fixed 16K block (where usually the main logic of the game lives) and then using some instructions (that can be different as there are several mapping chips), we can swap 16K banks in the upper ROM memory. Looks complicated, but we'll learn to do this later on. All this is reflected in this GameBoy memory map with all the different blocks:

 
  General Memory Map                        Bank select registers
 -------------------------                 ----------------------------------

  Interrupt activation register
 ---------------------------------- $FFFF
  High main RAM
 ---------------------------------- $FF80
  Not available
 ---------------------------------- $FF4C
  I/O ports
 ---------------------------------- $FF00
  Not available
 ---------------------------------- $FEA0
  Sprite attributes (OAM)
 ---------------------------------- $FE00
  Mirror of main RAM 8kB
 ---------------------------------- $E000
  8kB main RAM
 ---------------------------------- $C000       ------------------------------
  8kB external swappable RAM                   /      MBC1 ROM/RAM selector
 ---------------------------------- $A000     /  -----------------------------
  8kB Video RAM                              /  /     RAM bank selector
 ---------------------------------- $8000 --/  /  ----------------------------
  16kB swappable ROM bank           $6000 ----/  /    ROM bank selector
 ---------------------------------- $4000 ------/  ---------------------------
  16kB Fixed ROM bank #0            $2000 --------/   External RAM activation
 ---------------------------------- $0000 ------------------------------------

* NOTE: b = bit, B = byte

* NOTE: Using RGBDS syntax, tutorial's code examples will write hexadecimal numbers with “$”, binary ones with ”%“ and decimal ones as is.

Structure of a GameBoy ROM

Gameboy ROMs need to keep a defined structure for the gameboy to run them, this is specially important for the ROM header. ROM parts and their functions:

GAMEBOY ROM CONTENTS
------------------------------------

$0    -  $100:	Interrupt vectors. But these addresses can also be used for normal code.
				
$100  -  $103:	Code entry point, usually here a NOP is used and then a jump to our code to 
                skip the header's data. 

				
$104  - $14E:	ROM header. It starts with the data for the Nintendo logo, (104h-133h)
		this is compared with the same data inside the Gameboy CPU, if they 
		don't match, the GameBoy stops running.
		
                Then we have the following data:
		
                $134 - Cart's name - 15bytes
		
                $143 - Gameboy Color support
		        $80 = GBColor, $00 or any other = B/N
					
                $144 - New licensee code, 2 bytes (not important)
		
                $146 - Super Gameboy support (SGB)
	                00 = GameBoy, 03 = Super GameBoy
					
                $147 - Cartridge type (ROM only, MBC1, MBC1+RAM.. etc)
		         0 - ROM Only                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 - ROM Size 
			  0 - 256Kbit =  32KByte =   2 banks
		  	  1 - 512Kbit =  64KByte =   4 banks
		   	  2 -   1Mbit = 128KByte =   8 banks
		  	  3 -   2Mbit = 256KByte =  16 banks
		  	  4 -   4Mbit = 512KByte =  32 banks
		  	  5 -   8Mbit =   1MByte =  64 banks
		  	  6 -  16Mbit =   2MByte = 128 banks
			$52 -   9Mbit = 1.1MByte =  72 banks
			$53 -  10Mbit = 1.2MByte =  80 banks
			$54 -  12Mbit = 1.5MByte =  96 banks
			
		$149 - RAM Size
			0 -  No RAM
			1 -  16kBit =   2kB =  1 bank
			2 -  64kBit =   8kB =  1 bank
			3 - 256kBit =  32kB =  4 banks
			4 -   1MBit = 128kB = 16 banks
			
		$14A - Destination code 
			0 - Japan
			1 - Not Japan
			
                $14B - Old licensee code, 2 bytes 
 			$33 - Look up the code at $0144/$0145
			(SGB functions don't work if this is not $33)
		
                $14C - ROM version (Usually $00)
		
                $14D - Header checksum (important, Gameboy won't boot if this is wrong)
		
                $14E - Global Checksum (not important)

$14F  - $3FFF:	Game Code. This is a fixed 16K bank, number 0.

$4000 - $7FFF:  Game Code, second 16K bank. When the Gameboy boots, this is mapped
		to bank 1 (for 32K ROMS), but it can be swapped for other 16K banks
		up to the maximum ROM size we can access using a mapper like the MBC1.

Video system

Gameboy video system is not a direct pixel-by-pixel access system like a framebuffer in a PC, but a system formed by blocks or “tiles”. It has a buffer of 256×256 pixels (32×32 tiles of 8×8 pixels) from what you can show 160×144 in screen (20×18 tiles). We have two registers SCROLLX and SCROLLY, that let us move the buffer around the screen. Also the buffer is circular, when you scroll the buffer to one of its limits, we start to see the data on the opposite side.

				  ^
				  |
				  v

		+------------------------------------+
		|(scrollx, scrolly)                  |
		|   +----------------+               |
		|   |                |               |
		|   |                |               |
		|   |                |               |
		|   |     20x18      |               |
		|   |  screen area   |               |
		|   |                |               |
		|   |                |               |
	<->	|   |                |               |	<->
		|   +----------------+               |
		|                                    |
		|                32x32               |
		|           background map           |
		|                                    |
		|                                    |
		|                                    |
		|                                    |
		|                                    |
		+------------------------------------+		
				  ^
				  |
				  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 http://anthony.bentley.name/rgbds/

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 teneis que compilarlo, pero vamos, es simplemente ejecutar como root, un “make install” en el directorio después de haberlo descargado con git.

Teneis la documentación del RGBDS en: http://anthony.bentley.name/rgbds/manual/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

Si vim es demasiado para vosotros (de momento), podeis probar con el Notepad++, que tiene soporte para ensamblador. http://notepad-plus-plus.org/es/home

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",HOME[$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",HOME[$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 HOME ($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",HOME[$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",HOME[$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",HOME[$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 deliminar 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",HOME[$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",HOME[$0040]
    call DibujaCronometro
    reti
 
; interrupción de desbordamiento del timer
SECTION "Timer_Overflow",HOME[$0050]
    ; cuando hay una interrupción del timer, llamamos a esta subrutina
    call    ControlTimer
    reti
 
; El programa comienza aqui:
SECTION "start",HOME[$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",HOME[$0040]
    reti     ; no hacemos nada, volvemos
 
; El programa comienza aqui:
SECTION "start",HOME[$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",HOME[$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

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