top of page

Paginació de la RAM


Introducció

En aquest article parlarem de com poder treballar amb més de 64K de memòria RAM i fer-ho de forma senzilla aprofitant les aventatges que dóna el MSX-DOS2.


Què és això de la paginació RAM?

El Z80 només permet direccionar 64Kb de memòria. Per superar aquesta dificultat, els dissenyadors del MSX van pensar en crear 4 slots que podia accedir el Z80 i després aquests slots dividir-los cadascun en 4 subslots i que aquests subslots són els que s'anessin mostrant al Z80 per tenir més memòria. Més endavant van aparèixer els mapejadors que permetien assignar els subslots d'un slot amb diferents segments de memòria portant ells el control d'aquests segments i trencant així el límit de memòria de 64Kb del MSX. Aquests segments de memòria és el que en diuen una pàgina de RAM, i el seu control és la paginació de la RAM.


El MSXDOS2 conté una sèrie de crides a la seva BIOS (BDOS) que s'encarreguen de fer tota aquesta gestió de la memòria, estalviant-nos així la problemàtica de saber en cada model de MSX quina distribució té de la memòria. En Fusion-C utilitzarem les funcions InitRamMapperInfo que inicialitza el mapeig de la memòria, _GetRamMapperBaseTable que actualitza les dades de la taula tipus MAPPERINFOBLOCK que conté la informació de l'estat de la memòria paginada: pàgines (segments de 16 Kb) utilitzades i lliures.


Si voleu llegir una altra explicació de la paginació de la RAM, podeu anar a aquest post del Javier Lavandeira que trobo que està molt ben explicat.


Què farem en el codi

Per provar la paginació carregarem uns fitxers en diferents pàgines de la memòria, posarem aquestes pàgines al segment 2 del Z80 que es correspon a l'adreça 0x8000 i llegirem el seu valor per comprovar que s'hagin carregat bé. Canviarem la pàgina i tornarem a llegir el valor. És molt semblant al que hi ha com exemple en el Fusion-C però a diferència d'aquest, nosaltres carreguem tot un fitxer a la pàgina de memòria escollida, en comptes de canviar només un valor.


Per què ho fem més complicat? La veritat és que el C genera arxius més grossos que si programes optimitzadament en ensamblador, amb la qual cosa de seguida omples un segment de 16K. Si aconsegueixes fer mòduls de 16K que tinguin una funcionalitat concreta, els podràs carregar a una pàgina de memòria i que el programa gestor que s'està executant a la pàgina principal el pugui cridar. És a dir, una programació modular. Per exemple, el joc bricks, utilitza aquesta estratègia per carregar les animacions inicials i finals de joc, cadascuna és un mòdul de menys de 16K, PantInic.c i PantFin.c són els fitxers en C que permeten compilar-ho i fer les proves, mentre que PantInic.c_mod i PantFin.c_mod són els corresponents binaris compilats dels anteriors eliminant les funcions comunes que ja es troben a la funció principal i afegint a aquesta adaptació una funció que dongui accés a aquests mòduls (els fitxers anteriors es poden trobar a gitlab).


Això també permet una programació paral·lela en equip. Ja que es poden carregar i desenvolupar els mòduls independentment.


Explicació del codi

El programa es troba al repositori de Gitlab amb el nom caBinMap.c i consisteix en:

Com sempre, les primeres línies (fins la 8) contenen les llibreries que utilitzarem. La primera variable que trobem (línia 10) és la de descripció del fitxer de DOS, el FCB, explicat en major detall a l'article "Carregar imatges al MSX". La variable BufferPagina és el buffer de 16Kb on es carregaran els bytes llegits del disquet. Es defineix amb la directriu __at 0x8000 per indicar-li al linkador que quan ajunti els binaris, posi aquesta variable a l'adreça 0x8000. Aquesta adreça coincideix amb la inicial de la pàgina 2 del Z80.


