Teile des
ersten ,
zweiten ,
dritten .
Der Rest der Maschine
Der Code, den wir für die Emulation des 8080-Prozessors geschrieben haben, ist recht allgemein gehalten und kann problemlos an jeden Computer mit dem C-Compiler angepasst werden. Um das Spiel selbst zu spielen, müssen wir jedoch mehr tun. Wir müssen die Ausrüstung des gesamten Arcade-Automaten emulieren und Code schreiben, der die spezifischen Funktionen unserer Computerumgebung auf den Emulator klebt.
(Möglicherweise möchten Sie sich den
Schaltplan der Maschine ansehen.)
Timings
Das Spiel läuft auf dem 2 MHz 8080. Ihr Computer ist viel schneller. Um dies zu berücksichtigen, müssen wir uns einen Mechanismus ausdenken.
Unterbrechungen
Interrupts sind so konzipiert, dass der Prozessor Aufgaben mit präzisen Ausführungszeiten wie E / A verarbeiten kann. Der Prozessor kann das Programm ausführen, und wenn der Interrupt-Pin ausgelöst wird, beendet er die Ausführung des aktuellen Programms und führt etwas anderes aus.
Wir müssen simulieren, wie ein Arcade-Automat Interrupts erzeugt.
Grafik
Space Invaders zeichnet Grafiken im Adressbereich 0x2400 in seinen Speicher. Ein echter Hardware-Videocontroller würde RAM lesen und eine CRT-Anzeige steuern. Unser Programm muss dieses Verhalten emulieren, indem es ein Bild des Spiels in einem Fenster rendert.
Knöpfe
Das Spiel verfügt über physische Tasten, die das Programm mit dem IN-Befehl des 8080-Prozessors liest. Unser Emulator muss die Tastatureingabe an diese IN-Befehle binden.
ROM und RAM
Ich muss zugeben: Wir „schneiden die Ecke ab“, indem wir einen 16-Kilobyte-Speicherpuffer erstellen, der die unteren 16 KB der Prozessorspeicherzuordnung enthält. Tatsächlich sind die ersten 2 KB der Speicherzuweisung ein echter Nur-Lese-Speicher (ROM). Wir müssen Schreiboperationen im Speicher in eine Funktion einfügen, damit es nicht möglich ist, in ein ROM zu schreiben.
Ton
Bisher haben wir nichts über Klang gesagt. Space Invaders verfügt über ein niedliches analoges Soundschema, das einen von 8 Sounds wiedergibt, die vom OUT-Befehl gesteuert werden und an einen der Ports übertragen werden. Wir müssen diese OUT-Befehle konvertieren, um Soundbeispiele auf unserer Plattform abzuspielen.
Es mag wie viel Arbeit erscheinen, aber es ist nicht so schlimm und wir können uns schrittweise bewegen. Als erstes möchten wir den Bildschirm sehen, für den wir Interrupts, Grafiken und einen Teil der Verarbeitung der IN- und OUT-Befehle benötigen.
Zeigt und aktualisiert
Die Grundlagen
Sie sind wahrscheinlich mit den Komponenten eines Videoanzeigesystems vertraut. Irgendwo im System befindet sich eine Art RAM, der ein Bild zur Anzeige auf dem Bildschirm enthält. Bei analogen Geräten gibt es Geräte, die diesen RAM lesen und die Bytes in analoge Spannung umwandeln, die an den Monitor übertragen wird.
Ein tieferes Verständnis des Systems hilft uns bei der Analyse des Zwecks der Speicherzuweisung und der Codefunktionalität.
Analoge Anzeigen stellen Anforderungen an Bildwiederholraten und Timings. Zu jedem Zeitpunkt verfügt die Anzeige über ein aktualisiertes spezifisches Pixel. Das auf den Bildschirm übertragene Bild wird Punkt für Punkt ausgefüllt, beginnend von der oberen linken Ecke und oben rechts, dann vom ersten Punkt der zweiten Zeile, vom letzten Punkt der zweiten Zeile usw. Nachdem die letzte Linie auf dem Bildschirm gezeichnet wurde, kann der Videocontroller einen vertikalen Leerzeichen-Interrupt (auch als VBI oder VBL bezeichnet) erzeugen.
Um eine reibungslose Animation zu gewährleisten, kann das vom Videocontroller verarbeitete Bild im RAM nicht geändert werden. Wenn die RAM-Aktualisierung in der Mitte des Frames erfolgt ist, sieht der Betrachter Teile von zwei Bildern. Dies führt zu einem "Riss" -Effekt, wenn ein Bild, das sich vom unteren Bild unterscheidet, oben auf dem Bildschirm angezeigt wird. Wenn Sie jemals einen Zeilenumbruch gesehen haben, wissen Sie, wie er aussieht.
Um Lücken zu vermeiden, muss die Software etwas tun, um zu vermeiden, dass der Speicherort der Bildschirmaktualisierung übertragen wird. Und dafür gibt es nur einen Weg.
VBL wird nach dem Ende der letzten Zeile generiert. In der Regel dauert es eine gewisse Zeit, bis die erste Zeile erneut gezeichnet wird. (Dies ist die vertikale Leerzeit und kann etwa 1 Millisekunde betragen.)
Wenn VBL empfangen wird, beginnt das Programm, den Bildschirm von oben zu rendern.
Jede Linie wird vor dem Frame-Scan-Umkehrvorgang gezeichnet.
Die CPU ist dem Return Hot immer voraus und vermeidet daher Zeilenumbrüche.
Space Invaders Videosystem
Eine sehr informative
Seite sagt uns, dass Space Invaders zwei Video-Interrupts haben. Eine ist für das Ende des Frames, erzeugt aber auch einen Interrupt in der Mitte des Bildschirms. Die Seite beschreibt das Bildschirmaktualisierungssystem - das Spiel zeichnet Grafiken in der oberen Hälfte des Bildschirms, wenn es eine Unterbrechung in der Mitte des Bildschirms erhält, und Grafiken im unteren Teil des Bildschirms, wenn es eine Unterbrechung am Ende des Rahmens empfängt. Dies ist eine ziemlich clevere Methode, um Zeilenumbrüche zu vermeiden, und ein gutes Beispiel dafür, was erreicht werden kann, wenn Sie gleichzeitig Hardware und Software entwickeln.
Wir müssen die Emulation unserer Maschine erzwingen, um solche Interrupts zu erzeugen. Wenn wir sie mit einer Frequenz von 60 Hz sowie die Space Invaders-Maschine erzeugen, wird das Spiel mit der richtigen Frequenz gezeichnet.
Im nächsten Abschnitt werden wir über die Mechanik von Interrupts sprechen und darüber nachdenken, wie sie emuliert werden können.
Tasten und Anschlüsse
Der 8080 implementiert E / A mithilfe der Anweisungen IN und OUT. Es hat 8 separate IN- und OUT-Ports - der Port wird durch das Datenbyte des Befehls bestimmt. Zum Beispiel setzt
IN 3
den Wert von Port 3 in Register A und
OUT 2
sendet A an Port 2.
Ich habe Informationen zum Zweck jedes Ports von der
Computer Archaeology- Website abgerufen. Wenn diese Informationen nicht verfügbar wären, müssten wir sie durch Studieren des Schaltplans sowie durch Lesen und schrittweise Codeausführung erhalten.
:
1
0 (0, )
1 Start
2 Start
3 ?
4
5
6
7 ?
2
0,1 DIP- (0:3,1:4,2:5,3:6)
2 ""
3 DIP- , 1:1000,0:1500
4
5
6
7 DIP-, 1:,0:
3
2 ( 0,1,2)
3
4
5
6 "" ? , ,
(0=a,1=b,2=c ..)
( 3,5,6 1=$01 2=$00
, (attract mode))
Es gibt drei Möglichkeiten, E / A in unserem Software-Stack zu implementieren (der aus einem 8080-Emulator, Maschinencode und Plattformcode besteht).
- Betten Sie Maschinenwissen in unseren 8080-Emulator ein
- Betten Sie das 8080-Emulatorwissen in den Maschinencode ein
- Erfinden Sie eine formale Schnittstelle zwischen den drei Teilen des Codes, um den Informationsaustausch über die API zu ermöglichen
Ich habe die erste Option ausgeschlossen - es ist ziemlich offensichtlich, dass sich der Emulator ganz unten in dieser Aufrufkette befindet und getrennt bleiben sollte. (Stellen Sie sich vor, Sie müssen den Emulator für ein anderes Spiel wiederverwenden, und Sie werden verstehen, was ich meine.) Im Allgemeinen ist das Übertragen von Datenstrukturen auf hoher Ebene auf niedrigere Ebenen eine schlechte architektonische Lösung.
Ich habe Option 2 gewählt. Lassen Sie mich zuerst den Code anzeigen:
while (!done) { uint8_t opcode = state->memory[state->pc]; if (*opcode == 0xdb)
Dieser Code implementiert die Verarbeitung von Opcodes für IN und OUT in derselben Schicht erneut, wodurch der Emulator für den Rest der Befehle aufgerufen wird. Meiner Meinung nach macht dies den Code sauberer. Dies ähnelt einer Überschreibung oder Unterklasse für die beiden Befehle, die sich auf eine Automatenebene bezieht.
Der Nachteil ist, dass wir die Emulation von Opcodes an zwei Stellen übertragen. Ich werde Sie nicht beschuldigen, die dritte Option gewählt zu haben. Bei der zweiten Option ist weniger Code erforderlich, aber Option 3 ist „sauberer“, aber der Preis erhöht die Komplexität. Dies ist eine Frage der Stilwahl.
Schieberegister
Die Space Invaders-Maschine verfügt über eine interessante Hardwarelösung, die einen Bitverschiebungsbefehl implementiert. Der 8080 verfügt über Befehle für eine 1-Bit-Verschiebung, aber Dutzende von 8080-Befehlen werden benötigt, um eine Mehrbit- / Multibyte-Verschiebung zu implementieren. Spezielle Hardware ermöglicht es dem Spiel, diese Operationen in nur wenigen Anweisungen auszuführen. Mit seiner Hilfe wird jedes Bild auf dem Spielfeld gezeichnet, dh es wird mehrmals pro Bild verwendet.
Ich glaube nicht, dass ich es besser erklären kann als die hervorragende
Analyse der Computerarchäologie:
; 16- :
; f 0
; xxxxxxxxyyyyyyyy
;
; 4 x y, x, :
; $0000,
; write $aa -> $aa00,
; write $ff -> $ffaa,
; write $12 -> $12ff, ..
;
; 2 ( 0,1,2) 8- , :
; offset 0:
; rrrrrrrr result=xxxxxxxx
; xxxxxxxxyyyyyyyy
;
; offset 2:
; rrrrrrrr result=xxxxxxyy
; xxxxxxxxyyyyyyyy
;
; offset 7:
; rrrrrrrr result=xyyyyyyy
; xxxxxxxxyyyyyyyy
;
; 3 .
Beim Befehl OUT wird beim Schreiben in Port 2 der Verschiebungsbetrag und beim Schreiben in Port 4 die Daten in den Schieberegistern festgelegt. Das Lesen mit IN 3 gibt Daten zurück, die um den Verschiebungsbetrag verschoben sind. In meiner Maschine ist dies folgendermaßen implementiert:
-(uint8_t) MachineIN(uint8_t port) { uint8_t a; switch(port) { case 3: { uint16_t v = (shift1<<8) | shift0; a = ((v >> (8-shift_offset)) & 0xff); } break; } return a; } -(void) MachineOUT(uint8_t port, uint8_t value) { switch(port) { case 2: shift_offset = value & 0x7; break; case 4: shift0 = shift1; shift1 = value; break; } }
Tastatur
Um die Antwort der Maschine zu erhalten, müssen wir Tastatureingaben daran binden. Die meisten Plattformen bieten die Möglichkeit, Tastenanschläge zu empfangen und Ereignisse freizugeben. Der Plattformcode für die Schaltflächen sieht folgendermaßen aus:
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { if (msg.message==WM_KEYDOWN ) { if ( msg.wParam == VK_LEFT ) MachineKeyDown(LEFT); } else if (msg.message==WM_KEYUP ) { if ( msg.wParam == VK_LEFT ) MachineKeyUp(LEFT); } }
Der Maschinencode, der den Plattformcode auf den Emulatorcode klebt, sieht ungefähr so aus:
MachineKeyDown(char key) { switch(key) { case LEFT: port[1] |= 0x20;
Wenn Sie möchten, können Sie den Code der Maschine und der Plattform beliebig kombinieren - dies ist die Wahl der Implementierung. Ich werde dies nicht tun, da ich die Maschine auf mehrere verschiedene Plattformen portieren werde.
Unterbrechungen
Nachdem ich das Handbuch studiert hatte, stellte ich fest, dass der 8080 Interrupts wie folgt behandelt:
- Die Interruptquelle (außerhalb der CPU) setzt den CPU-Interrupt-Pin.
- Wenn die CPU bestätigt, dass der Interrupt empfangen wurde, kann die Quelle des Interrupts einen beliebigen Opcode an den Bus senden und die CPU sieht ihn. (Meistens verwenden sie den Befehl RST.)
- Die CPU führt diesen Befehl aus. Wenn es sich um RST handelt, ist dies ein Analogon des CALL-Befehls für eine feste Adresse am unteren Rand des Speichers. Es schiebt den aktuellen PC auf den Stapel.
- Der Code in der unteren Speicheradresse verarbeitet, was der Interrupt dem Programm mitteilen möchte. Nach Abschluss der Verarbeitung wird RST mit einem Aufruf von RET beendet.
Die Videoausrüstung des Spiels erzeugt zwei Interrupts, die wir programmgesteuert emulieren müssen: das Ende des Frames und die Mitte des Frames. Beide werden mit 60 Hz (60 Mal pro Sekunde) ausgeführt. 1/60 Sekunde ist 16,6667 Millisekunden.
Um die Arbeit mit Interrupts zu vereinfachen, werde ich dem 8080-Emulator eine Funktion hinzufügen:
void GenerateInterrupt(State8080* state, int interrupt_num) {
Der Plattformcode muss einen Timer implementieren, den wir aufrufen können (im Moment nenne ich ihn einfach time ()). Der Maschinencode leitet damit einen Interrupt an den 8080-Emulator weiter. Wenn der Timer im Maschinencode abläuft, rufe ich GenerateInterrupt auf:
while (!done) { Emulate8080Op(state); if ( time() - lastInterrupt > 1.0/60.0)
Es gibt einige Details darüber, wie der 8080 tatsächlich Interrupts verarbeitet, die wir nicht emulieren werden. Ich glaube, dass eine solche Verarbeitung für unsere Zwecke ausreichen wird.