Heute habe ich die Addressierungsarten der CPU und eine Opcode-Tabelle implementiert. In der Tabelle sind alle Opcodes mit Namen, Länge in Bytes und Dauer in CPU Zyklen hinterlegt. Mit dieser Tabelle konnte ich in der Statusseite einen einfachen Disassembler einfügen.
Adressierungsarten
Die Addressierungsarten geben die Art und Weise an, wie die Operanden zu einem Befehl ermittelt werden. Man kann zum Beispiel den Operanden durch direkte Angabe übergeben, oder durch einen Verweis auf eine Speicheradresse. Die jeweils zu nutzende Adressierungsart wird durch den Opcode der Anweisung festgelegt. Das ist der Grund, warum es einen Befehl im Befehlssatz sehr oft geben kann.
Im Speicher werden der Befehl (1 Byte) und die Operanden (0-2 Bytes) nacheinander abgelegt. Je nach Adressierungsart benötigt ein Befehl daher 1-3 Bytes. Die Werte, die als Argumente für den Befehl verwendet werden sollen, werden anhand der Adressierungsart ermittelt.
Die CPU im C64 beherrscht die folgenden 13 Adressierungsarten:
- Accumulator
- Implied
- Immediate
- Relative
- Absolute
- Absolute Indexed by X
- Absolute Indexed by Y
- Absolute Indirect
- Zero Page
- Zero Page Indexed by X
- Zero Page Indexed by Y
- Zero Page Indirect Y-Indexed
- Zero Page X-Indexed Indirect
Accumulator
Bei allen Befehlen, bei denen der Akkumulator als Operand implizit genutzt wird, spricht man vom Accumulator Addressing. Ein Beispiel ist der LSR (Logical Shift Right) Befehl.
Bei der Implementierung muss keine Adresse errechnet werden, da die Quelle und Ziel bereits feststeht.
Implied
Hierbei steht das Ziel bereits fest und muss nicht als Adresse angegeben werden. Beispiele sind RTS (Return from Subroutine) oder TSX (Transfer Status-Register into X-Register).
Bei der Implementierung muss keine Adresse berechnet werden.
Immediate
Bei dieser Addressierungsart, ist der Operand ein fester Wert und keine Speicheradresse. Der Befehl LDA #$42 lädt den Wert $42 in den Akkumulator.
Da der Operand direkt um Anschluss an den Befehl im Speicher steht, wird auch hier keine Adresse berechnet.
Relative
Relative Adressierung wird von Branch-Instruktionen verwendet. Bespiele sind BNE, BEQ usw. Dabei wird ein 8-Bit Offset werdet, wobei das 7. Bit als Vorzeichen dient. Dies ermöglicht Sprünge vorwärts (positiver Wert) und rückwärts (negativer Wert) vom aktuellen Wert des Program Counters. Die Branch-Instruktionen werden für kurze Sprünge verwendet, da sie schneller als JMP Befehle sind.
Absolute
Hier wird aus den zwei folgenden Bytes eine 16 Bit Adresse erzeugt und als Quelle oder Ziel des Befehls verwendet.
Beispiel: Der Befehl LDA $1234 lädt den Akkumulator mit dem Wert aus der Speicheradresse $1234. Dabei enthält der Speicher nach dem Befehl die Werte $34 und dann $12. Um die effektive Adresse zu ermitteln, wird $12 mit 256 multipliziert (die Bits 8-mal nach links schieben) und dann $34 addiert.
Absolute Indexed by X
Bei dieser Adressierungsart werden wir bei der absoluten Adressierung aus den zwei folgenden Bytes eine 16 Bit Adresse erzeugt. Zu dieser Adresse wird der Inhalt des X-Registers addiert und das Ergebnis als effektive Adresse verwendet.
Beispiel: Wenn das X-Register $05 enthält, dann lädt der Befehl LDA $1234,X lädt den Akkumulator mit dem Wert aus der Speicheradresse $1239.
Absolute Indexed by Y
Diese Adressierungsart macht das gleiche wie "Absolute Indexed by X", nur das das Y-Register addiert wird.
Absolute Indirect
Diese Adressierungsart wird von dem JMP Befehl verwendet. Es werden die zwei der Instruktion folgenden Bytes gelesen und als 16-Bit Adresse interpretiert. Der Inhalt des Speichers an der Adresse und der folgenden werden dann als neue Zieladresse verwendet.
Beispiel: JMP ($FFFC) => RESET Routine in der Standard Commodore Sprungtabelle im ROM
- Der Befehl liest den Wert an der Speicherstelle $FFFC ($E2) gefolgt von dem Wert in $FFFD ($FC).
- Wegen der Little-Endian Byte-Reihenfolge ergibt sich die Zieladresse $FCE2.
- Die RESET-Routine an Adresse $FCE2 wird ausgeführt. Diese kann bei einem anderen Rechner an einer anderen Stelle stehen, solange in der Sprungtabelle die korrekte Adresse hinterlegt ist.
Zero Page
Diese Adressierungsart verhält sich wie die Absolute Adressierung, mit dem Unterschied, dass die oberen 8 Bit der Adresse immer 0x00 sind. Dadurch werden die Befehle kürzer und schneller.
Zero Page Indexed by X
Das Verhalten ist das gleiche wie bei der Zero Page Adressierung, nur das X-Register wird zur Adresse addiert.
Zero Page Indexed by Y
Das Verhalten ist das gleiche wie bei der Zero Page Adressierung, nur das Y-Register wird zur Adresse addiert.
Zero Page Indirect Y-Indexed
Die Zero Page Indirect Y-Indexed Adressierung ist eine sehr beliebte Lösung, um jeden Ort im C64-Speicher mit nur zwei Bytes zu adressieren. Die Idee dahinter ist, dass zwei Speicherstellen im Zero Page Bereich verwendet werden, um eine 16-Bit-Adresse zu speichern. Die Adresse mit dem niederwertigen Byte (LSB) wird in Verbindung mit dem Y-Register verwendet, um jeden Speicherort im C64-Speicher zu adressieren.
Beispiel: Der Speicherbereich ab $0400 soll geleert werden. Dazu legt man den niederwertigen Wert ($00) in $00fb ab und den höherwertigen Wert ($04) in $00fc.
Jetzt kann der Befehl STA ($fb),Y dazu genutzt werden, um die ersten 256 Stellen von $0400 bis $04ff mit einem Zwei-Byte-Befehl zu adressieren und mit dem Inhalt des Akkumulators zu füllen.
Sobald Y wieder 0x00 enthält, muss nur der Wert in $00fc erhöht werden, und die Schleife von neuem durchlaufen werden, um den Bereich von $0500 bis $05ff zu adressieren.
Zero Page X-Indexed Indirect
Zero Page X-Index Indirect Adressierung wird selten verwendet. Hier wird der Wert im X-Register zur gegebenen 16-Bit Zero Page Adresse addiert.
Beispiel: Das X-Register enthält den Wert $01. Der Befehl LDA ($fb, x) zeigt auf den $00fc und $00fd. Die darin enthaltenen Werte bilden die effektive Adresse, die gelesen wird. X wird zu dem niederwertigen Wert ($fb) der hinzugefügt.
Implementierung
Der Code enthält eine neue Klasse AddressingMode, um die Logik der Adressierungsmodi auszulagern.
Der Modus wird über die Konstanten am Anfang der Klasse festgelegt. Diese werden auch in der OpcodeTable Klasse verwendet, in der alle Opcodes erfasst sind.
Mittels der Opcode-Tabelle wird am Anfang der CPU-Initialisierung ein Array mit 256 Werten erstellt, das alle Opcodes enthält. Damit kann man jeden Opcode per Index des Arrays erreichen. Nicht vorhandene Opcodes werden mit einem Dummy initialisiert.
// build the final opcode array from the OpcodeTable data.
for ( let i = 0; i < OpcodeTable.opcodes.length; i++ ) {
this.opcodes[OpcodeTable.opcodes[i].opcode] = OpcodeTable.opcodes[i];
}
// fill non existing opcodes
for ( let i = 0; i < this.opcodes.length; i++ ) {
if ( this.opcodes[i] == undefined ) {
this.opcodes[i] = { opcode : undefined, label : '', length : 1, cycles : 0, mode : AddressingMode.IMPLIED }
}
}
Beispielhaft sind bereits die Befehle LDA, LDX, LDY, NOP, STA, STX und STY in der CPU Klasse implementiert. Es müssen jedoch noch Tests geschrieben werden, die die Funktion verifizieren.
In der CPUView Klasse wurde die Funktion writeDisassemblerContent hinzugefügt. Diese gibt den Speicherinhalt ab der Adresse address in lesbaren Assemblercode zurück. Mittels der Anzahl der Zeilen kann bestimmt werden, wie viele Befehle zurückgeliefert werden.
/**
* returns a string with the disassembled memory contents from the given address.
*
* @param address - starting address
* @param memory - memory array to read from
* @param lines - lines to print
*/
private writeDisassemblerContent(address : number, memory : Uint8Array, lines : number) : string
Das Ergebnis für diesen Teil befindet sich in meinem Gitlab-Projekt TSC64Emu. Die ausführbare Applikation ist hier erreichbar.
Ausblick
Im folgenden Teil beginnt die langwierige Arbeit für jeden CPU Befehl inklusive zugehörigen Tests zu implementieren.
Um den Überblick bei der Implementierung zu behalten, teile ich die Tests in verschiedene Gruppen auf, die die Instruktionen thematisch zusammenfassen.
- Move Commands (Laden und Speichern von Daten)
- Arithmetic Commands (Rechenfunktionen, Vergleichsfunktion)
- Logical Commands (Binäroperationen AND, OR usw.)
- Jump Commands (Instruktionen die den Program Counter verändern)
- Flag Commands (Befehle die das Status-Register betreffen)