Retro Gaming: Wie man einen Emulator programmiert

Hier wird das Techie-Herz erwärmt: Diese Serie ist für alle von euch, die sich jeden Tag eine kleine Auszeit von der Weltlage wünschen. Es gibt täglich eine Geschichte für euch aus unserem Archiv - geeignet für ein wenig fröhlichen Eskapismus. Viel Spaß!
Hat die Welt nicht eigentlich schon genug Emulatoren? Für jede Plattform gibt es einen, man muss ihn nur herunterladen - im schlimmsten Fall steht davor noch die Entscheidung, welcher es genau sein soll. Oder man lädt sich gleich Retropie herunter und kann direkt Dutzende Retro-Computer und -Konsolen nutzen.
Wer gern tüftelt und sich für Hardware interessiert, wird sicher ein weitergehendes Interesse an dem Projekt Emulator entwickeln. Eine gute Umsetzung ist nämlich gar nicht so einfach, dafür umso lehrreicher. Man gewinnt nicht nur spannende Einblicke in den Aufbau der jeweiligen Hardware, ein Emulator ist auch eine hervorragende Programmierübung. Begnügt man sich mit der Emulation einer 8- oder 16-Bit-Konsole, ist nicht einmal Assembler-Code erforderlich.
Teile des Codes in Assembler zu implementieren, bietet zwar enormes Optimierungspotenzial, ein halbwegs aktueller Computer führt aber selbst nicht-optimierten Java-Code schnell genug aus. Für erste Versuche ist das ein großer Vorteil, da selbst ohne Optimierung und große Hirnakrobatik ein voll funktionsfähiger Emulator entsteht.
Wo fange ich an?
Am Anfang steht natürlich die Frage: Was möchte ich emulieren? Für mich stand die Antwort fest - den klassischen Gameboy. Er hat drei Vorteile: Er ist gut dokumentiert, die Hardware ist übersichtlich und einfach - und er ist die Konsole meiner Kindheit. Das Gameboy-CPU-Manual lag seit Jahren auf meiner Festplatte, im Internet Archive findet sich sogar das Programmierhandbuch von Nintendo(öffnet im neuen Fenster) .
Die nächste Frage: Welche Programmiersprache nutze ich? Grundsätzlich funktioniert jede Sprache, so lange sie nicht zur Laufzeit interpretiert wird. Ich habe für meine Implementierung Java genutzt, denn eigentlich wollte ich den Emulator ursprünglich auf Android-Geräten laufen lassen. Daraus wurde nie etwas, aus diesem Grund kommen aber in diesem Artikel Begriffe aus der Java-Welt vor. Generell fand ich einen objektorientierten Ansatz hilfreich. Verwendet man Templates und Interfaces, erspart man sich viele manuelle Prüfungen - das kann helfen, Fehler zu vermeiden.
Und das möchte man, denn einen Emulator zu debuggen ist kein Vergnügen, da das erwartete Verhalten nur teilweise bekannt ist. Was die emulierte Software tut, ist in der Regel unbekannt, weshalb man bei Fehlern schnell im Trüben fischt. Daher lege ich allen, die sich an einem Emulator versuchen wollen, sehr ans Herz, alle entwickelten Komponenten zu testen. Ich habe tagelang einen Fehler gesucht, der durch die fehlerhafte Implementierung eines Prozessorbefehls entstanden war.
Was macht eigentlich ein Emulator?
Damit der Emulator originalgetreu funktioniert, ist eine exakte Nachbildung des Prozessors und der Peripherie als Software erforderlich. Die Software tut am Ende das, was auch der originale Prozessor macht: Sie liest und verarbeitet Befehle, zusätzlich werden Komponenten benötigt, die Grafik und Sound erzeugen.
Beim Gameboy steckt all das in einem Chip, das Mainboard ist recht übersichtlich. Darauf finden sich neben dem Chip 16 KByte RAM, von denen jeweils 8 KByte als Arbeits- und Grafikspeicher dienen. Der Prozessor ist ein Derivat des beliebten Z80 , allerdings wurde der Befehlssatz stark beschnitten. So fehlen fast alle Befehle für 16-Bit-Arithmetik, dafür kamen einige eigene hinzu. Neben Operationen für binär codierte Dezimalzahlen (BCD) finden sich Befehle zur Manipulation einzelner Bits.



