Amit a PRIMO hardverről tudni kell

(ha valaki emulátort szeretne készíteni)

Az alábbi dokumentumban összefoglalom azokat a hardver jellegzetességeket, amelyeket ismernie kell annak, aki PRIMO emulátort szeretne készíteni. A dokumentum saját tapasztalataimon alapul, illetve a primoemu-ban megvalósított egy-egy funkcióra is, ezért meglehetősen szubjektív. Ha bizonyos funkciók emulációját saját szempontomból fontosabbnak tartottam, vagy netán mellőztem, azt külön jelzem majd.

Processzor

A PRIMO processzora eredetileg egy Z80 kompatíbilis NDK gyártmányú chip volt, amelynek névleges órajele 2,5 Mhz. Ettől eltekintve a processzor minden szempontból egy szabványos Z80-nak tekinthető, az emulátorhoz szükséges tehát egy teljes Z80 processzor emulátor, amely emulálja a Z80 ciklusait és utasításait, természetesen a nem dokumentált utasításokat is.

Memória

A többféle típus miatt a memóriák felosztása eltérő volt, itt most csak az A64-el foglalkozom, amely 64K címezhető memóriát tartalmazott az alábbi (kicsit szubjektív) felosztás szerint:

0000–3FFF:    16K rendszer ROM, itt található az A64 ROM image
4000–BFFF:    32K RAM terület.
C000-FFFF:    16K RAM terület, amely a képernyőterületet tartalmazza.

Képernyő

A PRIMO két egymástól független képlap kezelésére volt képes. A kép felbontása 256x192 pixel, tehát 192 rasztersor, soronként 32 bájt. A képterület cím-folytonos volt, azaz, az első sor első bájtja a 0. képbájt, az első sor utolsó bájta a 31. képbájt, a második sor első bájtja a 32. képbájt, és így tovább. A képterület ezek szerint 192x32, azaz 6144 bájtot igényel. Az elsődleges képterület a memória legutolsó 6K-s része E800-FFFF-ig. A másodlagos képlap 8K-val lejjebb helyezkedik el, azaz a C800-DFFF tartományban. A két lap között a KI-1 I/O port egy bitjével lehetett váltani (lásd később).


Általános I/O

A Z80 64K I/O területet tud kezeli, ebből a PRIMO-ban csak az alsó 256 I/O portot definiálják, ezt a területet 4 db 64bájt hosszú címterületként lehet kezelni mind inputként, mind outputként.

I/O Címterület	Kimenetek			Bemenetek
00-3F     KI-1 port (általános out bitek) BE-1 port (általános in bitek)
40-7F     KI-2 port (CBM soros busz out)  BE-2 port (CBM soros busz in)
80-BF     Nem használt tartomány   Nem használt tartomány
C0-FF     Nem használt tartomány     Nem használt tartomány

Bemenetek

A BE-1 port (IN 00-3F) a rendszer általános input bitjeit tartalmazza, ezek közül néhányat mindenképp meg kell valósítani az emuláció során.

7. Bit:    nincs használva
6. Bit:    nincs használva
5. Bit:    képkioltás, azaz a vertikális visszafutás jelzése. Az eredeti PRIMO-n e bit 1 értéke jelezte az elektronsugár visszafutását. A PAL frekvenciák miatt ott ez egy 50Hz-es jel volt, viszont, ha VGA kártyán emuláljuk a PRIMO képernyőjét és megvalósítjuk ezt a bitet, a VGA frissítési frekvenciái miatt ez 70Hz lesz. Implementálását javaslom, hiszen néhány alkalmazás használhatja a bitet a képszinkronizáláshoz. A VGA rendelkezik olyan I/O porttal, amelyen keresztül a visszafutás detektálható (03DA, 3. bit).
4. Bit:    BE-2/4 bit. A PRIMO rendszerbuszára kivezetett általános I/O vonal. Implementálása nem létszükséglet, ha valaki mégis meg szeretné valósítani, a nyomtató port valamelyik bemeneti bitjét lehet erre felhasználni.
3. Bit:    BE-2/3 bit. A PRIMO rendszerbuszára kivezetett általános I/O vonal. Implementálása nem létszükséglet, ha valaki mégis meg szeretné valósítani, a nyomtató port valamelyik bemeneti bitjét lehet erre felhasználni.
2. Bit:    Magnó adatbemenet. Mivel a primoemu PTP magnófájlokat használt, én nem implementáltam, de az előző két esethez hasonlóan nem lehetetlen megoldani a nyomtatóportra csatlakoztatott megfelelő illesztő segítségével, ha valaki direkt magnó olvasást szeretne.
1. Bit:    RESET nyomógomb állapota. A PRIMO RESET nyomógombjának állapotát tükrözi. Valójában ez a nyomógomb nem RESET gomb, hanem egy egyszerű bementi bit, amelyet NMI alatt kérdez le a rendszer. Mivel az NMI kikapcsolható, ezért az alkalmazások deaktiválni tudják ezt a gombot. Az implementáció során valamely speciális billentyű lenyomása válthatja ki ennek a bitnek a beállítását.
0. Bit:    Billentyűzet bemenet. A megcímzett gomb lenyomott/felengedett állapotát mutatja.

