MSX turbo R - MSXDOS 2 Memory management

Last updated on: August 26, 1998

Deze keer maak ik een uitstapje naar het aansturen van de memory mapper onder MSXDOS 2. Dit omdat de MSX turbo R nogal veel onder DOS 2 wordt gebruikt.

Inleiding
Vroeger, voor de komst van MSXDOS 2, was het aansturen van de memory mapper vrij eenvoudig. Als een programma meer dan 64 kB RAM nodig had, kon het programma onderzoeken hoe groot de memory mapper was en vervolgens de gewenste geheugenblokken inschakelen door rechtstreeks naar de memory mapper in/out poorten te schrijven. Deze geheugenblokken in een memory mapper worden segmenten genoemd. Ze zijn ieder 16 kB groot.
Met de komst van MSXDOS 2 is het geheugenbeheer ingewikkelder geworden. Dit komt doordat MSXDOS 2 zelf ook segmenten uit de memory mapper gebruikt en daarom bij iedere aanroep van een MSXDOS functie met de memory mapper schakelt. Hierbij houdt MSXDOS 2 zelf bij hoe de memory mapper ingesteld is.
Indien nu een programma rechtstreeks met de memory mapper schakelt, zonder dit aan MSXDOS 2 door te geven, zal die nieuwe mapper instelling bij de eerste de beste aanroep van MSXDOS 2 teniet gedaan worden. Dit kan tot vastlopers leiden omdat dan bijvoorbeeld gegevens naar verkeerde segmenten worden ingeladen.
Om dit probleem op te lossen biedt MSXDOS 2 een aantal zogenaamde memory mapper support routines. Er zijn routines om op te vragen hoe groot de memory mapper is, hoeveel geheugen er nog vrij is, om segmenten te reserveren en weer vrij te geven en verder zijn er routines om de stand van de memory mapper op te vragen en om segmenten in te schakelen op de vier beschikbare geheugen pagina's.

Mapper initialisatie.
Bij het opstarten zoekt MSXDOS 2 alle slots af om de aanwezige memory mappers te vinden. Vervolgens wordt de grootste memory mapper ingeschakeld en bouwt MSXDOS 2 een tabel op van alle segmenten in deze memory mapper. Deze memory mapper wordt de primaire memory mapper genoemd. Vervolgens reserveert MSXDOS 2 twee segmenten uit de primaire mapper voor eigen gebruik en hierna gaat MSXDOS 2 nog tabellen opbouwen voor de overige memory mappers, waarin alle segmenten als vrij worden gemarkeerd.
De MSXDOS 2 die in te MSX turbo R is ingebouwd neemt overigens altijd de interne memory mapper als primaire mapper. Ook als in een cartride slot een grotere memory mapper zit. Dit is gedaan omdat de R800 het intern geheugen een stuk sneller kan aansturen dan geheugen in de cartridge slots.

Variabelen en routines
MSXDOS 2 houdt een aantal variabelen bij in het systeem gebied. Deze variabelen, die in een tabel staan, mogen gebruikt worden door de programmeur. Ze mogen overigens alleen worden gelezen, het is niet toegestaan voor een programma om er naar te schrijven. De indeling van deze variabelentabel is te vinden in tabel 1.
Tabel 1
Offset Functie
0 Slot adres van het mapper slot
1 Totaal aantal 16kB RAM segmenten
2 Aantal vrije segmenten
3 Aantal gealloceerde systeem segmenten
4 Aantal gealloceerde user segmenten
5-7 Ongebruikt. Altijd nul
8 Entries voor andere mapper sloten. Als er geen andere sloten zijn, is de inhoud van offset 8 nul

