1. Einführung in die Assembler-Programmierung

 

1.1 Was genau ist Assembler?

 

Eins schon mal vorweg: Der C64 ist als BASIC-Lerncomputer konzipiert worden, und bietet deshalb von Haus aus keine Möglichkeit, Maschinenprogramme zu erstellen - also Programme, die der Prozessor direkt ausführen kann. Das Einzige, das Sie mit BASIC tun können, ist, die Bytes von Maschinenprogrammen einzeln in den Speicher zu schreiben - mittels READ und DATA. Anschließend können Sie diese Maschinenprogramme mit SYS ausführen. Dies ist dann auch das, was die Entwickler des C64 am Anfang taten: Sie erstellten die Prozessor-OP-Codes (also die Zahlenreihen, die der Prozessor direkt verstehen kann) für das Kernal per Hand und schrieben sie anschließend in einen ROM-Baustein, das Kernal-ROM. Auch der BASIC-Interpreter befindet sich in einem ROM-Baustein, und ist deshalb auch von Anfang an verfügbar. Der BASIC-Interpreter ist jedoch schon eine Weiterentwicklung des ursprünglich per Hand erstellten Kernals und wurde auf anderen Computern, nämlich auf PCs, entworfen. Im Gegensatz zum Kernal ist BASIC schon ausgereifter, und kann z.B. Klammern auflösen und Funktionen ausführen. An dieser Stelle würde ich Ihnen gerne verraten, wie BASIC genau arbeitet, aber dies rauszufinden, wird noch einige Zeit dauern.

Im Laufe der Zeit wurden jedoch immer mehr Anwendungen für den C64 entwickelt, darunter auch sogenannte Assembler-Monitore. Mit diesen Programmen können Sie Maschinensprache in einer Art einfachem Programmierdialekt erstellen, der zwar abstrakt, aber dennoch nah genug an der Maschine ist. Diesen einfachen Programmierdialekt nennt man nun Assembler. Im Endeffekt ist Assembler erst einmal nur ein Ersatz für die sehr abstrakten OP-Code-Zahlen, die der Prozessor direkt versteht. Anstatt z.B. das Zahlen-Tupel

 

160,01

 

zu benutzen, um die Zahl 1 in das Rechenregister A zu laden, schreibt man

 

LDA #1

 

LDA ist hier einfach die Abkürzung für „load accumulator“, und das Akkumulator-Register ist die Einheit im 6510-Prozessor, mit dem Sie Berechnungen ausführen können. Der 6510 kennt jedoch noch mehr Befehle, hinter denen sich auch verschiedene Zahlen verbergen, die den entsprechenden Befehl auslösen. Diese Zahlen nennt man OP-Codes, was die Abkürzung für „operation execution codes“ ist (also Codes, die ein bestimmtes Verhalten des Prozessors auslösen). Natürlich werden in der Assembler-Sprache auch für alle anderen OP-Code-Zahlen (außer 160) kurze Buchstabenfolgen benutzt. So können Sie auch in jedes der anderen Register außer A eine Zahl laden. Die Abkürzungen hierfür sind LDX (load index register X) und LDY (load index register Y). So lädt dann z.B.

 

LDX #1

 

den Wert 1 in das X-Register, und

 

LDY #1

 

den Wert 1 in das Y-Register.

 

Bleibt nur noch eine Frage übrig: Was ist ein Register? Die Antwort ist, dass ein Prozessor eine bestimmte Anzahl an internen Speicherstellen besitzt, auf die er auch sehr schnell zugreifen kann. Diese internen Speicherstellen können Sie z.B. dafür benutzen, Berechnungen oder logische Operationen auszuführen. Diese internen Speicherstellen nennt man Register. Die einzelnen Prozessor-Register haben spezielle Funktionen - auch beim 6510 ist dies so. So führen Sie mit dem Akkumulator-Register Additionen und Subtraktionen durch. Mit den Indexregistern X und Y dagegen können Sie mittels Zeigern auf Datenfelder zugreifen, was Sie allein mit dem Akkumulator nicht tun können. Assembler zu lernen, bedeutet also vor Allem, sich mit den verschiedenen Zugriffsarten auf den Hauptspeicher zu beschäftigen - man spricht hier auch von Adressierungsarten. Was eine Adresse ist, kennen Sie bereits aus dem BASIC-Kurs von der POKE-Funktion: Eine Adresse ist eine Zahl, die die Position eines Bytes im Hauptspeicher angibt. Mit BASIC können Sie nur eine einzige Art von Adressen erzeugen, nämlich absolute Adressen. Wenn Sie z.B. POKE den Wert 1024 und 1 übergeben, dann wird exakt in diese Adresse (also die Adresse 1024) der Wert 1 geschrieben. Sie könnten jetzt folgendes Programm schreiben:

 

10 Z=1024

20 POKE Z,1

 

Dies ändert jedoch nichts an der Tatsache, dass Sie POKE nach wie vor (durch die Auflösung der Variablen Z) den Wert 1024 für eine absolute Adresse übergeben. Wenn Sie Assembler lernen, dann müssen Sie jedoch mehrere Adressierungsarten beherrschen, nämlich die folgenden:

 

·       Die direkte, absolute Adressierung über 16-Bit-Adressen

·       Die direkte, absolute Adressierung mit zusätzlicher Indizierung über Index-Register

·       Die indirekte, Y-indizierte Adressierung über Zeiger

·       Die indirekte, X-indizierte Adressierung über Zeiger

 

Der Trick bei der indirekten Adressierung ist, dass die eigentliche Adresse, auf die Sie zugreifen, in einer anderen Adresse steht. Eine Speicherstelle kann also auf eine andere Speicherstelle verweisen, in der dann der eigentliche Wert steht, den Sie in den Akkumulator laden wollen. Natürlich gibt es auch von der indirekten Adressierung, die Sie hier anwenden, mehrere Varianten.

 

