Tutorial de NES. Aprendiendo a programar la NES con ejemplos. Capítulo I - Paletas de colores
La Jaquería - 16 de febrero del 2025
Prefacio
NES, diseñada por Masayuki Uemura y lanzada en Japón en 1983 fue una consola icónica que hizo historia. A diferencia de las consolas actuales, que intentan ser pioneras, la NES fue diseñada como un juguete robusto y barato. La NES tenía que tener un precio suficientemente bajo para resultar atractivo a los padres y un hardware lo suficientemente potente para que los juegos resultaran atractivos a los niños. Para hacer esto posible, Uemura usó dos estrategias: por un lado la NES tendría el mínimo de memoria RAM posible (ya que en los 80 era tremendamente cara) y por otro lado encargarían los chips necesarios al fabricante en cantidades muy altas con un precio unitario muy bajo.
Introducción
Por ser la NES una consola tan icónica, hay numerosos tutoriales en internet, siendo a mi parecer el mejor, el conocido como Famicon Party. Teniendo esto en cuenta, ¿qué sentido tiene hacer uno más?, por un lado, siempre es motivo de orgullo tener cosas hechas por nuestra asociación, y por otro lado, en el tutorial antes mencionado se hace una explicación en profundidad del funcionamiento de la NES, los ejemplos propuestos son algo liosos y dificiles de entender a mi parecer, de aquí surge la idea de hacer un tutorial basado en ejemplos, en los que se explique en detalle lo que se hace en cada línea de código.
Hardware de la NES
El procesador principal de la NES es el RICOH 2A03 a 1.79MHz en regiones NTSC y RICOH 2A07 a 1.66MHz en regiones PAL. Este procesador es una implementación pirata del MOS 6502, al que se le ha añadido una unidad de generación de sonidos llamada APU capaz de generar 5 canales de sonido: Dos de onda cuadrada, uno de onda triangular, un canal para ruido y un canal PCM. Para evitar posibles demandas por parte de Commodore International, propietaria del MOS 6502, se deshabilitó el modo BCD.
Tiene su gracia que la misma empresa que en los años 80 decidió saltarse la propiedad intelectual del 6502, hoy se dedique a perseguir sistemáticamente a todo aquel que aún con fines altruistas y de conservación histórica ose a enlazar una imagen de cartucho de NES en su web, cartuchos que no se fabrican ni distribuyen desde hace décadas.
La NES cuenta con un coprocesador gráfico, diseñado a medida, el RICOH 2C02. A este procesador se le conoce como PPU por sus siglas en inglés Picture Processing Unit. Este coprocesador se encarga de generar la señal de video que llegará a la tele.
En cuanto a la memoria RAM, el procesador principal de la NES está conectado a 2KB de RAM interna, situada en las direcciones de memoria $0000–$07FF. El coprocesador gráfico tiene su propio espacio de direcciones de memoria independiente del procesador principal. Tiene 2KB de memoria estática de video + 256 bytes de memoria diámica usada para guardar la información de los sprites.
Tabla de colores
La PPU de la NES tiene codificado en su interior una tabla de 64 colores, aunque algunos están duplicados o son muy parecidos a otros, lo que acaba resultando en una tabla efectiva de unos 56 colores. Las tablas completas de colores de las distintas versiones de PPU se pueden consultar en la wiki de NesDev. Para este ejemplo, vamos a usar la tabla que viene en el tutorial de Famicon Party
Primer ejemplo: Paletas
Este es el ejemplo más sencillo que podemos hacer con la NES. Vamos a cargar en las paletas unos cuantos colores y usaremos un emulador con opciones de depuración para comprobar que efectivamente se han cargado correctamente. La NES cuenta con un total de 8 paletas, 4 de ellas se utilizan para fondos y las otras 4 para sprites. Cada paleta tiene un total de 4 colores, con una limitación, el primer color de las 8 paletas debe ser el mismo, y será el color de fondo universal, que se pintará en la pantalla cuándo no haya otra cosa que pintar. En este ejemplo, dicho color será el que se verá en toda la pantalla. Se incluye a continuación el código del ejemplo que se irá explicando a lo largo de éste artículo.
; Ejemplo de paletas NES
; ---------------------------------------------------------------------|
; Compilar con xa -C ejemplo.s -o ejemplo.nes |
; Ejecutar el .nes en un emulador, por ejemplo fceux, y comprobar que |
; aparece el fondo celeste y si abrimos el visor de la PPU, disponible |
; en el menu debug / ppu viewer vemos las ocho paletas de colores |
; cargadas con los colores usados en el codigo |
;----------------------------------------------------------------------|
; Tabla colores NES (extracto)
; La tabla completa se puede consultar en
; https://famicom.party/_app/immutable/assets/NES_color_palette_with_numbers.pvUMp0SZ.webp
AZUL = $01
VIOLETA = $14
VIOLETA_CLARO = $24
NARANJA = $27
ROSA = $25
VERDE = $2a
CELESTE = $2c
ROJO = $16
NEGRO = $0F
CELESTE_CLARO = $3c
AMARILLO = $38
; Registros PPU
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
PPUADDR = $2006
PPUDATA = $2007
PPUPALETTES = $3F00
DMC_IRQ = $4010
; Cabecera emuladores
*=$0000
.byt $4e,$45,$53,$1a,$02,$01,$01,$00,$00,$00,$00,$00,$00,$00,$00,$00
; Direccion de memoria de inicio de la ROM
*=$8000
main:
.(
; Especificamos a la PPU a que direccion de memoria vamos a enviar datos
; En primer lugar leemos PPUSTATUS, y a continuacion enviamos la primera
; direccion de la PPU en la que queremos escribir, primero el byte mas
; significativo, seguido del byte menos significativo
LDX PPUSTATUS
LDX #>PPUPALETTES
STX PPUADDR
LDX #<PPUPALETTES
STX PPUADDR
; En NES tenemos 8 paletas de cuatro colores cada una. Primero van
; las 4 de BACKGROUND, seguidas de las 4 de SPRITES
; Enviamos los 4 colores de la primera paleta de fondo
LDA #CELESTE_CLARO
STA PPUDATA
LDA #NEGRO
STA PPUDATA
LDA #ROJO
STA PPUDATA
LDA #AZUL
STA PPUDATA
; Enviamos la segunda paleta de fondo. El primer color debe ser el mismo
; en las 8 paletas. Sera usado como color de trnasparencia en los sprites.
; Podemos enviar a la PPU tantos datos seguidos como queramos, se iran
; guardando en posiciones contiguas de memoria
LDA #CELESTE_CLARO
STA PPUDATA
LDA #NARANJA
STA PPUDATA
LDA #ROSA
STA PPUDATA
LDA #VERDE
STA PPUDATA
; Enviamos la tercera paleta de fondo. Mantenemos el primer color.
; No es necesario informar todas las paletas si no las vamos a usar, pero en
; este ejemplo vamos a enviarlas todas y comprobar en el emulador que
; se hayan actualizado
LDA #CELESTE_CLARO
STA PPUDATA
LDA #ROJO
STA PPUDATA
LDA #VERDE
STA PPUDATA
LDA #AMARILLO
STA PPUDATA
; Enviamos la ultima paleta de los fondos
LDA #CELESTE_CLARO
STA PPUDATA
LDA #ROJO
STA PPUDATA
LDA #AZUL
STA PPUDATA
LDA #ROSA
STA PPUDATA
; Ahora vamos a enviar las cuatro paletas de sprites, en este
; caso vamos a usar un bucle, y vamos a enviar las cuatro iguales
LDY #4 ; y=4
bucle: ; do {
LDA #CELESTE_CLARO
STA PPUDATA
LDA #ROJO
STA PPUDATA
LDA #NARANJA
STA PPUDATA
LDA #VERDE
STA PPUDATA
DEY ; y = y-1
BNE bucle ; } while(y!=0)
; Activar PPU
LDA #$1e
STA PPUMASK
; No hacer nada mas. En el 6502 no hay wait ni nada parecido,
; Solo podemos hacer un bucle infinito para detener la ejecucion
forever:
JMP forever
.)
; Rutina de arranque de la NES. Esta rutina la mantendremos siempre igual
init:
.(
; Inicializar CPU
SEI
CLD
LDX #$FF
TXS
; Inicializar APU
LDX #$40
STX $4017
; Inicializar PPU
LDX #$0
STX PPUCTRL
STX PPUMASK
STX DMC_IRQ
; Esperar a que la PPU este lista
BIT PPUSTATUS
vblankwait:
BIT PPUSTATUS
BPL vblankwait
vblankwait2:
BIT PPUSTATUS
BPL vblankwait2
; Ir al codigo principal
JMP main
.)
; Rutinas de interrupciones. Sin uso en este ejemplo
irq_handler:
RTI
nmi_handler:
RTI
; Vectores 6502. Indican la direccion de inicio del programa.
; Siempre lo vamos a dejar igual
.dsb $fffa-*, $ff
.word nmi_handler
.word init
.word irq_handler
; CHR ROM
; Espacio para guardar las losetas (tiles). Sin uso en este ejemplo
*=$0000
.dsb $2000-*, $00
Compilar y ejecutar
- Instalamos xa65 con el comando apt-get -y install xa65
- Copiamos el código del ejemplo y lo pegamos en un fichero de texto
- Compilamos el ejemplo con el comando xa -C ejemplo.s -o ejemplo.nes
- Ejecutamos el ejemplo con un emulador, como fceux y buscamos el visor de la PPU para ver los colores cargados en las paletas
Explicación del código paso a paso
En primer lugar, vamos a definir una serie de constantes que nos serán útiles.
AZUL = $01
VIOLETA = $14
De la tabla de colores, vamos a coger unos cuantos y los definimos como constantes
; Registros PPU
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
PPUADDR = $2006
PPUDATA = $2007
PPUPALETTES = $3F00
Recordemos que la PPU tiene su propio espacio de direcciones de memoria. La forma de controlar la PPU es escribir en ese espacio de memoria, en las direcciones adecuadas los valores que provocan el comportamiento que queremos. Definimos las direcciones de la PPU que vamos a usar como constantes.
Dada la comodidad que supone poder probar el juego en el ordenador usando un emulador, más teniendo en cuenta que ya no se fabrican NES, en este ejemplo vamos a generar un archivo ROM que puede ser usado en un emulador. Se podría grabar en un cartucho físico, o copiar a un Everdrive. Por esto, el principio del archivo tiene que ser la cabecera de una ROM de NES.
; Cabecera emuladores
*=$0000
.byt $4e,$45,$53,$1a,$02,$01,$01,$00,$00,$00,$00,$00,$00,$00,$00,$00
En primer lugar especificamos $0000 como dirección de inicio, puesto que la cabecera no se considera parte de la ROM. A continuación ponemos los valores para indicar que se trata de una ROM de NES NTSC básica, con 32 KB de ROM de programa y 8 KB de ROM de caracteres gráficos.
A continuación, empezamos con la propia ROM
; Direccion de memoria de inicio de la ROM
*=$8000
Le indicamos al ensamblador, que la ROM va a estar ubicada en el espacio de direcciones del procesador a partir de la dirección $80000.
En ensamblador, no tenemos la posibilidad de usar funciones o procedimientos, pero si que tenemos algo parecido, que es el uso de subrutinas, para indicar al ensamblador que cada rutina tenga su propio contexto y se puedan usar etiquetas con el mismo nombre en distintas subrutinas, se utiliza la siguiente sintaxis:
main:
.(
; [...] código
.)
En primer lugar especificamos la etiqueta de la subrutina seguido de dos puntos, en la siguiente línea ponemos punto seguido de abrir paréntesis, añadimos el código de la subrutina en las siguiente líneas, y finalizamos con punto y cierre de paréntesis. Las etiquetas que usemos dentro de los paréntesis, se podrán volver a utilizar en otras subrutinas.
Dijimos al comienzo de este artículo que íbamos a escribir en las paletas una serie de colores y luego comprobar con un emulador que efectivamente lo hemos conseguido. Las 8 paletas están en el espacio de memoria de la PPU, en las direcciones $3F00 a $3F20. La CPU no puede escribir directamente en la memoria de la PPU, en su lugar lo que se hace es primero indicar la dirección en la que queremos escribir, y luego enviar el dato.
LDX PPUSTATUS
Antes de iniciar la escritura de la dirección de la PPU, hacemos una lectura de PPUSTATUS, ésto es porque como la dirección de memoria tiene 2 bytes, que se envían consecutivos, al hacer esta lectura le estamos diciendo a la PPU, que el siguiente valor que vamos a enviar es el primer byte de una dirección de memoria. En ensamblador 6502, la forma de leer de una posición de memoria es usando LDX, que carga el dato leído en el registro X, o LDY que lo carga en el registro Y o por último podemos usar LDA que lo carga en el registro A.
LDX #>PPUPALETTES
STX PPUADDR
LDX #<PPUPALETTES
STX PPUADDR
Enviamos la dirección de memoria, en este caso la que tenemos definida en la constante PPUPALETTES ($3F00), y lo hacemos escribiendo en PPUADDR dos veces, primero escribimos el byte más significativo ($3F) y acontinuación escribimos el menos significativo ($00). Como tenemos una constante de 2 bytes definida, la forma de referirnos al primer o segundo byte, es usando los símbolos mayor y menor. La almohadilla indica que es una constante. La forma de escribir en una posición de memoria es usando la instrucción STX, que escribe el valor del registro X en la posición de memoria indicada. De igual forma STY escribe en valor del registro Y y STA escribe el valor del registro A.
Ya le hemos dicho a la PPU que queremos escribir en $3F00, que es la dirección de memoria de inicio de las paletas. A continuación podemos enviar el dato que queremos, es decir los colores.
LDA #CELESTE_CLARO
STA PPUDATA
Escribimos en PPUDATA el primer color, $3C que corresponde a celeste claro. De igual forma escribimos el resto de colores. No es necesario volver a indicar la dirección de memoria en la que queremos escribir, los datos que vayamos escribiendo en PPUDATA, se irán escribiendo en posiciones consecutivas de memoria. Como las paletas están en memoria consecutiva, podemos enviar los 32 colores (4 colores por paleta * 8 paletas) uno detrás de otro.
LDY #4
bucle:
; [...] codigo
DEY
BNE bucle
Las últimas cuatro paletas, las enviamos usando un bucle. La forma de repetir un código N veces, es cargando en el registro Y el número de veces que queremos repetir el código, seguido de una etiqueta, bucle, y tras el código a repetir ponemos un decremento de Y, DEY y un salto condicional BNE bucle, de forma que el bucle se repita mientras el registro Y sea distrinto de cero.
Por último habilitamos la PPU para que se muestre la imagen, lo que se hace escribiendo el valor $1E en PPUMASK
LDA #$1e
STA PPUMASK
Usamos LDA Para cargar en A el valor que queremos escribir y usamos STA para escribir el valor que acabamos de cargar en A en PPUMASK.
Por último finalizamos el programa con un bucle infinito. En un programa destinado a un procesador moderno jamás haríamos esto, porque boicotearíamos la gestión de energía del procesador, haciéndole creer que tiene una carga de trabajao gorda, cuando en realidad queremos que se suspenda. Sin embargo el procesador de la NES es tan sencillo, que la forma correcta de parar el procesador es saltando una y otra vez a la misma dirección de memoria.
forever:
JMP forever
Declaramos una etiqueta, y saltamos a dicha etiqueta que es la propia intrucción de salto, de forma que lo que estamos haciendo es saltar a la posición de memoria en la que estamos, quedándose el procesador “atascado” en esa instrucción. Como en la NES quién genera la señal de video es la PPU, no hay ningún problema en dejar a la CPU parada.
A continuación se define un procedimiento de inicio (init) de la NES, procedimiento que pondremos siempre en nuestros juegos.
SEI
En primer lugar se deshabilitan las interrupciones, puesto que no las vamos utilizar.
CLD
Se desactiva el modo decimal del 6502, en la CPU este modo no está soportado, de todas formas conviene deshabilitarlo.
LDX #$FF
TXS
Se inicializa la pila a la posición $FF, la pila en el procesador 6502 está ubicada en la página $01, con esta instrucción establecemos la cima de la pila en $FF.
LDX #$0
STX PPUCTRL
STX PPUMASK
STX DMC_IRQ
Se inicializa la PPU, escribiendo $0 en los registros PPUCTRL, PPUMASK Y DMC_IRQ
BIT PPUSTATUS
vblankwait:
BIT PPUSTATUS
BPL vblankwait
vblankwait2:
BIT PPUSTATUS
BPL vblankwait2
Esperar una serie de vblanks para dar tiempo a que la PPU esté inicializada.
JMP main
Por último, se salta al código principal, marcado con la etiqueta main
irq_handler:
RTI
nmi_handler:
RTI
.dsb $fffa-*, $ff
.word nmi_handler
.word init
.word irq_handler
A continuación se especifican las rutinas de interrupción, que como no las necesitamos, están en blanco
irq_handler:
RTI
nmi_handler:
RTI
Especificamos los vectores de inicio
.dsb $fffa-*, $ff
.word nmi_handler
.word init
.word irq_handler
La primera instrucción, es para indicar al ensamblador que los vectores de inicio van en la dirección $fffa, se especifican las direcciones de la rutina de interrupción no enmascarable, la dirección de inicio del programa y la rutina de interrupción normal.