Teile
eins und
zwei .
8080 Prozessor Emulator
Emulator-Shell
Sie sollten nun über alle erforderlichen Kenntnisse verfügen, um mit der Erstellung eines 8080-Prozessoremulators zu beginnen.
Ich werde versuchen, meinen Code so klar wie möglich zu machen, jeder Opcode wird separat implementiert. Wenn Sie sich damit vertraut gemacht haben, möchten Sie es möglicherweise neu schreiben, um die Leistung zu optimieren oder Code wiederzuverwenden.Zunächst werde ich eine Speicherstruktur erstellen, die Felder für alles enthält, was mir beim Schreiben eines Disassemblers notwendig erschien. Es wird auch einen Platz für einen Speicherpuffer geben, der RAM sein wird.
typedef struct ConditionCodes { uint8_t z:1; uint8_t s:1; uint8_t p:1; uint8_t cy:1; uint8_t ac:1; uint8_t pad:3; } ConditionCodes; typedef struct State8080 { uint8_t a; uint8_t b; uint8_t c; uint8_t d; uint8_t e; uint8_t h; uint8_t l; uint16_t sp; uint16_t pc; uint8_t *memory; struct ConditionCodes cc; uint8_t int_enable; } State8080;
Erstellen Sie nun eine Prozedur mit einem Fehleraufruf, die das Programm mit einem Fehler beendet. Es wird ungefähr so aussehen:
void UnimplementedInstruction(State8080* state) {
Lassen Sie uns einige Opcodes implementieren.
void Emulate8080Op(State8080* state) { unsigned char *opcode = &state->memory[state->pc]; switch(*opcode) { case 0x00: break;
So. Für jeden Opcode ändern wir den Status und den Speicher, wie es ein auf einem echten 8080 ausgeführter Befehl tun würde.
Der 8080 hat ungefähr 7 Typen, je nachdem, wie Sie sie klassifizieren:
- Datenübertragung
- Arithmetik
- Logisch
- Zweige
- Stapel
- Eingabe-Ausgabe
- Besonderes
Schauen wir uns jeden einzeln an.
Rechengruppe
Arithmetische Anweisungen sind viele der 256 Opcodes des 8080-Prozessors, die verschiedene Arten der Addition und Subtraktion enthalten. Die meisten arithmetischen Anweisungen arbeiten mit Register A und speichern das Ergebnis in A. (Register A wird auch als Akkumulator bezeichnet).
Es ist interessant festzustellen, dass diese Befehle Bedingungscodes beeinflussen. Statuscodes (auch Flags genannt) werden abhängig vom Ergebnis des ausgeführten Befehls gesetzt. Nicht alle Befehle wirken sich auf Flags aus, und nicht alle Teams, die sich auf Flags auswirken, wirken sich auf alle Flags gleichzeitig aus.
Flaggen 8080
In einem 8080-Prozessor werden Flags als Z, S, P, CY und AC bezeichnet.
- Z (Null, Null) nimmt den Wert 1 an, wenn das Ergebnis Null ist
- S (Vorzeichen) nimmt den Wert 1 an, wenn Bit 7 (das höchstwertige Bit, das höchstwertige Bit, MSB) des mathematischen Befehls gegeben ist
- P (Parität, Parität) wird gesetzt, wenn das Ergebnis gerade ist, und wird zurückgesetzt, wenn es ungerade ist
- CY (Übertrag) nimmt den Wert 1 an, wenn als Ergebnis des Befehls eine Übertragung oder Ausleihe in ein höherwertiges Bit durchgeführt wird
- AC (Auxillary Carry) wird hauptsächlich für BCD-Mathematik (Binary Coded Decimal) verwendet. Weitere Informationen finden Sie im Handbuch. In Space Invaders wird dieses Flag nicht verwendet.
Statuscodes werden in bedingten Verzweigungsbefehlen verwendet. Beispielsweise führt JZ eine Verzweigung nur durch, wenn das Z-Flag gesetzt ist.
Die meisten Anweisungen haben drei Formen: für Register, für unmittelbare Werte und für den Speicher. Lassen Sie uns ein paar Anweisungen implementieren, um ihre Formulare zu verstehen und zu sehen, wie die Arbeit mit Statuscodes aussieht. (Beachten Sie, dass ich das Hilfsübertragungsflag nicht implementiere, da es nicht verwendet wird. Wenn ich es implementiert habe, konnte ich es nicht testen.)
Anmeldeformular
Hier ist eine beispielhafte Implementierung von zwei Anweisungen mit einem Registerformular; Im ersten Fall habe ich den Code bereitgestellt, um seine Arbeit verständlicher zu machen, und im zweiten Fall wird eine kompaktere Form vorgestellt, die dasselbe tut.
case 0x80:
Ich emuliere 8-Bit-Mathematikbefehle mit einer 16-Bit-Zahl. Dies erleichtert es, Fälle zu verfolgen, in denen die Berechnungen einen Übertrag erzeugen.
Formular für unmittelbare Werte
Die Form für unmittelbare Werte ist fast dieselbe, außer dass das Byte nach dem Befehl die Quelle des hinzugefügten ist. Da "opcode" ein Zeiger auf den aktuellen Befehl im Speicher ist, ist opcode [1] sofort das nächste Byte.
case 0xC6:
Form für die Erinnerung
In der Speicherform wird ein Byte hinzugefügt, zu dem die in einem Paar von HL-Registern gespeicherte Adresse angibt.
case 0x86:
Anmerkungen
Die übrigen arithmetischen Anweisungen werden auf ähnliche Weise implementiert. Ergänzungen:
- In verschiedenen Versionen mit Übertrag (ADC, ACI, SBB, SUI) verwenden wir gemäß dem Referenzhandbuch Übertragsbits in den Berechnungen.
- INX und DCX wirken sich auf Registerpaare aus, diese Befehle wirken sich nicht auf Flags aus.
- DAD ist ein weiterer Befehl eines Registerpaares, der nur das Übertragsflag betrifft
- INR und DCR beeinflussen das Übertragsflag nicht
Zweiggruppe
Nachdem Sie sich mit den Statuscodes befasst haben, wird die Zweigstellengruppe für Sie klar genug. Es gibt zwei Arten der Verzweigung: Übergänge (JMP) und Aufrufe (CALL). JMP setzt den PC nur auf den Wert des Sprungziels. CALL wird für Routinen verwendet, schreibt die Rücksprungadresse in den Stapel und weist dem PC dann die Zieladresse zu. RET kehrt von CALL zurück, empfängt die Adresse vom Stapel und schreibt sie auf den PC.
Sowohl JMP als auch CALL gehen nur zu absoluten Adressen, die nach dem Opcode in Bytes codiert sind.
Jmp
Der JMP-Befehl verzweigt bedingungslos zur Zieladresse. Es gibt auch bedingte Verzweigungsbefehle für alle Statuscodes (außer AC):
- JNZ und JZ für Null
- JNC und JC für die Migration
- JPO und JPE für Parität
- JP (Plus) und JM (Minus) für das Zeichen
Hier ist eine Implementierung einiger von ihnen:
case 0xc2:
CALL und RET
CALL schiebt die Adresse des Befehls nach dem Aufruf auf den Stapel und springt dann zur Zieladresse. RET empfängt die Adresse vom Stapel und speichert sie auf dem PC. Bedingte Versionen von CALL und RET existieren für alle Zustände.
- CZ, CNZ, RZ, RNZ für Null
- CNC, CC, RNC, RC zur Übertragung
- CPO, CPE, RPO, RPE für Parität
- CP, CM, RP, RM für Zeichen
case 0xcd:
Anmerkungen
- Der PCHL-Befehl springt bedingungslos zu einer Adresse in einem Paar von HL-Registern.
- Ich habe das zuvor diskutierte RST nicht in diese Gruppe aufgenommen. Es schreibt die Rücksprungadresse in den Stapel und springt dann zu der vordefinierten Adresse am unteren Rand des Speichers.
Logische Gruppe
Diese Gruppe führt logische Operationen aus (siehe den
ersten Beitrag des Tutorials). Aufgrund ihrer Natur ähneln sie einer arithmetischen Gruppe darin, dass die meisten Operationen mit Register A (Laufwerk) arbeiten und die meisten Operationen Flags beeinflussen. Alle Operationen werden mit 8-Bit-Werten ausgeführt. In dieser Gruppe gibt es keine Befehle, die Registerpaare betreffen.
Boolesche Operationen
UND, ODER, NICHT (CMP) und "exklusiv oder" (XOR) werden als Boolesche Operationen bezeichnet. ODER und UND habe ich früher erklärt. Der NOT-Befehl (für den 8080-Prozessor heißt er CMA oder Komplementakkumulator) ändert einfach die Bitwerte - alle Einheiten werden zu Nullen und Nullen werden zu Einsen.
Ich nehme XOR als „Differenzerkenner“ wahr. Ihre Wahrheitstabelle sieht so aus:
AND, OR und XOR haben eine Form für Register, Speicher und unmittelbare Werte. (CMP hat nur einen Befehl, bei dem zwischen Groß- und Kleinschreibung unterschieden wird). Hier ist eine Implementierung eines Paares von Opcodes:
case 0x2F:
Zyklische Schaltbefehle
Diese Befehle ändern die Reihenfolge der Bits in den Registern. Eine Verschiebung nach rechts verschiebt sie um ein Bit nach rechts und eine Verschiebung nach links - um ein Bit nach links:
(0b00010000) = 0b00001000
(0b00000001) = 0b00000010
Sie scheinen wertlos zu sein, aber in Wirklichkeit ist dies nicht so. Sie können verwendet werden, um durch Zweierpotenzen zu multiplizieren und zu teilen. Nehmen Sie als Beispiel die Linksverschiebung.
0b00000001
ist dezimal 1, und
0b00000001
es nach links verschieben, wird es
0b00000010
,
0b00000010
dezimal 2. Wenn wir eine weitere Verschiebung nach links durchführen, erhalten wir
0b00000100
,
0b00000100
4. Eine weitere Verschiebung nach links, und wir multiplizieren mit 8. Dies funktioniert mit jedem durch Zahlen: 5 (
0b00000101
) ergibt bei
0b00001010
nach links 10 (
0b00001010
). Eine weitere Verschiebung nach links ergibt 20 (
0b00010100
). Eine Verschiebung nach rechts bewirkt dasselbe, jedoch zur Teilung.
Der 8080 verfügt nicht über einen Multiplikationsbefehl, kann jedoch mit diesen Befehlen implementiert werden. Wenn Sie verstehen, wie das geht, erhalten Sie Bonuspunkte. Einmal wurde mir eine solche Frage bei einem Interview gestellt. (Ich habe es getan, obwohl ich ein paar Minuten gebraucht habe.)
Diese Befehle drehen den Antrieb zyklisch und wirken sich nur auf das Übertragsflag aus. Hier sind einige Befehle:
case 0x0f:
Vergleich
Die Aufgabe von CMP und CPI besteht nur darin, Flags (zum Verzweigen) zu setzen. Sie tun dies, indem sie Flags subtrahieren, aber das Ergebnis nicht speichern.
- Gleichermaßen: Wenn zwei Zahlen gleich sind, wird das Z-Flag gesetzt, da ihre Subtraktion voneinander Null ergibt.
- Größer als: Wenn A größer als der zu vergleichende Wert ist, wird das CY-Flag gelöscht (da die Subtraktion ohne Ausleihen erfolgen kann).
- Kleiner: Wenn A kleiner als der Vergleichswert ist, wird das CY-Flag gesetzt (da A die Ausleihe abschließen muss, um die Subtraktion abzuschließen).
Es gibt Versionen dieser Befehle für Register, Speicher und Sofortwerte. Die Implementierung ist eine einfache Subtraktion, ohne das Ergebnis zu speichern:
case 0xfe:
CMC und STC
Sie vervollständigen die logische Gruppe. Sie werden verwendet, um das Übertragsflag zu setzen und zu löschen.
Gruppe von Eingabe-Ausgabe- und Spezialbefehlen
Diese Befehle können keiner anderen Kategorie zugewiesen werden. Ich werde sie der Vollständigkeit halber erwähnen, aber es scheint mir, dass wir wieder zu ihnen zurückkehren müssen, wenn wir beginnen, die Hardware von Space Invaders zu emulieren.
- EI und DI aktivieren oder deaktivieren die Fähigkeit des Prozessors, Interrupts zu verarbeiten. Ich habe das Interrupt_enabled-Flag zur Prozessorstatusstruktur hinzugefügt und es mit diesen Befehlen gesetzt / zurückgesetzt.
- Es scheint, dass RIM und SIM hauptsächlich für serielle E / A verwendet werden. Wenn Sie interessiert sind, können Sie das Handbuch lesen, aber diese Befehle werden in Space Invaders nicht verwendet. Ich werde sie nicht emulieren.
- HLT ist ein Stopp. Ich glaube nicht, dass wir es emulieren müssen, aber Sie können Ihren Beendigungscode (oder Beendigungscode (0)) aufrufen, wenn Sie diesen Befehl sehen.
- IN und OUT sind Befehle, mit denen das 8080-Prozessorgerät mit externen Geräten kommuniziert. Während wir sie implementieren, werden sie nichts anderes tun, als ihr Datenbyte zu überspringen. (Später werden wir zu ihnen zurückkehren).
- NOP ist "keine Operation". Eine Anwendung von NOP besteht darin, das Timing des Bedienfelds zu steuern (die Ausführung dauert vier CPU-Zyklen).
Eine weitere Anwendung von NOP ist die Codemodifikation. Angenommen, wir müssen den ROM-Code des Spiels ändern. Wir können nicht einfach unnötige Opcodes löschen, da wir nicht alle CALL- und JMP-Befehle ändern möchten (sie sind falsch, wenn mindestens ein Teil des Codes verschoben wird). Mit NOP können wir den Code loswerden.
Das Hinzufügen von Code ist viel schwieriger! Sie können es hinzufügen, indem Sie irgendwo im ROM Speicherplatz suchen und den Befehl in JMP ändern.Stapelgruppe
Wir haben die Mechanik für die meisten Teams in der Stapelgruppe bereits abgeschlossen. Wenn Sie die Arbeit mit mir gemacht haben, sind diese Befehle einfach zu implementieren.
PUSH und POP
PUSH und POP funktionieren nur mit Registerpaaren. PUSH schreibt ein Registerpaar in den Stapel, und POP nimmt 2 Bytes vom oberen Rand des Stapels und schreibt sie in ein Registerpaar.
Es gibt vier Opcodes für PUSH und POP, einen für jedes der Paare: BC, DE, HL und PSW. PSW ist ein spezielles Paar von Laufwerksflagregistern und Statuscodes. Hier ist meine Implementierung von PUSH und POP für BC und PSW. Es gibt keine Kommentare darin - ich denke nicht, dass es hier etwas besonders Kniffliges gibt.
case 0xc1:
SPHL und XTHL
Es gibt zwei weitere Teams in der Stapelgruppe - SPHL und XTHL.
SPHL
verschiebt HL zu SP (wodurch SP gezwungen wird, eine neue Adresse zu erhalten).XTHL
tauscht das, was sich oben auf dem Stapel befindet, mit dem, was sich in einem Paar HL-Registern befindet. Warum sollten Sie das tun müssen? Ich weiß nicht.
Ein bisschen mehr über Binärzahlen
Wenn Sie ein Computerprogramm schreiben, müssen Sie unter anderem die Art der Daten auswählen, die für die Zahlen verwendet werden - ob sie negativ sein sollen und wie groß sie maximal sein sollen. Für den CPU-Emulator benötigen wir den Datentyp, der mit dem Datentyp der Ziel-CPU übereinstimmt.
Signiert und nicht signiert
Als wir über Hex-Zahlen sprachen, betrachteten wir sie als vorzeichenlos - das heißt, jede Binärziffer der Hexadezimalzahl hatte einen positiven Wert und jede wurde als Zweierpotenz (Einheiten, zwei, vier usw.) betrachtet.
Wir haben uns mit der Frage der Computerspeicherung negativer Zahlen befasst. Wenn Sie wissen, dass die betreffenden Daten ein Vorzeichen haben, dh negativ sein können, können Sie eine negative Zahl am höchstwertigen Bit der Zahl (höchstwertiges Bit, MSB) erkennen. Wenn die Datengröße ein Byte beträgt, ist jede Zahl mit einem bestimmten MSB-Bitwert negativ und jede mit einem MSB von Null ist positiv.
Der Wert einer negativen Zahl wird als zusätzlicher Code gespeichert. Wenn wir eine vorzeichenbehaftete Zahl haben und das MSB gleich eins ist und wir herausfinden möchten, wie die Zahl lautet, können wir sie wie folgt konvertieren: Führen Sie ein binäres „NICHT“ für Hex-Zahlen aus und fügen Sie dann eine hinzu.
Für die Hex-Zahl 0x80 ist beispielsweise ein MSB-Bit gesetzt, dh es ist negativ. Das binäre "NICHT" der Zahl 0x80 ist 0x7f oder dezimal 127. 127 + 1 = 128. Das heißt, 0x80 in dezimal ist -128. Zweites Beispiel: 0xC5. Nicht (0xC5) = 0x3A = Dezimalzahl 58 +1 = Dezimalzahl 59. Das heißt, 0xC5 ist Dezimalzahl -59.
Was bei Zahlen mit zusätzlichem Code überrascht, ist, dass wir mit ihnen wie mit vorzeichenlosen Zahlen Berechnungen durchführen können und sie weiterhin
funktionieren . Der Computer muss mit Schildern nichts Besonderes tun. Ich werde einige Beispiele zeigen, die dies beweisen.
Beispiel 1
Dezimal-Hex-Binär
-3 0xFD 1111 1101
+ 10 0x0A +0000 1010
----- -----------
7 0x07 1 0000 0111
^ Dies wird im Übertragsbit aufgezeichnet
Beispiel 2
Dezimal-Hex-Binär
-59 0xC5 1100 0101
+ 33 0x21 +0010 0001
----- -----------
-26 0xE6 1110 0110
In Beispiel 1 sehen wir, dass das Addieren von 10 und -3 zu 7 führt. Das Additionsergebnis wurde übertragen, sodass das C-Flag gesetzt werden kann. In Beispiel 2 war das Additionsergebnis negativ, daher dekodieren wir dies: Nicht (0xE6) = 0x19 = 25 + 1 = 26.
0xE6 = -26
Explosion des Gehirns!
Wenn Sie möchten, lesen Sie mehr über den zusätzlichen Code auf
Wikipedia .
Datentypen
In C besteht eine Beziehung zwischen Datentypen und der Anzahl der für diesen Typ verwendeten Bytes. Tatsächlich interessieren wir uns nur für ganze Zahlen. Die Standard- / Old-School-C-Datentypen sind char, int und long sowie ihre Freunde char ohne Vorzeichen, int ohne Vorzeichen und long ohne Vorzeichen. Das Problem ist, dass diese Typen auf verschiedenen Plattformen und in verschiedenen Compilern unterschiedliche Größen haben können.
Daher ist es am besten, einen Datentyp für unsere Plattform auszuwählen, der die Größe der Daten explizit deklariert. Wenn Ihre Plattform stdint.h hat, können Sie int8_t, uint8_t usw. verwenden.
Die Größe einer Ganzzahl bestimmt die maximale Anzahl, die darin gespeichert werden kann. Bei vorzeichenlosen Ganzzahlen können Sie Zahlen von 0 bis 255 in 8 Bit speichern. Wenn Sie in hexadezimal übersetzen, ist dies von 0x00 bis 0xFF. Da 0xFF "alle Bits gesetzt" hat und der Dezimalzahl 255 entspricht, ist es völlig logisch, dass das Intervall einer vorzeichenlosen Einzelbyte-Ganzzahl 0-255 beträgt. Intervalle sagen uns, dass alle Größen von ganzen Zahlen genau gleich funktionieren - Zahlen entsprechen der Zahl, die erhalten wird, wenn alle Bits gesetzt sind.
Typ | Intervall | Hex |
---|
8-Bit ohne Vorzeichen | 0-255 | 0x0-0xFF |
8-Bit signiert | -128-127 | 0x80-0x7F |
16-Bit ohne Vorzeichen | 0-65535 | 0x0-0xFFFF |
16-Bit signiert | -32768-32767 | 0x8000-0x7FFF |
32-Bit ohne Vorzeichen | 0-4294967295 | 0x0-0xFFFFFFFFFF |
32-Bit signiert | -2147483648-2147483647 | 0x80000000-0x7FFFFFFF |
Noch interessanter ist, dass -1 in jedem vorzeichenbehafteten Datentyp eine Zahl ist, für die alle Bits gesetzt sind (0xFF für vorzeichenbehaftetes Byte, 0xFFFF für vorzeichenbehaftete 16-Bit-Zahl und 0xFFFFFFFF für vorzeichenbehaftete 32-Bit-Zahl). Wenn die Daten als vorzeichenlos betrachtet werden, wird für alle gegebenen Bits die maximal mögliche Anzahl für diesen Datentyp erhalten.
Um Prozessorregister zu emulieren, wählen wir den Datentyp aus, der der Größe dieses Registers entspricht. Es lohnt sich wahrscheinlich, standardmäßig nicht signierte Typen auszuwählen und zu konvertieren, wenn Sie sie als signiert betrachten müssen. Zum Beispiel verwenden wir den Datentyp uint8_t, um ein 8-Bit-Register darzustellen.
Hinweis: Verwenden Sie einen Debugger, um Datentypen zu konvertieren
Wenn gdb auf Ihrer Plattform installiert ist, ist es sehr praktisch, es für die Arbeit mit Binärzahlen zu verwenden. Im Folgenden werde ich ein Beispiel zeigen. In der unten gezeigten Sitzung sind Zeilen, die mit # beginnen, Kommentare, die ich später hinzugefügt habe.
# /c, gdb
(gdb) print /c 0xFD
$1 = -3 '?'
# /x, gdb hex
# "p" "print"
(gdb) p /c 0xA
$2 = 10 '\n'
# 2 " "
(gdb) p /c 0xC5
$3 = -59 '?'
(gdb) p /c 0xC5+0x21
$4 = -26 '?'
# print , gdb
(gdb) p 0x21
$9 = 33
# , gdb,
# ,
(gdb) p 0xc5
$5 = 197 #
(gdb) p /c 0xc5
$3 = -59 '?' #
(gdb) p 0xfd
$6 = 253
# ( 32- )
(gdb) p /x -3
$7 = 0xfffffffd
# 1
(gdb) print (char) 0xff
$1 = -1 '?'
# 1
(gdb) print (unsigned char) 0xff
$2 = 255 '?'
Wenn ich mit Hex-Zahlen arbeite, mache ich das immer in gdb - und das passiert fast jeden Tag. So viel einfacher als das Öffnen eines Programmierrechners mit einer grafischen Benutzeroberfläche. Öffnen Sie auf Linux- (und Mac OS X-) Computern zum Starten einer GDB-Sitzung einfach ein Terminal und geben Sie "GDB" ein. Wenn Sie Xcode unter OS X verwenden, können Sie nach dem Starten des Programms die Konsole in Xcode verwenden (die Konsole, an die die printf-Ausgabe ausgegeben wird).
Unter Windows ist der GDB-Debugger von Cygwin erhältlich.CPU Emulator Termination
Nachdem Sie all diese Informationen erhalten haben, sind Sie bereit für eine lange Reise. Sie müssen entscheiden, wie Sie den Emulator implementieren - entweder eine vollständige 8080-Emulation erstellen oder nur die Befehle implementieren, die zum Abschließen des Spiels erforderlich sind.Wenn Sie sich für eine vollständige Emulation entscheiden, benötigen Sie einige weitere Tools. Ich werde im nächsten Abschnitt darüber sprechen.Eine andere Möglichkeit besteht darin, nur die vom Spiel verwendeten Anweisungen zu emulieren. Wir werden weiterhin das riesige Switch-Konstrukt ausfüllen, das wir im Abschnitt Emulator Shell erstellt haben. Wir werden den folgenden Vorgang wiederholen, bis wir einen einzigen nicht realisierten Befehl haben:- Starten Sie den Emulator mit ROM Space Invaders
- Der Aufruf wird beendet,
UnimplementedInstruction()
wenn der Befehl nicht bereit ist - Emulieren Sie diese Anweisung
- Gehe zu 1
Das erste, was ich tat, als ich anfing, meinen Emulator zu schreiben, war, Code von meinem Disassembler hinzuzufügen. So konnte ich einen Befehl ausgeben, der wie folgt ausgeführt werden sollte: int Emulate8080Op(State8080* state) { unsigned char *opcode = &state->memory[state->pc]; Disassemble8080Op(state->memory, state->pc); switch (*opcode) { case 0x00:
Ich habe am Ende auch Code hinzugefügt, um alle Register und Statusflags anzuzeigen.Gute Nachrichten: Um in das Programm für 50.000 Teams einzutauchen, benötigen wir nur eine Teilmenge der 8080-Opcodes. Ich werde sogar eine Liste der Opcodes geben, die implementiert werden müssen:Opcode | Das Team |
---|
0x00 | Nein |
0x01 | LXI B, D16 |
0x05 | DCR B. |
0x06 | MVI B, D8 |
0x09 | Vater b |
0x0d | DCR C. |
0x0e | MVI C, D8 |
0x0f | Rrc |
0x11 | LXI D, D16 |
0x13 | Inx d |
0x19 | Papa d |
0x1a | LDAX D. |
0x21 | LXI H, D16 |
0x23 | Inx h |
0x26 | MVI H, D8 |
0x29 | Papa h |
0x31 | LXI SP, D16 |
0x32 | STA adr |
0x36 | MVI M, D8 |
0x3a | Lda adr |
0x3e | MVI A, D8 |
0x56 | MOV D, M. |
0x5e | MOV E, M. |
0x66 | MOV H, M. |
0x6f | MOV L, A. |
0x77 | MOV M, A. |
0x7a | MOV A, D. |
0x7b | MOV A, E. |
0x7c | MOV A, H. |
0x7e | MOV A, M. |
0xa7 | ANA A. |
0xaf | XRA A. |
0xc1 | Pop b |
0xc2 | Jnz adr |
0xc3 | Jmp adr |
0xc5 | DRÜCKEN B. |
0xc6 | ADI D8 |
0xc9 | Ret |
0xcd | Rufen Sie adr |
0xd1 | Pop d |
0xd3 | OUT D8 |
0xd5 | DRÜCKEN D. |
0xe1 | Pop h |
0xe5 | DRÜCKEN H. |
0xe6 | ANI D8 |
0xeb | Xchg |
0xf1 | POP PSW |
0xf5 | PSW DRÜCKEN |
0xfb | Ei |
0xfe | CPI D8 |
Dies sind nur 50 Anweisungen, und 10 davon sind Bewegungen, die trivial implementiert werden.Debuggen
Aber ich habe schlechte Nachrichten. Ihr Emulator wird mit ziemlicher Sicherheit nicht richtig funktionieren, und Fehler in einem solchen Code sind sehr schwer zu finden. Wenn Sie wissen, welcher Befehl sich schlecht verhält (z. B. ein Übergang oder ein Aufruf von bedeutungslosem Code), können Sie versuchen, den Fehler zu beheben, indem Sie Ihren Code untersuchen.Neben der Überprüfung des Codes gibt es noch eine andere Möglichkeit, das Problem zu beheben: Vergleichen Sie Ihren Emulator mit einem Emulator, der mit Sicherheit funktioniert. Wir gehen davon aus, dass ein anderer Emulator immer korrekt funktioniert und alle Unterschiede Fehler in Ihrem Emulator sind. Zum Beispiel können Sie meinen Emulator verwenden. Sie können sie manuell parallel ausführen. Sie können Zeit sparen, wenn Sie meinen Code in Ihr Projekt integrieren, um den folgenden Prozess zu erhalten:- Erstellen Sie einen Status für Ihren Emulator
- Erstelle einen Zustand für mich
- Für das nächste Team
- Rufen Sie Ihren Emulator mit Ihrem Status an
- Ich rufe meine mit meinem Vermögen an
- Vergleichen Sie unsere beiden Staaten
- Suche nach Fehlern in irgendwelchen Unterschieden
- gehe zu 3
Eine andere Möglichkeit besteht darin, diese Site manuell zu verwenden . Dies ist ein 8080 Javascript-Prozessor-Emulator, der sogar ROM Space Invaders enthält. Hier ist der Prozess:- Starten Sie die Space Invaders-Emulation neu, indem Sie auf die Schaltfläche Space Invaders klicken
- Drücken Sie die Taste „Run 1“, um den Befehl auszuführen.
- Wir führen den folgenden Befehl in unserem Emulator aus
- Vergleichen Sie den Prozessorstatus mit Ihrem
- Wenn die Bedingungen übereinstimmen, gehe zu 2
- Wenn die Bedingungen nicht übereinstimmen, ist Ihre Anweisungsemulation fehlerhaft. Korrigieren Sie es und beginnen Sie erneut mit Schritt 1.
Ich habe diese Methode am Anfang verwendet, um meinen 8080-Emulator zu debuggen. Ich werde nicht lügen - der Prozess kann langwierig sein. Infolgedessen stellten sich viele meiner Probleme als Tippfehler und Fehler beim Kopieren und Einfügen heraus, die nach der Erkennung sehr einfach zu beheben waren.Wenn Sie Ihren Code Schritt für Schritt ausführen, werden die meisten der ersten 30.000 Anweisungen in einem Zyklus von etwa $ 1a5f ausgeführt. Wenn Sie sich Javascript im Emulator ansehen , können Sie sehen, dass dieser Code Daten auf den Bildschirm kopiert. Ich bin sicher, dass dieser Code oft aufgerufen wird.Nach dem ersten Rendern des Bildschirms bleibt das Programm nach 50.000 Befehlen in dieser Endlosschleife stecken: 0ada LDA $20c0 0add ANA A 0ade JNZ $0ada
Es wartet, bis sich der Wert im Speicher bei $ 20c0 auf Null ändert. Da der Code in dieser Schleife $ 20c0 nicht genau ändert, muss es sich um ein Signal von einem anderen Ort handeln. Es ist Zeit, über die Emulation des „Eisens“ eines Arcade-Automaten zu sprechen.Bevor wir mit dem nächsten Abschnitt fortfahren, stellen Sie sicher, dass Ihr CPU-Emulator in diese Endlosschleife fällt.Als Referenz siehe meine Quellen .Volle 8080-Emulation
Eine Lektion, die mich viel gekostet hat: Implementieren Sie keine Teams, die Sie nicht testen können. Dies ist eine gute Faustregel für jede in der Entwicklung befindliche Software. Wenn Sie das Team nicht überprüfen, wird es definitiv kaputt sein. Und je weiter Sie sich von der Implementierung entfernen, desto schwieriger wird es, Probleme zu finden.Es gibt eine andere Lösung, wenn Sie einen vollständigen 8080-Emulator erstellen und sicherstellen möchten, dass er funktioniert. Ich habe einen Code für 8080 namens cpudiag.asm entdeckt, der zum Testen jedes 8080-Prozessorbefehls entwickelt wurde.Ich führe Sie aus mehreren Gründen nach dem ersten in diesen Prozess ein:- Ich wollte, dass die Beschreibung dieses Prozesses für einen anderen Prozessor wiederholt wird. Ich glaube nicht, dass das Analogon von cpudiag.asm für alle Prozessoren existiert.
- Wie Sie sehen können, ist der Prozess ziemlich mühsam. Ich denke, ein Anfänger beim Debuggen von Assembler-Code wird große Schwierigkeiten haben, wenn diese Schritte nicht aufgeführt sind.
So habe ich diesen Test mit meinem Emulator verwendet. Sie können es verwenden oder einen besseren Weg finden, um es zu integrieren.Baugruppe testen
Ich habe ein paar Dinge ausprobiert, aber als Ergebnis habe ich mich für diese schöne Seite entschieden . Ich habe den Text cpudiag.asm in den linken Bereich eingefügt und den Build ohne Probleme abgeschlossen. Ich habe eine Minute gebraucht, um herauszufinden, wie ich das Ergebnis herunterladen kann. Durch Klicken auf die Schaltfläche „Make Beautiful Code“ unten links habe ich eine Datei namens test.bin heruntergeladen, die den Code 8080 kompiliert. Ich konnte dies mit meinem Disassembler überprüfen.Laden Sie cpudiag.asm aus dem Spiegel auf meiner Website herunter.Laden Sie cpudiag.bin (kompilierter Code 8080) von meiner Website herunter .Laden Sie einen Test auf meinen Emulator hoch
Anstatt Invasoren zu laden. * Dateien lade ich diese Binärdatei.Hier ergeben sich kleine Schwierigkeiten. Erstens enthält der Quell-Assembler-Code eine Zeile, ORG 00100H
dh, die gesamte Datei wird unter der Annahme kompiliert, dass sich die erste Codezeile in 0x100 hex befindet. Ich hatte noch nie zuvor Code in Assembler 8080 geschrieben, daher wusste ich nicht, was diese Zeile bewirkt. Ich brauchte nur eine Minute, um herauszufinden, dass alle Zweigstellenadressen falsch waren und der Speicher bei 0x100 beginnen musste.Zweitens muss ich, da mein Emulator von vorne anfängt, zuerst zum echten Code übergehen. Nachdem ich den Hex-Wert an der Null-Adresse in den Speicher eingefügt hatte JMP $0100
, habe ich mich damit befasst. (Oder Sie können den PC einfach mit einem Wert von 0x100 initialisieren.)Drittens habe ich einen Fehler im kompilierten Code gefunden. Ich denke, der Grund ist die falsche Verarbeitung der letzten Codezeile STACK EQU TEMPP+256
, aber ich bin mir nicht sicher. Wie dem auch sei, der Stapel während der Kompilierung befand sich bei $ 6ad, und die ersten PUSH begannen, den Code neu zu schreiben. Ich schlug vor, dass die Variable wie der Rest des Codes auch um 0x100 versetzt werden sollte, also habe ich sie behoben, indem ich "0x7" in die Codezeile eingefügt habe, die den Stapelzeiger initialisiert.Da ich in meinem Emulator keine DAA- oder Zusatzmigration implementiert habe, ändere ich den Code, um diese Prüfung zu überspringen (wir überspringen sie nur mit JMP). ReadFileIntoMemoryAt(state, "/Users/kpmiller/Desktop/invaders/cpudiag.bin", 0x100);
Der Test versucht, eine Schlussfolgerung zu ziehen
Offensichtlich stützt sich dieser Test auf die Hilfe des CP / M-Betriebssystems. Ich fand heraus, dass CP / M einen Code für $ 0005 hat, der Nachrichten an die Konsole druckt, und änderte meine CALL-Emulation, um dieses Verhalten zu behandeln. Ich bin nicht sicher, ob alles richtig gelaufen ist, aber es hat für die beiden Nachrichten funktioniert, die das Programm zu drucken versucht. Meine CALL-Emulation zum Ausführen dieses Tests sieht folgendermaßen aus: case 0xcd:
Bei diesem Test habe ich mehrere Probleme in meinem Emulator gefunden. Ich bin mir nicht sicher, welcher von ihnen in das Spiel involviert sein würde, aber wenn sie es wären, wäre es sehr schwierig, sie zu finden.Ich habe alle Opcodes implementiert (mit Ausnahme von DAA und seinen Freunden). Ich habe 3-4 Stunden gebraucht, um Probleme in meinen Herausforderungen zu beheben und neue zu implementieren. Es war definitiv schneller als der oben beschriebene manuelle Prozess - bevor ich diesen Test fand, verbrachte ich mehr als 4 Stunden mit dem manuellen Prozess. Wenn Sie diese Erklärung herausfinden können, empfehle ich, diese Methode zu verwenden, anstatt sie manuell zu vergleichen. Die Kenntnis des manuellen Prozesses ist jedoch auch eine große Fähigkeit. Wenn Sie einen anderen Prozessor emulieren möchten, sollten Sie darauf zurückgreifen.Wenn Sie diesen Vorgang nicht ausführen können oder er zu kompliziert erscheint, lohnt es sich auf jeden Fall, den oben beschriebenen Ansatz mit zwei verschiedenen Emulatoren zu wählen, die in Ihrem Programm ausgeführt werden. Wenn mehrere Millionen Befehle im Programm erscheinen und Interrupts hinzugefügt werden, ist es unmöglich, zwei Emulatoren manuell zu vergleichen.