Ein guter Assembler-Monitor (man spricht an dieser Stelle auch häufig einfach nur von „Assembler“) unterstützt Sie an dieser Stelle, und bietet Ihnen unterschiedliche, einfache Schreibweisen an, durch die Sie schon beim Lesen Ihrer Programme direkt die Adressierungsart erkennen können. Assembler wie der freie Hypra-Ass-Assembler, den ich Ihnen auch zum Download anbiete, bieten aber noch viel mehr Funktionen, die Ihnen das Leben erleichtern. So kann der 6510 z.B. auch Sprungbefehle zu bestimmten Adressen ausführen. Damit Sie sich nicht dauernd Zahlen merken müssen, unterstützt Sie Hypra-Ass Labels. Ein Label ersetzt eine (Sprung-) Adresse durch ein Wort, und Sie müssen sich nur noch die Wörter merken. Diese Wörter sollten natürlich möglichst aussagekräftig sein, denn Label-Namen wie XYZ oder ABC sagen genau so wenig aus, wie Zahlenkolonnen. Sie können aber auch Konstanten definierten, sogenannte Equates. Auch dies erleichtert Ihnen das Leben enorm, weil Sie z.B. für die Startadresse des VIC schlicht V statt 53248 schreiben können. Aber nicht nur das: Hypra-Ass unterstützt sogar Konstrukte wie V+21, und kann sogar Bytes und Texte direkt in den Speicher schreiben. Im Laufe des Assembler-Kurses werde ich Ihnen die einzelnen Zusatz-Funktionen von Hypra-Ass näherbringen (Sie sollen ja keine OP-Codes in mühseliger Weise per Hand erstellen müssen, sondern eher wie ein Profi programmieren lernen).

 

1.2 Das erste Programm erstellen und übersetzen

 

Im Download-Bereich dieser Homepage befindet sich die D64-Datei Assembler-Kurs.D64, die Sie mit dem Transferprogramm TRANSDISK von Ihrem PC (mit Hilfe eines Arduino als Bindeglied zwischen PC und C64) auf Ihren C64 übertragen können (siehe dazu auch „Tipps und Tricks“ im BASIC-Kurs). Wahlweise können Sie das D64-Disketten-Image zusammen mit einem Emulator wie VICE benutzen, wenn Sie gerade keinen C64 zur Hand haben. Hypra-Ass wird ganz einfach mit

 

LOAD “HYPRA-ASS“,8

 

geladen, und anschließend mit

 

RUN

 

gestartet. Wundern Sie sich bitte nicht darüber, dass Hypra-Ass anschließend

 

BREAK IN 0

 

anzeigt. Hypra-Ass ersetzt nämlich BASIC teilweise, und Sie können deswegen Assembler-Programme wie BASIC-Programme eingeben. Allerdings führt RUN nun zur Übersetzung Ihres Assembler-Codes in Maschinensprache, und nicht mehr zum Start eines BASIC-Programms. An dieser Stelle möchte ich von vornherein mit einem Missverständnis aufräumen, das nämlich besagt, dass Assembler und Maschinensprache das gleiche sind. Dies ist nicht der Fall, denn im Endeffekt ist Maschinensprache das, was der Assembler am Ende generiert, nämlich eine Ansammlung von OP-Code-Zahlen und der zugehörigen Parameter-Bytes im Speicher. Diese und nur diese Programme kann man anschließend mit SYS aufrufen. SYS benötigt natürlich stets eine Speicheradresse, die Sie auch in Ihrem Assembler-Programm angeben müssen. Die meisten Maschinenprogramme, die Sie in BASIC einbinden können, starten an der Adresse 49152 und benutzen deshalb am Anfang die folgende Zeile:

 

.BA 49152

 

.BA (der Punkt muss mitgeschrieben werden) ist die Abkürzung von „base address“, was Basisadresse bedeutet.

 

Nun macht ein solches Programm noch nicht viel. Mehr noch: Ein solches Programm wird sogar höchstwahrscheinlich abstürzen. Der Grund ist, dass der Befehl SYS 49152 im Endeffekt nur den Assembler-Befehl

 

JSR 49152

 

imitiert. JSR ist die Abkürzung für „jump to subroutine“, also ein direkter Sprung an eine bestimmte Adresse. Wenn Sie Ihrem Programm nun nicht irgendwann sagen, dass es zum Aufrufer zurückkehren soll, dann läuft Ihr Programm ewig, bzw. Ihr Prozessor stößt irgendwann auf Datenmüll, der noch im Speicher steht. Was dann passiert, kann niemand vorhersagen, und genau deswegen müssen Sie Ihr Programm auch irgendwann mit dem folgenden Befehl zum Aufrufer zurückkehren lassen:

 

RTS

 

RTS ist die Abkürzung für „return from subroutine“, also einer Rückkehr zum Aufrufer (dieser muss aber wirklich vorher JSR und nicht z.B. JMP benutzt haben). Nun soll Ihr Programm auch etwas tun. Was nun für C das Hallo-Welt-Programm

 

printf(“Hallo Welt!\n“);

 

ist, ist beim C64 das A-Programm. Dieses A-Programm macht nichts Anderes, als ein großes A an die Adresse 1024 des Bildschirmspeichers zu schreiben. Leider können Sie in Assembler das A nicht direkt in den Speicher POKEn. An dieser Stelle sind wir nun auch schon bei dem ersten sehr wichtigen Punkt angekommen, nämlich bei dem Begriff Load/Store-Architektur. Der 6510-Prozessor ist natürlich eine solche: Load/Store bedeutet, dass Sie den Speicher nie direkt beschreiben können, sondern dass Sie stets den Zwischenweg über ein Register gehen müssen. Sie müssen also stets zunächst einen Wert in ein Register laden (load) und diesen anschließend im Speicher ablegen (store). Im Falle Ihres A-Zeichens gehen Sie den Weg über den Akkululator, in den Sie zunächst den Wert 1 laden müssen:

 