Verder biedt MSXDOS 2 nog een aantal routines aan, die te bereiken zijn via een zogenaamde jump tabel. Dit is een tabel die bestaat uit sprong instructies naar de diverse mapper support routines. De opbouw van de jump tabel is te vinden in tabel 2.
Tabel 2
Offset Entry naam Functie
00h ALL_SEG Alloceer een 16kB segment
03h FRE_SEG Geef een 16kB segment vrij
06h RD_SEG Zet byte op adres A:HL in A
09h WR_SEG Schrijf byte E naar adres A:HL
0Ch CAL_SEG Intersegment call. Adres in IYh:IX
0Fh CALLS Intersegment call. Adres in regels na call instructie
12h PUT_PH Schakel segment in op pagine HL
15h GET_PH Vraag huidig segment van pagina HL op
18h PUT_P0 Schakel segment in op pagina 0
1Bh GET_P0 Vraag huidig segment van pagina 0 op
1Eh PUT_P1 Schakel segment in op pagina 1
21h GET_P1 Vraag huidig segment van pagina 1 op
24h PUT_P2 Schakel segment in op pagina 2
27h GET_P2 Vraag huidig segment van pagina 2 op
2Ah PUT_P3 Schakel segment in op pagina 3
2Dh GET_P3 Vraag huidig segment van pagina 3 op

De extended bios hook
Het adres van de variabelen tabel en van de jump tabel is op te vragen via de extended bios hook. Dit is dezelfde hook die bijvoorbeeld door MemMan gebruikt wordt. De extended bios hook, met als officiele aanduiding 'EXTBIO', bevindt zich op adres 0FFCAh. Hij kan worden aangeroepen met in register D het zogenaamde device nummer en in register E het functie nummer. De mapper support routines hebben 4 als device nummer. Bij de aanroep van de extended bios hook moet de stack zich in pagina 3 bevinden want anders kan de computer vastlopen. De routines die aan de extended bios hook hangen, worden een voor een aangeroepen en kijken in register D of ze de functie aanroep mogen afhandelen. Indien de aanroep voor hen bestemd is, mogen ze de registers AF, HL en BC eventueel veranderen. Anders moeten ze die registers ongewijzigd laten doorgeven. Het registerpaar DE moet altijd ongewijzigd blijven en de alternatieve registers en de index registers mogen altijd veranderd worden. Dit houdt in dat alleen de registers AF, HL en BC gebruikt kunnen worden om parameters door te geven aan een routine die via de extended bios hook wordt aangeroepen en dat zo'n routine alleen via deze registers een waarde terug kan geven.
Voordat de extended bios hook aangeroepen wordt, moet officieel worden gecontroleerd of deze wel geinitialiseerd is. Dit kan door naar bit 0 van de byte op adres 0Fb20h te kijken. Indien dit bit 0 is, is er geen extended bios hook aanwezig. Anders is die hook er wel en kan hij aangeroepen worden.
Indien MSXDOS 2 aanwezig is, is de extended bios hook altijd geinitialiseerd omdat dan de mapper support routines eraan hangen. Zonder MSXDOS 2 hoeft dat niet het geval te zijn. Overigens is het ook mogelijk om zelf mapper support routines te schrijven -wie voelt zich hiertoe geroepen?- voor als MSXDOS 2 afwezig is, en die op dezelfde manier te laten functioneren als de mapper support routines van MSXDOS 2.

De functies
De mapper support routines van MSXDOS 2 bieden twee functies aan via de extended bios hook:

Get mapper variable table
In: A = 0
D = 1
E = 1
Uit: A = Slot van primaire mapper.
DE = Ongewijzigd.
HL = Startadres van mapper variabelen tabel (zie tabel 1 voor de indeling).
Get mapper support routine address
In: A  = 0
D  = 4
E  = 2
Uit: A  = Aantal mapper segmenten.
B  = Slot van primaire mapper.
C  = Aantal nog vrije segmenten in de primaire mapper. 
DE = Ongewijzigd. 
HL = Start adres van de jump tabel (zie tabel 2 voor de indeling). 
Bij de aanroep van deze routines hoeft register A niet perse nul te zijn. Het is echter wel aan te bevelen om de functies met nul in register A aan te roepen. Als de mapper support routines namelijk afwezig zijn, blijft de waarde in register A ongewijzigd, terwijl er een waarde, altijd ongelijk aan nul, in wordt teruggeven als de mapper support routines wel aanwezig zijn. Hieraan valt dus te zien of de mapper support routines aanwezig zijn.