A BE-1 port 1-5 bitjei a 00…3F portok olvasásakor egyenértékűek, a 0. bit viszont a címzett billentyű állapotát adja vissza. A billentyűkhöz tartozó I/O címeket a „PRIMO szoftver füzetek” tartalmazza. Az implementáció során az alábbiakat javaslom: az 1-5 biteket egy változó (byte, unsigned char) tartalmazhatja, míg a billentyűzet állapotát egy 64 elemű szintén bájtokat tartalmazó tömb tárolhatja. Ezt a tömböt a PC billentyűzet megszakítása folyamatosan frissíti, így a Z80 IN utasítás emulációjakor csupán ki kell venni a megfelelő elemet a tömbből és összeállítani a bementi bájtot.

A BE-2 port (IN 40..7F) az opcionális CBM (Commodore) soros busz vonalainak állapota, illetve a léptetőregiszteres botkormány olvasható be. Én ezt a primoemu-ban nem implementáltam, de szükséges lehet a megvalósítása, ha valaki disk vagy joy emulációt is szeretne.

7. Bit:    nincs használva
6. Bit:    nincs használva
5. Bit:    SCLK
4. Bit:    SDATA
3. Bit:    SRQ
2. Bit:    Botkormány adatbemenet, 2. bit
1. Bit:    ATN
0. Bit:    Botkormány adatbemenet, 1. bit


Kimenetek

A KI-1 port (OUT 00..3F) a rendszer működése szempontjából fontos biteket tartalmaz, majdnem mindegyik megvalósítása szükséges az emulátorhoz.

7. Bit:    NMI engedélyezés (1=engedélyezve). Az implementáció során ezt a bitet kell figyelnie a rendszernek, és ha értéke 0, nem kell Z80 NMI megszakítást generálni. Implementálása szükséges.
6. Bit:    Botkormány léptetése. A primoemu-ban nincs joy emuláció, ezért nem implementáltam.
5. Bit:    Magnó távvezérlés vagy V24/2. Eredetileg a magnót vezérelte, de általános kimeneti bitként is használható volt. Implementálása nem szükségszerű, de megvalósítható a nyomtatóport valamely kimeneti bitjével.
4. Bit:    Hanggenerátor, speaker. Ez tulajdonképpen a PRIMO hangja, megvalósítása mindenképp ajánlott. Szerencsére a PC speaker ki-be kapcsolható nagyon hasonló módon, mint a PRIMO hangszóró, ezért a megvalósítás nem nehéz.
3. Bit:    Képernyő lapkijelölés. A két létező képlap közül választja ki a láthatót. A BASIC ezt a bitet 1-re állítja, ilyenkor az E800-FFFF terület látható. Az emulátornak érdemes két logikai képpel dolgoznia, és a bit megváltozásakor ezek közül a megfelelőt kiválasztani. A primoemu a fizikai VGA kép kezdőcímét változtatja meg, ezáltal a képváltás gyors. (Gyorsabb, mintha a teljes képet kellene újrarajzolni.)
2. Bit:    Magnó távvezérlés vagy V24/1. Eredetileg a magnót vezérelte, de általános kimeneti bitként is használható volt. Implementálása nem szükségszerű, de megvalósítható a nyomtatóport valamely kimeneti bitjével.
1. Bit:    Mangó kimenet jelszint vezérlés 1.
0. Bit:    Magnó kimenet jelszint vezérlés 2. A két alsó bit a magnó kimenet jelszintjét határozta meg (lásd a szoftver füzetek F-40 oldalán). Mivel a primoemu PTP magnófájlokat használ, én nem implementáltam, de ez is megoldható a nyomtató csatlakozón keresztül.