LDA #1

 

Erst jetzt können Sie diesen Wert an der Adresse 1024 ablegen:

 

STA 1024

 

STA ist die Abkürzung von „store accumulator“, in diesem Fall bekommt STA eine absolute Adresse als Parameter übergeben, an der der Inhalt des Akkumulators abgelegt werden soll. Wie Sie sehen, wechseln sich Lade- und Speichervorgänge stets ab, aber genau dies ist das Hauptmerkmal einer Load/Store-Architektur. Das gesamte A-Programm, das Sie auch von BASIC aus mit SYS 49152 aufrufen können, und das dann auch korrekt mit READY zurückkehrt, sieht so aus (bitte geben Sie das Programm genau so ein, wie es hier abgedruckt ist):

 

10 - .BA 49152

20 - LDA #1

30 - STA 1024

40 - RTS

RUN

SYS 49152

 

Wenn Sie alles korrekt machen, dann erkennen Sie, dass Hypra-Ass Ihren Code einrückt. Dies ist ein Zeichen für ein korrektes Verhalten, denn Sie können vor Ihre Anweisungen auch Label-Namen setzen. Dies erreichen Sie dadurch, dass Sie nach einer Zeilennummer nur ein einziges Leerzeichen, gefolgt von einem Minus-Zeichen setzen, aber hinter dem Minuszeichen selbst kein Leerzeichen lassen. Auf diese Weise können Sie 16 Zeichen für einen Label-Namen benutzen, der noch vor dem eigentlichen Assembler-Befehl steht. Die eigentlichen Befehle dagegen erscheinen stets um 18 Zeichen nach rechts verrückt - auch dies ist korrekt, denn Sie wollen wahrscheinlich nur vor bestimmte Zeilen, zu denen Sie unter Umständen auch springen wollen, Label-Namen setzen. In dem A-Beispiel benötigen Sie jedoch noch keine Labels, ich werde aber ab jetzt die Programmbeispiele trotzdem schon auf die folgende Weise formatieren (hier ist die erste Zeile wieder der Programmname, der nicht zum eigentlichen Listing gehört):

 

01-A

10    -                  .BA 49152

20    -                  LDA #1

30    -                  STA 1024

40    -                  RTS

 

1.3 Adressierungsarten

 

Viele Assembler-Bücher behandeln an dieser Stelle nun detailliert die einzelnen Register (A, X, Y, P, S, PC) und ihre Funktionen. Leider führt dies zu mehr Verwirrung, als es nützt, denn die Funktionen der einzelnen Register hängen sehr stark von der verwendeten Adressierungsart ab. Deshalb bekommen bei mir die Adressierungsarten Vorrang, und wenn Sie hiermit gut umgehen können, dann haben Sie am Ende nur noch sehr wenig Probleme mit der Assemblerprogrammierung. Ich wage an dieser Stelle sogar zu behaupten, dass Sie die einzelnen Adressierungsarten wirklich im Schlaf beherrschen sollten, und dass dies essentiell für die Programmierung auch anderer Prozessoren ist. Aber keine Angst: Es ist wirklich alles nicht so schwer, wie es scheint. Sie müssen sich nur fragen, ob es noch andere Wege gibt, die Speicherstelle 1024 zu adressieren. Sie könnten z.B. die Zahl 1024 in ein Lo- und Hi-Byte aufteilen, und in der Zeropage als Zahlen-Tupel der Form {0,4} ablegen (die Zeropage ist die Speicherseite 0, also die Adressen 0-255). Und genau dies können Sie auch tun, indem Sie die indirekte Adressierung benutzen. Wenn Sie z.B. das A-Beispiel durch indirekte Adressierung realisieren wollen, dann gehen Sie am besten wie folgt vor:

 

02-AINDIREKT

10    -                  .BA 49152

20    -                  LDA #0

30    -                  STA 248

40    -                  LDA #4

50    -                  STA 249

60    -                  LDY #0

70    -                  LDA #1

80    -                  STA (248),Y

90    -                  RTS

 

1.3.1 Y-indizierte indirekte Adressierung

 

In dem letzten Beispiel legen Sie die Zahl 1024 in Form von Lo-Byte (=0) und Hi-Byte(=4) als 16-Bit-Zeiger in den Adressen 248 und 249 ab. Im Y-Register muss noch ein Offset-Wert stehen (also ein Versatz), der zu der Zeiger-Adresse 1024 (=4*256+0) addiert wird. In dem letzten Beispiel ist der Offset nicht vorhanden, also wird das Y-Register mit dem Wert 0 geladen. Um nun den Wert 1 in die Zeiger-Adresse 1024 zu schreiben, die in den Speicherstellen 248 und 249 steht, verwenden Sie im letzten Beispiel die Y-indizierte Adressierungsart wie folgt:

 

STA (248),Y

 

Die Y-indizierte indirekte Adressierungsart erkennen Sie also an den Klammern um die Zahl 248 und der zusätzlichen Verwendung des Y-Registers. STA kann also (wie auch LDA) zusätzlich zu der absoluten Adressierung die indirekte Adressierung über das Y-Register benutzen. Bedenken Sie an dieser Stelle unbedingt, dass der Offset im Y-Register immer benutzt wird, und wenn dort in dem letzten Beispiel z.B. statt 0 der Wert 100 steht, wird die Adresse 1124, und nicht die Adresse 1024 benutzt. Ebenfalls wichtig ist, dass indirekte Zeigeradressen nur in der Zeropage stehen können. Folgendes ist also nicht erlaubt:

 

STA (2048),Y

 

Wichtig ist auch, dass die Indexregister nur 8 Bit breit sind, und die folgenden Anweisungen z.B. nicht automatisch auf die nächste Speicherseite führen (Y enthalte den Wert 255):

 

