22. 4. 2014

Jak efektivně emulovat osmibitový hardware

Jelikož můj emulátor počítače PMI-80 funguje již řadu týdnů beze změn a pracuji pouze na systému rozšiřujících zařízení (add-ons), mohu některá technická řešení pokládat za definitivní a podělit se s čtenáři o to, jakými úvahami jsem byl při programování veden a pro které z dostupných variant se rozhodl.

Emulovat/simulovat číslicový počítač je možné několika způsoby. Nabízí se přísně synchronní varianta, při které se na zařízení díváme jako na stavový automat a na základě každého pulsu, resp. každé náběžné i sestupné hrany, systémových hodin provedeme výpočet nového stavu všech prvků. To je arci methoda, která se pro emulaci konkrétního počítače nehodí, protože dává sice správné výsledky, ale její rychlost může být ve výsledku drasticky nižší než skutečná rychlost emulovaného stroje: ve velké většině případů se totiž neděje nic, stav prvku se nemění, a je zbytečné to pokaždé deklarovat a nový stav zapisovat kamsi do paměti.

Vůbec nejjednodušší by bylo převzít methodu events a listeners, jak je implementována např. v Javě. Každý výstupní pin každého prvku by při změně stavu vytvářen event, na které by ostatní prvky reagovaly přes své listeners.

To však naráží na základní obtíž spočívající v tom, že takto nelze nasimulovat chování obousměrných sběrnic a signálů: zařízení na jejich konci jsou někdy přijímačem, někdy vysílačem. Zvlášť obtížná je situace u obvodu PPI (Intel 8255), který může v Mode 2 měnit svůj stav vstup/výstup podle vnějšího signálu (\ACK), a to zcela asynchronně k hodinám CPU.

Rozhodl jsem se proto pro jinou methodu, kterou nyní v určitém zjednodušení popíši (detaily budou dostupné ve zdrojových kodech, jakmile je uvolním). Tento přístup dovoluje simulovat všechny procesy, které jsou pro emulaci potřebné, včetně takových nestandardních zapojení, jako je sdílení jednoho pinu 8255 k různým účelům (v případě PMI-80 např. pro obsluhu displaye a zároveň pro generování výstupního signálu pro magnetofon).

Každý pin, jehož stav je relevantní pro stav počítače, representuji objektem třídy IOPin (přesněji řečeno jeho podtřídy, obvykle nestované v třídě příslušného hardwarového elementu, např. integrovaného obvodu, protože třída IOPin je abstraktní; její instance by byly tak obecné, že by byly zcela neužitečné: elektricky by jim odpovídal pin, který není nikam zapojen a nic nedělá). Piny, které jsou elektricky (vodivě) propojeny, tvoří uzel, pro jehož representaci vytvářím objekt třídy IONode. Pro představu, v celém PMI-80 je takových uzlů 48.

Každý IOPin zná svůj IONode a každý IONode si vede seznam svých IOPinů. Proč nemůže být jeden IOPin připojen na víc IONodů, je triviálně jasné: propojení representuje vodič a ve skutečnosti by se tak nejednalo o dva různé uzly, nýbrž jen o jeden.

Nejprve je nutné vytvořit piny a uzly, a ty poté vzájemně propojit. K tomu slouží methoda add() uzlu a pinu. V praxi stačí zavolat tuto methodu na uzlu, připojení pinu si uzel zajistí sám uvnitř methody. V programu to může vypadat např. takto:

new IONode().add(ppi1.getPin(6)) .add(displayHardware.getDataPin(6)) .add(tapeRecorderHardware.getOutPin(0));


Vidíme, že methoda add() je naprogramována tak, aby podporovala chaining, ale to je jen kosmetický detail. Dále vidíme, že referenci na uzel nikam neukládám: prostě jen hardwarové prvky propojím a dál se o ně nestarám, fungují samočinně.

Komunikaci si ukážeme na příkladu pinu, který je sdílen klávesnicí a displayem. Nejprve tedy vytvoříme všechny čtyři objekty a vodivé spojení mezi uzlem a jeho piny:


Podstatou toho, jak emuluji komunikaci – a důvodem, proč o tom vůbec píši – je princip duální notifikace, který mi umožňuje ošetřit všechny případy, které se v praxi mohou vyskytnout, včetně těch relativně nejsložitějších, jako je Mode 2 u 8255.

