Assembler-Kurs Teil 1 -----------------------
EINLEITUNG:
Dieser Assembler-Kurs soll allen BASIC-Programmierern den Einstieg in Assembler
ermöglichen.
Ich werde mich in der Folge darum bemühen, die komplizierten Fachbegriffe zu
umgehen und leicht verständliche Erklärungen zu geben.
Zunächst sollen innerhalb dieses Kurses
alle Assembler-Befehle behandelt werden.
In späteren Teilen wird dann auf die
Verwendung von Kernal-Routinen und die
Interrupt-Technik eingegangen.
Zu jedem Teil des Kurses befindet sich
ein Programm auf der zweiten Seite der
Diskette (" ASSEMBLER-KURS 1"), das
einige Assembler-Routinen enthält und
Ihnen deshalb gleich die Auswirkungen
der jeweiligen Assembler-Befehle demonstrieren kann.
DAS WERKZEUG:
Zum Programmieren in Assembler benötigen
Sie unbedingt ein Programm, das die Befehle in reine Maschinensprache umwandelt. Diese Programme werden Assembler
oder Monitore genannt. Ein solches Programm finden Sie unter anderem auf der
Rückseite dieser Ausgabe ( WIZ-MON) ; die
Beschreibung dazu entnehmen Sie der
entsprechenden Rubrik.
LDA, STA und BRK: ----------------
Und jetzt können wir schon mit den
ersten Befehlen loslegen.
Wir fangen an mit LDA ( load accumulator) Der Akkumulator ( oder kurz Akku genannt) ist eine Art Verteilerstelle. Mit diesem
Befehl lädt man also einen Wert ( Zahl) in den Akku.
STA ( store accumulator) schreibt diesen
Akkuinhalt dann in die angegebene
Speicherstelle.
Der Befehl BRK entspricht etwa dem
BASIC-Befehl STOP und sollte zunächst
immer am Ende Ihrer ersten Programmierveruche mit Assembler stehen.
LDA und STA sind durchaus mit PEEK und
POKE aus dem BASIC vergleichbar.
Beispiel: POKE 8192,8
Dieser Befehl schreibt also den Wert 8 in die Speicherstelle 8192 . Eine Routine
in Assembler, die die gleiche Aufgabe
erfüllt, lautet:
LDA #$08 STA $2000 BRK
Lassen Sie sich nicht dadurch verwirren, daß hier Hexadezimalzahlen ( Erklärung
im Programm!) verwendet werden.
Das Zeichen "#" in der ersten Zeile
zeigt an, daß eine konstante Zahl und
nicht der Inhalt einer Speicherzelle in
den Akku geladen wird. Die hexadezimale
Zahl $2000 entspricht haargenau der
dezimalen Zahl 8192 .
Zunächst wird also die Zahl 8( bzw.$08) in den Akku geladen. In der zweiten
Zeile wird dieser Wert in der Adresse
8192($2000) abgelegt.
Ein anderes Beispiel:
Nach der Eingabe von POKE 12288, PEEK
(8192) hätte die Adresse 12288 ebenfalls
den Inhalt 8 .
Die entsprechenden Assembler-Befehle lauten: LDA $2000 STA $3000 BRK
Mit LDA wird diesmal keine Zahl, sondern
der Inhalt der Speicherstelle $2000( ähnlich PEEK(8192) in den Akku geladen.
STA legt den Akkuinhalt daraufhin bei
$3000( dezimal 12288) ab.
DER UMGANG MIT DEM MONITOR:
Eingegeben werden die Assembler-Befehle
mit einem Monitor durch:
A Speicherstelle Assembler-Befehl
z. B. : A C000 LDA #$08
Wenn Sie Ihre eingegebenen Befehlsfolgen
noch einmal betrachten wollen, so können
Sie dies mit " D Speicherstelle"( z. B. :
D C000) tun.
Es wird Ihnen dann bei unserem Beispiel folgende Zeile gezeigt:
. C000 A908 LDA #$08
Die Zahl C000 ist die Speicherstelle, in
der der Befehl steht. A9 ist die Codezhl
für den LDA-Befehl. Die darauffolgende
Zahl zeigt den Wert an, der in den Akku
geladen wird. Uns brauchen zunächst nur
die Assembler-Befehle im Klartext zu
interessieren.
Laden Sie nun das Beispielprogramm
" ASSEMBLER-KURS 1" von der zweiten
Seite. Es wird Ihnen weitere Hilfen zu
den ersten Assembler-Befehlen und zum
hexadezimalen Zahlensystem geben.
In der nächsten Ausgabe behanden wir die
Indexregister und die dazugehörigen
Assmblerbefehle.
Bis dahin wünschen ich Ihnen viel Spaß
beim ausprobieren der gelernten Befehle!
(rt) Maschinensprache-Kurs Teil 2 ------------------------------
Diesmal werden gleich 15 neue, aber
dafür doch recht einfache Befehle vorgestellt.
Vorher müssen wir uns jedoch etwas näher
mit den Indexregistern ( X-Register und
Y-Register) beschäftigen: Jedes dieser
beiden Register kann ein Byte aufnehmen, d. h. Werte zwischen $00 und $ FF. Sie
werden hauptsächlich im Zusammenhang mit
den verschiedenen Adressierungsarten
verwendet.
LDX
Beispiel: LDX #$0 a oder LDX $1000
LDX entspricht in seiner Form dem schon
bekannten LDA-Befehl.
LDY --- ist der entsprechende Befehl für das
Y-Register.
STX/ STY ------- erinnern sehr stark an den STA-Befehl. Und so ist es auch. Bei STX bzw.
STY wird der Inhalt des Xbzw. Y-Regi- sters in die angegebene speicherstelle
geschrieben.
Sie bemerken sicherlich die enge Beziehung zwischen dem Akku und den Indexregistern. Für einen schnelleren Datenaustausch zwischen diesen Registern sind
die Befehle TAX, TXA, TAY und TYA verantwortlich.
TAX/ TAY ------- Der Inhalt des Akkus wird im
X-Register abgelegt. Dadurch verändert
sich jedoch der Inhalt des Akkumulators
nicht. Für das Y-Register lautet der
Befehl TAY.
TXA/ TYA ------- ist die Umkehrung des TAX bzw.
TAY. Nun wird der Inhalt des Xbzw.
Y-Registers in den Akku geschrieben, ohne daß sich das Xbzw. Y-Register
verändert.
DEX, DEY und DEC ---------------- Während DEX und DEY das
jeweilige Indexregister um 1 erniedrigen, wirkt der DEC-Befehl auf die nach
ihm folgende Speicherstelle. Auch von
deren Inhalt wird der Wert 1 abgezogen.
Der Akku ist dvon nicht betroffen.
INX, INY und INC ---------------- Diese Assemblerbefehle
bewirken genau das Gegenteil der eben
vorgestellten. Hier werden die jeweiligen Register um den Wert 1 erhöht.
Was passiert aber, wenn z. B. im X-Register der Wert $ FF steht und der
Prozessor auf den Befehl INX trifft ?
Da ein Byte keinen größeren Wert als $ FF
annehmen kann, wird das X-Register
kurzerhand auf $00 gesetzt. Ähnlich
sieht es bei $00 und einem DEX aus. Dann
wird einfach wieder " oben" angefangen
und $ FF gesetzt.
RTS --- steht gewöhnlich am Ende eines
Assemblerprogrammes oder Unterprogrammes
und soll künftig unser BRK ersetzen.
Wenn ein Programm auf den RTS-Befehl
trifft, wird es nicht mehr unterbrochen, sondern es wird ins Basic gesprungen.
Laden Sie nun den zum Kurs gehörende
Programm aus dem GAMESMENUE oder direkt
von der ersten Seite dieser MAGIC DISK
64- Ausgabe. Es werden Ihnen die hier
vorgestellten Befehle noch einmal
verdeutlicht.
Auf der nächsten Seite folgt eine Zusammenstellung der neuen Befehle.
Ich wünsche Ihnen viel Erfolg beim Ausprobieren der neu erlernten Befehle.
In der nächsten Ausgabe der MAGIC DISK 64 behandeln wir Vergleichsbefehle und die bedingte Verzweigung.
LDX (LoaD X-register) LDY (LoaD Y-register) STX (STore X-register) STY (STore Y-register) DEC (DECrement memory) DEX (DEcrement X-register) DEY (DEcrement Y-register) INC (INCrement memory) INX (INcrement X-register) INY (INcrement Y-register) TAX (Transfer Accu into X) TAX (Transfer Accu into Y) TXA (Transfer X into Accu) TYA (Transfer Y into Accu) RTS (ReTurn from Subroutine) ---------------------------------------- (wk) Assembler-Kurs Teil 3 -----------------------
Diesmal geht es in unserem Kurs um die
vergleichsbefehle und die bedingte
Verzweigung. Dafür muß jedoch zunächst
das Statusregister des Prozessors näher
erklärt werden.
Dieses 8- Bit-Register bildet die Grundlage für die bedingte Verzweigung.
Die einzelnen Bits des Statusregisters
nennt man Flaggen ( oder Flags) . Jeder
dieser Flaggen kann direkt angesprochen
oder abgefragt werden und wird automatisch gesetzt oder gelöscht.
Es existiern folgende Flaggen:
Bit 0 Carry-Flag: Dieses Flag wird
----- gesetzt, wenn ein " Übertrag" stattfindet. Wenn z. B. bei der
Addition zweier 8- Bit-Zahlen das
Ergebnis größer als 255($ FF) ist
und nicht mehr in ein Byte paßt, dann wird das Carry-Bit gesetzt.
Bei der Subtraktion erfüllt das
Carry-Bit eine entgegengesetzte
Aufgabe. Hierbei ist das Carry-Bit zunächst gesetzt. Beim Subtrahieren zweier Zahlen kann
selbstverständlich kein Überlauf
stattfinden. Sollte das Ergebnis
jedoch kleiner als null werden
(" Unterlauf"), so wird das Carry-Flag gelöscht.
Bit 1 Zero-Flag: Dieses Bit ist gewöhn------ lich gelöscht. Wenn das Ergebnis
einer Befehlsfolge aber Null ergibt, wird sie gesetzt.
Bit 2 IRQ-Flag: Sie ermöglicht oder ver------ hindert Interrupts. Sie spielt in
den späteren Kursteilen eine
größere Rolle.
Bit 3 Dezimal-Flag: Von ihr hängt es ab,----- ob eine Rechenoperation im Dezimalmodus ausgeführt wird.
Normalerweise ist dieses Bit gelöscht und alle Rechnungen werden
binär ausgeführt.
Bit 4 Break-Flag: Wenn das Programm auf
----- einen BRK-Befehl trifft, wird
diese Flagge gesetzt.
Bit 5 ist nicht belegt. -----
Bit 6 V-Flagge (" Overflow") : Dieses Bit
----- wird nur im Zweierkomplement-Modus
benötigt. In diesem Modus wird das
7 . Bit einer Zahl als Vorzeichen
aufgefaßt. Es wird automatisch gesetzt, wenn ein Überlauf eingetreten ist, d. h. wenn der Zahlenbereich überschritten wurde und
nun das höherwertigste Bit gesetzt
wurde, obwohl es eigentlich das
Vorzeichen enthalten sollte.
Bit 7 Negativ-Flag: Wenn das Ergebnis
----- einer Operation größer als 127($7 F) ist, wird diese Flagge gesetzt. Wie oben erwähnt, gibt es
einen Modus, in dem Zahlen, die
größer als $7 F sind (7 . Bit gesetzt), als Negativ angesehen
werden.
Daß fast jeder Befehl auf die Flaggen
einwirkt, soll uns der altbekannte LDA-Befehl zeigen.
Beispiel: LDA #00
In den Akku wird der Wert $00 geladen.
Da das Ergebnis dieser Aktion null ist, wird die Zero-Flagge gesetzt.
Bei LDA # FF hingegen bleibt die Zero-Flagge gelöscht. Da aber ein größerer
Wert als $7 F geladen wurde, wird nun die
Negativ-Flagge gesetzt.
Wir benötigen in diesem Kursteil lediglich die Flaggen C, Z, N und V.
Die anderen Flaggen sollten hier nur
kurz erwähnt werden. Da Sie nun die Bedeutung der Flaggen kennen, wird Ihnen
die Registeranzeige Ihres Maschinensprache- Monitors " N V - B D I Z C" sicherlich etwas mehr sagen.
Die Vergleichsbefehle: ---------------------
CMP:( CoMPare to accu) : Dieser Befehl
--- ermöglicht einen Vergleich zwischen
dem Inhalt des Akkus und einem beliebigen Wert ( oder dem Inhalt
einer Speicherstelle) . Die Register
werden dadurch verglichen, daß der
adressierte Wert vom Inhalt des
Akkus abgezogen wird. Dabei werden
( je nach Ergebnis) die Flaggen C, Z
und N verändert. Der Akkumulatorinhalt bleibt unberührt.
Beispiel: LDA #$05 CMP #$01
Es wird also der adressierte Wert ($01) vom Inhalt des Akkus ($05) abgezogen.
Unsere Flaggen zeigen folgende Inhalte:
Carry-Flagge : gesetzt, da bei dieser
" Subtraktion" kein Unterlauf aufgetreten ist.
Zero-Flagge : gelöscht, da die Differenz ($04) größer als Null ist.
Negativ-Flagge: gelöscht, da $04 kleiner
als $7 F ist.
Der CMP-Befehl erlaubt folgende ( schon
bekannte) Adressierungsarten:
Unmittelbare Adressierung, z. B: CMP #$10 Absolute Adressierung , z. B: CMP $2000
CPX/ CPY: Die Vergleichsbefehle CPX
-------( ComPare to X) und CPY ( ComPare
to Y) entsprechen dem CMP-Be- fehl, nur daß anstelle des
Akkus mit den angegebenen
Indexregistern verglichen wird.
Da die Vergleichsbefehle nur die Flaggen
verändern, benötigen wir aber noch
Befehle, die auf den Inhalt dieser
Flaggen reagieren.
Die Verzweigbefehle: -------------------
BEQ ( Branch on EQual)--- Verzweige zur angegebenen Adresse, falls die Zero-Flagge gesetzt ist.
BNE ( Branch on Not Equal)--- Verzweige, falls die Zero-Flagge gelöscht ist.
BCC ( Branch on Carry Clear)--- Verzweige, falls die Carry-Flagge gelöscht ist.
BCS (Branch on Carry Set) --- Verzweige, falls die Carry-Flagge gesetzt ist. BMI (Branch on MInus) --- Verzweige, falls die Negativ-Flagge gesetzt ist. BPL (Branch on PLus) --- Verzweige, falls die Negativ-Flagge gelöscht ist. BVC (Branch on oVerflow Clear) --- Verzweige, falls die V-Flagge gelöscht ist. BVS (Branch on oVerflow Set) --- Verzweige, falls die V-Flagge gesetzt ist.
Wie Sie sehen, richten sich jeweils zwei
dieser Befehle ( gegensätzlich) nach
einer Flagge.
Wenn die Bedingungen für eine Verzweigung nicht gegeben ist, wird dieser Befehl einfach vom Programm übergangen.
Hier liegt eine deutliche Parallele zur
IF. . . Then. . .- Anweisung in BASIC.
Bei den bedingten Verzweigungen gibt es
noch eine weitere Besonderheit; Sehen wir uns zunächst den Befehl
. 2000 D0 03 BNE $2005
etwas näher an. Wie Sie sehen, handelt
es sich um einen 2- Byte-Befehl, obwohl
alleine die Angabe der Sprungadresse
schon 2 Bytes benötigen müßte. Diese
speicherplatzsparende Besonderheit wird
durch die relative Adressierung ermöglicht. Es wird also die Sprungadresse
nicht direkt angegeben, sondern nur die
Entfernung zum Sprungziel.
Wenn Sie jede Sprungadresse selbst ausrechnen müßten, würde dies zu einer
wüsten Rechnerei führen. Da uns der
Assembler jedoch diese lästige Arbeit
abnimmt, können wir dieses Thema getrost
übergehen. Sie müssen nur wissen, daß
Sie nicht weiter als 129 Bytes hinter
und 126 Bytes vor Ihren Verzweigungsbefehl springen dürfen.
Dieser eingeschränkte Aktionsradius der
relativen Adressierung fällt Ihnen bei
der Programmierung keum noch auf.
Jetzt, am Ende des dritten Teiles haben
Sie schon die wichtigsten Grundlagen
erlernt und sind in der Lage, kleinere
Programme zu schreiben. Was Sie mit
Ihrem Wissen anfangen können, zeigt
Ihnen auch diesmal das Begleitprogramm
zu diesem Kurs.
Laden Sie nun den "Assembler-Kurs 3" von Diskette und genießen Sie, was Sie bisher gelernt haben. (wk)
Assembler-Kurs Teil 4
Heute behandeln wir zwei neue Assemblerbefehle und die Erstellung eines Programmablaufplans. Auf Diskette wird
außerdem noch die Zeropage-Adressierung
erklärt.
JMP (JuMP to adress)
Zuletzt wurde die bedingten Verzweigung
erläutert. Der JMP-Befehl hängt nich von
solchen Bedingungen ab. Wenn das Programm diesen Befehl erreicht, wird
sofort zur angegebenen Adresse
verzweigt.
Beispiel:
JMP $2000 springt zur Adresse $2000
Sicher haben Sie schon die Ähnlichkeit
mit der GOTO-Anweisung in BASIC erkannt.
Die SPrungadresse des JMP-Befehls kann
auch in indirekter Form vorliegen, z. B. :
JMP ($2000) .
Eine Hexadezimalzahl besteht immer aus
einem höherwertigen Byte ( Bei der Zahl
$2000) ist dies $20) und einem niederwertigen Byte ( hier:$00) . Das höherwertige Byte entspricht also den ersten
beiden Stellen, das niederwertige den
letzten beiden Stellen der Hexzahl.
Bei dem letzten Beispiel wird nun nicht
zur Adresse $2000 verzweigt. Stattdessen
werden die Speicherstellen $2000 und
$2001 als " Sprungvektoren" behandelt.
Enthält z. B. die Adresse $2000 das Byte
$05( niederwertiges Byte) und $2001 den
Wert $10( höherwertiges Byte), so
springt das Programm nach $1005 . Diese
Adressierungsart des JMP-Befehls wird
jedoch nur selten verwendet.
JSR (Jump to SubRoutine)
Ein Unterprogramm ist eine selbstständige Gruppe von Befehlen, die vom
Hauptprogramm getrennt sind.
Nach Aufruf des Hauptprogramms wird es
abgearbeitet, bis ein JSR-Befehl
erreicht wird ( z. B. : JSR $2000) . Nun
wird in das Unterprogramm verzweigt. Es
werden jetzt alle Befehle ausgeführt, bis das Programm auf ein RTS trifft.
Daraufhin wird in das Hauptprogramm
zurückgesprungen.
Selbstverständlich kann auch aus dem
Unterprogramm ein weiteres Unterprogramm
aufgerufen werden ( Verschachtelung von
Unterprogrammen) .
Die Kombination JSR. . . RTS enspricht
unseren bekannten BASIC-Befehlen
GOSUB. . . RETURN.
Der Vorteil eines Unterprogramms liegt
darin, daß es von beliebig vielen
Stellen aus dem Hauptprogramm aufgerufen
werden kann. So müssen mehrmals
benötigte Routinen nicht immer wieder
neu geschrieben werden. Dadurch fällt
weniger Programmierarbeit an und es wird
Speicherplatz gespart.
Wie schon erwähnt, liegt einer der
größten Nachteile der Assemblerprogrammierung in der Übersichtlichkeit
längerer Programme. Ohne ein geeignetes
Hilfsmittel wird das Programmieren zu
einem reinen Glücksspiel ( Fehlersuche in
Assembler ist weniger spaßig!) .
Doch zum Glück gibt es die Programmablaufpläne. Bei diesen wird durch einfache Symbole der ganze Aufbau eines
Programmes verdeutlicht.
Die Ablaufrichtung eines PAP geht immer
von oben nach unten vor, wenn sie nicht
durch Pfeile gekennzeichnet wird.
Laden Sie nun den ASSEMBLER-KURS 4 aus dem GAME-MENÜ. Dort erhalten Sie anhand von Beispielprogrammen weitere Informationen. (rt/wk)
Assembler-Kurs Teil 5
Das Thema, das wir diesmal behandeln, wird Sie sicherlich nicht in Verzückung
geraten lassen, aber es ist leider
notwendig: Das Rechnen in ASSEMBLER.
Wie Sie sicher schon ahnen, wird die
Sache etwas komplizierter als in BASIC.
Mit einer Zeile, wie C = A + B ist es in
ASSEMBLER leider nicht getan. Um diesen
Kursteil nicht unnötig zu komplizieren, werden wir uns heute nur mit der
Addition und Subtraktion von ganzzahligen Werten ( genannt: Integers) beschäftigen. Dabei kommt man allerdings
um die Kenntnis der Flaggen des Statusregisters ( bekannt aus Kursteil 3) nicht
herum.
Bevor wir munter addieren und subtrahieren, sollten Sie vielleicht doch
erstmal einen Blick auf die dualen
Zahlendarstellungen des C64 werfen. Das
Verlassen des erlaubten Zahlenbereiches
kann einige unschöne Nebeneffekte haben.
Unser Computer kennt die ganzzahlige
Werte von -128 bis +127 bei 8- Bit-Zahlen und von -32768 bis +32767 bei
16- Bit-Zahlen. Positive Dualzahlen sind
für uns ja kein Problem, aber wie stellt
man den negative Zahlen dar?
Das soll an dem Beispiel der Zahl -18 erklärt werden:
Die Zahl +18 wird bekanntlich als
00010010 kodiert. Um die negative Zahl
zu erhalten, muß man das Komplement der
Zahl bilden ( d. h. die Zahl wird invertiert: aus jeder 0 wird eine 1 und umgekehrt) und +1 dazuaddieren. Das Komplement von +18 ist 11101101 . Nun muß noch
+1 dazuaddiert werden und man kommt zu
11101110, was der Zahl -18 entspricht.
Das äußerste rechte Bit ( Bit 7) nennt
man das Vorzeichenbit, wobei 0 eine
positive und 1 eine negative Zahl kennzeichnet. Diese Darstellungsform, bei
der die negativen Zahlen umgewandelt
werden, die positiven aber wie gewohnt
aussehen, nennt man ZWEIERKOMPLEMENT.
ADC (ADd with Carry)
Wenn zwei Zahlen addiert werden, dann
muß die eine im Akkumulator stehen, die
zweite kann unmittelbar oder als Inhalt
einer Speicherstelle gegeben sein. Das
Ergebnis der Rechenoperation steht
wieder im Akkumulator. Dies gilt auch
für die Subtraktion. Das Ergebnis sollte
aus dem Akkumulator schnell in eine
Speicherstelle geschrieben werden, da es
sonst beim weiteren Programmablauf
verlorengehen könnte.
Unser neuer Befehl ADC addiert aber
nicht nur zwei Zahlen, sondern auch noch
den Wert des Carry-Bits ( was uns bei
8- Bit-Additionen gar nicht recht sein
kann) . Daher sollte vor dem ADC zuerst
der Befehl CLC stehen, mit dem das
Carry-Bit gelöscht wird. Nun erhalten
wir auf jeden Fall des richtige
Ergebnis.
Dualzahlen werden fast wie Dezimalzahlen
addiert:
0 + 0 = 0 0 + 1 = 1 1 + 0 = 1 1 + 1 = 0 und zusätzlich enthält hier das Carry-Bit den Übertrag 1, der bei der nächsten Stelle nicht berücksichtigt wird.
Ein kleines Beispiel:
CLC LDA #$5D 01011101 (Dez. 93) ADC #$0E + 00001110 (Dez. 14) STA $1500 ---------- 01101011 (Dez. 107)
Das Ergebnis #$6 B (=107) steht jetzt im
Akkumulator und in der Speicherstelle
$1500 .
Ein weiteres Rechenbeispiel:
115 01110011 + 14 + 00001110 ----- --------- 129 10000001
Hier ist offensichtlich etwas schiefgegangen. Das Ergebnis hätte +129 lauten
sollen; herausgekommen ist aber 10000001, was der Zahl -127 im Zweierkomplement
entspricht. Das diese Rechnung nicht
korrekt ist, liegt am Verlassen des
erlaubten Zahlenbereiches. Der Computer
gibt uns hier zwar keine Fehlermeldung
aus, er warnt uns jedoch vor dem
Ergebnis, indem er die V-Flagge setzt.
Diese Overfolw-Flagge wird immer dann
gesetzt, wenn ein unbeabsichtigter
Übertrag in das Vorzeichenbit stattfand.
Die gesetzte N-Flagge ( Vorzeichenflagge) sagt uns, daß das Ergebnis eine negative
Zahl ist.
Die V-Flagge wird eigentlich automatisch
gesetzt oder gelöscht. Dennoch gibt es
den Befehl CLV, mitdem man die V-Flagge
auch selbst wieder löschen kann, und
somit das Warnsignal unterdrückt.
Bisher wurde nur die Addition von
1- Byte-(8- Bit) Zahlen geschildert.
Die Addition von 2- Byte-(16- Bit) Zahlen
wird noch etwas komplizierter, da der
Akkumulator nur 8 Bits aufnehmen kann.
Des Rätsels Lösung: Man addiert zunächst
nur die niederwertigen Bytes der 16- Bit-Zahlen. Sollte dort ein Übertrag über
das 8 . Bit hinaus vorliegen, so gelangt
dieser ins Carry-Bit. Nun werden die
beiden höherwertigen Bytes addiert, und
der Inhalt des Carry-Bits hinzugezählt.
Das Abspeichern der beiden Bytes muß
ebenfalls getrennt erfolgen. Erwähnt
sei noch, daß bei 16- Bit-Zahlen das Bit
15 das Vorzeichen enthält.
Beispiel für eine 16- Bit-Addition:
$1000/$1001 enthalten die erste Zahl $1100/$1101 enthalten die zweite Zahl $1200/$1201 sollen das Ergebnis enthalten
Das Programm dafür sieht folgendermaßen aus:
CLC LDA $1000 ADC $1100 ; Addition des niederwerigen STA $1200 Bytes LDA $1001 ADC $1101 ; Addition des höherwertigen STA $1201 Bytes ohne CLC
SBC (SuBtract with Carry)
Dieser Befehl, wer hätte es gedacht, subtrahiert zwei Zahlen voneinander.
Das Dumme ist nur: Unser Prozessor kann
überhaupt nicht subtrahieren, er kann
nur addieren! Um dennoch Zahlen voneinander abziehen zu können, wendet er
einen Trick an. Eine der beiden Zahlen
wird einfach zu einer negativen Zahl
gemacht und dann werden beide Zahlen
addiert. Diese Uwandlung nimmt uns der
Befehl SBC jedoch schon ab. Es wird zu
dem ersten Operanden nicht nur das
Zweierkomplement der zweiten Zahl, sondern auch noch das Komplement des Carry-Bits addiert. D. h. wenn das Carry-Bit das Ergebnis bei der Subtraktion
nicht verfälschen soll, muß es zuvor gesetzt sein. Dazu dient der Befehl SEC
( SEt Carry), der vor jedem SBC stehen
sollte.
Ein Beispiel:
SEC LDA #$7D SBC #$0A STA $1500
Per Hand sieht die Rechnung so aus:
Dezimal Binär 125 01111101 - 10 + 11110110 ------- ----------- 115 (1) 01110011
Das Ergebnis $73(115) steht im Akku
und in der Speicherstelle $1500 .
Wie Sie sehen, erhalten wir das richtige
Ergebnis. Das Vorzeichen der Zahl ist
positiv, da die N-Flagge nicht gesetzt
ist. Das gesetzte "9 . Bit" steht im
Carry und wird bei der 8- Bit-Subtraktion
nicht benötigt. Auch bei der Subtraktion
signalisiert die V-Flagge ein ubeabsichtigtes Verlassen des Zahlenbereichs.
Der SBC-Befehl hat also große Ähnlichkeit mit dem ADC, und so ist es nicht
verwunderlich, daß eine 16- Bit-Subtrak- tion einer 16- Bit-Addition auch sehr
verwandt ist.
Beispiel für eine 16- Bit-Subtraktion:
$1000/$1001 enthalten die erste Zahl $1100/$1101 enthalten die zweite Zahl $1200/$1201 sollen das Ergebnis enthalten
Das Programm sieht folgendermaßen aus:
SEC LDA $1000 SBC $1100 ; Subtraktion der STA $1200 niederwertigen Bytes LDA $1001 SBC $1101 ; Subtraktion der höher- STA $1201 wertigen Bytes ohne SEC
Abschließend möchte ich noch zeigen, daß
es dem Programmierer selbst überlassen
bleibt, ob er die Zahlen wirklich als
vorzeichenbehaftet ansieht, wie es
eigentlich vorgesehen ist, oder ob er
die Zahlen als vorzeichenlos betrachtet.
Dazu ein Beispiel:
Dezimal Binär -6 11111010 + 2 + 00000010 ---- ---------- -4 11111100
Das Ergebnis ist bei vorzeichenbehafteten Zahlen richtig, da -6($ FA) und -4($ FC) im Zweierkomplement gegeben sind.
Nun vergessen Sie mal das Zweierkomplement und betrachten Sie die Zahlen als
vorzeichenlos. Dann hätten wir $ FA +$02=$ FC ( also dezimal 250+2=252) gerechnet. Auch dieses Ergebnis ist
korrekt. Dieses Beispiel zeigt, daß es
am Programmierer liegt, welches der
beiden Ergebnisse er beabsichtigt.
BCD-Darstellung (Binary Coded Decimals)
Da die Befehle ADC und SBC sowohl im
Binär-, als auch im Dezimalmodus
arbeiten, muß es wohl Befehle geben, die
zwischen den beiden Modi umschalten.
Wenn es nicht vom Programm geändert
wurde, ist automatisch der Binärmodus
angeschaltet.
CLD: schaltet den Binärmodus an SED: schlaten auf Dezimalmodus um
Bei deisen " binärkodierten" Dezimalzahlen gibt es nur die Ziffern 0 bis
9 . Es wird also jede Dezimalstelle für
sich getrennt kodiert. Um diese 10 Ziffern kodieren zu können, benötigt man
4 Bits, mit denen bis zu 16 Kombinationen möglich wären. Sechs der Kombinationsmöglichkeiten bleiben daher ungenutzt. Die Dezimalzahl 0 wird als 0000, die 9 als 1001 kodiert. Die BCD-Dar- stellung der Zahl 128 lautet z. B. :
0001 0010 1000 binär 1 2 8 dezimal
Da jede Ziffer 4 Bits in Anspruch nimmt, fasst man je zwei BCD-Ziffern in einem
Byte zusammen. Die Zahl 128 liegt
folgendermaßen imnm SPeicher:0000000100101000 . Die BCD-Zahlen seien hier
jedoch nur am Rande erwähnt, da Sie nur
sehr selten verwendet werden. Rechnen
werden wir nicht mit diesen Zahlen.
Das richtige Verständnis für die diesmal
erlernten Befehle werden Sie erst durch
eigenes Ausprobieren erhalten. Also
nichts wie hingesetzt und losprogrammiert. Auch diesmal gibt es selbstverständlich wieder ein Begleitprogramm zu
dem Kurs auf Diskette. Es nennt sich
" ASSEMBLER-KURS 5" und enthält neben
einer Joystickabfrage in ASSEMBLER auch
noch einige nützliche Anwendungen der
neuen Befehle.
Zusammenfassung der Befehle: ---------------------------
ADC (ADd with Carry)
Zwei Zahlen werden zusammen mit dem
Inhalt des Carry-Bits addiert. Das
Ergebnis steht wieder im Akkumulator.
Dieser Befehl gilt sowohl für die
binäre, als auch für die dezimale ( BDC) Addition. Der ADC-Befehl kann die
Flaggen N, V, Z und C beeinflussen.
SBC (SuBtract with Carry)
Zwei Zahlen werden subtrahiert, indem
das Zweierkomplement der zweiten Zahl zu
der ersten dazuaddiert wird. Das Ergebis
der Operation steht im Akkumulator. Auch
SBC kann sowohl im Binär-, als auch im
Dezimalmodus arbeiten.
CLC (CLear Carry)
Löscht die Übertragungsflagge. Dieser
Befehl sollte immer vor einem ADC
benutzt werden.
SEC (SEt Carry)
Setzt die Übertragsflagge. SEC sollte
vor jedem SBC stehen.
CLD (CLear Decimal mode)
Löscht die Dezimalflagge und schaltet
somit den Binärmodus ein.
SED (SEt Decimal mode)
Setzt die Dezimalflagge auf 1 . Dadurch
werden alle Rechenoperationen im
Dezimalmodus ausgeführt.
CLV (CLear oVerflow flag)
Löscht die Überlaufflagge.
(Ralf Trabhardt/wk)
Assembler-Kurs Teil 6
Diesmal geht es um die logischen
Verknüpfungen AND, ORA und EOR, von
denen Sie vielleicht einige schon von
BASIC aus kennen.
Außerdem werden Sie die Befehle BIT und
NOP kennenlernen.
AND (AND with accumulator)
Die logischen Befehle verknüpfen den
Akkumulatorinhalt bitweise mit den angegebenen Daten. Die AND-Verknüpfung
läuft nach folgendem Muster an:
0 AND 0=0 Wie Sie sehen, erhält man 0 AND 1=0 als Ergebnis nur dann eine 1 AND 0=0"1", wenn beide Operanden 1 AND 1=1 an dieser Stelle das Bit auf 1 gesetzt haben.
Beispiel für eine AND-Verknüpfung:
LDA #$9A AND #$D1 STA $1000
Der Akku wird mit dem Wert $9 A (=144) geladen und mit der Zahl $ D1(=209) UND-Verknüpft. Das Ergebnis der Verknüpfung
steht wieder im Akku und kann von dort
in Speicherstelle $1000 abgespeichert
werden. Dasselbe Ergebnis erhielte man
selbstverständlich auch durch:
LDA $1001 AND $1002 STA $1000
wenn vorausgesetzt wird, daß in
Speicherstelle $1001 der Wert $9 A und in
$1002 die Zahl $ D1 steht.
Führen wir nun die bitweise Berechnung
durch, um zu sehen, welcher Wert in
Speicherstelle $1000 abgelegt wird:
$9A : 10011010 AND $D1 : 11010001 --------
10010000
Wenn Sie genau hinsehen, wird Ihnen auffallen, daß überall dort, wo in dem
zweiten Operanden eine 0 auftaucht, auch
das Ergebnis eine 0 enthält. Da haben
wir auch schon das häufigste Einsatzgebiet des AND-Befehls gefunden, nämlich
das gezielte Löschen von Bits.
Ein weiteres Beispiel soll dies verdeutlichen. Hier verknüpfen wir eine beliebige Zahl mit dem Wert $ F0(=240) .
Für jede Bitposition an der ein X steht, kann entweder eine 1 oder eine 0 eingesetzt werdene. Dies spielt für
unser Beispiel keine Rolle:
xxxxxxxx AND $F0 : 11110000 --------
xxxx0000
An unserem Ergebnis sieht man, daß die
untersten vier Bits gelöscht wurden, während die obersten Bits ihren alten
Wert beibehalten haben.
Der AND-Befehl beeinflußt die Flaggen N
( wenn im Ergebnis Bit 7 gesetzt ist) und
Z ( falls das Ergebnis gleich Null ist) .
ORA (inclusivE OR with Accumulator)
Die Verknüpfung verläuft nach folgendem
Schema:
0 ORA 0=0 Man erhält als Ergebnis 0 ORA 1=1 eine "1", wenn das Bit des
1 ORA 0=1 ersten Operanden oder das 1 ORA 1=1 Bit des zweiten Operanden an dieser Stelle auf 1 gesetzt ist.
Ansonsten ist ORA dem AND-Befehl sehr
ähnlich. Er beeinflußt sogar dieselben
Flaggen ( N-Flag und Z-Flag) .
Ein Beispiel für eine ORA-Verknüpfung:
LDA #$9A ORA #$D1 STA $1000
Auch hier wollen wir die Verknüpfung
bitweise nachvollziehen:
$9A : 10011010 ORA $D1 : 11010001 --------
11011011
Im Unterschied zu AND dient der ORA-Befehl dem gezielten Setzen von
Bits. Als Beispiel möchte ich wieder
ein beliebiges Byte mit dem Wert $ F0 ODER-Verknüpfen:
xxxxxxxx ORA $F0 : 11110000 --------
1111 xxxx
Das Ergebnis zeigt, daß die obersten
vier Bits auf 1 gesetzt wurden, während
die anderen gleichgeblieben sind. Bei
der ODER-Verknüpfung sind es jetzt die
Einsen des Operanden $ F0, die eine
Veränderung des Akkuinhaltes bewirken.
Vergleichen Sie das Ergebnis mit dem
Beispiel der AND-Verknüpfung, so stellen
Sie fest, daß dort die Nullen des
zweiten Operanden für das Ergebnis
entscheidend waren.
EOR (Exclusive OR with accumulator)
0 EOR 0 = 0 Man erhält an einer Bit- 0 EOR 1 = 1 stelle nur dann eine "1", 1 EOR 0 = 1 wenn das Bit des ersten 1 EOR 1 = 0 Operanden oder das Bit des zweiten Operanden an dieser Position auf 1 gesetzt ist.
Schaut man sich die Verknüpfungstabelle
des EXKLUSIV-ODER an, so fällt ein
Unterschied zum ORA direkt ins Auge: Nur
wenn genau ein Operand das Bit auf 1 hat, wird auch im Ergebnis dieses Bit
gesetzt, aber nicht, wenn beide
Operanden eine gesetztes Bit haben.
Ein Beispiel für eine EOR-Verknüpfung:
LDA #$9A EOR #$D1 STA $1000
Bitweise ergibt sich folgendes Ergebnis:
$9A : 10011010 EOR $D1 : 11010001 --------
01001011
Aus diesem Ergebnis läßt sich auf Anhieb
kein besonderer Sinn für diesen Befehl
erkennen. Aber was passiert, wenn der zweite Operand mit dem ersten operanden
übereinstimmt ? Richtig, das Ergebnis
der EOR-Verknüpfung wäre Null ( d. h. das
Zero-Flag wäre gesetzt) . EOR kann also
für Vergleiche benutzt werden, denn bei
Ungleichheit der Operanden erhielte man
ein Ergebnis ungleich Null.
Der EOR-Befehl hat aber noch ein weiters
Einsatzgebiet. Um dies zu veranschaulichen, verknüpfen wir den Akkuinhalt
$9 a mit $ FF:
$9A : 10011010 EOR $FF : 11111111 --------
01100101
Und siehe da, wir erhalten als Ergebnis
das Komplement des vorherigen Akkuinhates. Man muß also, um das Komplement
einer Zahl zu bekommen, diese mit EOR
$ FF verknüpfen.
Der BIT-Befehl
Eigentlich hat der Bit-Befehl nicht sehr
viel mit den logischen Verknüpfungen zu
tun. Man kann ihn eher zu den
Vergleichsbefehlen zählen. Mit BIT führt
man eine UND-Verknüpfung zwischen dem
Akkuinhalt und der angegebenen
Speicherstelle durch. Dabei wird aber
der Akkumulatorinhalt nicht verändert.
Das Ergebnis dieser Verknüpfung wird in
der Zero-Flagge festgehalten, da diese
auf 1 gesetzt ist, falls das Ergebnis 0 war. Andernfalls ist das Zero-Flag
gelöscht.
Die Adressierung des BIT-Befehles kann
nur absolut erfolgen:
LDA #$9A BIT $1500
Bei diesem Beispiel wollen wir annehmen, daß in Speicherstelle $1500 der Wert $ D1 abgelegt ist. Dann lautet die logische
Verknüpfung wie beim AND-Befehl:
$9A : 10011010 AND $D1 : 11010001 --------
10010000
Der Akkumulatorinhalt wird diesmal
jedoch nicht verändert, und das Z-Flag
ist gelöscht, da das Ergebnis ungleich 0 ist.
Der NOP-Befehl
Zum Abschluß des diesmaligen Kursteiles
soll hier noch ein ganz einfacher befehl
vorgestellt werden. Der NOP-Befehl tut
nämlich nichts. Aber wozu kann man einen
Befehl gebrauchen, der nur zwei Taktzyklen abwartet und ein Byte
Speicherplatz beansprucht, aber sonst
keine Wirkung hat ? Man nimmt ihn als
Platzhalter innerhalb des ASSEMBLER-Programmes. Wenn man bei seinem Programm
plötzlich feststellt, daß man noch einen
Befehl vergessen hat, muß man
normalerweise den gesamten Programmcode
im Speicher verschieben. Hat man aber
z. B drei NOP' s hintereinander geschrieben, so kann man diese einfach mit
einem JMPoder JSR-Befehl überschreiben. Durch einen solchen " Notausstieg" hat man die Möglichkeit, weitere
Befehle einzufügen.
Wie immer finden Sie auf der Diskette
ein Begleitprogramm mit vielen
nützlichen Tips. Starten Sie dazu das
Programm " ASSEMBLER-KURS 6" aus dem
Gamesmenü.
Ralf Trabhardt/( wk)
Assembler-Kurs Teil 7
Bevor wir uns den ROM-Routinen des C64 zuwenden, werden noch die vier
Bit-Verschiebebefehle ASL, LSR, ROL und
ROR erklärt. Diese Befehle benötigen wir
erst im Kursteil 9 bei der Multiplikation und Division von Ganzzahlen.
Trotzdem sollen diese Befehle schon
jetzt behandelt werden, da Sie in der
Folge 9 genug Probleme mit dem
Verständnis der Fließkomma-Zahlen haben
werden und nicht noch zusätzlich mit
neuen Befehlen konfrontiert werden
sollen.
Die Verschiebebefehle
ASL (Arithmetic Shift Left)
Bei diesem Befehl wird der gesamt Akkuinhalt, oder der Inhalt der angegebenen
Speicherstelle bitweise nach links verschoben. Das Bit 7 wird in das Carry-Bit ausgelagert, in Bit 0 des Bytes wird
eine 0 eingefügt.
7 6 5 4 3 2 1 0 Carry <= * * * * * * * * <= 0
Ein einmaliges Linksverschieben entspricht einer Verdoppelung der ursprünglichen Zahl.
Beispiel: LDA #$19 ASL STA $1000
$19 entspricht der binären Bitfolge:
00011001 . Ein ASL führt nun zu 00110010($32) und einer 0 im Carry-Bit. Das
Ergebnis der Verschiebung wird in der
Speicherstelle $1000 abgelegt.
Aber neben der Möglichkeit der Verdoppelung einer Zahl kann man mit ASL
auch bequem die Bits einer Speicherstelle über das Carry-Bit testen. Es werden das Negativund das Zero-Flag
beeinflußt.
Der Befehl ASL bietet folgende
Adressierungsmöglichkeiten:
ASL Akku-orientiert ASL $1000 absolut ASL $1000,X absolut X-indiziert ASL $FA Zeropage-absolut ASL $FA,X Zeropage-abs. X-indiziert
LSR (Logical Shift Right)
LSR bildet das Gegenstück zu ASL. Das
betroffene Bit wird nun nach rechts
geschoben. Dabei gelangt das bit 0 in
das Carry-Bit und in Bit 7 wird eine 0 eingesetzt.
7 6 5 4 3 2 1 0 0 => * * * * * * * * => Carry Beispiel: LDA #$19 LSR STA $1000
Aus der Bitfolge 00011001 wird durch LSR
ein 00001100 . Außerdem enthält das
Carry-Bit jetzt die links aus dem Byte
geschobene 1 . Das Ergebnis wird wieder
in der Speicherstelle $1000 abgelegt.
LSR hat in diesem Fall die dezimale Zahl
25($19) ganzzahlig halbiert. Das
gesetzte Carry-Bit signalisiert dabei, daß ein Rest aufgetreten ist (25 :2=12 Rest 1) .
LSR erlaubt die selben Adressierungsarten und beeinflußt die gleichen
Falggen wie der ASL-Befehl.
ROL (ROtate Left)
Der ROL-Befehl hat große Ähnlichkeit mit
dem ASL, nur daß das Byte jetzt nicht mehr nur verschoben wird, sondern um 1 Bit rotiert. Wie auch bei ASL landet das
Bit 7 im Carry. Das Carry-Bit wird
anschließend in Bit 0 übertragen. Somit
ist das Bit 7 schließlich in Bit 0 gelangt.
7 6 5 4 3 2 1 0 Carry <= * * * * * * * * <= Carry Beispiel: LDA #$B1 ROL STA $1000
In den Akku wird die Bitkombination
10110001 geladen. Nach dem ROL befindet
sich der Wert 01100011($63) im Akku, der in die Speicherstelle $1000 abgelegt
wird. Das Carrybit enthält ebenfalls
eine 1 .
ROR (ROtate Right)
Wie nicht anders zu erwarten war, ist
ROR wieder das Gegenstück zu ROL. Jetzt
wird das Byte nicht um ein Bit nach
links, sondern nach rechts rotiert. Es
befindet sich daher Bit 0 im Carry. Der
Inhalt des Carry-Bits rotiert nun in Bit
7 hinein.
7 6 5 4 3 2 1 0 Carry => * * * * * * * * => Carry Beispiel: LDA #$B1 ROR STA $1000
Auch für ROR wird das Beispiel durchgeführt. Aus 10110001 wird nach ROR die
bitfolge 11011000 . Das Carry-Bit ist
auch in diesem Beispiel gesetzt.
Die zuletzt besprochenen Befehle lassen
dieselben Adressierungsarten wie der
ASL-Befehl zu, und auch sie verändern
die Flaggen N und Z.
ROM-Routinen
Unsere Assembler-Programme stehen immer
im RAM ( Random Access Memory), dem
Lese-/ Schreib-Speicher. In das ROM ( Read
Only Memory) können wir nicht hineinschreiben, es kann nur gelesen werden.
Folglich sind die oben erwähnten ROM-Routinen einige gegebene unterprogramme, die wir in unsern eigenen Assembler-Programmen verwenden können.
Der Sinn der Benutzung dieser ROM-Routinen soll Ihnen am Beispiel des
" Bildschirm löschens" verdeutlicht
werden:
Bisher benötigten wir eine Schleife, die
den gesamten sichtbaren Bildschirmbereich mit dem SPACE-Zeichen "" vollgePOKEd hat. Es war also ein erheblicher
Programmieraufwand erforderlich. Nun
geht das Ganze aber viel kürzer. Wir springen einfach mit JSR ein ROM-Unter- programm an, das das Bildschirmlöschen
für uns übernimmt. Ein " Clear Screen" wird jetzt einfach mit JSR $ E544 ausgeführt.
Übrigens werden alle ROM-Routinen mit
einem JSR angesprungen, da diese
allesamt mit RTS enden.
Einige ROM-Routinen benötigen vor ihrem
Aufruf gewisse Informationen, ohne die
sie nicht fehlerfrei arbeiten können.
Zum Beispiel braucht die Routine zum
Setzen des Cursors die Zeilenposition im
X-Register und die der Spalten im Y-Register.
LDX #$00 LDY #$08 JSR $E50C
Dieses Programm setzt den Cursor in der
Zeile 0 auf die Spalte 8 .
Das folgende Programm gibt den ASCII-Text an der aktuellen Cursorposition aus
LDA #$00 LDY #$C1 JSR $AB1E
Der auszugebende Text für diese Routine
muß im Speicher an der Stelle $ C100 beginnen, und mit einem Byte $00 enden.
Als Zeiger auf diese Textstelle fungieren hierbei der Akku ( niederwertiges
Byte der Adresse) und das Y-Register
( höherwertiges Byte) . Wie Sie einen
ASCII-Text direkt im Speicher ablegen
können, verrät Ihnen das Handbuch Ihres
Assemblers. Oft gibt es dafür einen
Befehl der Art
C100 . ASC " beispieltext" Der Text liegt dann folgendermaßen im Speicher:
C100 42 45 49 53 50 49 45 4C beispiel C108 54 45 58 54 00 00 00 00 text
Eine weitere wichtige Routine ist die
Tastaturabfrage JSR $ FFE4 . Hierbei steht
der ASCII-Code der gedrückten Taste im
Akkumulator.
Das Beispielprogramm C000 JSR $FFE4 C003 BEQ $C000 überprüft, ob irgendeine Taste gedrückt wurde. In BASIC hätten wir 10 GET A$ 20 IF A$ = "" THEN 10 geschrieben.
Natürlich kann ich Ihnen in diesem Kurs
nicht alle verfügbaren ROM-Routinen vorstellen. Wenn ich aber Ihr Interesse
daran geweckt habe, dann sollten Sie
sich ein Buch zulegen, das
" dokumentierte ROM-Listings" enthält.
Aber wo genau liegt eigentlich der ROM-Bereich im Speicher?
Wir haben bis jetzt von $ AB1 E bis $ E50 C
aufgerufen. Bei $ C000-$ CFFF liegt aber
ein RAM-Bereich, den wir schon oft
genutzt haben. Es muß daher mehr als nur
einen ROM-Bereich geben.
In der Tat gibt es 3 unterschiedliche
ROMbereiche. Von $ A000-$ BFFF liegt das
ROM des BASIC-Interpreters. Dieser
Interpreter sorgt dafür, daß Sie gleich
nach dem Einschalten des Computers das
BASIC verfügbar haben.
Zwischen $ D000-$ DFFF befinden sich die
I/ O-Bausteine, die für die Einund
Ausgaben verantwortlich sind.
Ab $ E000 bis zum Ende des Speichers
$ FFFF liegt das Betriebssystem-ROM.
Speicherbelegungsplan des C64 :
$FFFF ------------------------------ |Betriebssystem| RAM | $E000 ---------------------------------- | I/O | Zeichensatz |RAM| $D000 ---------------------------------- | RAM | $C000 ------------------------------ | BASIC-ROM | RAM | $A000 ------------------------------ | | | RAM | | | $0800 ---------------- | (RAM) | $0000 ----------------
Der mit " Zeichensatz" gekennzeichnete
Bereich enthält den Zeichensatz-Generator ( character generator), den ich
im Kursteil 8 mit der Erstellung selbstdefinierter Zeichen näher erläutern
werde.
Wenn Sie den Speicherbelegungsplan
ansehen, stellen Sie fest, daß einige
bereiche mit derselben Adresse mehrfach
belegt sind. So liegt z. B. von $ E000 bis
$ FFFF nicht nur das Betriebssystem," darunter" befindet sich auch noch ein
RAM-Bereich. Ähnlich verhält es sich mit
dem Bereich von $ D000-$ DFFF, der sogar
dreifach belegt ist.
Aber wie kann man zwischen den mehrfach
belegten bereichen auswählen?
Dazu dient die Speicherstelle $01, der
sogenannte Prozessorport. Je nachdem, welcher Wert in dieser Speicherstelle
steht, verändern sich die Zugriffsmöglichkeiten auf den Speicher.
Als Assembler-Programmierer könnten wir
zum Beispiel mit
LDA #$36 STA $01
das gesamte BASIC abschalten un das
darunterliegende RAM für unsere Zwecke
nutzen. Wir hätten somit einen
durchgehenden RAM-Speicher von $0800 bis
$ CFFF geschaffen. Leider können wir aber
nicht mehr auf die ROM-Routinen des
BASIC-Interpreters zugreifen.
LDA #$37 STA $01 stellt den Ausgangszustand wieder her und schaltet das BASIC-ROM an.
$01 $A000-$BFFF $D000-$DFFF $E000-$FFFF
$37 BASIC I/O ROM $36 RAM I/O ROM $35 RAM I/O RAM $34 RAM RAM RAM $33 BASIC Charset ROM $32 RAM Charset ROM $31 RAM Charset RAM $30 RAM RAM RAM
Diese Tabelle des Prozessorports zeigt
Ihnen die Möglichkeiten auf.
Auf den ersten Blick wirkt das mit der
Mehrfachbelegung un dem Umschalten
zwischen Bereichen alles etwas
umständlich. Anders geht es aber nicht.
Mit unserem maximal 2 Byte langem
Adressteil ( z. B. LDA $ FFFF) können wir
nur den Bereich von $0000-$ FFFF, also 64 KByte andressieren. Würde man alle
mehrfach belegten Bereiche
hintereinander schreiben, käme man aber
auf einen Speicherbereich von 64 KByte +24 KByte =88 KByte.
Als praktischen Anschauungsunterricht
laden Sie nun am Besten das Beispielprogramm aus dem Menü: ASSEMBLER-KURS 7 .
(rt/wk)
Assembler-Kurs Teil 8
Der Stapel
Der Stapel ist der Speicherbereich von
$0100 bis $01 FF, der direkt von der CPU
verwaltet wird. Der Zugriff auf diese
" Page 1" ist daher sehr schnell.
Ein Stapel wächst von oben nach unten, d. h. in unserem Fall, daß zuerst ein
Byte in $01 FF, das nächste in $01 FE usw.
abgelegt wird.
Bemerkenswert ist beim Stapel, daß er
nach dem LIFO-Prinzip ( last infirst
out) arbeitet. Das zuletzt auf den
Stapel gebrachte Byte muß also als
erstef wieder heruntergeholt werden, wenn die darunterliegenden Bytes gelesen
werden sollen.
Wenn Sie Schwierigkeiten haben, sich das
LIFO-Prinzip zu verdeutlichen, dann
denken Sie sich den Stapel doch einfach
als einen Stapel von Getränkekisten.
Dann wird Ihnen klar, daß Sie keinesfalls eine der unteren Kisten herausziehen können. Die zuletzt auf den
Stapel gebrachte Kiste, das ist die
Kiste ganz oben auf dem Stapel, muß als
erste entfernt werden, um an die
darunterliegende Kiste heranzukommen.
Aber woher weiß der Computer, welcher
Wert auf dem Stapel der oberste ist?
Dazu benötigen wir einen Zeiger
( Pointer), der auf die jeweilige Spitze
des Stapels zeigt, den sogenannten
Stapelzeiger ( Stackpointer) . Unser
Stapelzeiger ist 8 Bits breit, also
eigentlich um ein Byte zu klein für die
Adressierung des Stapelbereichs ($01 FF
bis $0100) . Sicher fällt Ihnen auf, daß
das höherwertige Byte ($01) über dem
gesamten Stapelbereich gleich bleibt. Es
ist daher unnötig, diesen Wert immer im
Stapelzeiger mit abzuspeichern. Stattdessen begnügt man sich damit, daß der
Stapelzeiger das niederwertige Byte der
Spitze des Stapels enthält.
Die effektive Adresse, auf die der
Zeiger deutet, kann man sich folglich
mit $0100+( Inhalt des Stapelzeigers) selbst errechnen.
Gültige Werte sind für den Stapelzeiger
zwischen $00 und $ FF.
So, jetzt wissen wir, wie ein Stapel
funktioniert, aber wozu man einen Stapel
benötigt, das wissen wir noch nicht.
Es gibt drei wichtige Anwendungsgebiete
für einen Stapel:
1 . Er übernimmt die Verwaltung der Rücksprungadressen bei Unterprogrammaufrufen. Diese Eigenschaft wird später
noch näher erläutert werden.
2 . Zur Zwischenspeicherung von Daten bei
Interrupts.
3 . Kurzzeitige Sicherung von Daten.
Diesen letzte Punkt wollen wir uns
nun genau ansehen.
Die Stapeloperationen
Im Grunde genommen benötigt man nur zwei
verschiedene Stapelbefehle, nämlich
einen, um etwas auf den Stapel zulegen
( PUSH) und einen, um einen Wert vom
Stapel zu holen ( PULL, POP) .
Stapel nach PUSH nach PUSH vorher: Wert1: Wert2: $01FD | | | | | |<-SP | | |--| |--| $01FE | | | |<-SP |W2| |--| |--| |--| $01FF | |<-SP |W1| |W1| ---- ---- ----
Wenn noch keine Stapeloperationen durchgeführt wurden, zeigt der Stapelzeiger
SP auf die Speicherstelle $01 FF. Nun
wird WERT1 auf den Stapel gelegt, und
zwar an der Adresse, auf die der
Stapelzeiger weist. Nach diesem Schreib- zugriff auf den Stapel wird der Stapelzeiger um 1 erniedrigt und zeigt so auf
die nächste freie Speicherstelle des
Stapels ($01 FE) . Eine weiters PUSH-Anweisung legt den WERT2 auf die Spitze
des Stapels ( Top of Stack), was der
Position des Stapelzeigers entspricht.
Anschließend erniedrigt sich der
Stapelzeiger wieder um 1 .
nach PULL Wert2 : nach PULL Wert1 :
$01FD | | | | | | |--| $01FE | |<-SP | | |--| |--| $01FF |W1| | |<-SP ---- ----
Während bei PUSH-Befehlen des Stapels zu
beobachten war, daß sich der Stapelzeiger erst nach dem Schreiben in die
Stapelspeicherstelle erniedrigt hat, stellt man nun fest, daß bei PULL- Anweisungen der Stapelzeiger vor dem
Lesezugriff inkrementiert wird. Das ist
auch nötig, da der Stapelzeiger immer
auf die nächste FREIE Speicherstelle des
Stapels zeigt. Wird der Stapelzeiger
zuvor inkrementiert, dann weist er nun
auf das zuletzt auf den Stapel geschobene Element. Dieser Wert kann nun
vom Stapel geholt werden.
Das obige Beispiel verdeutlicht, daß der
zuerst auf den Stapel gelegte Wert
( Wert1) erst als letzter vom Stapel
geholt werden kann.
Die PUSH-Befehle
PHA (PusH Accumulator)
Mit diesem Befehl wird der Akkuinhalt
auf den Stapel geschoben. Anschließend
wird der Stapelzeiger um 1 erniedrigt.
Der Akkuinhalt bleibt dabei, ebenso wie
die Flaggen, unverändert.
PHP (PusH Processor status)
Anstelle des Akkuninhaltes wird jetzt
das gesamte Statusregister des
Prozessors mit allen Flaggen auf den
Stapel gelegt.
Danach wird der Stapelzeiger um 1 erniedrigt.
Die PULL-Befehle
PLA (PuLl Accumulator)
Diese Anweisung ist das Gegenstück zu
PHA. Der Stapelzeiger wird zuerst um 1 erhöht. Nun wird der Inhalt der
Speicherstelle, auf die der Stapelzeiger
deutet, in den Akku eingelesen. Neben
dem Akkuinhalt können sich auch die
Flaggen N und Z ändern.
PLP (PuLl Processor status)
Der Stapelzeiger wird inkrementiert und der aktuelle Stapelwert wird in das
Prozessor-Statusregister übertragen.
Dabei ändern sich selbstverständlich
alle Flaggen.
Zusätzlich zu den Stapeloperationen gibt
es auch noch zwei Transferbefehle, die
sich direkt auf den Stapelzeiger
beziehen.
Die Stapel-Transferbefehle
TSX (Transfer Stackpointer to X)
Wie der Name der Anweisung schon aussagt, überträgt der Befehl den Stapelzeiger in das X-Register. Somit kann die
Adresse, auf die der Stapelzeiger weist, ermittelt werden. Das X-Register kann
dementsprechend Werte zwischen $00 und
$ FF enthalten.
TXS (Transfer X to Stackpointer)
Dieser gegenteilige Befehl zu TSX ermöglicht eine direkte Einflußnahme des
Programmierers auf den Stapelzeiger, indem der Wert des X-Registers in den
Stapelzeiger übertragen wird.
Alle oben genannten Befehle können nur
impliziert adressiert werden, d. h. sie
haben keinen Adressteil.
Der Einsatz des Stapels bei Unterprg's --------------------------------------
Bisher haben wir es als selbstverständlich erachtet, daß unser Assembler-Programm nach einem RTS bei der nächsten
Anweisung nach dem JSR im aufrufenden
Programm fortfährt.
Das Programm hat sich also die Stelle, von der aus der Unterprogrammsprung erfolgte, gemerkt. Womit wir wieder
beim Stapel wären.
Der Prozessor muß also vor jeder Verzweigung in ein Unterprogramm die momentane Adresse auf den Stapel
speichert, was der Adresse die
Instruktion nach dem JSR-Befehl entspricht. Diese Adresse muß nach einer
speziellen Regel " gestackt" werden, da
unser Stapel bekanntlich immer nur ein
Byte aufnehmen kann, eine Adresse jedoch
aus 2 Bytes besteht. Zuerst wird das
höherwertige Byte mit einem PUSH auf den
Stapel gelegt und anschließend der
Stapelzeiger um 1 vermindert. Erst jetzt
wird das niederwertige Byte auf den
Stapel gebracht. Erneut muß der Stapelzeiger dekrementiert werden.
Hauptprogramm Unterprogramm
----------- ----- | . | -> $C200 | . | | . | | | . | $C100 | JSR $C200 | --- | . | $C103 | . | <----------- | RTS | | . | -----
-----------
Der Stapel nach JSR $ C200 :
| |<-SP |---| $01FE |$03| |---| $01FF |$C1| -----
Nach einem RTS-Befehl läuft der umgekehrte Prozess ab. Zuerst wird der
Stapelzeiger um 1 erhöht und das niederwertige Byte vom Stapel geholt. Im
Anschluß daran holt man das höherwertige
Byte vom Stapel und der Stapelzeiger
wird wieder erhöht. nun kann die
aktuelle Adresse wieder zusammengesetzt
werden und das Hauptprogramm fährt mit
seiner Abarbeitung an der gewünschten
Stelle fort.
Es fällt uns auf, daß diese abgelegten
Rücksprung-Adressen denselben Stapelbereich belegen, den auch der Programmierer zur vorübergehenden Datenspeicherung benutzen kann. Da der Speicherbereich des Stapels begrenzt ist, müssen
wir auch die Struktur dieser automatisch
ablaufenden Prozesse der Unterprogramm-Verwaltung kennen.
( rt/ wk)
Assembler-Kurs Teil 9
Multipilkation und Division
Die Addition und Subtraktion von ganzen
Zahlen mit ADC und SBC war noch recht
einfach. Leider gibt es aber keine
Assmblerbefehle wie MULT oder DIV, die
unsere Berechnungen übernehmen könnten.
Wenn wir eine Multiplikation durchführen
wollen, dann müßten wir eigentlich ein
Programm dafür schreiben, was allerdings
eine ziemliche Quälerei wäre, da wir das
Problem über Additionen lösen müßten.
Glücklicherweise gibt es auch für dieses
Problem eine geeignete ROM-Routine
($ B357) . Der Faktor muß dafür in den
Speicherstellen $28 und $29, der zweite
Faktor in $71/$72 bereitgestellt werden.
Das Ergebnis dieser 16- Bit-Multi- plikation erhalten wir im X-( niederwertiges Byte) und Y-Register ( höherwertiges Byte) .
Dummerweise existiert keine entsprechende Routine für die Division. Hier
tritt außerdem noch das Problem auf, daß
beim Dividieren zweier ganzer Zahlen in
den seltensten Fällen das Ergebnis eine
ganze Zahl sein dürfte. Jetzt kommen wir
um die gebrochenen Zahlen nicht mehr
herum.
Die Flißkommazahlen (Floating Point,FLP)
Gebrochene Zahlen werden im allgemeinenn
als Fließkommazahlen bezeichnet. Fließkommazahlen bestehen immer aus drei
Teilen: der Mantisse, der Basis und dem
Exponenten.
Betrachten wir zunächst eine dezimale
FLP-Zahl:4500 ist darstellbar als 4 .5*10↑3, was der Darstellung 4 .5 E3 entspricht (4 .5 ist die Mantisse,3 der
Exponent und 10 die Basis) . Die Zahl
0 .045 ließe sich auch als 4 .5*10↑-2(4 .5 E-2) schreiben. beachten Sie bitte, daß beide Zahlen auf dieselbe Matissenform gebracht wurden und sich
lediglich noch im Exponenten
unterscheiden. Das haben wir durch ein
Linksbzw. Rechtsverschieben des
Dezimalpunktes erreicht.
Bei dezimalen FLP-Zahlen ist das alles
noch recht einfach, aber wie kann man
binäre FLP-Zahlen in ein einheitliches
Format bringen, so daß alle Zahlen
dieselbe Anzahl von Bytes haben ?
Betrachten wir das Problem anhand eines
Beispiels:
Die Dezimalzahl 50 .125 soll in eine
binäre FLP-Zahl umgewandelt werden. Die
Umwandlung erfolgt in 5 Schritten:
1 . Vorkommateil umwandeln Der Vorkommateil wird als Integerzahl
behandelt und wie gewohnt umgerechnet
50 : 2 = 25 Rest: 0 niederw. Bit 25 : 2 = 12 Rest: 1 12 : 2 = 6 Rest: 0 6 : 2 = 3 Rest: 0 3 : 2 = 1 Rest: 1 1 : 2 = 0 Rest: 1
Das Ergebnis :110010
2 . Nachkommateil umwandeln Die Stellen nach dem Binärpunkt haben
die Wertigkeiten 2↑-1,2↑-2 usw.
Bei der Berechnung muß daher die
Dezimalzahl immer mit 2 multipliziert
werden, bei auftretenden Vorkommastellen wird das jeweilige Bit
gesetzt.
0.125 * 2 = 0.25 Vorkommast.: 0 0.25 * 2 = 0.5 Vorkommast.: 0 0.5 * 2 = 1 Vorkommast.: 1 n.Bit
Das Ergebnis : .001
3 . Normalisierung der Mantisse Unter Normalisierung versteht man das
Anpassen der Mantisse an ein bestimmtes Format. Der Binärpunkt muß
soweit verschoben werden, bis er
links genau neben der höchstwertigen
binären 1 steht.
Vorherige Mantisse: 110010.001 Normalisierte Man.: 0.110010001 * 2↑6
In unserem Bespiel mußte der Binärpunkt um 6 Stellen nach links verschoben werden, was durch eine Multiplikation mit 2↑6 ausgegelichen
werden muß, damit das Ergebnis nicht
verfälscht wird.
Die Mantisse ist nun in der richtigen
Form und wir haben auch unseren binären Exponenten (69 gefunden. Die
binäre Basis ist logischerweise 2 .
4 . Umwandlung des Exponenten Zu unserem Exponenten 6 wird nun noch
die Zahl 128 hinzuaddiert, damit es
möglich ist, auch negative Exponenten, die bei der Normalisierung der
Mantisse entstehen können, darzustellen. Bei der Rückumrechnung
( Binär -> Dezimal) ist daher daruaf
zu achten, daß vom binären Exponenten
128 abzuziehen ist.
6 + 128 ------- 134 (binär : 10000110)
Ergebnis : Mantisse:0 .110010001 Exponent:10000110
Nun muß nur noch festgelegt werden, in wievielen Bytes diese Zahl abgespeichert werden soll.
5 . Floatingpoint-Zahlen des C64 Dieser Computer kennt zwei verschiedene Fließkommaformate, die sich
hauptsächlich durch die Art der Speicherung des Vorzeichens unterscheiden. Die eine wird nur innerhalb der
beiden Fließkommakkumulatoren verwendet, die andere beim Abspeichern der
Ergebnisse im Arbeitsspeicher.
a) In den beiden Fließkommaakkumulatoren
( abgekürzt: FAC und ARG) werden alle
Berechnungen von Fließkommazahlen mit
Hilfe von ROM-Routinen durchgeführt.
Der FAC belegt in der Zeropage den
bereich von Speicherstelle $61 bis
$66, ARG von $69 bis $6 E. Welche Aufgabe die einzelnen Bytes havben, können Sie der nachfolgenden Tabelle
entnehmen.
FAC ARG -------------------- Exponent $61 $69 Mantisse 1 $62 $6A Mantisse 2 $63 $6B Mantisse 3 $64 $6C Mantisse 4 $65 $6D Vorzeichen $66 $6E
Schauen wir nun, wie die FLP-Zahl aus
unserem Beispiel im FAC oder ARG
abgelegt wird. Der Exponent erhält 8 Bits, während sich die Mantisse auf 32 Bits ausbreiten darf. Die normalisierte
Mantisse wird aber ab dem Binärpunkt
linksbündig in die zur Verfügung
stehenden Bytes eingetragen. Ist die
Mantisse länger als 4 Bytes, so wird der
überschüssige Teil einfach weggelassen.
So entstehen u. a. die heißgeliebten
Rechenfehler in einem Computer.
Zusätzlich gibt es noch ein Byte, das
das Vorzeichen enthält. Von diesem Byte
ist jedoch nur das Bit 7 von Bedeutung(0-> positiv ;1-> negativ), die
anderen Bits sind uninteressant.
Format der Fließkommaakkumulatoren:
/--------\ |10000110| \--------/ Exponent /--------+--------+--------+--------\ |11001000|10000000|00000000|00000000| \--------+--------+--------+--------/ 1 2 3 4 M A N T I S S E /--------\ |0xxxxxxx| \--------/ Vorzeichen
In hexadezimaler Form ergibt das:
86 C880000000
Sicher halten Sie die benutzung eines
ganzen Bytes für nur ein Vorzeichenbit
für Verschwendung. In den Flißkommaakkumulatoren stört das jedoch
niemanden, da es ohnehin nur zwei davon
gibt. Außerdem erleichtert das getrennte
Vorzeichen die Berechung bei der Fließkommaarithmetik.
Hier nun eine kleine Auswahl der
möglichen Rechenoperationen:
/----------------------------+-------\ |Rechenoperation |Routine| +--------------+-------------+-------+ |Addition |FAC :=ARG+FAC| $B86A | |Subtraktion |FAC :=ARG-FAC| $B853 | |Multiplikation|FAC :=ARG*FAC| $BA2B | |Division |FAC :=ARG/FAC| $BB12 | |Potenzierung |FAC :=ARG↑FAC| $BF7B | \--------------+-------------+-------/
Vor dem Aufruf der Routinen müssen FAC
und ARG natürlich die gewünschten Zahlen enthalten.
Wie Sie sehen, enthält der FAC immer das
Ergebnis der Berechnungn. Spontan fallen
uns zwei Probleme auf: Wie bekomme ich
die Zahlen in den FAC oder ARG und wie
kann ich das Ergebnis abspeichern, damit
es erhalten bleibt? Auch dafür stehen
uns wieder einige ROM-Routinen zur Verfügung, die Sie gleich noch kennenlernen
werden. Zunächst jedoch zurück zu dem
zweiten Fließkomme-Format von demm die
Rede war.
b) Es handelt sich um das Format, in dem
die Fließkommazahlen im Speicher abgelegt werden, entweder als Ergebnis einer
Berechung, oder als Operanden für die
Rechenoperationen in FAC und ARG. Ein
Format, das sich auf den Arbeitsspeicher
bezieht, sollte möglichst kurz und
speicherplatzsparend sein. Ein eigenes
Byte für ein Vorzeichen ist jetzt völlig
ausgeschlossen. Die ganze Fließkommazehl wird nun durch einen kleinen Trick auf 5 Bytes beschränkt.
Wir wissen, daß rechts neben dem Binärpunkt der normalisierten Mantisse eine 1 steht ( deshalb haben wir den Punkt ja
dorthin gesetzt) . Wenn uns sowieso
bekannt ist, daß es sich um eine 1 handelt, dann brauchen wir sie doch
nicht mehr mit abzuspeichern. Diese 1 muß lediglich bei allen Berechnungen
berücksichtigt werden. Diese Bits nennt
man Hidden Bits ( versteckte Bits) . Da
das äußerst links stehende Bit der
Mantisse nun frei geworden ist, können
wir dort unser Vorzeichen unterbringen.
Tatsächlich benötigen wir jetzt nur noch
5 Bytes für die Zahlendarstellung.
Format im Arbeitsspeicher:
/--------\ |10000110| \--------/
Exponent
/Vorzeichen-Bit /+-------+--------+--------+--------\ |01001000|10000000|00000000|00000000| \--------+--------+--------+--------/ 1 2 3 4 M A N T I S S E
Hexadezimal erhält man:8648800000
ROM-Routinen zur Handhabung der Fließkommazahlen
Innerhalb dises Kursteiles kann ich
Ihnen leider nur eine kleine Auswahl
dieser ROM-Routinen vorstellen.
$ B3 A2| Wandelt eine vorzeichenlose ganze
| Zahl (0 . . .255), die im Y-Register
| steht, in eine FLP-Zahl im FAC um.
| $BBA2|Lädt FAC mit einer FLP-Zahl aus |dem Arbeitsspeicher, auf die der |Zeiger Akku/Y-Register weist und |wandelt sie in das benötigte 6- |Byte-Format um. | $BA8C|Lädt ARG mit einer FLP-Zahl aus |dem Arbeitsspeicher, auf die der |Zeiger Akku/Y-Register weist und |wandelt sie in das benötigt 6- |Byte-Format um. | $BC0C|Kopiert den Inhalt des FAC in den |ARG. | $BBFC|Kopiert den Inhalt des ARG in den |FAC. -----+---------------------------------- $B7F7|Wandelt den Inhalt des FAC in eine |vorzeichenlose Integerzahl (0... |65535) in den Speicherzellen $14/ |$15 und Y-Register/Akkumulator um. |
$ BDDD| Wandelt den Inhalt von FAC in eine
| ASCII-Zeichenkette und legt sie im
| Puffer am $0100 ab.
|$ BBD4| Wandelt den FAC ( Rechenergebnis)| in eine 5- Byte-FLP- Zahl und legt | sie im Speicher under der Adresse | ab, die der Zeiger X-Register/ Y-| Register bildet.
Die USR-Funktion ( User callable machine
language SubRoutine)
Bisher waren wir es gewohnt, unsere
ASSEMBLER-Programme von BASIC aus mit
SYS-Befehlen aufzurufen. Die Parameterübergabe konnte nur durhc das POKEn der
Werte an die jeweilige Adresse erfolgen.
Wenn der zu übergebende Wert jedoch eine
FLP-Zahl ist, dann müßte die Zahl in
BASIC zerlegt werden, um sie dann stückweise in den FAC zu POKEn. Das wäre doch
sehr umständlich. Speziell für das Bearbeiten von Fließkommazahlen gibt es
daher die USR-Funktion. Der Vorteil
dieser Funktion ist, daß der Inhalt der
angegebenen Variablen automatisch in den
FAC übertragen wird. Aber woher weiß der
Computer , wo unsere ASSEMBLER-Routine
beginnt? Bei der Ausführung von USR wird
der indirekte Sprung JMP ($0311) ausgeführt. Die Einsprungadresse des
ASSEMBLER-Programms muß als Zeiger in
den Adressen $0311( niederwertiges Byte) und $0312( höherwertiges Byte) vor dem
Aufruf bereitgestellt werden.
Unter anderem enthält das Programm
" ASSEMBLER-KURS 9" auch ein Beispiel für
die Benutzung von USR.
Im nächsten ( und letzten) Kursteil geht
es dann um den Umgang mit den
Interrupts.
Also, bis zum nächsten Kursteil... (rt/wk)
Assembler-Kurs Teil 10
Wie versprochen, handelt der letzte Teil
dieses Kurses von en Interrupts.
Interrupt bedeutet soviel wie " Unterbrechung" des laufenden Programms. Der
C64 reagiert auf verschiedene Arten von
Interrupts, die man in Hardund Softwareinterrups unterteilen kann.
Hardware-Interrupts
Es gibt zwei unterschiedliche Arten von
Hardware-Interrupts, den IRQ ( interrupt
request = Interrupt Anforderung) und den
NMI ( non maskable interrupt = Nicht
maskierbarer Interrupt) . Beide sind
eigentlich Pins am Prozessor, an denen
durch ein Impuls ein entsprechender
Interrupt ausgelöst werden kann. Die
Reaktion des Prozessors auf beide
Interrupts ist etwas unterschiedlich.
Grundsätzlich sagt man, daß der NMI die
höhere Priorität beider Interrupt-Arten hat und somit für die für das System
wichtigen Aufgaben eingesetzt wird.
a) Ablauf eines NMI:
1 . Der momentan zu bearbeitende Befehl
des laufenden Programms wird noch ausgeführt.
2 . Der Programmzähler ( program counter), d. h. das Register, das immer auf den nachfolgenden Befehl eines Programms zeigt, wird auf den
Stack geschoben. Dabei wird zuerst das höherwertige und dann das niederwertige Byte des Programmzählers abgelegt.
3 . Das Statusregister, das alle Flags enthält, wird auf den Stack gebracht.
4 . Der Inhalt der Adressen $ FFFA ( als niederwertiges Byte) und $ FFFB ( als
höherwertiges Byte) wird zusammengesetzt und als neuer Programmzähler benutzt. Ab jetzt wird also
das ( unterbrechende) Programm an der Stelle ausgeführt, auf die der neue Programmzähler zeigt.
b) Ablauf eines IRQ:
1 . Zunächst scheint der IRQ genauso zu verlaufen, wie der NMI. Der augenblicklich zu bearbeitende Befehl des laufenden Programms wird
noch vollständig abgearbeitet.
2 . Anschließend erfolgt eine Überprüfung des Interrupt-Flags ( Bit 2 des Statusregisters) . Wenn das Bit gesetzt ist, dann wird die Interrupt-Anforderung einfach ignoriert, und der Prozessor fährt fort, als ob nichts geschehen wäre.
Ist das Interrupt-Bit jedoch gelöscht, so wird der IRQ ausgeführt.
3 . Der Programmzähler wird auf den Stack geschrieben.
4 . Das Statusregister wird gestackt.
5 . Nun wird das Interrupt-Flag gesetzt, so daß alle nachfolgenden Interrupt-Anforderungen ignoriert werden. Man bezeichnet diesen Vorgang als ' Sperren' des IRQ.
6 . Der Inhalt der Adressen $ FFFE und $ FFFF wird zusammengesetzt und als neuer Programmzähler benutzt.
Der Hauptunterschied zwischen NMI und
IRQ liegt darin, daß der IRQ maskiert
( d. h. gesperrt) werden kann.
Wie oben bereits beschrieben, wird beim
IRQ zuerst das I-Flag untersucht und
erst daraufhin entschieden, ob ein
Interrupt auszuführen ist, oder nicht.
Dieses Interrupt-Flag des maskierten
Interrupts IRQ kann vom Programmierer
selbst direkt beeinflußt werden.
Mit dem Befehl SEI ( SEt Interrupt mask) wird das I-Flag gesetzt und somit werden
alle folgenden IRQs gesperrt. Durch CLI( CLear Interrupt mask) kann dieser
Zustand wieder aufgehoben werden, indem
das I-Flag gelöscht wird. Ein IRQ ist
nun für den nachfolgenden Programmteil
wieder zugelassen. Häufige Anwendung
finden diese beiden Befehle auch beim
Umschalten der Speicherkonfiguration
durch den Prozessorport. Ohne das
vorherige Abschalten der Interrupts
mittels SEI kann es beim Ausschalten von
ROM-Berichen zum Systemabsturz kommen.
Wodurch können die Interrupt IRQ und NMI
ausgelöst werden ?
Selbstverständlich nur durch die Hardware des Computers:
IRQ: a) durch den Videocontroller VIC b) durch den CIA-Baustein ($ DC00)
NMI: a) durch den CIA-Baustein ($ DD00) b) durch die RESTORE-Taste
Der Software-Interrupt
Software-Interrupts werden durch den
Befehl BRK ( BReaK) ausgelöst. Das war
einer der ersten Befehle, die wir in
diesem Kurs kennengelernt haben. Nun
wollen wir untersuchen, was genau
passiert, wenn der Prozessor auf einen
BRK-Befehl im Programm stößt:
1 . Das Break-Flag ( Bit 4 des Statusregisters) wird gesetzt.
2 . Der Programmzähler wird auf den Stack
gebracht.
3 . Das Statusregister wird gestackt.
4 . Nun wird das Interrupt-Flag gesetzt, so daß alle IRQs gesperrt werden.
5 . Der Inhalt der Adressen $ FFFE und $ FFFF ( dieselben Adressen wie beim IRQ) wird zusammengesetzt und als neuer Programmzähler benutzt.
Meistens wird der BRK-Interrupt von den
Assembler-Monitoren dazu genutzt, um das laufende Programm abzubrechen und das
Statusregister mit den aktuellen Flags
anzuzeigen. Es wäre aber auch eine
andere Nutzung dieses Interrupts
möglich.
Der Software-Interrupt BRK wird also
im Gegensatz zu den Hardware-Interrupts
durch einen Assembler-Befehl ausgelöst.
Egal, ob IRQ, NMI oder der Softwareinterrupt BRK. Eines haben alle
gemeinsam. Sie lesen alle einen Zeiger
aus ($ FFFE/ FFFF bei IRQ, BRQ;$ FFFA/$ FFFB
bei NMI) der auf eine ROM-Routine weist.
Diese ROM-Routinen werden nun als
Interruptprogramm abgearbeitet und
führen am Ende einen unbedingten Sprung
aus. Dieser unbedingt e Sprung beruft
sich auf einen Zeiger, der im RAM steht.
Diese Zeiger sind: für IRQ $0314/$0315 für BRK $0316/$0317 für NMI $0318/$0319
Diese Zeiger sind schon mit einem
bestimmten Wert initialisiert, so daß
der Sprung ohne unser Eingreifen bestens
funktioniert. Wie Sie schon ahnen
werden, müssen wir diesen Zeiger
lediglich auf unsere selbstgeschriebenen
Routinen umbiegen, und schon haben wir
eigene Interruptroutinen.
Das Ende der Interrupts: RTI
Wenn das Programm durch einen Interrupt
unterbrochen wurde und der Prozessor nun
eine Interrupt-Routine bearbeitet, dann
muß ihm auch irgendwie kenntlich gemacht
werden, wann diese Unterbrechung beendet
werden soll. Auch die Interruptroutinen
benötigen daher ein Programmende. Für
' normale' Programme haben wir als
Kennzeichnung für das Programmende immer
den RTS-Befehl ( ohne vorherigen
JSR-Befehl) im Hauptprogramm benutzt, wodurch wir zurück ins BASIC kamen.
Der Befehl für das Ende unser selbstgeschriebenen Interrupt-Routine lautet RTI
( ReTurn from Interrupt) . Bei der Bearbeitung dieses Befehls läuft der
umgekehrte Prozeß des Interrupt-Aufrufs
ab.
1 . Das Statusregister muß vom Stack geholt werden.
2 . Der Programmzähler wird vom Stack geholt und wieder als aktueller Zähler benutzt.
3 . An der Stelle, auf die der Programmzähler zeigt, wird das unterbrochene Programm fortgeführt.
Der Hauptunterschied zwischen RTS und
RTI liegt darin, daß beim RTI zusätzlich
zum Programmzähler auch noch das Statusregister vom Stack geholt werden muß.
Ein Interrupt hat doch einige Ähnlichkeiten mit einem Unterprogrammaufruf.
Ein Unterprogramm wird jedoch von einer klar definierten Stelle im Hauptprogramm
( durch den JSR-Befehl) aufgerufen. Die
Interrupts NMI und IRQ werden jedoch
hardwaremäßig ausgelöst, so daß die
Unterbrechung des Hauptprogramms an
jeder beliebigen Stelle stattfinden
kann. Diese Eigenschaft erweckt den
Eindruck, daß die Interruptroutine und
das Hauptprogramm parallel arbeiten, was
aber natürlich nicht der Fall ist.
Im Begleitprogramm " ASSEMBLER-KURS10" werden wir eigene Interrupt-Routinen
schreiben. Dabei geht es um die
Benutzung des Systeminterrupts und der
Interrupts durch den Videocontroller
( Rasterzeilen-Interrupt) .
Dies war der letzte Teil des Assembler- Kurses. Ich hoffe, er konnte Ihnen einige Kenntnisse und Anregungen verschaffen. rt/wk