INY

STA (248),Y

 

Mit INY (Inkrement Y) können Sie zwar Y um 1 erhöhen, aber nach dem Wert 255 fängt die Zählung wieder bei 0 an. Sie landen also in dem letzten Beispiel wieder an der Adresse 1024, und nicht an der Adresse 1280, wie ursprünglich beabsichtigt. Dies gilt auch für DEY (Dekrement Y), deshalb landen Sie im nächsten Beispiel auch nicht an der Adresse 1023, wenn Y=0 ist:

 

DEY

STA (248),Y

 

1.3.2 X-indizierte indirekte Adressierung

 

Nun wissen Sie, was ein Zeiger ist: Ein Zeiger ist ein Konstrukt, bei dem der Inhalt einer Speicheradresse zusammen mit einem Indexregister dazu benutzt wird, eine neue Speicheradresse zu berechnen, auf die Sie danach zugreifen können. Zeiger werden auch von BASIC verwendet, um auf die Variablentabelle oder Strings zuzugreifen. Meistens wird hierfür die Y-indizierte indirekte Adressierung benutzt, aber manchmal müssen Sie auch z.B. ein Datenfeld benutzen, in dem mehrere Zeiger stehen. In C würde dies der folgenden Struktur entsprechen:

 

void *ZeigerArray[FeldGroesse];

 

Mit dem 6510-Assembler können Sie eine solche Struktur über die X-indizierte indirekte Adressierung erreichen. Angenommen, Sie wollen ein Zeiger-Array an der Adresse 192 ablegen, und in Ihrem Array 10 Zeiger ablegen. Da ein Zeiger 2 Bytes umfasst, geht Ihr Array bis zur Adresse 211. Angenommen, Sie wollen nun den 5. Zeiger auswählen, und ein Byte aus der Adresse laden, auf die der 5. Zeiger zeigt. In diesem Fall müssen Sie das X-Register in der folgenden Weise benutzen:

 

LDX #10

LDA (192,X)

 

Wie Sie sehen, wird bei der X-indizierten indirekten Adressierung zu der Basisadresse Ihres Zeiger-Arrays, die Sie in den runden Klammern angeben (also 192) zunächst der Wert des X-Registers addiert. Dies geschieht in diesem Fall noch bevor Sie die indirekte Adresse auflösen. Das heißt, dass Sie den eigentlichen Zeiger im letzten Beispiel aus den Adressen 202 und 203 holen, und das ist auch genau das, was Sie hier beabsichtigt hatten.

 

1.3.3 Absolute indizierte Adressierung

 

Absolute Adressen sind wie der Name schon sagt absolut, und keine Zeiger. Deshalb werden absolute Adressen auch von Ihrem Assembler direkt als Byte-Werte in Ihr späteres Programm eingesetzt. Nun können Sie auch absolute Adressen zusammen mit den Index-Registern X und Y benutzen, z.B. auf die folgende Weise:

 

03-ABINDIZIERT

10    -                  .BA 49152

20    -                  LDA #1

30    -                  LDX #0

40    -                  STA 1024,X

50    -                  LDA #2

60    -                  LDY #1

70    -                  STA 1024,Y

80    -                  RTS

 

Das letzte Beispielprogramm schreibt ein A an die erste Bildschirmposition, und ein B an die zweite. Das heißt, dass Sie hier als Basisadresse die absolute Adresse 1024 benutzen. Um Speicher zu sparen, gibt es nun zwei Varianten, nämlich einmal absolute Adressen, die 16-Bit-Adressen benutzen, und einmal absolute Adressen, die nur die Zeropage benutzen. Da Zeropage-Adressen statt 2 Bytes nur 1 Byte verwenden, verwenden intelligente Assembler wie Hypra-Ass automatisch immer dann Zeropage-Adressen, wenn die verwendete Adresse in der ersten Speicherseite liegt. Sie müssen sich also um den ganzen Zeropage-Kram normalerweise nicht kümmern.

 

1.4 Werte korrekt aus Speicheradressen lesen und korrekt in diesen ablegen

 

Mit LDA, LDX und LDY können Sie nicht nur Zahlen (sogenannte Immidiates) in ein Register laden, sondern auch aus einer Speicheradresse. Für STA, STX und STY gilt diese Aussage ebenfalls, Sie können also einen Registerinhalt auch im Speicher ablegen. Dies gilt sowohl für A, als auch für X und Y, das heißt, der Befehl

 

STX 1024

 

legt den Inhalt der X-Registers in der Adresse 1024 ab, und

 

STY 1024

 

legt den Inhalt des Y-Registers im Speicher ab. Allerdings beherrschen die Befehle LDX, LDY, STX und STY im Gegensatz zu LDA und STA nicht alle Adressierungsarten. Z.B. sind folgende Konstrukte nicht erlaubt:

 

LDX (248),Y

LDY (248,X)

LDX (248,X)

LDY (248),Y

LDX 1024,X

LDY 1024,Y

 

Sie können also nur dann indirekt indiziert adressieren, wenn das Zielregister der Akkumulator ist, und ein Indexregister kann auch nicht gleichzeitig Zielregister und Teil einer Adressabgabe sein. Außerdem können Sie nur mit dem Akkumulator Rechenoperationen oder logische Operationen ausführen, aber nicht mit den Indexregistern. Zum Glück gibt Ihr Assembler eine Fehlermeldung aus, wenn Sie versuchen, eine nicht erlaubte Adressierungsart zu benutzen.

 

1.5 Rechnen mit dem 6510

 