Uzel je zcela pasivní, sám o sobě nikdy komunikaci s piny nevyvolá: je to také jen vodič. Každý pin může být v jednom ze třech stavů, a to 0, 1 a HIGH_IMPEDANCE (-1). Kdykoli se jeho stav změní, pošle pin uzlu notifikaci. Její součástí však není informace o novém stavu, pouze to, že došlo ke změně:


(Ve skutečnosti se tato methoda jmenuje jinak, protože v tomto jmenném prostoru v Javě nelze identifikátor notify použít, ale to je opět jen nepodstatný detail.) Uzel poté rozešle notifikaci o změně všem svým pinům, takto:


A to je vše, pokud žádný z pinů informaci o novém stavu nepotřebuje, není mu vnucována.

Jestliže naopak pin potřebuje stav uzlu znát, a to kdykoli, nejen po notifikaci, pošle dotaz zavoláním methody uzlu query():


IONode ovšem svůj stav nezná, a než odpoví, resp. se z methody vrátí, musí se dotázat svých pinů, jaký je jejich aktuální stav:


To je určité zjednodušení, protože uzel se neptá všech pinů, ale jakmile mu některý z nich vrátí hodnotu 0 nebo 1, uzel polling ukončí a hodnotu vrátí pinu, který se dotazoval. Neuspěje-li u žádného pinu, vrátí HIGH_IMPEDANCE. Alternativně by bylo možné, minimálně v diagnostickém régimu, polling provést vždy do konce a zkoumat, zda na uzlu nedochází ke kolisi, a minimálně tyto případy logovat, podobně jako ty, kdy je vracena vysoká impedance: pin by se totiž v takové situaci nikdy neměl ptát, pokud tak činí, je to příznakem chyby v programu.

Půvab shora popsaného mechanismu je v tom, že lze velmi efektivně a jednoduše implementovat chování nejrůznějších zařízení. Příkladmo vstupní obvod interface pro magnetofon: pokud se procesor prostřednictvím 8255 neptá, interface neodpovídá a nemusí svůj stav zjišťovat. Zeptá-li se, sáhne do treesetem implementované kasety a zjistí, že v daném časovém okamžiku (měřeném cykly CPU) by měl mít takovou-a-takovou úroveň.

Naopak display musí znát údaj o každé změně úrovně, aby mohl spočítat, po jak velkou část intervalu byl segment rozsvícen a zda se tedy má rozsvítit i na displayi pro uživatele, a v takovém případě je vhodné, aby pin displaye reagoval na každou notifikaci dotazem a zařídil se podle odpovědi.

Pro některá zařízení ale tento přístup nepostačuje. To jsou ta, která mají vlastní časovač, tedy např. Intel 8250 nebo 8253. Když časovač doběhne, je vyvolána určitá akce. Pro tyto případy jsem implementoval scheduler řízený systémovými hodinami, CPUScheduler. Podstatná část jeho algorithmu je zde:


Methodou addScheduledEvent() může kterýkoli objekt (vlastník) přidat do rozvrhu svou událost, a když nastane, dostane notifikaci o ní jako callback, včetně celočíselného parametru, se kterým ji zadal. Methoda removeAllScheduledEvents() odstraní všechny naplánované události daného vlastníka. 

Takto jsem implementoval např. klávesnici IBM PC. Ta má vlastní mikrořadič, Intel 8048, jehož hodiny jsou na hodinách připojeného PC (případně PMI-80 či čehokoli jiného, co s ní chce komunikovat) nezávislé. Když uživatel stiskne, příp. uvolní klávesu, vytvořím sekvenci událostí, odpovídajících seriově posílanému kodu – parametr používám přímo jako úroveň, kterou má datový výstup mít. Tyto události se poté ve správném pořadí a ve správnou dobu volají. Asi takto:


Zbývá dodat, že timeout je privátní pole uchovávající čas, kdy bude klávesnice moci vyslat další scancode. Skutečná klávesnice musela pracovat s konečnou délkou fronty, avšak to lze zanedbat, už s ohledem na to, že žádnou klávesnici tohoto typu nemám, abych frontu experimentálně změřil.

V případě, že je klávesnice počítačem resetována, všechny události daného vlastníka jsou odstraněny a neprovádějí se.

Domnívám se, že dva shora popsané mechanismy mi dovolí efektivní implementaci všech zařízení, která kdy budu potřebovat naemulovat; vím, že to není právě objev Ameriky, a také to nepředstírám, přesto se domnívám, že by se někomu mohly tyto výsledky hodit.

1 komentář:

  1. Jako šikovný člověk určitě zvládnete sbastlit i Smartphone.

    http://tinyurl.com/l7w4cac

    OdpovědětVymazat