El Z80 només pot gestionar 64K simultàniament. Per superar-ho, l'arquitectura MSX utilitza Slots, dividint l'espai en quatre pàgines de 16K. Tot i que això ja existia al MSX1, va ser amb el MSX2 i l'aparició del Memory Mapper que es va poder gestionar fins a 4MB de RAM. Aquest mapper es controla mitjançant els ports d'entrada/sortida 0xFC-0xFF. Com que cada fabricant ubicava la RAM en slots diferents (per exemple, el Philips VG-8235 la té al slot 3-2, mentre que un Sony HB-F1XDJ la té al 3-0), i si a part tenia algun mapejador de memòria (memory mapper) addicional per tenir més RAM, la gestió era complexa. Per això, el MSX-DOS2 va introduir un gestor de memòria estàndard que permetia als programadors utilitzar la RAM sense preocupar-se de l'arquitectura física del model.
En aquest article investigarem aquesta gestió i com poder-la utilitzar en els nostres programes. A l'article Paginació de la RAM ja vam tractar aquest tema i vam explicar les funcions del DOS2 en el Fusion-C, aquí veurem les equivalents en MSXgl.
A més a més, com que hem aprofundit en el coneixement de l'entorn sdcc mirarem com podem enllaçar d'una forma menys costosa els diferents mòduls. A l'article hi havia una gestió de desenvolupament com a un mòdul a part i després integració. Aquesta estratègia també serveix per al MSXgl, però aquí presentarem una nova (igualment complexe però més eficient) que també es pot aplicar al Fusion-C.
bool DOS_Mapper_Init()u8 DOSMapper_GetPage(u8 page)page.u8 DOSMapper_GetPage0()DOSMapper_GetPage1, DOSMapper_GetPage2 i DOSMapper_GetPage3.bool DOSMapper_Alloc(u8 type, u8 slot, DOS_Segment* seg)type pot ser de dos tipus:DOS_ALLOC_USER a on la memòria reservada queda alliberada quan el nostre programa acaba, i DOS_ALLOC_SYS que un cop acaba el programa segueix reservada. Aquesta última és útil per fer RAM disks o per la memòria d'algun driver. slot la pàgina/slot que volem que s'ubiqui: 0,1,2,3. En el cas de DOS2, recomano utilitza les pàgines 1 i 2, ja que a la part baixa de la 0 i a la part alta de la 3 hi ha variables del sistema i s'ha d'anar en compte si les manipulem de no malmetre el sistema i que es quedi penjat.
Hem creat un petit exemple per treballar la paginació en DOS2. La primera part canvia els segments a la pàgina/slot 1 (adreça 0x4000) i escriu diferents valors en el mapa de memòria. La segona part crida a funcions creades en altres fitxers i que han estat carregades a la pàgina 1. El codi del programa principal es troba al fitxer paginate.c i les dues funcions que són cridades als fitxers funcio1.c i funcio2.c
També utilitzarem la llibreria Debug que ens permet escriure missatges a la consola del PC a través de l'openMSX activant el dispositiu -ext debugdevice i que també ens servirà per veure el que està fent l'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;
Comencem posant els includes de les llibreries que utilitzarem:
I passem ara a les variables, primer de tot array que és de 16K i ens reserva la memòria per tal que el linker no hi posi cap altra funció o variable a la pàgina 1 que anirem carregant amb els diferents blocs de codi de les funcions externes. Encara que hem de tenir en compte, tal i com l'Aoineko ha senyalat, que la documentació del SDCC diu que si posem contingut manual utilitzant les directives __at, no hi ha cap grantia que aquest espai no és sobreescrit per contingut acumulat del compilador. Per tant, si el nostre programa peta, una de les causes pot ser aquesta sobreescriptura. Les dues variables que guardaran el retorn de les funcions externes que cridem res_func1 i res_func2. Després guardem el número de pàgina inicial a newSeg i el retorn d'assignar les dues noves pàgines. Després tenim les estructures de DOS_Segment que guardarà la informació dels segments assignats. Finalment les variables per llegir del disquet, el buffer de lectura g_StrBuffer i la d'estructura del fitxer 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}
Aquesta és la funció que hem utilitzat moltes vegades i que s'encarrega de posar el nom a l'estructura de lectura de fitxers DOS_FCB per tal de llegir els bytes de l'anomenat fitxer
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();
Les diferents inicialitzacions de la pantalla escollint el mode de pantalla i borrant la VRAM. Tembé inicialitzem la tipografia de font que hem escollit així com el mapejador de memòria i 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ínia 57 s'encarrega de posicionar el cursor per escriure les frases que anirem fent. Després d'explicar per pantalla el que farem, creem un bucle a on hi posem el valor 1 per totes les posicions del bloc de 16K. A la línia 66 recuperem el número de segment que hi havia a l'inicialitzar l'aplicació. A la 69 reservem 16K de la memòria d'usuari i ho guardem a l'estructura g_newSeg, el valor retornat si ha funcionat bé és el 0xFF que el guardem a newSeg. A la línia 73 posem a la pàgina 1 aquest nou segment i l'omplim del número 2. A les línies 81-93 fem el mateix per al segment g_newSeg2 però ara guardem el valor 3 en aquest nou segment. Hem anat assignant valors als segments utilitzant la variable array que hem dit que es troba a l'adreça 0x4000, que té un tamany de 16K (0x4000 bytes) que correspon a tota la pàgina 1 del Z80. Així el processador s'ha pensat que sempre omplia la pàgina 1 però nosaltres hem canviat aquesta pàgina per diferents segments que corresponen a una adreça diferent.
En aquest punt, si anem al menú Debugger de l'openMSX i escollim l'opció Add hex editor i després slotted memory veurem tota la memòria de l'ordinador i si naveguem amunt i avall, ens serà fàcil veure els blocs amb els números 1, 2 ó 3. En el meu cas aquests començaven a 0x8000, 0x14000 and 0xC4000.
A la imatge de sota veureu un moment de la simulació a on es poden veure els valors de la memòria a la pàgina 1 i a la memòria 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]);
Ara fem les passes enrere, tornant als segments que havíem guardat per llegir els valors d'aquests segments quan tornen a estar a la pàgina 1. Imprimim els resultats tant en pantalla utilitzant Print_DrawInt com a la consola amb 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}
Ja hem acabat la primera part del test, la que canviem bancs de memòria. A la segona part fem el mateix, però ara a cada nou segment hi guardarem una funció. Recordem que aquests mòduls amb les funcions addicionals no poden ser superiors a 16K.
Comencem reservant aquests segments a les línies 101 i 102, per després posar el primer a la pàgina 1. Mostrem diferent informació per pantalla i per al log de l'estat de l'experiment. Carreguem el binari de la funció funcio1 que es troba en el fitxer funcio1.bin en el segment recent assignat g_func1 entre les línies 110 i 115. Assignem el valor 4 a la variable res_func1, llegim i presentem aquest valor, cridem a la funció funcio1 i tornem a llegir i presentar el valor de la variable res_func1.
Entre les línies 125 i 145 fen el mateix que abans però ara per la funció funcio2.
Ja només queda escriure el text que es premi l'espai i esperar que apretin aquesta tecla per acabar.
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í tenim les dues funcions que fan el mateix, retornar un valor, però aquest valor és diferent. Les dues utilitzen la funció DEBUG_LOG que es trobarà en els 16K principals inicials, en la funció principal que aquest cas és la de paginate.c i és el linker (enllaçador) que s'encarregarà de buscar la seva adreça quan faci la unió de tots els binaris.
A diferència de l'anterior article, Paginació de la RAM, aquí no carreguem una altra vegada la funció DEBUG_LOG que ja està en la memòria de la funció principal. D'aquesta manera els mòduls queden més petits però augmentem la complexitat per fer bé el link.
La directriu del compilador #pragma codeseg funcio1 serveix per dir-li al compilador que tot aquest codi ha d'anar a una adreça concreta de la memòria. Així quan creï els fitxers .map i tradueixi el codi a binari, usarà l'adreça indicada a l'hora de compilar, quan li passem el paràmetre --code-loc 0x4000 al sdcc
A la següent imatge podeu veure l'execució de l'script tant en la pantalla emulada del MSX com a la sortida de la consola. Podem veure com fa els diferents passos i com els valors llegits corresponen als valors esperats segons el canvi de pàgina que s'havia fet.