Im Gegensatz z.B. zu PC-Prozessoren beherrscht der 6510 nur die Addition und Subtraktion von 8-Bit-Zahlen ohne Komma. Wenn eine 8-Bit-Zahl überläuft, wird ein zusätzliches 9. Bit, das sogenannte Carry-Flag, benutzt. Eine Addition können Sie mit ADC (add with carry), eine Subtraktion mit SBC (substract with carry) ausführen. Das Carry-Flag können Sie explizit mit CLC (clear carry) löschen oder mit SEC (set carry) setzen. Zusätzlich gibt es ein Overflow-Flag, das immer dann auf 1 gesetzt wird, wenn bei einer vorzeichenbehafteten Operation wie SBC der zulässige Rechenbereich über- bzw. unterschritten wird. Dieser Bereich ist sehr beschränkt, wenn Sie auch zusätzlich negative Zahlen zulassen, bei denen das 8. Bit als Vorzeichenbit betrachtet wird. So wird z.B. schon bei einer Subtraktion von -127 und 1 das Overflow-Flag gesetzt, weil die Zahl -128 nicht mehr mit 8 Bits darstellbar ist. Allerdings werden negative Zahlen anders dargestellt, als positive Zahlen, nämlich wie folgt: Wenn Sie von 0 die Zahl 1 abziehen, dann läuft das Ergebnis über und Sie erhalten die Zahl 255. 255 wird nun als -1 betrachtet, 254 als -2 und so weiter. Die Zahl 128 ($80 Hex) wird dann auf die Zahl -128 abgebildet - aber eben nur, wenn Sie auch wirklich vorzeichenbehaftete Operationen wie SBC benutzen.

 

Addition und Subtraktion

 

Da der obige Abschnitt etwas verwirrend ist, folgen nun einige einfache Beispiele, an denen Sie sehen können, wie der 6510 rechnet. Zunächst wollen wir einfach die Zahlen 1 und 1 addieren. Dazu reichen die folgenden Zeilen aus:

 

LDA #1

ADC #1

 

Sie können also einen Wert (hier 1) in den Akkumulator laden, und anschließend einen konstanten Wert, ein sogenanntes Immidiate, addieren. Ein Immidiate können Sie an dem Nummernzeichen erkennen, das auch nur hierfür benutzt wird. Nun kann es sein, dass Sie statt dem Ergebnis 2 den Wert 3 herausbekommen. Keine Angst, Ihr Prozessor ist nicht kaputt. Sie bekommen immer dann als Ergebnis eine um 1 zu große Zahl heraus, wenn das Carry-Flag gesetzt ist, weil es zufälligerweise einen Übertrag aus der letzten Addition enthält. Deshalb ist auch erst das folgende Konstrukt wirklich korrekt:

 

CLC

LDA #1

ADC #1

 

Wenn Sie nun von 1 den Wert 1 abziehen wollen, verwenden Sie das folgende Konstrukt:

 

SEC

LDA #1

SBC #1

(hier kommt 0 heraus)

 

Wie Sie sehen, müssen Sie vor einer Substraktion das Carry setzen, und nicht löschen. Dies können Sie sich wie folgt klar machen: Bei einer Substraktion borgen sie sich bei einem Unterlauf einen Taler, bei einer Addition haben Sie dagegen bei einem Überlauf einen Taler zu wenig bezahlt. Da der 6510 genau so streng ist, wie Dagobert Duck, betrachtet er die Substraktion getrennt von der Addition, und spricht hier auch von einem „borrow flag“ anstatt einem „carry flag“.

 

Was haben Sie aber nun davon? Die Antwort ist, dass dieses seltsame Verhalten die einzig vernünftige Möglichkeit ist, auch Zahlen zu addieren und zu subtrahieren, die größer als 255 sind. In diese Verlegenheit kommen Sie schneller, als Sie denken, z.B. in dem Moment, in dem Sie mit Zeigern rechnen müssen. Stellen Sie sich nun vor, Sie müssen von der Speicheradresse 1024 aus 500 Bytes nach vorn springen. Mit dem Y-Register allein können Sie dies nicht tun, denn dies speichert ja nur Werte von 0 bis 255. Sie müssen also wohl oder übel den Wert 500 ($1F4 Hex) zu Ihrem Zeiger addieren, der z.B. in den Adressen 248 und 249 steht. Zum Glück geht dies über das folgende einfache Konstrukt:

 

CLC

LDA 248

ADC #$F4

STA 248

LDA 249

ADC #1

STA 249

 

Der Trick ist hier, dass ein eventueller Überlauf des 1. ADC-Befehls das Carry-Flag auf 1 setzt, das dann bei der nächsten Addition erhalten bleibt. Auch auf die übernächste Addition würde dieses Flag eine Auswirkung haben - falls es dann immer noch gesetzt ist. Natürlich gibt es auch einen großen Nachteil bei dieser Vorgehensweise: Sie können Zahlen immer nur byteweise addieren, und erhalten dann auch sehr oft lange Programme.

 

Logische Operationen

 

Neben ADC und SBC gibt es noch die logischen Operationen. Im Gegensatz zu den BASIC-Operatoren AND und OR arbeiten die Assembler-Operatoren stets mit einzelnen Bits. Sie können also in einem Assembler-Programm keine IF-Statements benutzen, sondern nur die bitweisen Wahrheitstafeln, die Sie schon aus dem BASIC-Kurs kennen. Der 6510 unterstützt die folgenden logischen Bit-Operationen:

 

·       ORA: Verknüpft den Inhalt des Akkumulators mit einem Immidiate oder dem Inhalt einer Speicheradresse durch ODER

·       AND: Verknüpft den Inhalt des Akkumulators mit einem Immidiate oder dem Inhalt einer Speicheradresse durch UND

·       EOR: Verknüpft den Inhalt des Akkumulators mit einem Immidiate oder dem Inhalt einer Speicheradresse durch ein exklusives ODER (EOR ist die Abkürzung für „exclusive or“, was bedeutet, dass a EOR b=0 ist, wenn a und b beide 1 sind)

 