Finalment tenim la variable table que contindrà la informació de les pàgines de memòria del DOS. Aquesta informació és la que imprimim a la funció printRamMapperStatus que cada cop que la cridem mostrarà per pantalla el número de slot, els segments 16K utilitzats, els lliures, els que ja hem sol·licitat i els que està utilitzant l'usuari.


A continuació hi ha la funció FT_SetName (que és la mateixa que havíem vist a "Carregar imatges al MSX") i la seva acompanyant per controlar els errors, la FT_errorHandler. Per acabar les funcions de lectura de disquet tenim a la línia 78 la FT_LoadBin que s'assembla molt a la FT_LoadSc5Image (del "Carregar imatges al MSX"), però que aquí només carrega les dades al buffer, sense haver de fer després una crida a HMMC per poder passar de la memòria RAM a la VRAM. Aquí a la línia 90, ja carreguem tot el fitxer a la pàgina 2, a la posició 0x8000 que és on està guardada la variable BufferPagina.


A la línia 95 tenim ja la funció principal main que comença definint les variables a utilitzar: un punter p que ens servirà per llegir les diferents posicions de memòria, la variable status que és de tipus SEGMENTSTATUS que guardarà la informació del segment sol·licitat, i tres variables de tipus char anomenades segmentId0, segmentId1 i initialSegment que guardaran el número de segment retornat per SEGMENTSTATUS.


A la línia 102 inicialitzem la paginació de memòria del DOS2, cridant a la seva BIOS, amb el valor 4 que és l'identificador del dispositiu (deviceID) del paginador de memòria. Netegem la pantalla amb el Cls i imprimim com està el paginador, obtenint:

A la línia 109 sol·licitem l'assignació d'un segment amb la comanda AllocateSegment(0,0) a on el primer 0 és per indicar que sigui un User Segment i el segon 0 és per dir que ho faci de manera automàtica. Guardem el retorn de la funció a status i el segment assignat a segmentId0 i imprimim aquesta informació per pantalla.


Tornem a fer el mateix amb un altre segment i guardem la informació a segmentId1.

El que veiem en pantalla en aquest punt és:

on podem observar que el número de User Segments a augmentat a 2 i el número de Free Segments (els lliures sense assignar) s'ha reduït a 3.


Borrem la pantalla i llegim l'adreça de memòria 0x8000 guardant-ho a la variable p (línia 123). Guardem el segment que estava a la pàgina 2 a initialSegment i imprimim quin segment hi havia originalment i el valor que hem llegit de p d'aquest segment original.


Entre les línies 130 i 134 posem a la pàgina 2 el primer segment reservat (segmentId0) i llegim el seu valor, guardem el valor 0xDD a la posició 0x8000 i imprimim aquest valor.


Finalment imprimim el valor del segment que hi ha a la pàgina 2 obitngut amb la funció Get_PN().

Així és com queda la pantalla arribats a aquest punt:

El primer segment que hi havia a la memòria era el #1 i que té com a valor 0xCD, aquest valor dependrà dels valors que hi havia en la RAM en aquell moment, nosaltres no hi hem posat un valor concret, això ho fem després de canviar al segment 4, que hi havia el valor 0x01 i el substituïm pel 0xDD. Finalment comprovem que el valor que obtenim amb Get_PN(2) és el mateix que havíem posat fent Set_PN(2).


Entre les línies 142 i 155 fem una cosa semblant a l'anterior, però aquest cop amb el segmentId1. Primer de tot pintem per pantalla el número de segment de segmentId1 i passem a posar aquest segment a la pàgina 2 del Z80. Imprimim el valor que hi ha a l'adreça 0x8000, el sobreescrivim a la memòria i el tornem a llegir. A la línia 148 carreguem el fitxer binari "16k_2.bin" que és un fitxer de 16k on cada byte val 2 i comprovem el valor de les posicions 0x8000 i 0x800A que ha de mostrar 2. Finalment comprovem que el segment llegit per Get_PN és el mateix que havíem usat en Set_PN. Això és el que veiem a la nostra pantalla:

Entre les línies 157 i 161 tornem a posar el segment segmentId0 a la pàgina 2 del Z80 i llegim el valor 0xDD que és el que havíem guardat a la línia 133 del nostre codi.


Les línies següents fins a la 171 fan el mateix que abans però ara carregant un fitxer binari ("16k_1.bin") que només conté el valor 1, però el carreguem al segmentId0. Tornem a posar el segmentId1 que contenia els valors de #2 durant 16K i llegim dues posicions i comprovem que continuen tenint el valor #2 (si utilitzéssim el openmsx-debugger podríem veure que segmentId1 està tot ple d'1s). El que veiem en pantalla és el següent:

A les línies 173-180 recuperem el segment original a la pàgina 2 (initialSegment) i comprovem que encara té el mateix valor que havíem vist a la 3a pantalla (el text que apareix és "Before setting segments #1") el 0xCD. Tal i com podem observar a la captura de pantalla:

Finalment alliberem els diferents segments i imprimim l'estatus del Mapper obtenint en pantalla:

Com podem observar, no hi ha hagut cap error i per tant els segments lliures tornen a estar a 5 i els segments d'usuari utilitzats estan a 0.


Resum i utilitats de la paginació

Volia ensenyar com carregar els blocs de 16, perquè en el joc bricks s'utilitza per carregar l'animació inicial i final. En no està gaire optimitzat el codi, de seguida va ocupar 32K de programa. Aleshores és quan es va pensar en dividir l'aplicació: el bucle principal són 32K, és el que s'encarrega de gestionar els diferents estats: presentació, elecció de jugadors, joc i presentació final. També gestiona la part del joc. Les animacions inicials i finals són menys de 16K i tal i com s'ha explicat a l'inici, es van desenvolupar de forma independent i després es van eliminar les funcions que ja estaven al bucle principal: càrrega de fitxers, interrupcions de la música, etcètera. Aquests mòduls es carreguen a la pàgina 2 del Z80. L'última pàgina (la 3) és utilitzada pel driver de la música que transcriu un fitxer MBM carregat en memòria i configura l'OPLL perquè faci la música.


Com compilem aquests fitxers per poder ser carregats a la pàgina 2?

Si compilem els anteriors fitxers com sempre, el SDCC posarà totes les funcions a partir de l'adreça 0x106. És per això que canviem les opcions com a --code-loc 0x8000 (en comptes del 0x106 que s'utilitza normalment, tal i com es va explicar al post Introducció d'aquet blog).


Un cop tenim ja les funcions a les adreces de la pàgina 2, hem de carregar aquest binari de forma semblant com hem fet en aquest tutorial però guardant-ho en un array a la posició 0x8000. Les comandes que executen això són:

__at 0x8000 char pag2[0x4000];
FT_openFile("PantIni.bMo");
FcbRead(&file, pag2, 0x4000);

amb el que obtenim el MSX carregat amb tots els opcodes a l'adreça 0x8000.


En el fitxer mòdul, hi ha d'haver una funció que crida a la funció principal del mòdul. En el cas del bricks, aquesta era:

void CallMain_asm()__naked {
  __asm
      call _animacio_pantalla_inicial
      ret
 __endasm;
 }

Finalment queda crida aquesta funció en el mòdul principal. Per fer-ho, investiguem en el fitxer .map generat quina és l'adreça de la funció anterior i la cridem directament. Assumint que l'adreça era 0x8EA6, haurem de cridar-la com:

__asm call #0x8ea6 __endasm;

Potser no queda prou clar i s'hauria de fer un post amb més detall. Si és així, no dubteu en comentar-nos-ho.


Finalment, una mica de deures. La paginació també ens permet fer una aplicació senzilla que comprovi la RAM del sistema. Amb la informació del RamMapperBaseTable sabem de seguida la quantitat de RAM que veu el DOS i podem comprovar també que es puguin escriure i llegir tots els bytes. Qui s'atraveix a desenvolupar-ho en C?


Clica aquí per a veure l'exemple funcionant.



Comentários


bottom of page