A KI-2 port (OUT 40..7F) az opcionális CBM (Commodore) soros busz vonalainak állapotát határozza meg. A primoemu-ban nincs megvalósítva.

7. Bit:    nincs használva
6. Bit:    nincs használva
5. Bit:    SCLK
4. Bit:    SDATA
3. Bit:    nincs használva
2. Bit:    SRQ
1. Bit:    ATN
0. Bit:    nincs használva

A soros busz vonalait az eszközök nyitott kollektoros kapukkal hajtják meg, ez azt jelenti, hogy a buszra csatlakoztatott bármely eszköz képes a kommunikáció kezdeményezésére. További információkat a CBM soros buszról a Commodore dokumentációkban, illetve a PRIMO hardver és szoftver füzetekben találunk. Mint látható, a KI-2 és BE-2 portokat csak akkor kell implementálnunk, ha disk és/vagy joystick emulációt is akarunk.


Billentyűzet

Ahogy BE-1 portnál már leírtam, a PRIMO úgy kezeli a billentyűzetet, hogy minden gombot külön portcíme n érhetünk el. A BE-1 port címtartomány lekérdezésekor a beolvasott adat 0. bitje adja vissza a megcímzett billentyű állapotát. Az eredeti (A típusú) billentyűzet kapacitív működési elve miatt a ROM-ban található lekérdező rutin egy billentyűt csak akkor tekint lenyomottnak, ha azt 128 lekérdezés után is aktívnak találja. Ez a megoldás az emulátor esetében azt eredményezi, hogy az emulált Z80 IN utasítást igen sokszor fogja végrehajtani a kód- Ha tehát az IN utasítás megvalósítása kicsit lassú, jelentősen ronthatja az emulátor teljesítményét.  

A primoemu-ban több olyan ROM patch is található, amelyek átírják az eredeti ROM rutinokat, ezáltal bizonyos funkciók módosulnak. Ezek közül az egyik az, hogy a sorozatos lekérdezések számát a ROM-ban kisebbre vettem, hogy a IN utasítások kevesebbszer hajtódjanak végre. (Lásd a szoftver füzetekben az F-71 oldalon: „gomb sokszoros lekérdezése”)


Egyéb

Magnófájlok: A primo PTP fájl formátumát korábban már közzétettem, így annak felépítése ismert. A magnófájlok kezeléséhez az szükséges, hogy az eredeti ROM rutinokat néhány helyen módosítsuk, így azok nem akarnak majd biteket beolvasni az I/O portokról, hanem a megfelelő külső szervizrutint hívják, amelyek a betöltés illetve az adatmentés megfelelő elemi lépéseit megvalósítják. A magnófájlokban elhelyezett eredeti PRIMO programok formátuma egyébként egyezik a szoftver füzetekben ismertetett formátummal, a különbség csupán annyi, hogy a PTP fájlok nem tartalmaznak szinkronizáló mezőket.

Képfrissítés: Az emuláció elég fontos kérdése a képfrissítés. Ez természetesen számtalan ötlettel megoldható, én itt most a primoemu-ban alkalmazott módszert ismertetem. Mivel az emulátor közvetlenül a VGA kártya képmemóriájába ír, és a PRIMO képfelépítés viszonylag egyszerű, ezért nincs valódi frissítő rutin, hanem minden egyes Z80 memóriaíráskor megvizsgálom, hogy a célbájt képbájt-e, és ha igen, akkor a VGA memóriát is módosítom. (Van egy WriteByte függvény, amelyet minden memóriaíró Z80 utasítás rutinja meghív, ha szükséges.) A Z80 processzor emulációnál a memória írás egyébként is speciális eset, hiszen meg kell vizsgálni, hogy az írandó bájt ROM területre vagy képernyő területre esik-e.