Nach der eigentlichen logischen Verknüpfung landet das Ergebnis stets im Akkumulator. Dies ist aber nicht von besonders großem Nachteil, denn die logischen Operationen können zusammen mit jeder Adressierungsart benutzt werden, auch der indirekten. So können Sie z.B. durchaus Zeiger in der folgenden Weise benutzen:

 

LDY #0

LDA (248),Y

AND (250),Y

ORA 1024

STA 1024

 

Zusätzlich zu den logischen Operationen können Sie den Inhalt des Akumulators um 1 Bit nach links (mit ASL) oder rechts (mit LSR) verschieben. Dabei entspricht eine Verschiebung von 1 Bit nach links einer Multiplikation mit 2 und eine Verschiebung um 1 Bit nach rechts einer Addition durch 2. Wenn Sie noch zusätzlich das Carry-Flag dazu benutzen wollen, das aus dem Akkumulator herausgeschobene Bit in diesem abzulegen, dann verwenden Sie ROL (rotate left) und ROR (rotate right). Auch ASL, LSR, ROL und ROR können mit sämtlichen Adressierungsarten benutzt werden.

 

1.6 Prozessor-Flags

 

Der 6510 besitzt zusätzlich zu A, X und Y ein paar Spezialregister. Eines dieser Register ist das Statusregister P (P=processor status). In diesem Register werden bestimmte Bits auf 1 gesetzt, wenn bestimmte Zustände auftreten. Wie bei einem Fußballspiel kann also der Prozessor Schiedsrichter spielen, und durch bestimmte Fähnchen anzeigen, dass etwas Außergewöhnliches passiert ist. Natürlich entsprechen die einzelnen Fähnchen einzelnen Bits im Statusregister P. Der Prozessor kann z.B. feststellen, dass eine Addition übergelaufen ist und das Ergebnis >255 sein muss. Da dieses Ergebnis nicht in den Akkumulator passt, wird anschließend das Carry-Flag auf 1 gesetzt. Wenn das Ergebnis negativ ist, wird das Negative-Flag auf 1 gesetzt. Ist das Ergebnis dagegen 0, wird das Zero-Flag auf 1 gesetzt. Sehr kritisch wird es, wenn zusätzlich das Overflow-Flag gesetzt ist - in diesem Fall muss der Programmierer eingreifen und eventuell das falsche Ergebnis verwerfen. Auch BASIC tut dies, indem es die Fehlermeldung OVERFLOW ERROR ausgibt. Der 6510-Prozessor besitzt folgende Flags, die auch immer wieder neu berechnet werden, nachdem ein Befehl ausgeführt wurde:

 

·       Das Zero-Flag Z zeigt an, dass das Ergebnis der letzten Berechnung 0 ist.

·       Das Negative-Flag N zeigt an, dass das Ergebnis der letzten Berechnung <0 ist.

·       Das Carry-Flag C zeigt an, dass es bei der letzten Berechnung einen Übertrag in das nächste Byte gab.

·       Das Overflow-Flag V zeigt an, dass ein vorzeichenbehaftetes Ergebnis den gültigen Bereich verlassen hat.

·       Das Interrupt-Disable-Flag I verhindert die Reaktion des Prozessors auf Interrupt-Quellen. Diese Tatsache ist wichtig, denn I ist kein Interrupt-Enable-Flag, das die Interrupts einschaltet, wie z.B. bei Intel-Prozessoren. Sie müssen also Ihre Interrupt-Routinen mit CLI und nicht mit SEI abschließen. Wahlweise können Sie auch RTI (return from Interrupt) benutzen- RTI löscht vor dem Rücksprung das I-Flag automatisch.

 

1.7 Direkte und bedingte Sprünge

 

Wenn Sie wollen, dann können Sie in Ihrem Programm beliebig hin und her springen. Was der Befehl GOTO in BASIC leistet, leistet der Befehl JMP in Assembler. Mit JMP können Sie an eine beliebige Adresse springen. Dies wird allerdings seltener gemacht, sondern es wird stattdessen JSR verwendet. JSR springt wie JMP zu einer bestimmten Adresse, JSR merkt sich aber vorher, wo Sie hergekommen sind (ähnlich wie GOSUB in BASIC). Auf diese Weise können Sie z.B. die Routine zur Ausgabe eines Zeichens an der aktuellen Cursorposition, die in den Registern X und Y steht, auslagern. Wenn Sie dann das Zeichen A an der Position {10,10} ausgeben wollen, dann können Sie einfach wie folgt vorgehen:

 

LDA #1

LDX #10

LDY #10

JSR CHAROUT

 

CHAROUT

RTS (CHAROUT muss mit RTS zurückkehren!)

 

Natürlich müssen Sie hier die Routine CHAROUT separat programmieren. Sie können aber Ihre Sprünge auch abhängig von Bedingungen machen, und hier kommen die Prozessor-Flags ins Spiel. Es gibt nämlich Sprungbefehle, die nur dann ausgeführt werden, wenn bestimmte Flags gesetzt sind. So können Sie z.B. immer dann eine bestimmte Aktion ausführen, wenn ein bestimmter Wert im Akkumulator steht, den Sie vorher über die Tastatur eingelesen haben. Diese sogenannten bedingten Sprünge bieten vielerlei Möglichkeiten, haben aber auch einige Nachteile. So können Sie mit bedingten Sprüngen z.B. nur über Distanzen von +- 127 Bytes springen. Dies kann sehr wenig sein, besonders, wenn Sie sehr viele verschiedene Bedingungen überprüfen müssen. Allerdings sind bedingte Sprünge auch die einzige Möglichkeit, Schleifen zu realisieren. Das nächste Beispiel erstellt eine solche Schleife, die den Bildschirm dadurch löscht, dass in die Adressen 1024 bis 2023 der Wert 32 geschrieben wird.

 

04-CLRSCR

10    -                  .BA 49152

20    -                  LDA #0

30    -                  STA 248

40    -                  LDA #4

50    -                  STA 249

60    -                  LDY #0

