C64 Assembler Programmierung – Zeropage und Addressing Modes
Im letzten Teil habe ich eine kurze Einführung in die Syntax für die C64 Assembler Programmierung gegeben. Beschäftigt man sich näher mit der Materie, dann werden 2 Begriffe wichtig: die Zeropage und die unterschiedlichen Addressing Modes. Diese werden in der weiteren Folge erklärt.
C64 Assembler Programmierung – Zeropage und Addressing Modes
Eine zentralen Bedeutung bei der Programmierung auf dem Commodore 64 hat der 64 kByte große Hauptspeicher. Dieser wird wird für die gesamte Peripherie verwendet, beinhaltet so beispielsweise auch den Videospeicher (die Bildausgabe).
Page
Ein wichtiger Begriff im Zusammenhang mit dem Speicher im C64 ist die Page. Unter einer Page versteht man einen Block von 256 Byte. Eine Page sich durch eine 8-bit Adresse direkt ansprechen Beispielsweise $00 oder $A4. Bei $FF erreicht man die Page-Grenze. Erhöht man diesen Wert, springt man zurück auf das erste Byte $00 der Page. Das ist wichtig zu wissen bei der Implementierung von Schleifen. Zählvariable haben einen maximalen Wert von $FF (255).Der 64 kByte große Speicher wird in 256 Pages eingeteilt. Die ersten beiden Pages haben eine besondere Bedeutung:
- $0000-$00FF Zeropage
- $0100-$01FF Processor Stack
Zeropage
Unter dem Begriff Zeropage bezeichnet man die ersten 256 Bytes des Hauptspeichers, die erste Page. Wieso? Der Adressraum geht von Adresse $0000 bis $FFFF. Die ersten 256 Bytes können aber ohne dem zweiten Byte adressiert werden $0000 bis $00FF oder kurz: $00 bis $FF. Auf einem 8-bit System braucht man für die Adressierung einer 16-bit Adresse 2 CPU Zyklen. Die Zeropage kann mit einem einzigen 8-bit Wert adressiert werden, weshalb diese 256 Bytes besonders effizient sind. Als Programmierer legt man dort idealerweise Werte ab die man oft benötigt (lesen) und oft ändern (schreiben) muss.
Processor Stack
Der Processor Stack (Prozessorstapel) liegt beim C64 in der zweiten Page. Der 6510 Prozessor nutzt diese 256 Bytes als Zwischenspeicher. Gespeichert werden die Werte auf dem Stack im FILO verfahren, gefüllt wird der Speicherbereich von der höchsten zur niedrigsten Adresse (erster Wert: $01FF, zweiter Wert: $01FE, usw.). Der Stack Pointer zeigt immer auf die zuletzt befüllte Adresse. Manche Opcodes schreiben Werte in diesen Speicher und erhöhen den Stack Pointer (z.B. JSR), andere holen sich Werte und vermindern diesen (z.B. RTS). Ein kurzes Beispiel in Pseudocode:
JSR FUNKTION1
JSR (Jump Sub Routine) springt im Code an eine andere Stelle um eine Funktion auszuführen. Diese Funktion wird irgendwann beendet sein und man möchte zurück zur JSR Position um den danach folgenden Code auszuführen. Was passiert also? Beim JSR Aufruf schreibt der Prozessor die Rückspringadresse in den Prozessorstapel, der Stack Pointer rückt um eine Adresse weiter. Irgendwann ist die Funktion zu Ende und es wird
RTS
RTS (ReTurn from Sub Routine) aufgerufen. Über den Stack Pointer holt sich der Prozessor die zuletzt gespeicherte Adresse um dort mit der Ausführung des Codes fortzufahren. Der Stack Pointer wird um eine Adresse zurück gesetzt.
Program Counter
Der Program Counter (auch Programmzähler) ist das einzige 16-bit Register der 6510 CPU. Dieser zeigt immer auf den nächsten Befehl im Speicher, den die CPU ausführen soll.
Status Register
Die 6510 CPU hat ein 8-bit großes Statusregister. In dem Werten Ergebnisse von Kommandos abgespeichert. Es enthält folgende Flags:
- Carry-Flag
Wird bei einem Überlauf gesetzt. Beispielsweise wenn INX ausgeführt wird im X-Register aber bereits der höchste Wert $FF steht. Das Ergebnis wäre dann $00 im X-Register und ein Carry-Flag auf 1. - Zero-Flag
Wird immer gesetzt, wenn ein Ergebnis $00 ist. Wird beispielsweise vom Befehl BEQ abgefragt. - Interrupt-Flag
Bei gesetzten Bit verhindert man die Unterbrechungen durch Interrupts. - Decimal-Flag
Ist normalerweise 0 und sollte auch nicht verwendet werden. Wird es aktiviert, dann schaltet die CPU in den Dezimalmodus und hexadezimale Werte werden bei Berechnungen wie dezimale Werte verstanden. - Break-Flag
Wird für Softwareinterrupts verwendet. - Expansion Bit
Hat keine Bedeutung. - Overflow Flag
Hat eine ähnliche Funktion wie das Carry-Flag, jedoch für Befehle wie ADC, die das Carry-Flag verwenden und so einen Überlauf erzeugen. - Negative-Flag
Wird gesetzt, wenn bei einer Operation das hächste Bit gesetzt wird und gibt an ob die Zahl positiv oder negativ ist.
Addressing Modes
Es gibt eine Reihe unterschiedlicher Möglichkeiten den (aus heutiger Sicht) stark begrenzten Speicher des Commodore 64 anzusprechen. Unterschiedliche Opcodes unterstützen dabei unterschiedliche Modes.
- Implizite Adressierung
Das ist die einfachste aller Modes, es wird gar keine Adresse angegeben. Der Opcode gibt an, auf welchen Speicher zugegriffen wird. Beispiel: INX erhöht das X Register im eins. - Akkumulator Adressierung
Ist eigentlich ein Spezialfall der impliziten Adressierung und verändert den im Akkumulator gespeicherten Wert. Nur die Kommandos ASL, LSR, ROL und ROR verwenden diese. - Unmittelbare Adressierung
Dem Kommando wird ein konstanter Wert zugeordnet. Beispielsweise setzt LDA #$10 den Akkumulator auf den hexadezimalen Wert $10. Konstante Werte werden durch das # Symbol definiert. - Zeropage Adressierung
Gibt man nur ein Byte an, dann adressiert man absolut in der ersten Page, der Zeropage. Beispiel: LDA $10 setzt den Akkumulator auf den Wert der aktuell an der Adresse $0010 gespeichert ist. Man beachte den Unterschied zur unmittelbaren Adressierung! - Absolute Adressierung
Ist der allgemeine Fall zur Zeropage Adressierung bei der am die Adresse komplett mit 2 Byte angibt. Beispiel: LDA $A010 setzt den Akkumulator auf den Wert, der aktuell an der Adresse $A010 gespeichert ist. - Absolute indizierte Adressierung
Mit Hilfe eines Registers X oder Y lässt sich indiziert adressieren. Beispiel: LDA $A010,X setzt den Akkumulator auf den Wert, der aktuell an der Adresse $A012 gespeichert ist, sofern aktuell im X Register #$02 gespeichert ist. Diese Art der Adressierung funktioniert auch mit der Zeropage Adressierung mit nur einem Adressbyte. - Indirekte Adressierung
Bei der indirekten Adressierung steht auf der angegebenen Position im Speicher kein Wert, sondern eine Adresse. Das funktioniert nur mit dem Befehl JMP. Beispiel: JMP ($A010) die Codeausführung springt auf die Adresse die an Position $A010 und $A011 im Speicher steht (eine Adresse besteht aus 2 Bytes!). Gibt man eine Page-Grenze an, dann kommt das zweite Byte nicht von der nächsten Page, sondern vom Beginn der selben Page. Beispiel: JMP ($A0FF) erzeugt eine Sprungadresse aus $A0FF und $A000 (nicht $A100)! - Indirekte indizierte Adressierung
Mit Hilfe eines Registers X oder Y holt man die Adresse indiziert. Beispiel: LDA ($A010, X) Sofern das X Register den Wert $02 enthält lädt der Akkumulator den Wert der über die Adresse an $A012 und $A013 ermittelt wird. - Indirekte nachindizierte Adressierung
Funktioniert ähnlich wie die indirekte indizierte Adressierung, jedoch wird der Index erst auf die ermittelte Adresse angewendet. Beispiel: LDA ($A010), X Sofern das X Register den Wert $02 enthält wird die Adresse über $A010 und $A011 ermittelt und um 2 erhöht. Der Wert der dort steht wird in den Akkumulator geladen. - Relative Adressierung
Bei dieser speziellen Art der Adressierung geht es um die bedingten Sprungbefehle. Je nach dem Status eines Flags im Registers wird an zur einer Adressen gesprungen oder auch nicht. Beispiel: BEQ $A010 der Code wird an der Position $A010 weiter ausgeführt sofern das Zero-Flag im Statusregister gesetzt ist. Ist dieses 0, dann folgt der nächste Befehl jener Adresse auf die der Program Counter aktuell zeigt + 1.
Beispiel
Zur Demonstration habe ich das Beispiel des letzten Artikels um die absolute Adressierung erweitert. Es werden die Werte der Adressen $D020 und $D021 jeweils um eins erhöht, wobei ich aber die zweite Adresse umständlich über eine indizierte Adressierung mit dem Index aus dem X-Register anspreche. Das Ergebnis zeichnet sowohl den Rahmen als auch den Content-Bereich mit wechselnden Farben.
Fazit
Ein Artikel mit sehr viel Theorie. Die einzelnen Themen scheinen für sich oft kompliziert, sind aber die Summe aller Funktionen der 6510 CPU. Hat man die Themen einmal verstanden, dann kann man jedes Ergebnis jeder Operation nachvollziehen und somit die CPU vom Commodore 64 komplett nutzen und verstehen. Jetzt muss man sich nur noch überlegen was man implementieren möchte und wie man die Eigenheiten der 6510er CPU dafür optimal nutzen kann.