Die meisten Befehle sind ein Byte lang, die Abarbeitung erfolgt in vier Stufen. Dabei handelt es sich um die klassische Zerlegung der Ausführung in(öffnet im neuen Fenster)
- Befehl aus dem Speicher holen (Instruction Fetch, IF)
- Befehl decodieren (Instruction Decode, ID)
- Befehl ausführen (Execute)
- Ergebnis schreiben (Write Back, WB)
Da der Prozessor kein Pipelining implementiert, benötigt jeder Befehl vier (oder ein ganzzahliges Vielfaches) Prozessortakte zur Ausführung. Befehle, die länger als vier Takte zur Ausführung benötigen, durchlaufen die einzelnen Schritte mehrfach. Dabei passiert gegebenenfalls in einzelnen Phasen nichts, sie können allerdings nicht ausgelassen werden. Damit arbeitet der Prozessor Befehle mit einer Frequenz von 1,05 MHz ab.
Einen Prozessor mit Software nachbauen
Die Funktion des Prozessors nachzuahmen, ist relativ leicht. Dafür braucht es lediglich eine Funktion, die einen Befehl aus dem Speicher liest. Danach ruft sie eine andere Funktion auf, die den Befehl ausführt. Das sind die ersten beiden Schritte der Befehlsabarbeitung, Instruction Fetch und Instruction Decode.
Die Auswahl der Funktion, welche den gerade gelesenen Befehlscode umsetzt, könnte mit einfacher Fallunterscheidung erfolgen (Switch-Konstrukt). Damit wird der Code allerdings schnell unübersichtlich und vor allem langsam. Daher ist es sinnvoller, ein Array zu nutzen, in dem Zeiger auf die jeweiligen Funktionen abgelegt sind. Da Java keine Funktionszeiger kennt, habe ich ein Interface definiert, für jeden Befehl gibt es eine eigene Klasse. Sie besteht nur aus einer Funktion, die die Befehlslogik umsetzt.
Die Prozessorfunktion holt also ein Byte aus dem Speicher, nutzt es zur Auswahl eines Array-Elements und führt die dort angegebene Funktion aus. Dieser wird der Befehl übergeben, da er häufig noch Parameter für die Ausführung enthält. Manche Befehle greifen auf den Speicher zu, daher muss die Komponente, die ihn emuliert - dazu kommen wir gleich - ebenfalls übergeben werden. Daneben muss die Prozessorfunktion prüfen, ob eine Hardwarekomponente eine Unterbrechung (Interrupt) ausgelöst hat. In diesem Fall muss sie an eine spezielle Adresse verzweigen.
Die Register
Auch die Register, in denen die meisten Befehle Daten bearbeiten, müssen als Datenstruktur angelegt werden. Der Prozessor des Gameboys verfügt über sieben 8-Bit-Register, hinzu kommen der Program Counter (PC, verweist auf den nächsten auszuführenden Opcode), der Stack Pointer (SP, primär für Rückkehradresse bei Funktionsaufrufen) sowie ein Statusregister (F für Flags).
Eines der Register, der Akkumulator, hat eine besondere Funktion, denn hier landen die Ergebnisse aller Rechenoperationen. Das Statusregister enthält vier Bits, die angeben, ob bei der letzten Rechenoperation
- das Ergebnis null war (Zero Flag),
- der vorangegangene Befehl eine Subtraktion war (N, vermutlich für negative),
- ein Übertrag von den niederwertigen vier Bit zu den höherwertigen auftrat (Half Carry) oder
- ein Übertrag aus dem höchstwertigen Bit auftrat (Carry Flag).
Die Flags geben Informationen an nachfolgende Operationen weiter. So können bedingte Sprünge oder Rechenoperationen auf mehr als acht Bit realisiert werden. Die übrigen sechs 8-Bit-Register können Operanden für Befehle aufnehmen oder einfach als Zwischenspeicher dienen. Außerdem können jeweils zwei zu einem 16-Bit-Register zusammengefasst werden. Das erlaubt, sie als Zeiger in den Adressraum zu verwenden. Auch lassen sich so zwei Register mit nur einem Befehl auf den Stack sichern.
Der Registersatz ist schnell als eigene Klasse implementiert. Eine Instanz hiervon gehört zur Prozessorumgebung. Um die Implementierung der Befehle zu vereinfachen, greifen diese nicht direkt auf die Registerklasse zu. Stattdessen bekommen sie zwei Quell- und ein Zielregister übergeben, damit geschieht ein Teil der Decodierung zur Programmierzeit. Das spart wieder Abfragen in der Befehlsumsetzung. Eine wirklich effiziente Implementierung würde Register des emulierenden Prozessors verwenden.
Der Speicher
Neben den Prozessorbefehlen ist es wichtig, den Aufbau des Adressraums zu kennen. Der Prozessor arbeitet mit 8-Bit-Werten, nutzt aber einen 16 Bit breiten Adressbus und kann so auf 64 KByte Speicher zugreifen. Im Adressraum finden sich neben Arbeits- und Grafikspeicher auch der ROM des Spielemoduls sowie eventuell auf diesem vorhandener RAM. Auch auf die Register der anderen Hardwarekomponenten kann hierüber zugegriffen werden.
Daneben gibt es noch zwei kleinere Speicher, die im Chip integriert sind. Dabei handelt es sich zum einen um einen ROM, in dem sich eine Firmware befindet. Sie überprüft nach dem Start des Gameboys, ob ein autorisiertes Spielemodul eingesetzt ist und initialisiert die Register. Damit kann ein Spiel beispielsweise prüfen, ob es auf einem klassischen Gameboy oder dem Gameboy Color läuft. Zum anderen sind 127 Byte RAM integriert, die für den Programmstack und spezielle Warteroutinen genutzt werden.
Den größten Teil des Adressraums nimmt der ROM des Spielemoduls ein. Er belegt die untere Hälfte (0x0000 bis 0x7fff) und ist in zwei Segmente mit je 16 KByte aufgeteilt. Am unteren Ende wird nach dem Start des Gameboys der Firmware-ROM eingeblendet. Erkennt er ein legitimes Spiel, startet er es und blendet sich aus.
Speicher mit besonderer Bedeutung
Einige Speicherbereiche haben eine spezielle Funktion und werden von bestimmten Hardwarekomponenten genutzt. Der Grafikhardware stehen gleich zwei spezielle Speicher zur Verfügung: der Grafikspeicher, der als SRAM-Modul auf der Platine sitzt, und der Object Attribute Memory (OAM). Auf beide geht der nächste Abschnitt genauer ein.
Darüber hinaus existiert eine Menge von Hardware-Registern. Über sie kontrolliert das Programm, was beispielsweise Grafik- und Sound-Hardware tun. Auch für die entgegengesetzte Richtung gibt es Register, über die die Hardware mitteilt, was sie gerade macht. Manche der Hardware-Register lösen direkt eine Aktion aus. Das sollte ein Emulator beachten, um das Verhalten des Systems korrekt nachzubilden.
Eine besondere Bedeutung hat auch der Beginn des Adressbereichs. Nach dem Einschalten befindet sich an Adresse 0x0000 der Firmware-ROM. Er erwartet im ROM von Adresse 0x0100 bis 0x014f Informationen über das Spielemodul, unter anderem das Nintendo-Logo, das er nutzt, um zu prüfen, ob das Modul authorisiert ist. Einige der weiteren Informationen werden später noch nützlich sein. Nachdem sich die Firmware ausgeblendet hat, wird der Bereich von 0x0000 bis 0x0067 als Einsprungpunkte zur Interrupt-Behandlung genutzt. Für jeden der dreizehn existierenden Interrupts stehen acht Byte zur Verfügung.
Wie emuliert man Speicher?
Speicher zu emulieren, klingt zunächst einmal nicht schwer. Einfach ein ausreichend großes Array anlegen, eventuell Daten hineinschreiben, fertig. Praktisch klappt das allerdings aus zwei Gründen nicht. Der erste ist, dass Schreiben in ein Register einer Hardwarekomponente in dieser eine Aktion auslösen kann. Auch liest man bei Hardware-Registern oft nicht das zurück, was hineingeschrieben wurde. Der zweite Grund, weshalb die Emulation der Speicher etwas komplexer ist, liegt im ROM-Bereich. Er wird nämlich nicht nur gelesen, oft wird hier auch geschrieben, den Grund dafür sehen wir später noch.
Um das komplexe Verhalten der verschiedenen Speicher nachzubilden, habe ich eine Klasse geschrieben, die den Zugriff regelt. Sie verfügt über Methoden, um Werte zu lesen und zu schreiben. Besonders elegant ginge das mit C++, indem der Operator für Arrayzugriffe (eckige Klammern) überladen wird; in meiner Java-Implementierung erledigen das zwei Funktionen. Die Klasse an sich delegiert Zugriffe nur an andere Klassen, die den verschiedenen Adressbereichen zugeordnet sind. Sie tun dann, was auch immer die jeweilige Hardware täte - im Fall der RAM-Bereiche bilden sie tatsächlich nur auf ein Array ab.
Nach dem Prozessor ist die Grafikeinheit die zweitwichtigste Komponente. Denn ohne Bild macht kein Spiel Spaß. Also schauen wir uns an, wie die Bilder auf das Display kommen.
Die Grafikeinheit
Die Grafikeinheit des Gameboys war lange etwas mysteriös. Zwar war bekannt, wie sie prinzipiell funktioniert. Die letzten Details wurden allerdings erst vor wenigen Jahren entschlüsselt, der weiter unten eingebettete Ultimate Gameboy Talk auf dem 33c3 erläutert sie. Auch ich habe versucht, mir einen Reim auf das Bekannte zu machen - wohl wissend, dass meine Lösung niemals in Hardware funktionieren würde.
Wie der Großteil der auf 2D-Grafik beschränkten Konsolen basiert die Grafik des Gameboys auf Tiles (Kacheln) und Sprites (wörtlich: Geist, Gespenst oder Kobold). Aus den Tiles wird der Bildhintergrund aufgebaut, darüber dürfen sich die Sprites frei bewegen. So lassen sich komplexe Szenerien mit geringem Speicherbedarf aufbauen. Nintendo benutzt statt Sprites den Begriff Objects. Beim Gameboy sind Tiles und Sprites normalerweise 8 x 8 Pixel groß. Für Sprites können allerdings auch zwei solcher Blöcke zusammengefasst werden. Da sie dann 8 x 16 Pixel groß sind, kann mehr Bildschirmfläche mit Sprites bedeckt werden.
Die Bilddaten für Sprites und Tiles werden im Grafikspeicher abgelegt, bis zu 384 Blöcke von 8 x 8 Pixeln sind möglich. Dabei sind Teile des Speichers zwischen Sprites und Tiles geteilt. Insgesamt 40 Sprites kann der Gameboy gleichzeitig darstellen, allerdings dürfen es nicht mehr als zehn pro Bildschirmzeile sein. Ihre Position und andere Parameter werden über den bereits erwähnten OAM konfiguriert.
Der Hintergrund einer Szene wird anhand eines 32-x-32-Rasters aus Tiles aufgebaut. Sie befinden sich also immer an festen Positionen. Damit ist der Hintergrund 256 x 256 Pixel groß, das Display des Gameboys kann allerdings nur 160 x 144 Pixel darstellen. Daher kann über zwei Register der darzustellende Teil des Hintergrunds ausgewählt werden. So kann im unsichtbaren Teil bereits ein neuer Hintergrund vorbereitet werden und die Welt bewegt sich flüssig.