Különleges Z80 kódsorozatok: Az emulációból való kilépéshez — például amikor egy magnófájl kezelő szervizrutin hívása szükséges — olyan illegális Z80 kódokat használok fel, amelyek nem szerepelnek az eredeti utasításkészletben. Például az ED 00 kód nem valódi Z80 utasítás. Ez tehát egy külső rutint hív meg, amely elvégzi a megfelelő műveleteket. Például egy bájt magnóra írásakor (szoftver füzetek, F-89 oldal) egy ilyen speciális kódon keresztül jut el a vezérlés a PTP kezelő rutinhoz, ami kiírja a bájtot a PTP fájlba.

NMI emuláció: ez a rész igazán egyszerű. A PC timer megszakítását beállítottam, hogy másodpercenként 50 megszakítást adjon, és ez a kiszolgáló rutin egy falg-et billent be, név szerint az NMIflag-et, ez jelzi a Z80  emulátor számára, hogy NMI történt. Természetesen itt figyelembe kell venni, hogy a KI-1 port NMI engedélyező bitje milyen állapotban van.

Emulációs ciklus: A megfelelő beállítások után (képernyőmód, ROM patch-ek, stb.) az emulátor fő működtető ciklusa valami ilyesmi:

while (!quit) {
 Z80fetch;         // végrehajtunk egy Z80 utasítást
 Z80delay;         // várakozunk, azaz lassítjuk az emulátort
  PC_keyboard_check;    // ellenőrizzük a speciális billentyűket, például menü, kilépés stb.
}

Amíg a főciklus látszólag egyszerű, a háttérben azért történik egy-két dolog: a timer megszakítás generálja (ha engedélyezett) az NMI megszakításokat a Z80 számára, a billentyűzet megszakítás pedig frissíti a billentyű táblázatot, amit a Z80 IN utasítás kiolvashat. Speciális billentyű lenyomása esetében a PC_keyboard_check hívja  a menükezelőt, vagy az egyéb rutinokat.


Példák az eredeti forrásból


A KI-1 és BE-1 szervizrutinok. Ezeket az eljárásokat hívja a Z80 emulátor OUT és IN utasítása.

procedure KI1out;                                               assembler;
asm
   { dx : portszam, cl : data }
   cmp  cl,KI1
   jz   @quit
   mov  KI1,cl           { a 7. bit maszkolja az NMI-t }
   { hangszoro vezerles }
@speaker:
   in    al,$61
   test  cl,$10
   jz    @spk_off
@spk_on:
   or    al,$03
   out   $61,al
   jmp   @setPAGE
@spk_off:
   and   al,$FC
   out   $61,al
   { keplap valtas }
@setPAGE:
   xor   bx,bx
   test  cl,$08
   jnz   @sp1
   test  tvout,3
   jnz   @sp0
   mov   bx,80*240
   jmp   @sp1
@sp0:
   mov   bx,80*200
@sp1:
   mov   ScrnStart,bx
   mov   al,0ch
   mov   dx,03d4h
   mov   ah,bh
   out   dx,ax
   mov   ah,bl
   inc   al
   out   dx,ax
@quit:
   mov   ioexec,1
end;

procedure BE1in;                                                assembler;
asm
   { dx : portszam, cl : data }
   lea  bx,Keys
   mov  al,dl           { billentyu sorszam 0..63 }
   and  al,$3F
   xlat                 { kivesszuk a tablazat megfelelo byte-jat }
   and  al,$01          { csak a 0. bit szamit                    }
   mov  cl,[Keys+64].byte { RESET nyomogomb }
   shl  cl,1
   and  cl,$02
   or   cl,al
   mov   dx,03dah     { vertikalis visszafutas bit }
   in    al,dx
   and   al,8
   shl   al,2
   or    cl,al
   or    cl,11011100b   { a nem implementalt biteket magasra allitjuk }
   mov   BE1,cl
   mov   ioexec,1
end;
Képbájt írása közvetlen módon. Ezt a kiegészítő rutint hívja a Z80 memóriaírás.

{ E800-FFFF }
procedure WriteScreenByte0;                                     assembler;
asm
   { di - address, cl - data }
   mov   ES,SegA000
   sub   di,[ScrTop1]
   add   di,di
   mov   di,[di+offset ScrnAdrTab]
   xor   bx,bx
   mov   bl,cl
   shl   bx,1
   add   bx,offset sbw
   mov   cx,[bx]
   mov   ES:[di],cx @quit:
end;

Joco