
En el artículo "Paginación de la memoria usando MSX-DOS 2" comentamos cómo solicitar páginas de RAM de 16K al MSX-DOS2 y colocar código en esos bloques para que el Z80 pudiera ejecutarlo. El problema es que los bloques que se van compilando en C deben tener una estructura de direcciones que no es fácil de conseguir. Se necesitan muchos cálculos para saber dónde se coloca cada bloque y cómo definirlo para que SDCC pueda encontrarlo. Otra solución consiste en abandonar MSX-DOS2 y pasar al formato ROM, el de los cartuchos de MSX, donde en lugar de almacenar la información en un disquete, esta se guarda en la ROM del cartucho, lo que proporciona un acceso mucho más rápido.
Tal y como hemos explicado en diferentes artículos, el Z80 puede direccionar un máximo de 64K, divididos en cuatro páginas de 16K. Para superar esta limitación, el estándar MSX creó un sistema de slots y subslots de 16K que permite cambiar la memoria visible para el Z80 asignando distintos slots a cada página. Normalmente, la página 0 está ocupada por la BIOS, la 1 por la primera ranura de cartuchos, la 2 por la segunda ranura de cartuchos y la 3 por la RAM. Dependiendo del fabricante, la BIOS y la RAM pueden estar ubicadas en diferentes slots y subslots.
Si tienes un cartucho de 16K y lo insertas en la primera ranura, el Z80 verá esos 16K en la página 1. A medida que evolucionó la tecnología y surgió la necesidad de disponer de ROMs de mayor capacidad, aparecieron los mappers, que son ROMs de más de 16K divididas en varios segmentos y con una lógica adicional que permite cambiar dinámicamente qué segmentos son visibles en cada una de las páginas que ve el Z80. Estos mappers nunca llegaron a estandarizarse y distintos fabricantes desarrollaron sus propias implementaciones, como ASCII-8, ASCII-16, Konami5, etc.
La biblioteca MSXgl permite programar para distintos tipos de mappers. En este artículo utilizaremos el ASCII-8, donde cada segmento tiene un tamaño de 8K. Este mapper también está explicado en la documentación de MSXgl.
Para poder aprovechar las facilidades que ofrece la biblioteca MSXgl para crear la ROM, debemos tener activados los siguientes parámetros en project_config.js:
Target = "ROM_ASCII8";BankedCall = true;RawFiles = [ { segment: 10, file: "./content/img/PatBosc.sc5" },
{ segment: 15, file: "./content/img/PatBosc.pl5" }
];RawFiles se incluyen todos los archivos binarios que no necesitan compilarse, como imágenes o sonidos, y que pueden incorporarse directamente a la ROM.
MSX-DOS2 disponía de funciones de paginación para crear y cambiar páginas de memoria. Ahora MSXgl nos permite gestionar los segmentos de la ROM mediante las siguientes funciones:
SET_BANK_SEGMENT(u8 page, u8 number_of_segment): Esta función carga uno de los segmentos de la ROM (number_of_segment) en la página del Z80 indicada por page.GET_BANK_SEGMENT(u8 page): Devuelve el número del segmento que se encuentra actualmente asignado a la página page del Z80.Un ejemplo de este uso puede encontrarse en el archivo squirrel.c, donde llamamos a una función ubicada en uno de los segmentos de la ROM:
189void obtenir_coordenades_rajola(char map_x, char map_y) { 190 u8 savedSeg = GET_BANK_SEGMENT(3); 191 SET_BANK_SEGMENT(3, 6); // El segment del mapa squirrel_s6_b3 192 char tipus = map1[map_x + (map_y * NOMBRE_RAJOLES_HOR)]; 193 stamp_x = (tipus % NOMBRE_RAJOLES_HOR_ORIGEN_PATRONS) * 8; 194 stamp_y = OFFSET_COORDENADAY_PAGINA_ACTIVA_1 + (tipus / NOMBRE_RAJOLES_HOR_ORIGEN_PATRONS)*8; 195 SET_BANK_SEGMENT(3, savedSeg); 196}
En primer lugar guardamos el segmento que se encuentra actualmente en la zona de memoria que queremos ocupar con el nuevo segmento (quizá no sea necesario si siempre es el mismo en la estructura de nuestro programa, pero de esta forma el código es más genérico). A continuación asignamos al banco 3 de la memoria del MSX el segmento 6 (que en la estructura de archivos de nuestra aplicación hemos denominado squirrel_s6_b3 y que el script que genera el proyecto de MSXgl ya se ha encargado de colocar en la posición adecuada al crear la ROM).
La variable map1 es una constante que se encuentra precisamente en el segmento que acabamos de activar, por lo que en la línea 192 se obtiene el valor correcto. Es muy importante definir correctamente las variables utilizando la directiva extern, indicando que no están definidas en ese archivo C y que será el linker el encargado de ubicarlas correctamente en memoria.
Por último, en la línea 195 restauramos el segmento de memoria que estaba activo antes de realizar el cambio para poder consultar map1.
En este ejemplo solo hemos realizado un cambio de memoria para acceder a una variable de gran tamaño, pero también podría utilizarse para llamar a una función situada en otro banco y regresar después al código original. Cuanto más autónoma sea una función (es decir, que no necesite variables ni llame a otras funciones), mejor candidata será para ubicarse en un segmento independiente.
Otra operación interesante es cómo cargar imágenes desde la ROM al VDP. Antes lo hacíamos leyendo los datos desde el disquete, pero ¿cómo accedemos ahora a la parte de la ROM donde MSXgl las ha almacenado? El siguiente código realiza precisamente esa operación:
200void ROM_LoadSc5Image() { 201 u16 dst = 0x8000; 202 u16 file_size = 33792; // PatCit.sc5 is 33KB 203 u16 bytes_loaded = 0; 204 u8 segment; 205 u8 savedSeg = GET_BANK_SEGMENT(3); 206 207 // Load PatCit.sc5 from segments 5-9 (spans 5 segments of 8KB each) 208 for (segment = 10; segment <= 14 && bytes_loaded < file_size; segment++) { 209 SET_BANK_SEGMENT(3, segment); 210 const u8 *src = (const u8 *)0xA000; 211 212 u16 size_to_copy; 213 if (bytes_loaded == 0) { 214 // First segment: skip 7 bytes header 215 src += 7; 216 size_to_copy = (file_size < 8185) ? file_size - 7 : 8185; 217 bytes_loaded = size_to_copy; 218 } else { 219 // Subsequent segments: copy full 8KB or remaining bytes 220 size_to_copy = (file_size - bytes_loaded < 8192) ? (file_size - bytes_loaded) : 8192; 221 bytes_loaded += size_to_copy; 222 } 223 224 VDP_WriteVRAM(src, dst, 0, size_to_copy); 225 dst += size_to_copy; 226 } 227 228 SET_BANK_SEGMENT(3, savedSeg); 229}
La variable dst es la dirección de la memoria VRAM donde queremos comenzar a escribir. file_size es el tamaño del archivo en bytes, que puede obtenerse fácilmente utilizando ls -l en Linux o consultando las propiedades del archivo en otros entornos gráficos. bytes_loaded es un contador que utilizaremos para conocer la cantidad de bytes que ya hemos leído y que nos permitirá cargar los bytes restantes del último segmento. En la línea 205 hacemos lo mismo que antes: guardamos el segmento actual para restaurarlo al finalizar la carga. A continuación comenzamos el bucle en la línea 208. ¿Cómo sabemos qué segmentos y cuántos debemos leer? Es muy sencillo: en nuestro archivo project_config.js hemos definido:
147//-- List of raw data files to be added to final binary (array). Each entry must be in the following format: { offset=0x0000, file="myfile.bin" } 148RawFiles = [ 149 { segment: 10, file: "./content/img/PatBosc.sc5" }, 150 { segment: 15, file: "./content/img/PatBosc.pl5" } 151];
Con ello hemos indicado que el archivo PatBosc.sc5 comienza en el segmento 10. Si sabemos que el tamaño total del archivo es de 33792 bytes, podemos calcular cuántos segmentos ocupa: 33792 / (8 × 1024) = 4,125. Es decir, ocupa cuatro segmentos completos y una parte de un quinto. Por tanto, el bucle debe llegar hasta el segmento 14, donde el último segmento, el quinto, no estará completamente lleno y solo contendrá los bytes restantes.
El if de la línea 213 únicamente sirve para descartar los primeros 7 bytes, que contienen información del archivo y no datos de la VRAM. En la línea 220 comprobamos si los bytes que quedan por cargar corresponden a un segmento completo (8 × 1024 = 8192 bytes) o únicamente al resto del último segmento. A continuación copiamos los datos desde la RAM a la VRAM mediante la función VDP_WriteVRAM. Finalmente, una vez terminado el bucle, restauramos el segmento original en el banco 3.
En este capítulo veremos distintas técnicas para programar utilizando segmentos sin provocar errores de sobrescritura, o para poder detectarlos correctamente en caso de que aparezcan.
Esta es una parte muy crítica y requiere un diseño previo para crear segmentos lo más autónomos posible, evitando llamadas a otros segmentos o el acceso a variables situadas en ellos. En una aplicación compleja esto es muy difícil de conseguir, por lo que la mejor estrategia consiste en colocar las variables compartidas entre distintos segmentos en el banco inicial, que nunca cambia. Lo mismo ocurre con las funciones: las funciones comunes a varios segmentos deben mantenerse en el banco inicial. De esta forma resulta mucho más sencillo garantizar que todas ellas sean visibles durante la ejecución.
Con el mapper ASCII-8 solo disponemos de cuatro bancos para ubicar nuestro programa. Otros mappers, como NEO, eliminan la parte correspondiente a la Main ROM, permitiendo trabajar con hasta seis bancos.
Si un segmento es demasiado grande, parte del código no se generará correctamente. Durante la compilación, MSXgl muestra un mensaje como el siguiente:
┌───────────────────────────────────────────────────────────────────────────┐
│ PACKAGE │
└───────────────────────────────────────────────────────────────────────────┘
Packaging binary...
Execute: "/home/jepsuse/MSX/MSXgl/tools/MSXtk/bin/MSXhex" /home/jepsuse/MSX/MSXgl/projects/learning-msxgl/2p_squirrelHunt/out/squirrel.ihx -e rom -s 0x4000 -l 131072 -b 8192 -f /home/jepsuse/MSX/MSXgl/projects/learning-msxgl/2p_squirrelHunt/out/msxhex.txt
Log: Add file './content/img/PatBosc.sc5' at offset 00014000h
Log: Add file './content/img/PatBosc.pl5' at offset 0001E000h
MSXhex 1.0.0 - Convert an Intel HEX file to binary
Start=00004000h Size=00020000h Bank=00002000h Pad=FFh
Saving /home/jepsuse/MSX/MSXgl/projects/learning-msxgl/2p_squirrelHunt/out/squirrel.rom...
Seg[0000]: Lower=4000h Higher=8A20h Size=18977 (Holes=0)
Seg[0004]: Lower=A000h Higher=ABC5h Size=3014 (Holes=0)
Seg[0005]: Lower=A000h Higher=A04Ah Size=75 (Holes=0)
Seg[0006]: Lower=A000h Higher=BAFFh Size=6912 (Holes=0)
Total size: 28978
Exit code: 0
Success
Aquí tenemos una lista de los segmentos de nuestro proyecto. Nuestra arquitectura es ASCII-8, por lo que cada segmento tiene un tamaño de 8192 bytes. Nuestra aplicación está formada por un segmento 0 que ocupa más de dos segmentos, de modo que los bancos 0, 1 y 2 ya están ocupados. Por eso, en los ejemplos anteriores hemos situado las funciones en el banco 3. Los otros tres segmentos, 4, 5 y 6, ocupan menos de 8K, por lo que no existe ningún solapamiento que pueda corromper la aplicación.
Si quieres comprobar que la ROM que está viendo openMSX es correcta, al abrir los depuradores, en la opción Add Hex Editor aparecerá el archivo de tu ROM y podrás visualizar su contenido binario. Si quieres verificar que MSXgl ha generado correctamente la ROM con los raw files, puedes comprobar la dirección mostrada por el depurador y compararla con el archivo binario. Por ejemplo, nosotros cargamos PatBosc.sc5 en el segmento 10, por lo que en la ROM debería encontrarse en la posición 10 × 8 × 1024 = 0x14000. Si lo comprobamos tanto en el depurador como, por ejemplo, con Okteta, veremos que ambas posiciones coinciden, tal y como muestra la siguiente imagen:


Otro depurador de openMSX que puede resultar muy útil es el visor de slots, que indica, para cada página visible por el Z80, qué slot está utilizando en ese momento. En nuestro caso hemos utilizado segmentos de 8K, por lo que cada página de 16K del Z80 está formada por dos segmentos. openMSX muestra esta información mediante la notación RX/Y, donde X corresponde al primer segmento e Y al segundo. Así, en nuestra aplicación, como se muestra en la imagen inferior, la página 1 del Z80 contiene los segmentos 0 y 1 de la ROM, mientras que la página 2 contiene los segmentos 2 y 5. Es precisamente este segundo segmento, el 5, el que vamos intercambiando según la parte del programa que lo necesite. Si dejamos la aplicación en ejecución, veremos cómo ese 5 va cambiando rápidamente a 6 y a otros números a medida que se realiza el intercambio de bancos.
