Tabla de Contenidos
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:
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