Liber VII · Capitulum I

El cartucho

Por qué un ARM dentro del cartucho y qué hace cada procesador.

Un juego de 100 KB en una consola de 4 KB

La evaluación inicial estimó que el port necesitaría entre 100 y 150 KB de programa y datos y unos 16 KB de RAM: la lógica completa del original (guiones, inteligencia de los monjes, buscador de caminos, puertas y objetos), las 94 salas, la música y los textos en dos idiomas. Una 2600 tiene 128 bytes de RAM y ve 4 KB de cartucho. Hacía falta un cartucho que pusiera el cerebro.

El formato elegido es ELF: el cartucho lleva un procesador ARM que ejecuta el juego escrito en C. El 6507 de la consola no ejecuta código del cartucho, sino lo que el ARM pone en el bus en cada ciclo en que el 6507 lee una instrucción. Con la biblioteca vcsLib, una llamada como vcsWrite3(COLUBK, 0x84) se convierte en una instrucción que el 6507 ejecuta en ese mismo momento.

La ROM, abadia2600.bin, es un fichero ELF: un objeto reubicable para ARM con el código y los datos del juego, en el que las llamadas a vcsLib quedan sin resolver. Es el cargador del cartucho, o el del emulador, el que lo copia a su memoria y enlaza esas llamadas con su propia vcsLib en el momento de cargarlo; después llama a la función de entrada del juego con el sistema de televisión y el reloj. En el Chameleon Cart ese enlace se hace al compilar: el juego se une a los objetos de su firmware en un único ejecutable, que se graba en el cartucho.

Lo que pesó en la decisión, según la evaluación: la lógica en C con espacio de sobra, las escrituras con bus stuffing en 3 ciclos (el capítulo siguiente), que el formato funcione en el PlusCart, el UnoCart, el Chameleon Cart, Stella 7 y Gopher2600, y que los dos emuladores permitan depurar y medir. Emular el Z80 del original se descartó: costaba unos 58 000 T-states por frame y 128 KB de RAM. La lógica es una reimplementación en C, comprobada tick a tick contra un emulador de Spectrum propio.

Quién hace qué

QuiénQué hace
ARMToda la lógica del juego. Compone la sala de 128 × 160 en un bitmap de 1 bit, con las baldosas y los personajes. Precalcula los datos de cada línea y sintetiza la música.
ARM, durante la imagenInyecta cada instrucción del 6507 con su ciclo exacto: lda #, sta, nop y un jmp por línea para que el contador de programa no salga de los 4 KB del cartucho.
6507Ejecuta lo inyectado. En el overblank (63 líneas) corre un programa desde la RAM del RIOT mientras el ARM calcula.
TIADibuja y suena, como siempre.

Los personajes no son sprites del TIA: el ARM los pinta en el bitmap de la sala, con el algoritmo del pintor del original, y el TIA solo ve una imagen por línea. Por eso los sprites del TIA quedan libres para dibujar la sala.

El frame: 199 líneas dictadas y 63 libres

Un frame NTSC tiene 262 líneas. En las 199 del kernel el ARM está atado al 6507: entre dos inyecciones solo le queda el tiempo que tarda el 6507 en ejecutar la anterior, un ciclo tras una escritura, unas 40 instrucciones del ARM a 48 MIPS. En las otras 63 (overscan, VSYNC y VBLANK), la llamada vcsStartOverblank() deja al 6507 ejecutando un programa fijo en la RAM del RIOT, que además escribe una muestra de sonido por línea, y el ARM queda libre: unas 191 000 instrucciones a 48 MIPS. Ahí va todo el trabajo pesado: un trozo de la lógica, la construcción de la sala, el dibujo y la preparación de las líneas del siguiente frame.

El Chameleon Cart, el PlusCart y el UnoCart

El cartucho de destino es el Chameleon Cart de Phantom Logic Studios, creado por Zackary Scolaro (ZackAttack): un STM32G070 con un Cortex-M0+ a 64 MHz como mucho, 128 KB de flash con el firmware dentro y 36 KB de RAM. Es el que permitiría una edición física. El juego cabe: con la plantilla del firmware ocupa el 93,6 % de la flash y el 97,6 % de la RAM, tras ahorrar en todo lo posible (la partida guardada solo en la SaveKey, los datos de línea de todos los modos en la misma memoria, la portada descomprimida sobre esa misma RAM, la pila reducida de 1,8 a 1,1 KB y fuera el marcador ancho de antes, que ya no usa ningún modo).

