El Z80 solo puede gestionar 64K simultáneamente. Para superarlo, la arquitectura MSX utiliza Slots, dividiendo el espacio en cuatro páginas de 16K. Aunque esto ya existía en el MSX1, fue con el MSX2 y la aparición del Memory Mapper cuando se pudo gestionar hasta 4MB de RAM. Este mapper se controla mediante los puertos de entrada/salida 0xFC-0xFF. Como cada fabricante ubicaba la RAM en slots diferentes (por ejemplo, el Philips VG-8235 la tiene en el slot 3-2, mientras que un Sony HB-F1XDJ la tiene en el 3-0), y si además tenía algún mapeador de memoria (memory mapper) adicional para disponer de más RAM, la gestión era compleja. Por eso, el MSX-DOS2 introdujo un gestor de memoria estándar que permitía a los programadores utilizar la RAM sin preocuparse de la arquitectura física del modelo.
En este artículo investigaremos esta gestión y cómo poder utilizarla en nuestros programas. En el artículo Paginación de la RAM ya tratamos este tema y explicamos las funciones de DOS2 en Fusion-C; aquí veremos las equivalentes en MSXgl.
Además, como hemos profundizado en el conocimiento del entorno sdcc, veremos cómo podemos enlazar de una forma menos costosa los diferentes módulos. En el artículo había una gestión de desarrollo como un módulo aparte y después su integración. Esta estrategia también sirve para MSXgl, pero aquí presentaremos una nueva (igualmente compleja pero más eficiente) que también se puede aplicar a Fusion-C.
bool DOS_Mapper_Init()u8 DOSMapper_GetPage(u8 page)page.u8 DOSMapper_GetPage0()DOSMapper_GetPage1, DOSMapper_GetPage2 y DOSMapper_GetPage3.bool DOSMapper_Alloc(u8 type, u8 slot, DOS_Segment* seg)type puede ser de dos tipos: DOS_ALLOC_USER, donde la memoria reservada se libera cuando nuestro programa termina, y DOS_ALLOC_SYS, que una vez finaliza el programa sigue reservada. Esta última es útil para crear RAM disks o para la memoria de algún driver. slot es la página/slot donde queremos que se ubique: 0,1,2,3. En el caso de DOS2, recomiendo utilizar las páginas 1 y 2, ya que en la parte baja de la 0 y en la parte alta de la 3 hay variables del sistema y hay que tener cuidado al manipularlas para no dañar el sistema y que se quede colgado.
Hemos creado un pequeño ejemplo para trabajar la paginación en DOS2. La primera parte cambia los segmentos en la página/slot 1 (dirección 0x4000) y escribe diferentes valores en el mapa de memoria. La segunda parte llama a funciones creadas en otros archivos y que han sido cargadas en la página 1. El código del programa principal se encuentra en el archivo paginate.c y las dos funciones que se llaman en los archivos funcio1.c y funcio2.c
También utilizaremos la librería Debug que nos permite escribir mensajes en la consola del PC a través de openMSX activando el dispositivo -ext debugdevice y que también nos servirá para ver lo que está haciendo el MSX.
1#include "dos_mapper.h" 2#include "dos.h" 3#include "print.h" 4#include "input.h" 5#include "vdp.h" 6#include "bios.h" 7#include "debug.h" 8 9#include "font/font_mgl_std3.h" 10 11#include "funcions.h" 12 13__at(0x4000) u8 array[0x4000]; 14__at(0x8000) u8 res_func1; 15__at(0x8001) u8 res_func2; 16 17u8 startingSeg; 18u8 newSeg; 19u8 newSeg2; 20DOS_Segment g_newSeg; 21DOS_Segment g_newSeg2; 22DOS_Segment g_func1; 23DOS_Segment g_func2; 24u8 g_StrBuffer[128]; 25DOS_FCB g_File;
Empezamos colocando los includes de las librerías que utilizaremos:
Y pasamos ahora a las variables. En primer lugar, array, que es de 16K y nos reserva la memoria para que el linker no coloque ninguna otra función o variable en la página 1 que iremos cargando con los diferentes bloques de código de las funciones externas. Aunque hemos de tener en cuenta, tal y como Aoineko ha señalado, que la documentación del SDCC dice que si ponemos manualmente contenido usando las directivas __at, no hay ninguna garantía que este espacio no sea sobreescrito por contenido acumulado del compilador. Por tanto, si nuestro programa peta, una de las causas puede ser esta sobreescritura. Las dos variables que guardarán el retorno de las funciones externas que llamamos son res_func1 y res_func2. Después guardamos el número de página inicial en newSeg y el retorno de asignar las dos nuevas páginas. Luego tenemos las estructuras de DOS_Segment, que guardarán la información de los segmentos asignados. Finalmente, las variables para leer del disquete: el buffer de lectura g_StrBuffer y la estructura del archivo DOS g_File.
27void FT_SetName(DOS_FCB *p_fcb, const char *p_name) { 28 char i, j; 29 Mem_Set(0, p_fcb, sizeof(DOS_FCB)); 30 for (i = 0; i < 11; i++) { 31 p_fcb->Name[i] = ' '; 32 } 33 for (i = 0; (i < 8) && (p_name[i] != 0) && (p_name[i] != '.'); i++) { 34 p_fcb->Name[i] = p_name[i]; 35 } 36 if (p_name[i] == '.') { 37 i++; 38 for (j = 0; (j < 3) && (p_name[i + j] != 0) && (p_name[i + j] != '.'); 39 j++) { 40 p_fcb->Name[8 + j] = p_name[i + j]; 41 } 42 } 43}
Esta es la función que hemos utilizado muchas veces y que se encarga de poner el nombre en la estructura de lectura de archivos DOS_FCB para poder leer los bytes del archivo indicado
45void main(){ 46 VDP_SetMode(VDP_MODE_TEXT2); 47 48 VDP_SetColor(0xF0); 49 VDP_FillVRAM_16K(0, 0x0000, 0x4000); // Clear VRAM 50 51 Print_SetTextFont(g_Font_MGL_Std3, 1); 52 Print_SetColor(0xF, 0x0); 53 54 DOSMapper_Init(); 55 DEBUG_INIT();
Las diferentes inicializaciones de la pantalla, eligiendo el modo de pantalla y borrando la VRAM. También inicializamos la tipografía de fuente que hemos escogido, así como el mapeador de memoria y el debug.
57 Print_SetPosition(0, 0); 58 Print_DrawText("We write value 1 to the starting segment"); 59 for (u16 j=0; j<0x3fff; j++) { 60 array[j] = 1; 61 } 62 Print_DrawText("\nValue written.\n"); 63 64 Print_DrawCharX('-', g_PrintData.ScreenWidth); 65 Print_DrawText("\nNow we add a new segment and we put it on page 1"); 66 startingSeg = DOSMapper_GetPage(1); 67 DEBUG_LOGNUM("startingSeg number, value returned by alloc: ", startingSeg); 68 69 newSeg = DOSMapper_Alloc(DOS_ALLOC_USER, DOS_SEGSLOT_PRIM, &g_newSeg); 70 DEBUG_LOGNUM("newSeg number, value returned by alloc: ", newSeg); 71 DEBUG_LOGNUM("g_newSeg number", g_newSeg.Number); 72 73 DOSMapper_SetPage(1, g_newSeg.Number); 74 Print_DrawText("\nSegment changed. Now we write value 2"); 75 for (u16 j=0; j<0x3FFf; j++) { 76 array[j] = 2; 77 } 78 Print_DrawText("\nDone.\n"); 79 DEBUG_LOG("Written value 2 to segment g_newSeg 2"); 80 81 Print_DrawCharX('-', g_PrintData.ScreenWidth); 82 Print_DrawText("\nNow we add a new segment and we put it again on page 1"); 83 newSeg2 = DOSMapper_Alloc(DOS_ALLOC_USER, DOS_SEGSLOT_PRIM, &g_newSeg2); 84 DEBUG_LOGNUM("newSeg2 value returned by alloc", newSeg2); 85 newSeg = DOSMapper_GetPage1(); 86 DEBUG_LOGNUM("newSeg value returned by getPage", newSeg); 87 DOSMapper_SetPage(1, g_newSeg2.Number); 88 DEBUG_LOG("set in page 1 g_newSeg2"); 89 Print_DrawText("\nSegment changed. Now we write value 3"); 90 for (u16 j=0; j<0x3FFf; j++) { 91 array[j] = 3; 92 } 93 Print_DrawText("\nDone");
La línea 57 se encarga de posicionar el cursor para escribir las frases que iremos mostrando. Después de explicar en pantalla lo que haremos, creamos un bucle donde asignamos el valor 1 a todas las posiciones del bloque de 16K. En la línea 66 recuperamos el número de segmento que había al inicializar la aplicación. En la 69 reservamos 16K de la memoria de usuario y lo guardamos en la estructura g_newSeg; el valor devuelto si ha funcionado bien es 0xFF, que guardamos en newSeg. En la línea 73 colocamos en la página 1 este nuevo segmento y lo llenamos con el número 2. En las líneas 81-93 hacemos lo mismo para el segmento g_newSeg2, pero ahora guardamos el valor 3 en este nuevo segmento. Hemos ido asignando valores a los segmentos utilizando la variable array, que hemos dicho que se encuentra en la dirección 0x4000, que tiene un tamaño de 16K (0x4000 bytes), lo que corresponde a toda la página 1 del Z80. Así, el procesador ha creído que siempre llenaba la página 1, pero nosotros hemos cambiado esta página por diferentes segmentos que corresponden a direcciones distintas.
En este punto, si vamos al menú Debugger de openMSX y escogemos la opción Add hex editor y después slotted memory, veremos toda la memoria del ordenador y, si navegamos arriba y abajo, nos será fácil ver los bloques con los números 1, 2 o 3. En mi caso, estos comenzaban en 0x8000, 0x14000 y 0xC4000.
En la imagen de abajo veréis un momento de la simulación donde se pueden observar los valores de la memoria en la página 1 y en la memoria general:

96 DOSMapper_SetPage(1, g_newSeg.Number); 97 Print_DrawText("Changed segment g_newSeg , read value: "); 98 Print_DrawInt(array[3]); 99 DEBUG_LOGNUM("Changed segment g_newSeg: ", array[3]);
Ahora hacemos los pasos inversos, volviendo a los segmentos que habíamos guardado para leer los valores de estos segmentos cuando vuelven a estar en la página 1. Imprimimos los resultados tanto en pantalla utilizando Print_DrawInt como en la consola con DEBUG_LOGNUM.
101 DOSMapper_Alloc(DOS_ALLOC_USER, DOS_SEGSLOT_PRIM, &g_func1); 102 DOSMapper_Alloc(DOS_ALLOC_USER, DOS_SEGSLOT_PRIM, &g_func2); 103 104 DOSMapper_SetPage(1, g_func1.Number); 105 106 Print_DrawText("\n"); 107 Print_DrawCharX('-', g_PrintData.ScreenWidth); 108 Print_DrawText("\nReading file funcio1.bin. "); 109 DEBUG_LOG("Reading file funcio1.bin"); 110 FT_SetName(&g_File, "funcio1.bin"); 111 DOS_OpenFCB(&g_File); 112 for (u16 i=0; i<g_File.Size; i+=128) { 113 DOS_SetTransferAddr(&array[i]); 114 DOS_SequentialReadFCB(&g_File); 115 } 116 res_func1 = 4; 117 DEBUG_LOGNUM("Value before func1: ", res_func1); 118 Print_DrawText("\nBefore func1: "); 119 Print_DrawInt(res_func1); 120 res_func1 = funcio1(); 121 DEBUG_LOGNUM("Value after calling func1: ", res_func1); 122 Print_DrawText("\nAfter func1: "); 123 Print_DrawInt(res_func1); 124 125 Print_DrawText("\nReading file funcio2.bin. "); 126 DEBUG_LOG("Reading file funcio2.bin"); 127 DOSMapper_SetPage(2, g_func2.Number); 128 FT_SetName(&g_File, "funcio2.bin"); 129 DOS_OpenFCB(&g_File); 130 for (u16 i=0; i<g_File.Size; i+=128) { 131 DOS_SetTransferAddr(&array[i]); // Els he de guardar a array, llegir directament a allà 132 DOS_SequentialReadFCB(&g_File); 133 } 134 135 res_func2 = 16; 136 DEBUG_LOGNUM("\nBefore func2: ", res_func2); 137 Print_DrawText("Before func2: "); 138 Print_DrawInt(res_func2); 139 res_func2 = funcio2(); 140 DEBUG_LOGNUM("After func2: ", res_func2); 141 Print_DrawText("\nAfter func2: "); 142 Print_DrawInt(res_func2); 143 144 Print_DrawText("\nWe have run the two function in different pages"); 145 Print_DrawText("\nYou can debug the memory page in openmsx debugger"); 146 147 Print_DrawText("\nPress space to go back to DOS"); 148 while (!Keyboard_IsKeyPressed(KEY_SPACE)); 149 150 Bios_Exit(0); 151}
Ya hemos terminado la primera parte del test, en la que cambiamos bancos de memoria. En la segunda parte hacemos lo mismo, pero ahora en cada nuevo segmento guardaremos una función. Recordemos que estos módulos con las funciones adicionales no pueden superar los 16K.
Empezamos reservando estos segmentos en las líneas 101 y 102, para después colocar el primero en la página 1. Mostramos diferente información por pantalla y en el log sobre el estado del experimento. Cargamos el binario de la función funcio1, que se encuentra en el archivo funcio1.bin, en el segmento recientemente asignado g_func1 entre las líneas 110 y 115. Asignamos el valor 4 a la variable res_func1, leemos y mostramos este valor, llamamos a la función funcio1 y volvemos a leer y mostrar el valor de la variable res_func1.
Entre las líneas 125 y 145 hacemos lo mismo que antes, pero ahora para la función funcio2.
Ya solo queda escribir el texto para que se pulse la barra espaciadora y esperar a que presionen esta tecla para finalizar.
1#pragma codeseg funcio1 2 3extern void DEBUG_LOG(const char* msg); 4 5unsigned char funcio1(){ 6 DEBUG_LOG("funcio1 has been called"); 7 return 0x5; 8}1#pragma codeseg funcio2 2 3extern void DEBUG_LOG(const char* msg); 4 5unsigned char funcio2(){ 6 DEBUG_LOG("funcio2 has been called"); 7 return 0xde; 8}
Aquí tenemos las dos funciones que hacen lo mismo, devolver un valor, pero este valor es diferente. Ambas utilizan la función DEBUG_LOG, que se encontrará en los 16K principales iniciales, en la función principal, que en este caso es la de paginate.c, y es el linker (enlazador) el que se encargará de buscar su dirección cuando realice la unión de todos los binarios.
A diferencia del artículo anterior, Paginación de la RAM, aquí no cargamos de nuevo la función DEBUG_LOG, ya que está en la memoria de la función principal. De esta forma, los módulos quedan más pequeños, pero aumentamos la complejidad para realizar correctamente el enlace.
La directiva del compilador #pragma codeseg funcio1 sirve para indicarle al compilador que todo este código debe ir a una dirección concreta de la memoria. Así, cuando cree los archivos .map y traduzca el código a binario, utilizará la dirección indicada en el momento de compilar, cuando le pasemos el parámetro --code-loc 0x4000 a sdcc.
En la siguiente imagen podéis ver la ejecución del script tanto en la pantalla emulada del MSX como en la salida de la consola. Podemos ver cómo realiza los diferentes pasos y cómo los valores leídos corresponden a los valores esperados según el cambio de página que se había realizado.

Para obtener los binarios de las funciones de 16K correctamente dirigidos a las funciones que ya se encuentran en la parte del programa principal, se deben seguir los siguientes pasos:
~/MSXgl/tools/sdcc/bin/sdcc -c -mz80 -DTARGET=TARGET_DOS2 -DMSX_VERSION=MSX_TR
-I~/MSXgl/projects/learning-msxgl/DOS2_pagination/
-I~/MSXgl/engine/src
-I~/MSXgl/engine/content
-I~/MSXgl/tools/
--opt-code-speed --debug -DAPPSIGN ./paginate.c
-o ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/
Hemos utilizado el mismo comando que usábamos para compilar el archivo principal del módulo en MSXgl. En este paso, el compilador crea las etiquetas para que posteriormente el linker pueda ubicar las funciones en el espacio de memoria, creando el archivo .rel. Este archivo contiene el código máquina, las definiciones de símbolos, las referencias externas y permite ser reubicado en memoria (la extensión .rel proviene de relocate, que significa reubicar).
~/MSXgl/tools/sdcc/bin/sdcc -mz80 -c --codeseg funcio1 funcio1.c -o funcio1.rel
~/MSXgl/tools/sdcc/bin/sdcc -mz80 -c --codeseg funcio2 funcio2.c -o funcio2.rel
Utilizamos el parámetro --codeseg para indicar que cree un segmento en memoria llamado funcio1 y que después podremos ubicar en la posición que queramos al enlazarlo con el parámetro de sdcc -Wl-g_funcio1.
~/MSXgl/tools/sdcc/bin/sdcc -mz80 --vc --no-std-crt0 -L~/MSXgl/tools/sdcc/lib/z80
--code-loc 0x0100 --data-loc 0x0000 --opt-code-speed --debug
~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/crt0_dos.rel
~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/msxgl.lib
~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/paginate.rel
-o ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/paginate.ihx
funcio1.rel funcio2.rel
Una vez tenemos los archivos .rel, se debe utilizar el enlazador de sdcc para colocarlos definitivamente en memoria y resolver todos los símbolos y referencias. Por eso también debemos proporcionarle los binarios de las funciones funcio1.rel y funcio2.rel que hemos compilado anteriormente y que son llamadas desde paginate.c.
DEBUG_LOG y añadirla en el linker
~/MSXgl/tools/sdcc/bin/sdcc -mz80 --no-std-crt0 --code-loc 0x4000 funcio1.rel -o funcio1.ihx -Wl-g_DEBUG_LOG=0x1a87 -L~/MSXgl/tools/sdcc/lib/z80;
~/MSX/MSXgl/tools/sdcc/bin/sdcc -mz80 --no-std-crt0 --code-loc 0x4000 funcio2.rel -o funcio2.ihx -Wl-g_DEBUG_LOG=0x1a87 -L~/MSXgl/tools/sdcc/lib/z80;
Ahora debemos buscar la dirección que tiene DEBUG_LOG en el programa principal paginate.c para indicarle a los módulos de 16K que cuando realicen la llamada a esta función salten a la dirección del programa principal. Una vez localizada en el archivo paginate.map, debemos indicárselo al linker. Esto lo hacemos con el parámetro -Wl-g, indicando el nombre de la función que aparece en el .map, en este caso _DEBUG_LOG, y la dirección obtenida.
~/MSXgl/tools/MSXtk/bin/MSXhex ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/paginate.ihx -e com -s 0x0100 -l 0;
~/MSXgl/tools/MSXtk/bin/MSXhex ~/MSXgl/projects/learning-msxgl/DOS2_pagination/funcio1.ihx -e bin;
~/MSXgl/tools/MSXtk/bin/MSXhex ~/MSXgl/projects/learning-msxgl/DOS2_pagination/funcio2.ihx -e bin;
Para traducir los binarios al formato de MSX, utilizamos la herramienta de MSXgl MSXhex. Observemos que la invocamos de forma diferente para el programa principal que para las funciones, ya que el primero debe seguir el formato .com de MSXDOS2, mientras que los binarios son solo la conversión.
cp funcio1.bin emul/dos2/;
cp funcio2.bin emul/dos2/;
cp out/paginate.com emul/dos2/;
~/openMSX/derived/openmsx
-machine Panasonic_FS-A1ST -ext moonsound -ext debugdevice
-ext msxdos2 -diska ~/MSXgl/projects/learning-msxgl/DOS2_pagination/emul/dos2
Con esto ya tendremos el emulador en funcionamiento y podremos ver el resultado de nuestras simulaciones tal y como se muestra en la imagen superior.
MSXgl ya permite crear proyectos de más de 64K, pero deben ser para ROM y te ayuda a compilarlos. Sin embargo, nosotros podemos crear un script en JavaScript para que MSXgl lo utilice.
El archivo build.sh, que se encuentra en el directorio de nuestro proyecto, llama a node, que es el comando del proyecto Node.js que permite ejecutar JavaScript fuera del navegador. El script que se ejecuta es el que se indica en build.sh, que en este caso es el que se encuentra en .../engine/script/js/build.js, encargado de orquestar toda la compilación. Lo primero que busca es ./project_config.js y, si no lo encuentra, busca .../projects/default_config.js y, si tampoco lo encuentra, .../engine/script/js/default_config.js. La estructura de estos archivos es la misma y contiene los diferentes pasos para compilar el proyecto. Finalmente, además de los anteriores (de los cuales solo carga uno según la prioridad), también carga, en caso de existir, [project_name].js.
El archivo project_config.js permite cargar scripts antes de la compilación principal y scripts después de la compilación. Esta característica es la que utilizaremos para compilar nuestros drivers antes de la compilación principal y después, en el postproceso, localizar las funciones del bloque principal que se utilizan en los módulos. Para ello, solo hay que rellenar las variables PreBuildScripts y PostBuildScripts con los comandos a ejecutar. En nuestro caso, hemos creado un script en JavaScript, build_pagination.js, que se encarga de realizar esto. Por lo tanto, nuestro código quedaría así:
231//-- Command lines to be executed before the build process (array)
232PreBuildScripts = [ "node build_pagination.js pre" ];
233
234//-- Command lines to be executed after the build process (array)
235PostBuildScripts = [ "node build_pagination.js post" ];
Una vez tenemos claros los pasos, vamos a ver cómo podemos incluirlos en un script de JavaScript para continuar utilizando el lenguaje que se usa en MSXgl. Esta implementación se encuentra en el archivo build_pagination.js.
1#!/usr/bin/env node 2// ____________________________ 3// ██▀▀█▀▀██▀▀▀▀▀▀▀█▀▀█ │ ▄▄▄ ▄▄ 4// ██ ▀ █▄ ▀██▄ ▀ ▄█ ▄▀▀ █ │ ▀█▄ ▄▀██ ▄█▄█ ██▀▄ ██ ▄███ 5// █ █ █ ▀▀ ▄█ █ █ ▀▄█ █▄ │ ▄▄█▀ ▀▄██ ██ █ ██▀ ▀█▄ ▀█▄▄ 6// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀────────┘ 7// DOS2 Pagination Build Handler - Pre & Post Build Steps 8 9const fs = require('fs'); 10const path = require('path'); 11const { execSync } = require('child_process'); 12 13const PROJECT_DIR = __dirname; 14const OUT_DIR = path.join(PROJECT_DIR, 'out'); 15const EMUL_DIR = path.join(PROJECT_DIR, 'emul', 'dos2'); 16 17// Load PaginatedFunctions from project_config.js 18let PaginatedFunctions = []; 19try { 20 // Create a context object to capture the PaginatedFunctions variable 21 const configCode = fs.readFileSync(path.join(PROJECT_DIR, 'project_config.js'), 'utf-8'); 22 const configModule = {}; 23 // Execute the config file in a controlled context 24 const vm = require('vm'); 25 const context = vm.createContext({ PaginatedFunctions: [] }); 26 vm.runInContext(configCode, context); 27 PaginatedFunctions = context.PaginatedFunctions; 28 29 if (!PaginatedFunctions || PaginatedFunctions.length === 0) { 30 console.error('PaginatedFunctions not found or empty in project_config.js'); 31 process.exit(1); 32 } 33} catch (err) { 34 console.error(`Error loading PaginatedFunctions from project_config.js: ${err.message}`); 35 process.exit(1); 36} 37 38const SDCC_PATH = '/home/jepsuse/MSX/MSXgl/tools/sdcc/bin'; 39const MSXHEX = '/home/jepsuse/MSX/MSXgl/tools/MSXtk/bin/MSXhex'; 40 41const colors = { 42 reset: '\x1b[0m', 43 green: '\x1b[32m', 44 yellow: '\x1b[33m', 45 red: '\x1b[31m', 46 blue: '\x1b[36m' 47}; 48 49function log(msg, color = 'reset') { 50 console.log(`${colors[color]}${msg}${colors.reset}`); 51} 52 53function exec(command, description) { 54 log(` ${description}...`, 'yellow'); 55 try { 56 execSync(command, { 57 cwd: PROJECT_DIR, 58 stdio: 'pipe' 59 }); 60 log(` ✓ Done`, 'green'); 61 } catch (error) { 62 log(` ✗ Failed: ${error.message}`, 'red'); 63 throw error; 64 } 65}
Comenzamos definiendo las variables que utilizaremos a lo largo del proceso de compilación. Cabe destacar la variable PaginatedFunctions, que contendrá el nombre de los archivos que queremos compilar como funciones para ser llamadas posteriormente. Este valor se obtiene de la variable con el mismo nombre PaginatedFunctions del archivo project_config.js.
69function preBuild() { 70 log('\n╔════════════════════════════════════════════════╗', 'blue'); 71 log('║ DOS2 Pagination - PRE-BUILD ║', 'blue'); 72 log('║ Step 1: Compile paginated functions ║', 'blue'); 73 log('╚════════════════════════════════════════════════╝\n', 'blue'); 74 75 if (!PaginatedFunctions || PaginatedFunctions.length === 0) { 76 log('No paginated functions configured. Skipping pagination pre-build.\n', 'yellow'); 77 return; 78 } 79 80 log('Compiling paginated functions:', 'yellow'); 81 82 // Create out directory if it doesn't exist 83 if (!fs.existsSync(OUT_DIR)) { 84 fs.mkdirSync(OUT_DIR, { recursive: true }); 85 } 86 87 for (const func of PaginatedFunctions) { 88 const file = `${func.name}.c`; 89 if (!fs.existsSync(path.join(PROJECT_DIR, file))) { 90 log(` ✗ ${file} not found`, 'red'); 91 continue; 92 } 93 const cmd = `"${SDCC_PATH}/sdcc" -mz80 -c "${file}" -o "${OUT_DIR}/${func.name}.rel"`; 94 exec(cmd, ` ${func.name}.c → out/${func.name}.rel`); 95 } 96 97 log('\n✓ Pre-build complete. Main build will now process paginated functions.\n', 'green'); 98}
Esta es la primera función que se ejecuta antes de la compilación estándar de MSXgl y que se encarga de crear los binarios de los archivos de un máximo de 16K que utilizaremos en nuestro programa.
100/** 101 * Extract extern function declarations from a C source file and its includes. 102 * Looks for patterns like: extern void FUNCTION_NAME(...); 103 */ 104function extractExternFunctions(filePath) { 105 const externFuncs = []; 106 const processedFiles = new Set(); 107 108 function processFile(currentPath) { 109 // Avoid processing the same file twice 110 if (processedFiles.has(currentPath)) { 111 return; 112 } 113 processedFiles.add(currentPath); 114 115 if (!fs.existsSync(currentPath)) { 116 return; 117 } 118 119 const content = fs.readFileSync(currentPath, 'utf-8'); 120 121 // Extract extern declarations: extern <return_type> FUNCTION_NAME(...) 122 const externRegex = /extern\s+\w+[\s\*]+(\w+)\s*\(/g; 123 let match; 124 125 while ((match = externRegex.exec(content)) !== null) { 126 const funcName = match[1]; 127 if (!externFuncs.includes(funcName)) { 128 externFuncs.push(funcName); 129 } 130 } 131 132 // Extract #include directives and process them 133 const includeRegex = /#include\s+["<]([^"<>]+)[">]/g; 134 while ((match = includeRegex.exec(content)) !== null) { 135 const includePath = match[1]; 136 // Try to find the included file in the project directory 137 const fullPath = path.join(PROJECT_DIR, includePath); 138 processFile(fullPath); 139 } 140 } 141 142 processFile(filePath); 143 return externFuncs; 144} 145 146/** 147 * Find symbol address in MAP file by function name. 148 * Looks for patterns like: 149 * 00001AA5 _DEBUG_LOG debug 150 */ 151function findSymbolInMap(mapFile, funcName) { 152 const content = fs.readFileSync(mapFile, 'utf-8'); 153 const lines = content.split('\n'); 154 155 // Look for the symbol with underscore prefix: _FUNCNAME 156 const symbolName = '_' + funcName; 157 158 for (const line of lines) { 159 // Match: ADDRESS _SYMBOL_NAME module 160 const match = line.match(/^\s*([0-9A-Fa-f]+)\s+_[\w]+\s+/); 161 if (match && line.includes(symbolName)) { 162 const addr = '0x' + match[1]; 163 return addr; 164 } 165 } 166 167 return null; 168} 169 170function postBuild() { 171 log('\n╔════════════════════════════════════════════════╗', 'blue'); 172 log('║ DOS2 Pagination - POST-BUILD ║', 'blue'); 173 log('║ Step 2: Relink & convert paginated functions ║', 'blue'); 174 log('╚════════════════════════════════════════════════╝\n', 'blue'); 175 176 if (!PaginatedFunctions || PaginatedFunctions.length === 0) { 177 log('No paginated functions configured. Skipping pagination post-build.\n', 'yellow'); 178 return; 179 } 180 181 // Step 2a: Verify MAP file exists 182 log('Step 2a: Checking for main program MAP file...', 'yellow'); 183 const mapFile = path.join(OUT_DIR, 'paginate.map'); 184 185 if (!fs.existsSync(mapFile)) { 186 log(` ✗ MAP file not found at ${mapFile}`, 'red'); 187 log(' The main program must be linked first to generate the MAP file.\n', 'red'); 188 return; 189 } 190 log(' ✓ MAP file found\n', 'green'); 191 192 // Step 2b: Relink each paginated function at specified address with required symbols 193 log('Step 2b: Relinking paginated functions at specified addresses...', 'yellow'); 194 195 for (const func of PaginatedFunctions) { 196 const name = func.name; 197 const pageAddr = typeof func.page === 'string' ? parseInt(func.page, 16) : func.page; 198 const codeLoc = `0x${pageAddr.toString(16).toUpperCase()}`; 199 const relFile = path.join(OUT_DIR, `${name}.rel`); 200 201 if (!fs.existsSync(relFile)) { 202 log(` ✗ ${relFile} not found`, 'red'); 203 continue; 204 } 205 206 // Extract extern function declarations from this source file 207 const sourceFile = path.join(PROJECT_DIR, `${name}.c`); 208 const externFuncs = extractExternFunctions(sourceFile); 209 210 if (externFuncs.length === 0) { 211 log(` Relinking ${name} at ${codeLoc} (no external dependencies)...`, 'yellow'); 212 } else { 213 log(` Relinking ${name} at ${codeLoc} with ${externFuncs.length} external function(s):`, 'yellow'); 214 } 215 216 // Find each extern function's address in the MAP file 217 let linkerFlags = ''; 218 for (const funcName of externFuncs) { 219 const addr = findSymbolInMap(mapFile, funcName); 220 if (addr) { 221 linkerFlags += ` -Wl-g_${funcName}=${addr}`; 222 log(` ${funcName} = ${addr}`, 'green'); 223 } else { 224 log(` ✗ ${funcName} NOT FOUND in MAP file!`, 'red'); 225 } 226 } 227 228 const cmd = `"${SDCC_PATH}/sdcc" -mz80 --no-std-crt0 --code-loc ${codeLoc} ` + 229 `"${relFile}"${linkerFlags} ` + 230 `-L"${SDCC_PATH}/../lib/z80" -o "${OUT_DIR}/${name}.ihx"`; 231 232 exec(cmd, ` Relinking ${name} at ${codeLoc}`); 233 } 234 235 // Step 2c: Convert IHX to binary format 236 log('\nStep 2c: Converting IHX files to binary format...', 'yellow'); 237 238 for (const func of PaginatedFunctions) { 239 const name = func.name; 240 const ihxFile = path.join(OUT_DIR, `${name}.ihx`); 241 242 if (fs.existsSync(ihxFile)) { 243 const cmd = `"${MSXHEX}" "${ihxFile}"`; 244 exec(cmd, ` Converting ${name}.ihx → ${name}.bin`); 245 } 246 } 247 248 // Step 2d: Copy files to emulator directory 249 log('\nStep 2d: Copying binaries to emulator directory...', 'yellow'); 250 251 if (!fs.existsSync(EMUL_DIR)) { 252 fs.mkdirSync(EMUL_DIR, { recursive: true }); 253 } 254 255 for (const func of PaginatedFunctions) { 256 const name = func.name; 257 const binFile = path.join(OUT_DIR, `${name}.bin`); 258 259 if (fs.existsSync(binFile)) { 260 fs.copyFileSync(binFile, path.join(EMUL_DIR, `${name}.bin`)); 261 log(` ✓ Copied ${name}.bin`, 'green'); 262 } 263 } 264 265 log('\n✓ Post-build complete!\n', 'green'); 266} 267
Esta es la parte que se ejecuta una vez ha terminado la compilación estándar de MSXgl. La primera función extractExternFunctions se encarga de buscar el nombre de las funciones que están definidas en el archivo C como extern; estos nombres son los que después la función findSybolInMap deberá localizar en el archivo map para encontrar la dirección de memoria que el link le ha asignado. Estas dos funciones son las que se utilizan en el postBuild para relinkar nuestros programas que irán a las páginas de 16K y saltarán a la dirección correspondiente.
268// Determine which phase to run 269const args = process.argv.slice(2); 270const phase = args[0] || 'pre'; 271 272try { 273 if (phase === 'pre') { 274 preBuild(); 275 } else if (phase === 'post') { 276 postBuild(); 277 } else { 278 log(`Unknown phase: ${phase}`, 'red'); 279 process.exit(1); 280 } 281} catch (error) { 282 log(`\n✗ Build failed!\n`, 'red'); 283 process.exit(1); 284}
Finalmente, solo queda controlar si cuando se llama al script se hace en la fase de prebuild o en la de postbuild y, según de dónde venga la llamada, ejecutar un bloque u otro.
Dentro del extenso ejemplo de MSXDOS2 que se puede encontrar en el directorio samples, s_dos2.c, hay una parte llamada driver que permite cargar binarios en memoria y llamarlos. Esto es más parecido a lo que habíamos hecho en Paginación de la RAM, donde el binario es autocontenido y no utiliza funciones comunes con el bloque principal. Pero la forma de compilarlo es más sencilla.
Primero de todo se debe tener el programa funcionando; en nuestro ejemplo sencillo, driv1.c, solo devolvemos un valor cuando se llama a la función. Para compilarlo podemos utilizar la misma plantilla que se usa para compilar los archivos en C de MSXgl; en nuestro caso la hemos copiado en driv1.js y se debe cambiar la línea:
//-- Sobrescribir la dirección de inicio del código (número). Por ejemplo, 0xE000 para un driver en RAM
ForceCodeAddr = 0xE000;
indicando la dirección de memoria donde será guardado. De esta forma, el linker redirige todas las direcciones de memoria a partir de esta base.
Una vez creado, se debe cargar en memoria y llamarlo desde el programa principal. En nuestro caso este es pag_driver.c y para llamarlo hacemos:
res_func1 = CallDriver(0xE000, 5);
Para cargarlo en memoria hemos usado las mismas instrucciones de disco utilizadas anteriormente.
Ya solo queda compilar el programa principal y ejecutarlo con el emulador.