Per tal d'obtenir els binaris de les funcions de 16K ben dirigits a les funcions que ja es troben a la part del programa principal, s'han de fer els següents passos:
~/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/
Hem utilitzat la mateixa comanda que fèiem servir per compilar el fitxer principal del mòdul en el MSXgl. En aquest pas, el compilador crea les etiquetes per després el linker pugui ubicar les funcions a l'espai de la memòria, creant el fitxer .rel. Aquest fitxer conté el codi màquina, les definicions dels símbols, les referències externes i permet ser recol·locat a la memòria (l'extensió .rel ve del relocate anglès que vol dir recol·locar).
~/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
Utilitzem el paràmetre --codeseg per indicar que creï un segment a la memòria anomenat funcio1 i que després podrem ubicar a la posició que volguem a l'hora de linkar-lo amb el paràmetre del 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
Un cop tenim els fitxers .rel s'ha d'utilitzar el linkador del sdcc per col·locar-lo definitivament a la memòria i resoldre tots els símbols i referències. Per axiò també li hem de donar els binaris de les funcions funcio1.rel i funcio2.rel que hem compilat abans i que són cridades pel paginate.c.
DEBUG_LOG i posar-la 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;
Ara hem de buscar l'adreça que té el DEBUG_LOG en el programa principal paginate.c per indicar-li als mòduls de 16K que quan faci la crida a aquesta fucnió que salti a l'adreça del programa principal. Un cop localitzada en el fitxer paginate.map li hem de dir al linker que l'utilitzi. Això ho fem amb el paràmetre -Wl-g i li indiquem el nom de la funció que apareix en el .map, en aquest cas _DEBUG_LOG i l'adreça que hem obtingut.
~/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;
Per tradui els binaris al format del MSX, utilitzem l'eina del MSXgl MSXhex. Fixem-nos-hi que el cridem de forma diferent per fer el programa principal que per fer les funcions, ja que el primer ha de seguir el format .com del MSXDOS2 mentre que els binaris és només la traducció.
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
Amb això ja tindrem l'emulador corrent i podrem veure el resultat de les nostres simulacions tal i com es pot veure a la imatge de dalt.
La MSXgl ja permet fer projectes de més de 64K, però han de ser per ROM i t'ajuda a compilar-ho. Però nosaltres podem crear un script en JavaScript i que la MSXgl l'utilitzi.
El fitxer build.sh que es troba en el directori del nostre projecte crida a node que és la comanda del projecte Node.js que permet executar javascript fora del navegador. L'script que s'executa és el que li indiquem en el build.sh que en aquest cas és el que es troba a .../engine/script/js/build.js que s'encarrega d'orquestrar tota la compilació. El primer que busca és ./project_config.js i si no el troba, busca .../projects/default_config.js i si no .../engine/script/js/default_config.js. L'estructura d'aquests fitxers és tota la mateixa, i conté els diferents passos per compilar el projecte. Finalment, a part dels anteriors que només carrega un dels tres segons les prioritats anteriors, també carrega, en cas que existeixi,[project_name].js
El project_config.js permet carregar scripts abans de la compilació principal i scripts després de la compilació. Aquesta característica és la que utilitzarem per compilar els nostres drivers abans de la compilació principal i després en el post, localitzar les funcions del bloc principal que s'utilitzen als mòduls. Per fer axiò només s'han d'omplir les variables PreBuildScripts i PostBuildScripts amb les comandes a executar. En el nostre cas, hem creat un script en javascript, build_pagination.js que s'encarrega de fer això. Amb el que el nostre codi quedaria:
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" ];
Un cop tenim clars els passos, anem a mirar com els podem posar en un script de JavaScript per continuar amb el llenguatge que s'utilitza a la MSXgl. Aquesta implementació la trobem al fitxer 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}
Comencem definint variables que utilitzarem al llarg del procés de compilació. Destacar la variable PaginatedFunctions que contindrà el nom dels fitxers que volem compilar com a funcions a ser cridades posteriorment. Extreu aquest valor de la variable amb el mateix nom PaginatedFunctions del fitxer 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}
Aquesta és la primera funció que s'executa abans que la compilació estàndard de la MSXgl i que s'encarrega de crear els binaris dels fitxers de màxim 16K que utilitzarem en el nostre 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
Aquesta és la part a executar un cop la compilació estàndard de la MSXgl ha acabat. La primera funció extractExternFunctions s'encarrega de buscar el nom de les funcions que estan definides al fitxer C com extern, aquests noms són els que després la funció findSybolInMap haurà de localitzar en el fitxer map per trobar l'adreça de memòria que el link li ha assignat. Aquestes dues funcions són les que s'utilitzen en el postBuild per relinkar els nostres programes que aniran a les pàgines de 16K i saltin a l'adreça corresponent.
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}
Finalment només queda controlar si quan es crida l'script es fa en la fase de prebuild o en la de postbuild i segons d'on vingui la crida executar un bloc o un altre.
Dins el llarg exemple de MSXDOS2 que es pot trobar al directori samples, s_dos2.c, hi ha una part que s'anomena driver que permet carregar binaris en memòria i cridar-los. Això és més semblant al que havíem fet a Paginació de la RAM a on el binari és autocontingut i no utilitza funcions comunes amb el bloc principal. Però la forma de compilar-ho és més senzilla.
Primer de tot s'ha de tenir el programa funcionant, en el nostre exemple senzill, driv1.c només retornem un valor quan es crida la funció. Per compilar-ho podem utilitzar la mateixa plantilla que s'utilitza per compilar els fitxers en C de la MSXgl, en el nostre cas l'hem copiada a driv1.js i s'ha de canviar la línia:
//-- Overwrite code starting address (number). For example. 0xE0000 for a driver in RAM
ForceCodeAddr = 0xE000;
indicant l'adreça de memòria a on serà guardada. D'aquesta manera el linker redirecciona totes les adreces de memòria a partir d'aquesta base.
Un cop ja l'hem creat s'ha de carregar en memòria i cridar-lo des del programa principal. En el nostre cas aquest és el pag_driver.c i per cridar-lo ho fem:
res_func1 = CallDriver(0xE000, 5);
Per carrega-lo en memòria hem usat les comandes de disc ja utilitzades prèviament.
Ja només queda compilar el programa principal i executar-lo amb l'emulador.