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.