In de volgende paragrafen bespreek ik de mapper support routines uit tabel 2. Bij de opsomming van de parameters van de routines maak ik gebruik van een aantal speciale afkortingen:

ALL_SEG en FRE_SEG
Deze eerste twee routines zijn nodig om mapper segmenten aan te vragen en weer vrij te geven. Een programma mag naast de vier segmenten die bij het opstarten zijn ingeschakeld, alleen gebruik maken van segmenten die met ALL_SEG zijn aangevraagd. Het is dus niet toegestaan om niet aangevraagde segmenten in te schakelen en te gebruiken. Het is namelijk heel goed mogelijk dat die segmenten al ergens anders voor worden gebruikt zoals voor de MSXDOS 2 ramdisk.
De routines ALL_SEG en FRE_SEG veranderen alleen de registers AF en BC.
Met de routine ALL_SEG kunnen twee soorten segmenten worden aangevraagd; user segmenten en system segmenten. De eerste categorie segmenten worden automatisch door MSXDOS 2 vrijgegeven als het programma wordt be-eindigd terwijl de tweede categorie segmenten alleen worden vrijgegeven als het programma dit zelf expliciet doet met de routine FRE_SEG. De user segmenten worden altijd van de laagst genummerde vrije segmenten afgehaald terwijl de systeem segmenten juist van de hoogst genummerde vrije segmenten worden gehaald.
De routines hebben de volgende parameters:

ALL_SEG : alloceer een segment
In: A  = 0: Alloceer user segment. 
A  = 1: Alloceer systeem segment. 
B  = 0: Alloceer uit primaire mapper slot. 
B <> 0: Alloceer uit een ander slot. Formaat voor register B: 
FxxxSSPP: Slotadres
xxx=000:  Alloceer alleen uit opgegeven slot.
xxx=001:  Alloceer alleen uit andere sloten dan het opgegeven slot.
xxx=010: Probeer eerst optie 000 en als dat mislukt optie 001.
xxx=011: Probeer eerst optie 001 en als dat mislukt optie 000. 
Uit:   Cf = Geen segment gevonden. 
NCf = Een segment gealloceerd: 
A = New segment nummer
B = Slot adres van mapper (B=0 als de routine was aangeroepen met B=0). 
FRE_SEG : geef een segment vrij
In:  A  =  Vrij te geven segment nummer. 
B  = 0: Segment zit in primaire mapper. 
B <> 0: Segment zit in slot B.
Uit:  Niks. Mischien dat sommige registers gewijzigd worden. 

RD_SEG en WR_SEG
De volgende twee mapper support routines kunnen gebruikt worden om data uit een mapper segment te lezen (RD_SEG) en om data naar een mapper segment te schrijven (WR_SEG). De twee hoogste bits van het adres worden genegeerd, de data worden altijd gelezen en geschreven via pagina 2. De mapper waaruit gelezen wordt of waarnaar geschreven wordt moet hierbij zijn ingeschakeld op pagina 2. De routines RD_SEG en WR_SEG schakelen namelijk niet met de slot indeling, ze schakelen alleen met de mapper segmenten. Deze routines veranderen alleen het registerpaar AF.
De routines hebben de volgende parameters:

RD_SEG : lees uit een segment
In:  A  = Segment om uit te lezen. 
HL = Adres binnen het segment. 
Uit: A  = De waarde uit segment. 
DI 
Overige registers ongewijzigd. 
WR_SEG : schrijf naar een segment
In:  A  = Segment om naar te schrijven. 
HL = Adres binnen het segment. 
E  = Te schrijven waarde. 
Uit: A  = Gewijzigd 
DI 
Overige registers ongewijzigd. 