So kommt das Bild auf den Bildschirm
Im Gegensatz zu moderneren Grafikkarten nutzt die Hardware des Gameboys keinen Speicher, in dem das ausgegebene Bild aufgebaut wird (Frame Buffer). Es wird direkt pixelweise auf dem Flüssigkristallbildschirm aufgebaut. Das klingt erst einmal unwichtig, hat allerdings konkrete Auswirkungen auf den Emulator: Die Funktionalität der Hardware muss eins zu eins nachgebaut werden. Ein Spiel kann nämlich zwischen zwei Bildschirmzeilen Parameter der Grafikhardware ändern, wodurch Effekte erzeugt werden. Sie funktionieren nicht, wenn beispielsweise die gesamten Bilddaten einmal pro Bild in einen Anzeigepuffer kopiert werden.
Die Grafikhardware liest beim Aufbau des Bildes aus OAM und Grafikspeicher. In letzterem befindet sich neben den Daten für Tiles und Sprites das Raster für den Hintergrund. Zusätzlich kann auch noch ein Fenster definiert werden, das über dem Hintergrund angezeigt wird. Es wird gern für Textausgaben genutzt, greift ebenfalls auf die Tiles zurück und wird mit einem 32-x-32-Raster aufgebaut.
Bevor Pixeldaten ausgegeben werden, sucht die Grafikhardware sich zuerst die in der aktuellen Zeile aktiven Sprites aus dem OAM. Danach erzeugt sie pixelweise das Bild, für jedes Pixel wird entschieden, ob es die Farbe des Hintergrunds, des Fensters oder eines Sprites bekommt. Die Auswahl des Farbwerts geschieht anhand fester Prioritäten. Die vier möglichen Graustufen sind übrigens nicht in den jeweiligen Bilddaten festgelegt. Sie werden anhand von Farbpaletten ausgewählt, die Werte in den Bilddaten sind Indizes in die aktuelle Palette.
Für Sprites und Tiles existieren unabhängige Paletten. Die Sprites haben sogar zwei, die anzuwendende wird im OAM-Eintrag des Sprites ausgewählt. Über die Paletten lassen sich wieder leicht Effekte realisieren. So kann beispielsweise das Bild ausgeblendet werden, ohne dass die zugrundeliegenden Bilddaten verändert werden müssen. Genauer wird das alles im folgenden Video erklärt.
Und so habe ich es für moderne Computer emuliert
Der Software-Renderer, den ich für meinen Emulator programmiert habe, setzt den oben beschriebenen Ablauf um. Dabei gibt es noch ein paar Kleinigkeiten zu beachten, beispielsweise die Prioritäten von Hintergrund, Fenster und Sprites. Auch untereinander haben die Sprites noch einmal Prioritäten. Sie hängen von der Position im OAM ab und bestimmen, welches Sprite gezeichnet wird, wenn zwei sich überlappen. Das wird für jedes Pixel entschieden.
Damit wird ein Bild mit vier Graustufen erzeugt - das gefällt mir besser als die Grüntöne des originalen Gameboy. Es wird mit Javas Swing-Bibliothek für graphische Oberflächen in ein Fenster kopiert und dabei skaliert. So füllt es das Fenster vollständig aus und die Emulatorfunktion muss nur ein Format erzeugen können - die 160 x 144 Pixel des Gameboy-Bildschirms. Kommen wir als nächstes zum Sound, denn auch den möchte ich bei den meisten Spielen nicht vermissen. Dem Taltal-Gebirge(öffnet im neuen Fenster) würde etwas fehlen!
Das Soundmodul
Verglichen mit anderen Konsolen ist das Soundmodul des Gameboys spartanisch. Von den Fähigkeiten beispielsweise des SID-Chips(öffnet im neuen Fenster) im C64 ist es so weit entfernt wie ein Kochtopf von einem Schlagzeug. Es wurde abgespeckt vom NES übernommen und verfügt über vier Soundkanäle: Zwei erzeugen Rechteckwellen, einer Rauschen, der vierte gibt gesampelte Sounds wieder. Die Lautstärke der einzelnen Kanäle kann in 16 Schritten geregelt werden.
Aufgrund der geringen Auflösung und der Nutzung von Rechteckwellen würden die erzeugten Sounds normalerweise schrecklich klingen. Speziell Rechteckwellen sind eine Tortur für die Ohren. Um einen angenehmen Klang zu erreichen, wird das erzeugte Soundsignal hochpassgefiltert. Das geschieht analog - durch einen Kondensator. Das Ergebnis ist ein charakteristischer Sound, der durchaus seine Liebhaber hat. In der Chiptune-Szene(öffnet im neuen Fenster) erfreut sich der Gameboy großer Beliebtheit.
Mit den beiden Kanälen für Rechteckwellen wird der größte Teil der Musik erzeugt. Über jeweils einen eigenen Teiler erzeugen sie aus dem Systemtakt den Takt der Rechteckwellen - woraus sich die Frequenz des entstehenden Tons ergibt. Um die Charakteristik des Tons zu variieren, ist noch eine einfache Pulsweitenmodulation implementiert. Die Welle kann ein Achtel, ein Viertel, die Hälfte oder drei Viertel der Periodendauer auf hohem Spannungspegel sein. Für gesampelte Sounds kann die Wiedergabegeschwindigkeit eingestellt werden.
Soundeffekte
Die Soundkanäle bringen ein paar Features mit, die für prozessorunabhängige Soundeffekte genutzt werden können. Das erste Feature ist der Envelope, in diesem Fall kein Briefumschlag, sondern die Hüllkurve(öffnet im neuen Fenster) . Damit kann die Lautstärke des Signals in festen Zeitschritten erhöht oder gesenkt werden. Die zweite ist der Sweep (die korrekte Übersetzung ist wobbeln(öffnet im neuen Fenster) ), der automatisch die Tonfrequenz ändert.
Die Soundkanäle können zudem kontinuierlich ein Signal erzeugen oder nach einer definierten Zeit die Ausgabe beenden. Nicht alle Soundkanäle verfügen über alle zuvor genannten Features, so dass jeder seine eigene Klasse benötigt. Auch unterscheidet sich die Realisierung des Sampling- und Rauschkanals von denen der Rechteckkanäle. Das Rauschen wird übrigens mittels eines linear rückgekoppelten Schieberegisters(öffnet im neuen Fenster) erzeugt. Es wird beispielsweise für Schlagzeugsounds genutzt.
Unerwartete Herausforderungen bei der Emulation
Die Beschreibung der Soundhardware klingt erst einmal nicht besonders herausfordernd - ein paar Register und den Speicher für die Samples implementieren, dazu ein wenig Programmlogik, die anhand der Werte ein Tonsignal erzeugt. Die Signale der einzelnen Kanäle werden lediglich addiert. Trotzdem gibt es zwei Dinge, die mich lange aufgehalten haben.
Zuerst der Kondensator. Wie in aller Welt baut man den in einem Programm nach? Im Physikunterricht habe ich zwar gelernt, wie man die zeitabhängige Spannung eines Kondensators berechnet. Allerdings nur für eine konstant anliegende Spannung. Zum Glück fand ich irgendwann im Internet eine Lösung(öffnet im neuen Fenster) , deren Herleitung ich zwar nicht verstand, sie aber umsetzte - und sie funktionierte. Der Kondensator bildet einen Hochpassfilter, dessen Verhalten das Programm nachbilden muss.
Das zweite Problem war weniger offensichtlich und hat mich Wochen beschäftigt. Die Soundausgabe klirrte unangenehm. Der Grund dafür ist die feste Abtastrate der Soundhardware des Computers. Die vom Gameboy erzeugten Signale passen nicht zu dieser Abtastrate, sie wechseln irgendwo zwischen zwei Abtastpunkten. Das bedeutet, dass über mehrere Wellenzyklen das erzeugte Signal nicht gleich ist.
Dadurch entstehen unerwünschte Signalkomponenten. Die Lösung für dieses Problem heißt Abtastratenkonvertierung(öffnet im neuen Fenster) . Ich habe das, als ich den Emulator programmiert habe, etwas hemdsärmelig mit Oversampling(öffnet im neuen Fenster) gelöst. Wechselt die Ausgabe des Gameboys zwischen zwei Samples, werden diese zwischen 0 und aktueller Lautstärke des Kanals interpoliert.
Um den Aufwand zu reduzieren, habe ich das allerdings nur für eine Schwingung der Welle gemacht und diese immer wieder ausgegeben. Das funktioniert - und klingt - ganz gut, ist allerdings nicht ganz richtig. Und manchmal fällt es auf, hier sollten also professionellere Ansätze zum Einsatz kommen. Jetzt ist zwar die Hardware komplett, aber etwas fehlt noch: die Spiele.
Die Spielemodule
Kaum eines der Spiele für den Gameboy passt in die 32 KByte ROM, die im Adressraum des Prozessors vorgesehen sind. Daher kommt eine in der 8- und 16-Bit-Ära häufig anzutreffende Technik zum Einsatz, das Banking. Der Speicher wird in viele gleich große Bereiche (Bänke) unterteilt, von denen nur einige gleichzeitig für den Prozessor zugreifbar sind. Beim Gameboy sind zwei Bänke mit einer Größe von 16 KByte gleichzeitig nutzbar, theoretisch ist so beliebig großer Speicher realisierbar.
Daher sitzt in den meisten Spielmodulen nicht nur ein Speicherchip, sondern zusätzlich ein sogenannter Memory Bank Controller (MBC). Dessen Grundfunktion ist sehr simpel: Er enthält ein Register, in das die Adresse der ausgewählten Bank geschrieben wird. Das klingt komplexer, als es ist, die Bankadresse sind lediglich die oberen Adressbits. Sie werden zusammen mit den 14 untersten Adressbits an die Adressleitungen des ROMs geführt.
Daneben ist ein (sehr einfacher) Adressdecoder enthalten, der die auszuführende Aktion wählt. Er wählt das passende Bankregister aus, je nachdem, ob die obere oder untere 16-Kbyte-Bank gelesen wird. Apropos Register: Wie kommt da eigentlich die Bankadresse rein? Auch dafür sorgt der Adressdecoder. In den Speicherbereich des Spielemoduls kann nämlich auch geschrieben werden. Den ROM interessiert das nicht, allerdings kann so das Register mit einem Wert befüllt werden.
Neben dem ROM sitzt in einigen Modulen - allen, die Spielstände speichern - zudem ein RAM. Auch der wird in den Speicherbereich des Prozessors eingeblendet, der entsprechende Bereich umfasst 8 KByte. Die Module können mehr RAM enthalten, dann wird auch dieser in Bänke unterteilt.