70    -CLEARLOOP         LDA #32

80    -                  STA (248),Y

90    -                  CLC

100   -                  LDA 248

110   -                  ADC #1

120   -                  STA 248

130   -                  LDA 249

140   -                  ADC #0

150   -                  STA 249

160   -                  LDA 248

170   -                  CMP #232

180   -                  BEQ CONT

190   -                  JMP CLEARLOOP

200   -CONT              LDA 249

210   -                  CMP #7

220   -                  BEQ EXIT

230   -                  JMP CLEARLOOP

240   -EXIT              RTS

 

Im Endeffekt enthält das letzte Beispiel nicht viel Neues, außer einige bedingte Sprungbefehle. Der Zeiger auf die aktuelle Bildschirmposition wird in Zeile 20 - 50 initialisiert, indem in Adresse 248 der Wert 0 (Lo-Byte) und in Adresse 249 der Wert 4 (Hi-Byte) abgelegt wird. Das Y-Register wird mit 0 initialisiert, und behält auch stets diesen Wert bei. Der Wert 32, der zunächst in Zeile 70 in den Akkumulator geladen wird, wird anschließend in der Adresse abgelegt, auf die der Zeiger in Adresse 248 und 249 zeigt. Dies bedeutet, dass an dieser Stelle ein Leerzeichen erscheint. Nun muss die nächste Position so ausgewählt werden, dass sie auch Seitengrenzen überschreiten kann. Dies geschieht in Zeile 90 - 150 derart, dass zu dem Zeiger in den Adressen 248 und 249 der 16-Bit-Wert 1 addiert wird. Dieser 16-Bit-Wert muss durch das Lo-Byte 1 und das Hi-Byte 0 dargestellt werden, deshalb wird in Zeile 140 auch der Wert 0 addiert. Wenn dort noch ein Übertrag aus der letzten Addition vorhanden ist, wird dieser ebenfalls übernommen, und auf diese Weise können Ihre Zeiger auch Seitengrenzen überschreiten. Das Neue in dem letzten Beispiel ist dann auch nicht die Addition, sondern das Label CLEARLOOP, zu dem Ihr Programm so lange zurückspringen muss, wie noch nicht der gesamte Bildschirm gelöscht worden ist. Doch wie stellt man dies fest?

Dazu muss zunächst der Inhalt der Adresse 248 mittels CMP mit dem Wert 232 verglichen werden (dies ist das Lo-Byte der Adresse 2023). CMP setzt daraufhin die Flags neu, und wenn anschließend das Zero-Flag gesetzt ist, dann steht in der Adresse 248 der Wert 232. Der nachfolgende BEQ-Befehl (branch if equal) springt dann auch genau dann zum Label CONT, wenn das Lo-Byte den Wert 232 hat. Allerdings reicht es nicht aus, nur das Lo-Byte des Zeigers in den Adressen 248 und 249 zu überprüfen, denn wenn dies wirklich den Wert 232 hat, muss das Hi-Byte nicht unbedingt auch den korrekten Wert haben. Damit Ihre Schleife nicht zu früh aussteigt, muss ab dem Label CONT noch das Hi-Byte in Adresse 249 durch einen CMP-Befehl überprüft werden. Erst, wenn dies den Wert 7 hat, dann wird das Programm beendet. Ansonsten landen Sie wiederum bei CLEARLOOP und überschreiben eine weitere Bildschirmposition mit einem Leerzeichen.

Vielleicht haben Sie sich bereits gefragt, ob es dann nicht so ist: Der Prozessor zieht hier intern (das heißt, ohne Register zu verändern) den Wert 232 bzw. 7 vom Inhalt des Akkumulators ab, und anschließend werden mit diesem (im Endeffekt temporären Wert) die Flags neu berechnet. BEQ reagiert hier nur auf das Zero-Flag, und springt dann auch nur, wenn der verglichene Wert mit dem zu vergleichenden Wert identisch ist. Sie haben Glück, denn genau so ist es: CMP verändert nur die Flags, nicht aber die Register (dies wäre an dieser Stelle auch fatal). Zusätzlich zu BEQ gibt es noch die folgenden bedingten Sprungbefehle, die Sie auch in den nächsten Kapiteln immer wieder antreffen werden:

 

·       BEQ springt nur dann, wenn das Zero-Flag gesetzt ist (identisch mit A=B)

·       BNE springt nur dann, wenn das Zero-Flag gelöscht ist (identisch mit A<>B)

·       BMI springt nur dann, wenn das Negative-Flag gesetzt ist (identisch mit A<0)

·       BPL springt nur dann, wenn das Negative-Flag gelöscht ist (identisch mit A>=0)

·       BCS springt nur dann, wenn das Carry-Flag gesetzt ist

·       BCC springt nur dann, wenn das Carry-Flag gelöscht ist

·       BVS springt nur dann, wenn das Overflow-Flag gesetzt ist

·       BCC springt nur dann, wenn das Overflow-Flag gelöscht ist

 

Die Flags selbst können mit folgenden Befehlen verändert werden:

 

·       SEC setzt das Carry-Flag

·       CLC löscht das Carry-Flag

·       SEI setzt das Interrupt-Disable-Flag

·       CLI löscht das Interrupt-Disable-Flag

 

Das Overflow-Flag V kann nicht direkt verändert werden, die Befehle SEV und CLV existieren nicht. Die Flags können aber mit dem Befehl PHP auf dem Prozessorstapel abgelegt werden, und mit dem Befehl PLP wieder vom Prozessorstapel geholt werden. Über Umwege können sie also sämtliche Flags (inklusive V-Flag) beeinflussen.

 

1.8 Der Prozessorstapel (Stack)

 

