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.
Oder nutzen Sie das Golem-pur-Angebot
und lesen Golem.de
- ohne Werbung
- mit ausgeschaltetem Javascript
- mit RSS-Volltext-Feed
Retro Gaming: Wie man einen Emulator programmiert | Der Speicher |
+1 Bin schon auf den Folgeartikel zur Umsetzung der parallel stattfindenden HW-Abläufe...
Sheep It Up ist für mich ein Musterbeispiel eines Homebrewspiels für den GB. Simpel...
Ich finde es daher umso erstaunlicher, wie die Leute Cores für den Mister erstellen. Ich...
Zuerst noch einmal vielen Dank für den Artikel. Bei aller Abkürzung hätte ich mir aber...
Kommentieren