Welche MBCs gibt es?
Von Nintendo sind vier verschiedene MBCs dokumentiert. Module, die nur einen ROM enthalten, beispielsweise Tetris, müssen extra behandelt werden. Die Chips heißen - ganz kreativ - MBC1, MBC2, MBC3 und MBC5 und unterscheiden sich in ihren Fähigkeiten. Alle unterstützen neben dem ROM auch RAM, der MBC3 enthält zusätzlich noch eine Echtzeituhr. Die Menge an unterstütztem Speicher variiert zwischen den verschiedenen MBCs.
Einige MBCs unterstützen zudem unterschiedliche Aufteilungen auf ROM und RAM, die über das Register vom Spiel eingestellt werden. Das muss bei der Implementierung berücksichtigt werden. Mit Ausnahme des MBC5 gibt es eine weitere Besonderheit: Die unterste ROM-Bank kann nicht gewechselt werden. Das ist durchaus sinnvoll, da sich hier die Einsprungpunkte für die Interrupts befinden. Zudem haben die meisten Spiele einige zentrale Programmteile, die immer genutzt werden und sich daher stets an einer festen Adresse befinden müssen.
Was bedeutet das für den Emulator?
Zuerst einmal muss für jeden MBC, den der Emulator unterstützen soll, eine entsprechende Funktionalität für den Speicherzugriff programmiert werden. Ich habe jeweils eine Klasse geschrieben, in die der Inhalt des jeweiligen Spielemoduls geladen wird - also die Spieldaten (ROM) und eventuell vorhandener RAM. Das ist sicher nicht die effizienteste Lösung, dafür ist sie kaum fehleranfällig.
Eine Frage stellt sich allerdings: Woher weiß der Emulator, welcher MBC verwendet werden muss? Muss am Ende gar eine Datenbank gepflegt werden, in der alle Spiele eingetragen sind? Nein, denn glücklicherweise hat Nintendo offensichtlich an Emulatorprogrammierer gedacht: Alle benötigten Daten, also der verwendete MBC und die Größe des auf dem Modul installierten ROM und RAM, stehen im Informationsblock des Spielemoduls.
Die Laderoutine für ROMs prüft daher zuerst, welcher MBC verwendet wird und wie die Speicheraufteilung ist. Dann erzeugt sie eine Instanz der zu verwendenden MBC-Klasse, die die benötigte Menge an Speicher anlegt. Anschließend werden die Daten des ROMs in die Klasseninstanz geladen, eine Datei mit dem RAM-Inhalt ausgewählt und deren Daten ebenfalls geladen. Hier zeigt sich ein Vorteil des Emulators: Während ein physisches Spielemodul nur einen RAM-Baustein hat, können in der Emulation beliebig viele existieren. So können beliebig viele Spielstände gespeichert und sogar ausgetauscht - oder manipuliert - werden.
Zusammenfassung
Dieser Artikel liefert einen kurzen Überblick, wie die Programmierung eines Emulators angegangen werden kann. Dabei sind ein paar Dinge unter den Tisch gefallen, beispielsweise die Direct-Memory-Access-Funktion (DMA) der Grafikhardware oder der Timer, den Programme für regelmäßige Abläufe nutzen. Informationen hierzu finden sich sowohl in Nintendos Programmierhandbuch als auch im Gameboy CPU Manual. Auch das Gameboy Development Wiki(öffnet im neuen Fenster) ist einen Blick wert.
Zum Schluss auch noch ein paar Lessons Learned: Zumindest beim Gameboy haben alle verfügbaren Informationen zur Hardware einige Fehler. Das Gameboy CPU Manual beschreibt einige Prozessorbefehle falsch, hier habe ich eine Assemblerreferenz für den Z80 hinzugezogen. Selbst Nintendos Programmieranleitung enthält erstaunlich viele, wenn auch kleinere, Fehler. Man sollte sich hier nie auf nur eine Quelle verlassen.
Die zweite Lektion, die ich gelernt habe, ist: testen, testen, testen. Wenn etwas nicht funktioniert, sollte man erst einmal den Fehler im eigenen Code suchen. Und am besten viel dokumentieren, denn ein Emulator ist kein Wochenendprojekt.
War das schon alles?
Ist der Emulator schon komplett, wenn die einzelnen Hardwarekomponenten nachprogrammiert sind? Nein, dann kommen noch zwei große Herausforderungen.
Die erste ist, dass der Emulator auf einem modernen Prozessor viel zu schnell läuft. Die zweite ist, dass wir eine Hardware mit Software nachbauen. In Hardware läuft alles parallel, der Prozessor arbeitet ein Programm ab, während die Grafikhardware Pixel ausspuckt und die Soundhardware Klänge erzeugt. Das muss ein Emulator umsetzen - und wie das geht, wird ein nächster Artikel zum Thema zeigen.
Update:
Der Artikel wurde auf seine Aktualität überprüft.