Sie können durch bestimmte Befehle den Inhalt des Akkumulators zwischenspeichern. Hierzu wird vom 6510 die erste Speicherseite an den Adressen 256 bis 511 benutzt. In dieser Speicherseite kann nun mit Hilfe des Registers S (stack pointer) der Inhalt des Akkumulators zwischengespeichert werden. Hierbei gelten die folgenden Regeln: Wenn Sie den Inhalt des Akkumulators mit Hilfe des Stack Pointers sichern, dann wird, nachdem der Akkumulator an die Adresse geschrieben wurde, auf die S zeigt, S um 1 erniedrigt. Wenn Sie anschließend den Akkumulator wieder „zurückholen“, dann wird S um 1 erhöht, bevor A wieder aus der Adresse ausgelesen wird, auf die S zeigt. Wenn Sie an dieser Stelle gut aufgepasst haben, dann sind Sie vielleicht stutzig geworden, denn S ist bestimmt 8 Bit breit. Wie soll dieser Zeiger dann auf die Adressen 256 bis 511 zeigen? Ganz einfach: An dieser Stelle wird stets ein zusätzliches 9. Bit für die Adressberechnung benutzt, und dieses Bit ist stets 1.

Was haben Sie aber nun gewonnen? Ganz einfach: Sie können die Werte im Akkumulator mit dem Befehl PHA (push accumulator) auf einer Art Kartenstapel ablegen, und auch auf dieselbe Art und Weise wieder mit dem Befehl PLA (pull accumulator) vom Stapel ziehen. Die Reihenfolge, in der Sie Zahlen auf dem Stapel ablegen und wieder von diesem herunterziehen, entspricht der Reihenfolge, die Sie auch bei Karten erwarten würden. Aber der Befehl JSR legt die Speicheradresse des Befehls, der dem JSR-Befehl folgt, zusätzlich auf dem Stapel ab, deshalb kann RTS anschließend erfolgreich zum Aufrufer zurückkehren. Allerdings gilt dies nur, wenn Sie vor der Rückkehr mit RTS den Stapel vorher aufräumen, und sämtliche Werte, die Ihr Unterprogramm dort abgelegt hat, entfernen.

Im Endeffekt ist die Verwaltung des Stacks also relativ einfach. Der Teufel liegt hier im Detail. So können Sie z.B. die Index-Register nicht auf dem Stack ablegen. Dies liegt daran, dass die Index-Register nicht mit der ALU, also der Recheneinheit des 6510, verbunden sind. Sie können deswegen die Index-Register nicht direkt auf dem Stack ablegen, sondern müssen den Umweg über den Akkumulator gehen. Auch können Sie keine 16- oder 32-Bit-Werte direkt auf dem Stack ablegen, sondern müssen diese stückweise ablegen.

Der letzte Abschnitt beantwortet aber noch nicht die Frage, wozu der Stack überhaupt nötig ist - außer, dass JSR dort die Rückkehradressen für die Unterprogramme ablegt. Am besten können Sie die Verwendung des Stacks an einem Beispiel sehen. Stellen Sie sich vor, Sie wollen den folgenden Ausdruck berechnen:

 

(1+2+3)-(4+5+6)

 

Wenn Sie hier schlicht nach Schema F vorgehen, bekommen Sie ein falsches Ergebnis heraus. Dies liegt einfach daran, dass Sie an dieser Stelle nicht beachtet haben, dass die Klammern Vorrang haben. Sie müssen also erst

 

1+2+3=6

 

und anschließend

 

4+5+6=15

 

berechnen, und können erst dann die Subtraktion

 

6-15=-9

 

ausführen. -9 ist dann auch das korrekte Ergebnis. Und genau hier kommt der Stack ins Spiel, auf dem Sie zunächst das Ergebnis der ersten Additionskette ablegen müssen, z.B. so:

 

10    -                  .BA 49152

20    -                  CLC

30    -                  LDA #1

40    -                  ADC #2

50    -                  ADC #3

60    -                  PHA (AàStack)

 

Am Ende schieben Sie den Wert 6 auf den Stack, und können nun in Ruhe die zweite Additionskette ausrechnen:

 

70    -                  CLC

80    -                  LDA #4

90    -                  ADC #5

100   -                  ADC #6

 

Nun müssen Sie nur noch die einzelnen Additionsterme zusammenzählen. Hierzu brauchen Sie leider eine zusätzliche Hilfsadresse, weil Sie mit den Indexregistern dummerweise nicht rechnen können. Ich selbst bevorzuge es, für meine temporären Werte, die ich nicht auf dem Stack speichern kann, die Adressen 1000 bis 1019 zu benutzen. Diese Adressen liegen noch nicht im Bildschirmspeicher, und werden auch vom VIC und von BASIC nicht benutzt. Schauen Sie sich nun in Ruhe an, wie ich das richtige Ergebnis herausbekomme:

 

110   -                  STA 1000

120   -                  SEC

130   -                  PLA (AßStack)

140   -                  SBC 1000

150   -                  STA 1024

160   -                  RTS

 

Der zweite Term muss also zwischengespeichert werden, um ihn in der korrekten Weise vom 1. Term abzuziehen. Wenn Sie alles richtig gemacht haben, dann müsste an der 1. Bildschirmposition ein Zeichen erscheinen, das den Wert 246 besitzt. Dies ist aber korrekt, denn -1 wird auf 255, und -9 auf 255-9=246 abgebildet.

 

Wir sind nun am Ende der Einführung angekommen, und Sie besitzen nun ein solides Grundlagenwissen, um mit der fortgeschrittenen Programmierung weiterzumachen. Im Laufe der Zeit werden Sie immer wieder auf Dinge stoßen, die im 1. Kapitel vorkommen. Schlagen Sie hier ruhig immer wieder nach, wenn Sie z.B. unsicher sind und sich fragen, wie das mit den Indexregistern oder der indirekten Adressierung denn noch mal war. Niemand kann von der ersten Stunde an Assembler, und man lernt Programmieren nur durch stetige Wiederholung des bereits Gelernten.