CAL_SEG en CALLS
Met deze twee routines kunnen intersegment calls worden uitgevoerd. Dit werkt op bijna dezelfde manier als de interslot calls uit de ROM BIOS. Ook deze routines schakelen alleen met de memory mapper. Het programma dat de routines gebruikt moet dus zelf het goede slot inschakelen voordat een intersegment call wordt gebruikt. Dit zal in de praktijk vaak geen probleem zijn omdat de meeste programma's data uit slechts één mapper gebruiken en daarom helemaal niet met de slots schakelen. De routines kunnen overigens niet gebruikt worden om een intersegment call uit te voeren naar een routine in page 3. Dit komt doordat de mapper support routines zelf in page 3 staan en daarom het segment in page 3 niet weg kunnen schakelen; als ze dit wel doen schakelen ze zichzelf uit en hangt de computer.
De intersegment call routines hebben de volgende parameters:

CAL_SEG : voer intersegment call uit
In:  IX = Segment waar de aan te roepen routine in staat. 
IY = Adres dat aangeroepen moet worden. 
AF, BC, DE en HL worden ongewijzigd doorgegeven.
Uit:  AF, BC, DE, HL, IX en IY worden ongewijzigd teruggeven van de aangeroepen routine. 
CALLS : voer intersegment call uit
In: AF, BC, DE en HL worden ongewijzigd doorgegeven aan de routine 
De parameters (segment en adres) moeten achter de aanroep staan: 
CALL CALLS
DB SEGMENT
DW ADDRESS
Uit:  AF, BC, DE, HL, IX en IY worden ongewijzigd teruggeven van de aangeroepen routine. 

PUT_Px en GET_Px
De laatste groep routines kan gebruikt worden om rechtstreeks met de memory mapper te schakelen. Deze routines zijn razendsnel. Dat komt doordat ze alleen met de memory mapper schakelen en geen enkele controle uitvoeren op de geldigheid van het doorgegeven segment nummer. Tijdens de ontwikkeling van Zone Terra heb ik bijvoorbeeld deze routines gebruikt om met de memory mapper te schakelen voor het mixen van de samples zonder dat ik bij iedere test van Zone Terra terug zou hoeven te gaan naar MSXDOS 1.
De PUT_Px routines schrijven het segment nummer rechtstreeks naar de memory mapper en slaan het mapper nummer tevens op in het geheugen. De GET_Px routines halen de segment indeling op uit die geheugen plaatsen. Ze lezen dus niet de memory mapper poorten uit. Deze werkmethode voorkomt problemen bij het gebruik van meerdere mappers tegelijk omdat dan de waarde die via de poorten teruggelezen wordt vaak niet betrouwbaar is.
De routine PUT_P3 is een dummy routine. Deze routine schakelt niet met de memory mapper. Ze is alleen aanwezig om de mapper support jump tabel systematisch opgebouwd te houden.
Programma's die met de PUT_Px routines de geheugen indeling gaan vervangen moeten eerst met de GET_Px routines de originele geheugenindeling opvragen. Een programma mag niet zomaar aannemen dat er een bepaalde standaard indeling van de memory mapper is. Als een programma bijvoorbeeld vanuit een zogenaamde shell wordt opgestart, die zelf resident aanwezig blijft, kan het best zijn dat de shell de 'standaard' mapper pagina's gebruikt en dat de mapper indeling anders is voor het door de shell ingeladen en aangeroepen programma.

De PUT_Px en GET_Px routines hebben de volgende parameters:

PUT_Pn : schakel een segment
In: n = Pagina nummer (0, 1, 2 of 3).
A  = Segment nummer. 
Uit: Niks, alle registers ongewijzigd.
GET_Pn : vraag segment indeling op
In: n  = Pagina nummer (0, 1, 2 of 3). 
Uit: A  = Segment nummer. 
Overige registers ongewijzigd. 
PUT_PH : schakel een segment
In: H  = MSB van het adres, bit 7 en 6 van H bepalen dus het pagina nummer. 
A  = Segment nummer. 
Uit: Niks, alle registers ongewijzigd. 
GET_PH : vraag segment indeling op
In: H  = MSB van het adres. 
Uit: A  = Segment nummer. 
Overige registers ongewijzigd.