En el Chameleon el código corre desde la flash, y su acelerador puede dar tirones que en el kernel serían fatales: si el ARM llega tarde, el 6507 lee basura. Por eso todo lo que alimenta al 6507 (el kernel de la partida, el de la portada, el arranque y la SaveKey, unos 8,3 KB) va en la sección .RamFunc, que el firmware copia a la RAM al arrancar. Una herramienta comprueba que nada de eso se quede en la flash. Ya funciona en el cartucho de verdad, aunque su firmware actual no trae todavía las funciones de bus stuffing: mientras tanto se compila una versión sin ellas, que solo ofrece DOBLE y FRANJA y empieza en DOBLE. En la consola apareció un cuelgue al pausar: la SaveKey se maneja por I²C desde el puerto del mando, y entre escritura y escritura el ARM se quedaba sin tiempo. Ahora cada paso del I²C va sin llamadas a funciones entre escrituras y acaba con un WSYNC, y la pausa y la SaveKey funcionan también en el Chameleon.

El PlusCart de Wolfgang Stubig (Al_Nafuur) (y el UnoCart, con el mismo firmware UCA) es otra historia. El cargador de la versión 2.3.28 de su firmware leía el ELF entero en un búfer de 96 KB de RAM y lo ejecutaba ahí, y vcsLib vive en la flash, demasiado lejos para un salto corto del ARM. Las llamadas a vcsLib son largas (#pragma long_calls y unos trampolines para memcpy y memset) y los datos constantes que no llevan punteros van en una sección .rodata aparte, para que el cargador nuevo de Wolfgang (Al_Nafuur) los deje en la flash. El juego se compila con una sección por dato, pero tras el enlazado parcial se juntan en cuatro: .bss, .data y las dos .rodata. Con cientos de secciones, Gopher2600, que busca la de cada acceso del ARM recorriéndolas una a una, iba a 48 imágenes por segundo en COMPLETO; con cuatro, a 121. Desde la versión 2.3.32, el firmware del PlusCart ya trae ese cargador, y con él el juego funciona en una consola real; con la 2.3.28 no cabía. En estos dos cartuchos el menú de la portada tiene una fila más, SALIR, que devuelve el control al firmware: el 6507 queda ejecutando NOP desde $1000 y el cartucho arranca su menú. Para escribir debajo PLUSCART o UNOCART, el juego inyecta al arrancar dos LDA # inocuos y mira en qué registro de salida del STM32F4 queda cada byte: el bus de datos va por el puerto C en el PlusCart y por el E en el UnoCart.

Cómo cabe todo

Para que el juego entre en el cartucho, cada tipo de dato se guarda de la forma que mejor le va. Las salas ni siquiera se guardan como imágenes: como en el original, cada una es una lista de bloques que un pequeño constructor monta al entrar. Lo demás se comprime solo si se puede leer sin coste en el momento en que hace falta.

DatosCómo se guardanTamaño
Las 94 salasLas listas de bloques del original y su constructor, reescrito en C. Como imágenes de 128 × 160 serían 235 KB.11,7 KB
Música de la intro y del finalLos cambios de los registros del AY, frame a frame, con un LZ77 propio cuyas referencias apuntan al propio flujo comprimido: se descomprime sobre la marcha, sin ventana en RAM y con un límite de lecturas por frame.1,4 + 3 KB (2,9 + 11,6 sin comprimir)
Melodías y efectos de la partidaLas partituras del original, con su reproductor.0,5 KB + 1,4 KB de código
PortadaEl programa de escrituras de cada línea, con un LZSS propio de troceado óptimo. Se descomprime una sola vez, al entrar en la portada, sobre la RAM de los datos de línea de la partida, que mientras tanto no se usa.2,8 KB (6,5 sin comprimir)
Textos de los pergaminos6 bits por carácter.2,9 KB (3,8 en ASCII)
Letra de los pergaminosCada glifo, como un código de cadena en nibbles que conserva el orden de la pluma. La inglesa solo guarda los seis glifos que cambian.1,5 + 0,3 KB
DiálogosComo en el original, frases hechas de números de palabra; el diccionario, a 5 bits por letra.0,9 KB de diccionario
SpritesSin comprimir: el motor los lee en cualquier orden, y comprimidos solo bajarían a 7,4 KB.13,3 KB