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 be- mühen, die komplizierten Fachbegriffe zu umgehen und leicht verständliche Erklär- ungen 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 demon- strieren kann. DAS WERKZEUG: Zum Programmieren in Assembler benötigen Sie unbedingt ein Programm, das die Be- fehle in reine Maschinensprache umwan- delt. Diese Programme werden Assembler oder Monitore genannt. Ein solches Pro- gramm 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 Programmier- veruche 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 A9 08 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 vor- gestellt. 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 --- lädt den nachfolgenden Wert oder den Inhalt der folgenden Speicheradresse in das X-Register.
Beispiel: LDX #$0a 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 X- bzw. Y-Regi- sters in die angegebene speicherstelle geschrieben. Sie bemerken sicherlich die enge Be- ziehung zwischen dem Akku und den Index- registern. Für einen schnelleren Daten- austausch zwischen diesen Registern sind die Befehle TAX, TXA, TAY und TYA ver- antwortlich. 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 X- bzw. Y-Registers in den Akku geschrieben, ohne daß sich das X- bzw. Y-Register verändert. DEX, DEY und DEC ---------------- Während DEX und DEY das jeweilige Indexregister um 1 erniedrig- en, 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 jeweili- gen 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 Zu- sammenstellung der neuen Befehle. Ich wünsche Ihnen viel Erfolg beim Aus- probieren 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 Grund- lage 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 auto- matisch 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 Sub- trahieren 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 er- gibt, 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 Dezi- malmodus ausgeführt wird. Normalerweise ist dieses Bit ge- lö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 ge- setzt, wenn ein Überlauf einge- treten ist, d.h. wenn der Zahlen- bereich ü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 ($7F) ist, wird diese Flagge ge- setzt. Wie oben erwähnt, gibt es einen Modus, in dem Zahlen, die größer als $7F sind (7. Bit ge- setzt), 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 $7F geladen wurde, wird nun die Negativ-Flagge gesetzt. Wir benötigen in diesem Kursteil ledig- lich die Flaggen C,Z,N und V. Die anderen Flaggen sollten hier nur kurz erwähnt werden. Da Sie nun die Be- deutung der Flaggen kennen, wird Ihnen die Registeranzeige Ihres Maschinen- sprache-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 be- liebigen 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 Akkumulatorin- halt 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 aufge- treten ist. Zero-Flagge : gelöscht, da die Differenz ($04) größer als Null ist. Negativ-Flagge: gelöscht, da $04 kleiner als $7F 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 Verzwei- gung nicht gegeben ist, wird dieser Be- fehl 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ög- licht. Es wird also die Sprungadresse nicht direkt angegeben, sondern nur die Entfernung zum Sprungziel. Wenn Sie jede Sprungadresse selbst aus- rechnen 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 Verzweigungsbe- fehl 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 Assembler- befehle und die Erstellung eines Pro- grammablaufplans. 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 Pro- gramm 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 nieder- wertigen Byte (hier: $00). Das höher- wertige Byte entspricht also den ersten beiden Stellen, das niederwertige den letzten beiden Stellen der Hex-zahl. 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 selbst- stä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 Assemblerprogram- mierung 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 Programmab- laufpläne. Bei diesen wird durch ein- fache 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 ganz- zahligen Werten (genannt: Integers) be- schäftigen. Dabei kommt man allerdings um die Kenntnis der Flaggen des Status- registers (bekannt aus Kursteil 3) nicht herum. Bevor wir munter addieren und subtra- hieren, 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 inver- tiert: aus jeder 0 wird eine 1 und umge- kehrt) und +1 dazuaddieren. Das Komple- ment 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 kenn- zeichnet. 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 #$6B (=107) steht jetzt im Akkumulator und in der Speicherstelle $1500. Ein weiteres Rechenbeispiel:
115 01110011 + 14 + 00001110 ----- --------- 129 10000001
Hier ist offensichtlich etwas schiefge- gangen. 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 von- einander 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 ge- setzt 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 ubeab- sichtigtes Verlassen des Zahlenbereichs. Der SBC-Befehl hat also große Ähnlich- keit 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 vorzeichenbehaftet- en Zahlen richtig, da -6 ($FA) und -4 ($FC) im Zweierkomplement gegeben sind. Nun vergessen Sie mal das Zweierkomple- ment 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är-kodierten" Dezimal- zahlen 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 Kombination- en möglich wären. Sechs der Kombi- nationsmöglichkeiten bleiben daher unge- nutzt. 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: 00000001 00101000. 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 losprogram- miert. Auch diesmal gibt es selbstver- stä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 an- gegebenen 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 $9A (=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 $9A 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 auf- fallen, 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 Einsatz- gebiet des AND-Befehls gefunden, nämlich das gezielte Löschen von Bits. Ein weiteres Beispiel soll dies ver- deutlichen. Hier verknüpfen wir eine be- liebige 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 ge- setzt 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 --------
1111xxxx 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 veranschau- lichen, verknüpfen wir den Akkuinhalt $9a mit $FF:
$9A : 10011010 EOR $FF : 11111111 --------
01100101 Und siehe da, wir erhalten als Ergebnis das Komplement des vorherigen Akku- inhates. 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 ge- schrieben, so kann man diese einfach mit einem JMP- oder JSR-Befehl über- schreiben. Durch einen solchen "Notaus- stieg" 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 Multipli- kation 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 Akku- inhalt, oder der Inhalt der angegebenen Speicherstelle bitweise nach links ver- schoben. 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 ent- spricht einer Verdoppelung der ur- sprü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 Ver- doppelung einer Zahl kann man mit ASL auch bequem die Bits einer Speicher- stelle über das Carry-Bit testen. Es werden das Negativ- und 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 Adressierungs- arten 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 Carry-bit 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 durchge- fü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 hinein- schreiben, 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 Bildschirm- bereich mit dem SPACE-Zeichen " " voll- gePOKEd 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 fun- gieren 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 vor- stellen. 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 $AB1E bis $E50C 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 ROM-bereiche. 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 Ein- und 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 selbst- definierter 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 Zugriffs- mö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 Beispiel- programm aus dem Menü: ASSEMBLER-KURS 7.
(rt/wk)
Assembler-Kurs Teil 8
Der Stapel ----------
Der Stapel ist der Speicherbereich von $0100 bis $01FF, 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 $01FF, das nächste in $01FE usw. abgelegt wird. Bemerkenswert ist beim Stapel, daß er nach dem LIFO-Prinzip (last in-first 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 keines- falls eine der unteren Kisten heraus- ziehen 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 ($01FF 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. Statt- dessen 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ück- sprungadressen bei Unterprogrammauf- rufen. 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 durch- geführt wurden, zeigt der Stapelzeiger SP auf die Speicherstelle $01FF. 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 Stapel- zeiger um 1 erniedrigt und zeigt so auf die nächste freie Speicherstelle des Stapels ($01FE). 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 Stapel- zeiger 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 ge- schobene 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 aus- sagt, überträgt der Befehl den Stapel- zeiger 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 er- mö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änd- lich 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 er- folgte, gemerkt. Womit wir wieder beim Stapel wären. Der Prozessor muß also vor jeder Ver- zweigung in ein Unterprogramm die momentane Adresse auf den Stapel speichert, was der Adresse die Instruktion nach dem JSR-Befehl ent- spricht. 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 Stapel- zeiger 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 umge- kehrte Prozess ab. Zuerst wird der Stapelzeiger um 1 erhöht und das nieder- wertige 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 Stapel- bereich belegen, den auch der Program- mierer zur vorübergehenden Datenspei- cherung benutzen kann. Da der Speicher- bereich 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- (nieder- wertiges Byte) und Y-Register (höher- wertiges Byte). Dummerweise existiert keine entsprech- ende 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.5E3 ent- spricht (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.5E-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 Links- bzw. 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 Vorkomma- stellen 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 be- stimmtes 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är- punkt um 6 Stellen nach links ver- schoben werden, was durch eine Multi- plikation 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 bi- nä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 Exponen- ten, die bei der Normalisierung der Mantisse entstehen können, darzu- stellen. 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 abge- speichert werden soll. 5. Floatingpoint-Zahlen des C64 Dieser Computer kennt zwei verschie- dene Fließkommaformate, die sich hauptsächlich durch die Art der Spei- cherung des Vorzeichens unterschei- den. Die eine wird nur innerhalb der beiden Fließkommakkumulatoren verwen- det, 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 $6E. Welche Auf- gabe 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 C8 80 00 00 00 Sicher halten Sie die benutzung eines ganzen Bytes für nur ein Vorzeichenbit für Verschwendung. In den Flißkomma- akkumulatoren 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 Ver- fü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 abge- legt 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är- punkt 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: 86 48 80 00 00 ROM-Routinen zur Handhabung der Fließ- kommazahlen Innerhalb dises Kursteiles kann ich Ihnen leider nur eine kleine Auswahl dieser ROM-Routinen vorstellen. $B3A2|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ück- weise 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) ausge- fü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 "Unter- brechung" des laufenden Programms. Der C64 reagiert auf verschiedene Arten von Interrupts, die man in Hard- und Soft- wareinterrups 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 Programm- zä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 zusammen- gesetzt und als neuer Programm- zä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 Hard- ware 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 Status- registers) 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 Software- interrupt 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 selbstge- schriebenen Interrupt-Routine lautet RTI (ReTurn from Interrupt). Bei der Bear- beitung 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 Programm- zä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 Status- register vom Stack geholt werden muß. Ein Interrupt hat doch einige Ähnlich- keiten 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