Unterprogramme
Zuletzt haben wir eine Zahl binär auf dem BS ausgegeben. Nun möchten wir zusätzlich den Hexwert anzeigen, den Text frei positionieren und unsere Routinen für eine Wiederverwendbarkeit in Unterprogramme packen. Außerdem soll ein eingegebenes Zeichen als Basis für die Ausgabe dienen. Das klingt erstmal nach einer Menge Arbeit, aber durch die Aufteilung in einzelne Probleme/Aufgaben wird alles wieder einfacher.
Programmstruktur
Beginnen wir damit unsere „Probleme“ aufzulisten:
- Zeichen vom BS lesen
- BS löschen
- Textausgabe
- Binärausgabe
- Hexausgabe
Durch die neue Möglichkeit der Unterprogramme, werden wir das Programm in Etappen entwickeln. Beginnen wir mit dem Hauptprogramm, das zunächst nur die Basis für weitere Ergänzungen darstellt. Das Programm wird nach jedem Schritt lauffähig sein, so dass ihr den Fortschritt direkt ausprobieren könnt.
Hey, ho, let‘s go…
SCREENPOS = $05e5 ;Position des Zeichens für die Ausgabe CHROUT = $ffd2 ;Jump-Table Adr.: Zeichenausgabe SETCURSOR = $fff0 ;Jump-Table Adr.: get/set cursor pos ;*** Startadresse & BASIC-Zeile *=$0801 !byte $0c,$08,$e2,$07,$9e,$20,$32,$30,$36,$32,$00,$00,$00 ;*** Start des Assemblerprogrammes lda #$15 ;Zur Sicherheit auf sta $d018 ;Großbuchstaben umschalten lda SCREENPOS ;Zeichen zwischen ( ) in den Akku laden pha ;und auf dem Stack merken lda #$93 ;BS-Löschen (wie PRINT CHR$(147) in Basic)
Zuerst legen wir drei Variablen für die von uns benötigten Adressen fest. Unser Programm wird für die Eingabe ein Zeichen von einer festen BS Position lesen, diese Position steht in SCREENPOS.
Außerdem wollen wir ja Systemroutinen verwenden. Wir werden die Routinen zur Ausgabe von Zeichen CHROUT = $ffd2 und zum Setzen bzw. Lesen der Cursorposition SETCURSOR = $fff0 verwenden. Die beiden Variablen CHROUT & SETCURSOR enthalten die jeweilige Einsprungadresse auf der sog. Jump-Table.
Die Jump-Table
Am Ende unseres Speichers, von Adresse $ff81 bis $fff5, haben die C64 Entwickler eine Jump-Table (Sprungtabelle) zu 39 Kernalfunktionen hinterlegt. Dies ist einfach eine Liste von 39 JMP-Befehlen. Sinn dieser Tabelle ist es, dass man über feste Einsprungadressen immer zur gewünschten Funktion gelangt, egal wo die sich im Speicher befindet. Damit sollte sichergestellt werden, dass Programme auch dann noch funktionieren, falls die Entwickler mal die Kernalfunktionen verschieben. Wir „springen“ also in die Jump-Table und diese springt weiter zur Kernalfunktion. Es gibt allerdings viel mehr als nur 39 interessante Funktionen im Kernal, sodass man, wenn man eine der anderen Funktionen verwenden wollte, nur das Risiko eingehen konnte diese direkt anzuspringen. Hätten sich die Entwickler dazu entschlossen diese Funktionen zu verschieben, dann wären die Programme natürlich nicht mehr lauffähig gewesen. Aber glücklicherweise haben sich die Adressen nicht verändert, es wurden im Laufe der Zeit nur Kleinigkeiten im internen Aufbau der Funktionen geändert.
Die BASIC-Zeile ist identisch zu all unseren bisherigen Programmen. Zu Beginn des Assemblerprogramms schalten wir zur Sicherheit erstmal auf Großbuchstaben um. Dies mache ich hier hauptsächlich für die Turbo Assembler-User, aber auch bei allen anderen schadet es nicht. Danach laden wir das Zeichen (genauer gesagt die Nr. des Zeichens im Char-ROM) in den Akku. Dieses Zeichen geben wir später zwischen zwei Klammern auf dem BS ein. Den Wert des eingegebenen Zeichens, wollen wir dann binär bzw. hexadezimal ausgeben. Da sich der Akku aber gleich ändert, merken wir uns den gerade ermittelten Wert auf dem Stack. Wir laden danach das PETSCII-Zeichen #$93 in den Akku, um es gleich auszugeben. Dies entspricht der euch evtl. bekannten Basic-Anweisung PRINT CHR$(147) bzw. PRINT ““, wodurch der Bildschirm gelöscht wird.
jsr CHROUT ;Jump-Table: Zeichenausgabe
JSR : Jump to SubRoutine (springe zum Unterprogramm)
JSR absolut ($20, 3B, 6T, <keine>)
Der JSR-Befehl springt zu einer beliebigen absolut angegebenen Adresse, aber im Gegensatz zum JMP, speichert er zusätzlich eine Rücksprungadresse auf dem Stack. Man benutzt ihn also, um häufiger verwendete Programmteile auszuführen. Er ist somit mit einem GOSUB unter BASIC vergleichbar.
Der genaue Ablauf des JSR sieht so aus:
- PC (ProgramCounter) +2 | Der PC zeigt also aufs MSB der angegebenen Adresse des JSR-Befehls (hier aufs $ff von $ffd2 , da im Speicher bekanntlich erst das LSB und danach das MSB gespeichert wird)
- MSB des PC auf den Stack (bei uns $ff).
- LSB des PC auf den Stack (hier $d2), da der Stapel von oben nach unten gefüllt wird, liegt unsere Rücksprungadresse also wieder wie gewohnt als LSB MSB im Speicher.
- PC auf das Sprungziel des JSR setzen
Jetzt wird unser Unterprogramm solange ausgeführt, bis es auf den bekannten RTS-Befehl trifft. Bisher haben wir RTS nur benutzt, um zurück ins BASIC zu springen.
Beim RTS wird zunächst das LSB und dann das MSB vom Stack geholt. Der PC wird dann auf diese Adresse gesetzt und abschließen wird der PC um 1 erhöht, damit wir mit dem nächsten Befehl weitermachen.
Da wir jetzt das Zusammenspiel von JSR / RTS komplett kennen, möchte ich die Gelegenheit nutzen, um nochmal auf einen häufig gemachten Fehler hinzuweisen.
Wir haben ja bereits weitere Stack-Befehle kennengelernt. Euch sollte nun klar sein, wie wichtig es ist jeden auf den Stack gelegten Wert (z. B. mit PHA) vor dem RTS auch wieder vom Stack zu holen (z. B. mit PLA). Anderenfalls springt euer Programm zu einer nicht gewollten Adresse. Im günstigsten Fall landet ihr wieder im BASIC, habt ihr weniger Glück ist ein RESET oder gar Aus-/Anschalten fällig.
Wir verwenden hier die Adresse $ffd2 von der Jump-Table, um die Kernalfunktion CharOut aufzurufen. CharOut kann dazu verwendet werden PETSCII-Zeichen auf unterschiedlichen Geräten (Bildschirm, Drucker, Floppy usw.) auszugeben. Wir verlassen uns hier einfach darauf, dass alles wie von uns gewünscht für eine Ausgabe auf den BS vorbereitet ist (das ist bei einem frisch gestarteten C64 immer der Fall). Eigentlich müssten wir eine ganze Reihe von Kernalfunktionen aufrufen, um sicher zu gehen, dass unsere Ausgabe wirklich auf dem BS stattfindet.
CharOut gibt an der aktuellen Cursorposition, das im Akku befindliche Zeichen aus. Da wir zum BS-Löschen #$93 in den Akku geladen haben, ist uns hier die Cursorposition hier erstmal egal.
;<Aufruf: Textausgabe> pla ;Unseren Akku wiederherstellen sta SCREENPOS ;und zurück zwischen die ( ) schreiben ;<Aufruf: Binärausgabe> ;<Aufruf: Hexausgabe> rts ;zurück zum BASIC ;<SUB: Textausgabe> ;<SUB: Binärausgabe> ;<SUB: Hexausgabe>
Die Kommentare wie ;<Aufruf: Textausgabe>, dienen als Platzhalter. Ich werde euch gleich nach und nach bitten den jeweiligen Kommentar durch neue Programmzeilen zu ersetzen.
Aber unseren Programmablauf können wir jetzt schon bis zum Schluß durchgehen. Wir werden also als erstes unsere Infozeile über einen Aufruf der Textausgabe vornehmen. Dann holen wir unser Zeichen vom Programmstart wieder in den Akku und schreiben es direkt in den BS-Speicher an unsere geplante Eingabestelle SCREENPOS. Anschließend geben wir direkt hinter unserer Infozeile, die Binär- und Hexzahl des eingegebenen Zeichens im Akku aus. Zum Schluß geht es per RTS wieder zurück zum BASIC. Hinter dem RTS seht ihr die Platzhalter für unsere Unterprogramme, die wir gleich entwickeln.
Der Rumpf für unser Programm ist jetzt erstmal fertig:
SCREENPOS = $05e5 ;Position des Zeichens für die Ausgabe CHROUT = $ffd2 ;Jump-Table Adr.: Zeichenausgabe SETCURSOR = $fff0 ;Jump-Table Adr.: get/set cursor pos ;*** Startadresse & BASIC-Zeile *=$0801 !byte $0c,$08,$e2,$07,$9e,$20,$32,$30,$36,$32,$00,$00,$00 ;*** Start des Assemblerprogrammes lda #$15 ;Zur Sicherheit auf sta $d018 ;Großbuchstaben umschalten lda SCREENPOS ;Zeichen zwischen ( ) in den Akku laden pha ;und auf dem Stack merken lda #$93 ;BS-Löschen (wie PRINT CHR$(147) in Basic) jsr CHROUT ;Jump-Table: Zeichenausgabe ;<Aufruf: Textausgabe> pla ;Unseren Akku wiederherstellen sta SCREENPOS ;und zurück zwischen die ( ) schreiben ;<Aufruf: Binärausgabe> ;<Aufruf: Hexausgabe> rts ;zurück zum BASIC ;<SUB: Textausgabe> ;<SUB: Binärausgabe> ;<SUB: Hexausgabe>
Wer mag kann das Programm jetzt starten. Auf den ersten Blick geschieht nicht viel, es wird nur der BS gelöscht.
Geht doch mal in die 13. Zeile. Gebt dort direkt am Zeilenanfang RUN:ABC ein und drückt anschließend RETURN. Unser Programm startet und wir sehen, dass trotz des BS-Löschens, das Zeichen B auf dem BS stehen bleibt. Wie ihr euch erinnert, wollen wir ja eine Eingabe ermöglichen und hier ist sie. Wir lesen unser Zeichen von der Stelle, an der das B steht und geben es nach dem Löschen auch wieder dort aus.
;<Aufruf: Textausgabe>
Ersetzt den Kommentar ;<Aufruf: Textausgabe> mit:
ldx #$0c ;Zeile und ldy #$00 ;Spalte für SETCURSOR jsr runtextout ;unseren Starttext 'RUN:( ) ' ausgeben
Wir werden für diese und die beiden anderen Funktionen das X– (Zeile) und Y-Register (Spalte) verwenden, um unsere Cursorposition für die Ausgaben zu bestimmen. Die Zählung beginnt jeweils bei 0!
Dann springen wir in unser Unterprogramm runtextout, um unsere Info-/Startzeile auszugeben.
;<SUB: Textausgabe>
Machen wir bei ;<SUB: Textausgabe> weiter:
;************************************************************ ;*** Den Text an der Adresse runtext ausgeben ;************************************************************ ;*** Übergabe: X = Zeile in der die Ausgabe beginnt ;*** Y = Spalte in der die Ausgabe beginnt ;*** Das Textende wird durch $00 gekennzeichnet ;************************************************************ ;*** Rückgabe: - ;************************************************************ ;*** ändert: A,X,Y,SR ;************************************************************ runtextout
Wenn man sich z. B. eine Sammlung an Hilfsroutinen anlegt, ist es keine schlechte Idee die Funktionen mit Kommentaren einzuleiten. Es sollte ersichtlich sein, was die Routine macht, welche Werte übergeben werden müssen, was evtl. zurückgeliefert wird und welche Register von der Funktion verändert werden und außen dann evtl. andere Werte zu erwarten sind. So „verschwenderisch“ wie hier könnt ihr natürlich nur bei einem Cross-Assembler, wie dem C64 Studio oder ACME sein. Mit dem Turbo Assembler, direkt auf einem C64, solltet ihr sparsamer bei den Kommentaren sein und ihr müsst außerdem noch die max. Zeilenlänge beachten!
clc ;C=0 für set / C=1 für get Cursor
CLC : CLear Carry (Carry-Flag löschen)
CLC implizit ($18, 1B, 2T, C)
Mit CLC löschen wir das Carry-Flag, wir setzen es hier also auf 0. Wichtig wird das C-Flag in Verbindung mit der Addition und Subtraktion, die werden wir in einem der nächsten Beiträge kennenlernen.
Hier benötigen wir das Carry-Flag, da die Kernalfunktion Get-/SetCursor daran festmacht, ob die Cursorposition gesetzt (C = 0) oder gelesen (C = 1) werden soll.
jsr SETCURSOR ;Jump-Table: get/set cursor ldx #$00 ;Pos. im Text runtextcharin lda runtext,x ;Aktuelles Zeichen in den Akku laden
Nachdem wir das Carry-Flag gelöscht haben, springen wir zur Kernelfunktion SETCURSOR. Diese setzt den Cursor (da C = 0 ist) auf die im X– & Y-Register angegebene Position. Danach setzen wir X auf 0. Wir verwenden das X-Register jetzt, um unser aktuelles Zeichen zu finden. Da wir einen beliebig langen Text ausgeben wollen, fügen wir das Label runtextcharin ein, um eine Schleife zu bilden. Dahinter holen wir unseren ersten Buchstaben in den Akku. Den auszugebenden Text, werden wir gleich hinter dem Label runtext ablegen. Das Textende kennzeichnen wir durch ein $00. Wir werden solange Textausgeben, bis wir auf dieses $00 treffen.
beq done ;wenn 0 dann sind wir fertig
BEQ : Branch on EQual (springe wenn gleich)
BEQ relativ ($F0, 2B, 2-4T, <keine>)
Der BEQ-Befehl prüft, ob das Zero-Flag auf 1 gesetzt ist, also ob die letzte Operation (z. B. ein Vergleich) eine Null geliefert hat. Wenn das Ergebnis also Null war (Z = 1), dann springen wir hier zum Label done, da wir fertig sind.
Wir kennen ja bereits den BNE-Befehl, der verzweigt bekanntlich, wenn das Ergebnis ungleich Null ist (also bei Z = 0). Hier gelten die gleichen Besonderheiten, was die Taktzyklen betrifft: 2T wenn nicht gesprungen wird, 3T beim Sprung, 4T falls der Sprung über die Page-Grenze hinaus geht.
Wir verwenden BEQ um festzustellen, ob wir unser Textende $00 erreicht haben. Wie ihr mittlerweile gelernt habt, setzt LDA bereits die Flags, wir benötigen daher keinen Vergleichsbefehl mehr! Sobald eine $00 in den Akku geladen wird, setzt der Computer die Z-Flagge auf 1 und wir springen dann direkt zum Label done.
jsr CHROUT ;Jump-Table: Zeichenausgeben inx ;X erhöhen, für nächstes Zeichen jmp runtextcharin ;und wieder hochspringen done ;Ziel, sobald wir eine 0 haben rts ;zurück zum Aufrufer (jsr runtextout) ;*** Unser Starttext, das Textende wird durch $00 erkannt! runtext !text "RUN:( ) " !byte $00
Wenn wir beim BEQ nicht zu done gesprungen sind, springen wir direkt zur Kernalfunktion CHROUT. Diese gibt, wie oben bereits erwähnt, das Zeichen im Akku an der aktuellen Cursorposition aus. Außerdem wird der Cursor automatisch um ein Zeichen nach rechts verschoben. Wir müssen uns also für die weiteren Zeichen nicht um die Cursor-Positionierung kümmern. Wenn wir wieder zurück in unserem Programm sind, erhöhen wir das X-Register, damit wir auf unser nächstes Zeichen im Text zeigen. Dahinter springen wir einfach wieder zum Label runtextchain, um das nächste Zeichen in den Akku zu laden. Sobald BEQ zum Label done springt, finden wir dort als letztes den RTS-Befehl und landen dann wieder im Hauptprogramm, also hinter jsr runtextout.
Zum Schluß legen wir hinter dem Label runtext noch unseren Text fest und kennzeichnen das Ende mit $00. Für die Angabe von Text verwenden !text. Dieses ähnelt der uns gut bekannten !byte-Anweisung. Bei !text handelt es sich also nicht um einen Mnemonic, sondern wieder um etwas, dass der Assembler auswertet. Der Text RUN:( ) erinnert an unser Beispiel von oben.
Damit ist auch dieser Schritt beendet, wenn ihr das Programm jetzt startet, sollte das Programm zu folgender Ausgabe führen:
Wir können jetzt zwischen den Klammern ( ) unser Zeichen für die Bin- & Hex-Ausgabe eintragen und direkt RETURN drücken. Das Programm startet dann sofort wieder, löscht den BS und gibt dann die Info-/Startzeile mit dem eingegebenen Zeichen erneut aus.
;<Aufruf: Binärausgabe>
Kommen wir zu ;<Aufruf: Binärausgabe>:
ldx #$0c ;Zeile und ldy #$08 ;Spalte für SETCURSOR jsr binaryout ;zur Binärausgabe (Akku) springen
Nachdem wir von runtextout zurück ins Hauptprogramm gekommen sind, haben wir ja den Akku vom Stack geholt und zwischen den Klammern ausgegeben. Anschließend legen wir im X– & Y-Register unsere Cursorpostion fest und geben den Akku-Inhalt mit unserem Unterprogramm binaryout, auf dem BS aus.
;<SUB: Binärausgabe>
Kümmern wir uns daher jetzt um ;<SUB: Binärausgabe>:
;************************************************************ ;*** Den Inhalt des Akkus als Binärzahl auf dem BS ausgeben ;************************************************************ ;*** Übergabe: A = Zahl, die ausgegeben wird ;*** X = Zeile in der die Ausgabe beginnt ;*** Y = Spalte in der die Ausgabe beginnt ;************************************************************ ;*** Rückgabe: - ;************************************************************ ;*** ändert: X,Y,SR ;************************************************************ binaryout pha ;Akku auf dem Stack merken (wg. SETCURSOR) clc ;C=0 für set / C=1 für get Cursor jsr SETCURSOR ;Jump-Table: get/set cursor lda #"%" ;Prozent-Zeichen in den Akku jsr CHROUT ;und ausgeben pla ;Akku wieder vom Stack holen pha ;und direkt nochmal merken ;(bis zum Rücksprung) ldy #$07 ;Schleife rückwärts von Bit 7 bis 0 binoutloop ldx #"0" ;Zeichen "0" ins X-Register
Zu Beginn merken wir uns den Akku auf dem Stack. Unsere Funktion soll den Akku unverändert lassen, damit wir den außen weiterverwenden können. Wir positionieren dann den Cursor und geben das Prozentzeichen % als Kennzeichen für eine Binärzahl aus. Da der Akku verändert wurde, holen wir ihn vom Stack und speichern ihn dort auch gleich wieder, damit wir vorm Rücksprung den original Akku-Inhalt wiederherstellen können. Im Y-Register merken wir uns die Bitposition, die wir aktuell prüfen. Wir gehen die Bits 7 bis 0 rückwärts durch.
Wer die eigentliche Binärausgabe ab dem Label binoutloop mit der aus dem letzten Beitrag vergleicht, wird feststellen, dass diese etwas anders funktioniert. Zu Übungszwecken und damit ihr seht, dass man unterschiedliche Lösungswege gehen kann, habe ich die Funktion etwas umgebaut.
Wir laden zunächst das Zeichen 0 in den Akku.
asl ;Akku nach <-links verschieben
ASL: Arithmetic Shift Left (bitweises verschieben nach links)
ASL Akku ($0A, 1B, 2T, NZC)
Den Befehl hatten wir am Ende des letzten Beitrags bereits, aber hier nochmal die Funktionsweise. Er ist das Gegenstück zum LSR, nur dass hier die Bits nach links verschoben werden. Dabei wird von rechts mit Nullen aufgefüllt und das herausfallende Bit landet im Carry-Flag.
Nur zur Info:
Ähnlich wie beim LSR, nutzen einige Assembler die Schreibweise ASL A. Das C64 Studio versteht beides, der Turbo Assembler wandelt bei der Eingabe ASL automatisch in ASL A um und ACME kennt nur ASL.
Da das Bit, dass links herausfällt, im Carry-Flag landet, können wir daran festmachen, ob wir eine 0 oder 1 haben.
bcc out ;Wenn es eine 0 ist, direkt zur Ausgabe,
BCC : Branch on Carry Clear (springe, falls das Carry-Flag gelöscht ist | C = 0)
BCC relativ ($90, 2B, 2-4T, <keine>)
Der BCC-Befehl prüft, ob das Carry-Flag auf 0 steht und springt, falls dies der Fall ist, zur angegebenen Adresse. Auch hier ist die Ausführungszeit wie bei allen Branch-Befehlen wieder variabel (vgl. BNE).
Wir testen hier, ob unser letztes Bit eine 0 war, falls ja behalten wir den Wert im X-Register und springen direkt zur Ausgabe beim Label out.
inx ;sonst erhöhen, damit wir eine 1 haben. out ;Sprungziel, wenn wir eine 0 haben pha ;Akku merken, er wird gleich überschrieben txa ;X -> Akku; Zeichen in den Akku jsr CHROUT ;Jump-Table: Zeichenausgeben pla ;Akku fürs nächste Bit wiederholen dey ;Schleife runterzählen
Haben wir aber eine 1, dann geht es nach dem BCC direkt mit dem nächsten Befehl weiter. Dort erhöhen wir den Inhalt des X-Registers und machen so aus unserem Zeichen „0“, das Zeichen „1“. Unsere Ausgabe beginnt hinter out damit, dass wir uns den Akku wieder auf dem Stack merken. Das ist, wie wir bereits erfahren haben notwendig, da wir gleich unser Zeichen aus dem X-Register in den Akku kopieren, um es über die Kernalfunktion, an der aktuellen Cursorposition auszugeben. Kommen wir von der Kernalfunktion zurück, dann holen wir den Akku auch wieder vom Stack. Wir verringern danach unsere Schleifenvariable im Y-Register um eins.
bpl binoutloop ;bis 8-Bit verarbetet sind -> binoutloop
BPL : Branch on PLus (springe wenn positiv)
BPL relativ ($10, 2B, 2-4T, <keine>)
Der nächste bedingte Sprungbefehl prüft, ob das Negativ-Flag gelöscht ist (also ob eine positive Zahl vorliegt) und verzweigt, solange N = 0 ist, zur angegebenen Adresse. Mit der Ausführungszeit verhält es sich wie bei allen Branch-Befehlen (s. BNE). Die Mathematiker unter euch werden den Namen des Befehls bemängeln, da hier auch Null als positive Zahl gewertet wird (wir wissen ja, dass die 0 in der Mathematik vorzeichenlos ist). Das Verhalten ist allerdings korrekt, da hier wie gesagt auf N = 0 geprüft wird und die 0 halt auch nicht negativ ist, verzeigt der Befehl eben auch dann zur angegebenen Adresse.
Wir wollen hier unsere Schleife solange durchlaufen, bis alle Bits geprüft wurden. Wenn der aktuelle Inhalt des Y-Registers $00 beträgt, dann führt ein DEY bekanntlich dazu, dass nun $ff im Y-Register steht und dies setzt die N-Flagge.
pla ;sonst, den 'alten' Akku wiederherstellen rts ;und zurück zum Aufrufer (jsr binaryout)
Wenn alle Bits ausgegeben wurden, dann müssen wir nur noch unseren original Akku-Inhalt vom Stack holen und kehren mit RTS zurück ins Hauptprogramm jsr binout.
Unser Programm nähert sich langsam aber sicher seinem Ende. Wenn ihr das Programm jetzt startet könnt ihr euch schon die Nr. des gewünschten Zeichens im Char-ROM binär auf dem BS ausgeben lassen.
Dass wir die Nummer aus dem Char-ROM und nicht die aus der PETSCII-Tabelle anzeigen, könnt ihr ganz einfach durch die Eingabe von A zwischen den Klammern ( ) und anschließendem RETURN kontrollieren. Im Char-ROM hat A die Nr. $01 im PETSCII $41.
;<Aufruf: Hexausgabe>
Lasst uns das Programm zu Ende führen. Kommen wir im Hauptprogramm zum Kommentar ;<Aufruf: Hexausgabe>
ldx #$0c ;Zeile und ldy #$12 ;Spalte für SETCURSOR jsr hexout ;zur Hex-Ausgabe springen
Nach der Rückkehr von binaout steht unsere Zahl für die Ausgabe ja wieder im Akku, daher können wir einfach im X– und Y-Register unsere neue Cursorposition eintragen und zur neuen Funktion hexout springen. Sobald wir von dort zurückkommen wird unser Programm durchs anschließende RTS beendet und wir landen wieder im BASIC.
;<SUB: Hexausgabe>
Ersetzt nun den letzten Kommentar ;<SUB: Hexausgabe>
;************************************************************ ;*** Den Inhalt des Akkus als Hexzahl auf dem BS ausgeben ;************************************************************ ;*** Übergabe: A = Zahl, die ausgegeben wird ;*** X = Zeile in der die Ausgabe beginnt ;*** Y = Spalte in der die Ausgabe beginnt ;************************************************************ ;*** Rückgabe: - ;************************************************************ ;*** ändert: X,Y,SR ;************************************************************ hexout pha ;Akku auf dem Stack merken (wg. SETCURSOR) clc ;C=0 für set / C=1 für get Cursor jsr SETCURSOR ;Jump-Table: get/set cursor lda #"$" ;Dollar-Zeichen in den Akku jsr CHROUT ;und ausgeben pla ;Akku wiederherstellen pha ;und merken bis zum Rücksprung pha ;gleich nochmal wg. LSR und CHROUT lsr ;jetzt msb -> ins lsb verschieben lsr ;dafür 4* LSR lsr lsr tax ;Akku ins X-Register, lda possiblehexchars,x ;das Zeichen fürs obere Nibble holen jsr CHROUT ;und auszugeben pla ;Akku vom Stack holen and #$0f ;oberes Nibble ausmaskieren tax ;Akku wieder nach X lda possiblehexchars,x ;um das Zeichen fürs untere Nibble zu holen jsr CHROUT ;und wieder ausgeben pla ;Ursprünglichen Akkuwert vom Stack holen rts ;und zurück zum Aufrufer (jsr hexout) ;*** die Hex-Ziffern possiblehexchars !text "0123456789ABCDEF"
Das Unterprogramm hexout beginnt, wie bereits binout damit, dass wir unseren Akku-Inhalt auf dem Stack speichern. Als nächstes setzen wir den Cursor an die gewünschte Position und geben wieder das Kennzeichen für die kommende Zahl (dieses Mal ein Dollar $) aus. Da der Akku verändert wurde, holen wir den gespeicherten Wert vom Stack und merken uns den sofort wieder auf dem Stack. Hier sogar 2x: Einmal fürs Ende der SubRoutine und einmal für die Ausgabe. Wir verwenden anschließend vier LSR-Befehle, um das obere Nibble ins untere zu verschieben. Dadurch erhalten wir eine Zahl zwischen $0 und $f, von links werden bekanntlich Nullen eingeschoben, im Akku. Danach kopieren wir den Akku-Inhalt ins X-Register, um unser Zeichen für die Ausgabe vom Label possiblehexchars zuholen. Dort sind einfach alle möglichen Hexzeichen von „0“ bis „F“ hinterlegt. Über das X-Register greifen wir per absoluter X-indizierter Adressierung auf das dazugehörige Zeichen zu. Dieses landet im Akku und wird wieder per Kernalfunktion CharOut auf dem BS ausgegeben. Sobald wir aus der Kernalfunktion zurückkehren, holen wir uns den original Akku-Inhalt vom Stack. Jetzt blenden wir per AND das obere Nibble aus und geben somit das untere, wie eben aus. Zum Schluß holen wir den Akkuwert wieder vom Stack und springen per RTS zurück ins Hauptprogramm jsr hexout.
Falls ihr den Turbo Assembler nutzt, müsst ihr beachten, dass ein Label max. 15 Zeichen lang sein darf! Kürzt in dem Fall daher überall possiblehexchars um ein Zeichen!
So das wars auch schon. Da uns immer mehr Befehle bekannt sind, konnten wir den Block schnell abhandeln.
Unser komplettes Programm:
SCREENPOS = $05e5 ;Position des Zeichens für die Ausgabe CHROUT = $ffd2 ;Jump-Table Adr.: Zeichenausgabe SETCURSOR = $fff0 ;Jump-Table Adr.: get/set cursor pos ;*** Startadresse & BASIC-Zeile *=$0801 !byte $0c,$08,$e2,$07,$9e,$20,$32,$30,$36,$32,$00,$00,$00 ;*** Start des Assemblerprogrammes lda #$15 ;Zur Sicherheit auf sta $d018 ;Großbuchstaben umschalten lda SCREENPOS ;Zeichen zwischen ( ) in den Akku laden pha ;und auf dem Stack merken lda #$93 ;BS-Löschen (wie PRINT CHR$(147) in Basic) jsr CHROUT ;Jump-Table: Zeichenausgabe ldx #$0c ;Zeile und ldy #$00 ;Spalte für SETCURSOR jsr runtextout ;unseren Starttext 'RUN:( ) ' ausgeben pla ;Unseren Akku wiederherstellen sta SCREENPOS ;und zurück zwischen die ( ) schreiben ldx #$0c ;Zeile und ldy #$08 ;Spalte für SETCURSOR jsr binaryout ;zur Binärausgabe (Akku) springen ldx #$0c ;Zeile und ldy #$12 ;Spalte für SETCURSOR jsr hexout ;zur Hex-Ausgabe springen rts ;zurück zum BASIC ;************************************************************ ;*** Den Text an der Adresse runtext ausgeben ;************************************************************ ;*** Übergabe: X = Zeile in der die Ausgabe beginnt ;*** Y = Spalte in der die Ausgabe beginnt ;*** Das Textende wird durch $00 gekennzeichnet ;************************************************************ ;*** Rückgabe: - ;************************************************************ ;*** ändert: A,X,Y,SR ;************************************************************ runtextout clc ;C=0 für set / C=1 für get Cursor jsr SETCURSOR ;Jump-Table: get/set cursor ldx #$00 ;Pos. im Text runtextcharin lda runtext,x ;Aktuelles Zeichen in den Akku laden beq done ;wenn 0 dann sind wir fertig jsr CHROUT ;Jump-Table Zeichenausgeben inx ;X erhöhen, für nächstes Zeichen jmp runtextcharin ;und wieder hochspringen done ;Ziel, sobald wir eine 0 haben rts ;zurück zum Aufrufer (jsr runtextout) ;*** unser Starttext, Textende wird durch BYTE $00 erkannt! runtext !text "RUN:( ) " !byte $00 ;************************************************************ ;*** Den Inhalt des Akkus als Binärzahl auf dem BS ausgeben ;************************************************************ ;*** Übergabe: A = Zahl, die ausgegeben wird ;*** X = Zeile in der die Ausgabe beginnt ;*** Y = Spalte in der die Ausgabe beginnt ;************************************************************ ;*** Rückgabe: - ;************************************************************ ;*** ändert: X,Y,SR ;************************************************************ binaryout pha ;Akku auf dem Stack merken (wg. SETCURSOR) clc ;C=0 für set / C=1 für get Cursor jsr SETCURSOR ;Jump-Table get/set cursor lda #"%" ;Prozent-Zeichen in den Akku jsr CHROUT ;und ausgeben pla ;Akku wieder vom Stack holen pha ;und direkt nochmal merken ;(bis zum Rücksprung) ldy #$07 ;Schleife rückwärts von Bit 7 bis 0 binoutloop ldx #"0" ;Zeichen "0" ins X-Register asl ;Akku nach <-links verschieben bcc out ;Wenn es eine 0 ist, direkt zur Ausgabe, inx ;sonst erhöhen, damit wir eine 1 haben. out ;Sprungziel, wenn wir eine 0 haben pha ;Akku merken, er wird gleich überschrieben txa ;X -> Akku; Zeichen in den Akku jsr CHROUT ;Jump-Table: Zeichenausgeben pla ;Akku fürs nächste Bit wiederholen dey ;Schleife runterzählen bpl binoutloop ;bis 8-Bit verarbetet sind -> binoutloop pla ;sonst, den 'alten' Akku wiederherstellen rts ;und zurück zum Aufrufer (jsr binaryout) ;************************************************************ ;*** Den Inhalt des Akkus als Hexzahl auf dem BS ausgeben ;************************************************************ ;*** Übergabe: A = Zahl, die ausgegeben wird ;*** X = Zeile in der die Ausgabe beginnt ;*** Y = Spalte in der die Ausgabe beginnt ;************************************************************ ;*** Rückgabe: - ;************************************************************ ;*** ändert: X,Y,SR ;************************************************************ hexout pha ;Akku auf dem Stack merken (wg. SETCURSOR) clc ;C=0 für set / C=1 für get Cursor jsr SETCURSOR ;Jump-Table: get/set cursor lda #"$" ;Dollar-Zeichen in den Akku jsr CHROUT ;und ausgeben pla ;Akku wiederherstellen pha ;und merken bis zum Rücksprung pha ;gleich nochmal wg. LSR und CHROUT lsr ;jetzt msb -> ins lsb verschieben lsr ;dafür 4* LSR lsr lsr tax ;Akku ins X-Register, lda possiblehexchars,x ;das Zeichen fürs obere Nibble holen jsr CHROUT ;und auszugeben pla ;Akku vom Stack holen and #$0f ;oberes Nibble ausmaskieren tax ;Akku wieder nach X lda possiblehexchars,x ;um das Zeichen fürs untere Nibble zu holen jsr CHROUT ;und wieder ausgeben pla ;Ursprünglichen Akkuwert vom Stack holen rts ;und zurück zum Aufrufer (jsr hexout) ;*** die Hex-Ziffern possiblehexchars !text "0123456789ABCDEF"
Starten wir das Programm und wir erhalten endlich die von uns gewünschte Ausgabe.
Anmerkung zu den Kernalfunktionen
Die Kernalfunktionen erlauben es uns schnell und einfach unser Programm aufzubauen. Aber man sollte auch immer bedenken, dass sie nicht unbedingt die schnellsten sind. Bei zeitkritischen Aufgaben sollten wir uns überlegen, ob wir nicht lieber eine eigene Funktion schreiben.
Einige Kernalfunktionen können übrigens fehlschlagen. Diese zeigen durch ein gesetztes Carry-Flag einen aufgetretenen Fehler an. Im Akku steht dann eine Fehler-Nr., die wir auswerten sollten. Dies ist aber ein Thema, das ich hier bei den Grundlagen nicht vertiefen möchte.
Weitere Adressierungsarten & Befehlsvarianten
sec
SEC : SEt Carry (Carry-Flag setzen)
SEC implizit ($38, 1B, 2T, C)
Mit SEC setzen wir das Carry-Flag auf 1. Wenn wir z. B. mit der Kernalfunktion SETCURSOR die aktuelle Position lesen wollen. Wie oben bereits erwähnt, wird das Carry-Flag und die beiden Befehle um dieses zu setzen / löschen, hauptsächlich bei der Addition und Subtraktion benötigt.
ASL bietet neben der Akku-Adressierung noch die selben vier Adressierungsarten wie LSR.
asl $d020
ASL absolut ($0E, 3B, 6T, NZC)
Das Byte an der angegebenen absoluten Adresse bitweise nach links verschieben.
asl $d020,x
ASL absolut X-indiziert ($1E, 3B, 7T, NZC)
Das Byte an der angegebenen absoluten Adresse + X-Register bitweise nach links verschieben.
asl $f0
ASL Zero-Page ($06, 2B, 5T, NZC)
Verschiebe das Byte an der angegebenen Zero-Page-Adresse bitweise nach links.
asl $f0,x
ASL Zero-Page X-indiziert ($16, 2B, 6T, NZC)
Bitweises verschieben des Bytes, das an der angegebenen Zero-Page-Adresse + X-Register zu finden ist.
Als nächstes nutzen wir unsere hier entwickelten Routinen und schauen uns endlich die Rechenmöglichkeiten an.
Hallo Jörn
Danke für den Kurs. Als absoluter Anfänger in Sachen Assembler bin ich auf “gute” Kommentare angewiesen. Entsprechend habe ich folgende Anmerkungen:
1. Beim ersten Quellcodelisting verweist Du auf einen Bereich, den es dort nicht gibt (Die Ausgabe der Klammern). Das ist eher suboptimal, da man sich durch alle Listings durcharbeiten muss um herauszufinden, was mit diesem Kommentar gemeint ist:
lda SCREENPOS ;Zeichen zwischen ( ) in den Akku laden
2. Du lädst den Wert 15 an die Speicherstelle $d018 und sagst nur, dass Du damit auf Grossbuchstaben umschaltest, doch leider fehlt eine Erklärung oder ein Link, was die Bits an dieser Speicherstelle bedeuten und auch, was genau sich hinter dieser Speicherstelle verbirgt.
3. Beim BS löschen wäre ein Link auf die Bedeutung der Steuerzeichen hilfreich, so dass auch ein Neuling nicht fragen muss, was denn PRINT CHR$(147) bewirkt und wieso man damit den BS löscht.
Gruss aus der Schweiz, Markus Grob
Moin Markus,
aktuell plane ich hier keine Änderungen.
Hi Jörn,
Super Tutorial.
Wie viele schon geschrieben haben, hätte ich das in den späten 80er gut gebrauchen können 🙂
Bist Du beruflich im Umfeld lehren tätig? Das ganze ist didaktisch super aufgebaut. Könnte man direkt ein Buch daraus machen.
Einen kleinen Zahlendreher/Typo denke ich gefunden zu habe:
> Wir laden zunächst das Zeichen “1” in den Akku.
Du fügst aber dem X-Register eine 0 hinzu.
Danke,
David
Hallo David,
sorry für die späte Freischaltung und vielen Dank für den Hinweis.
Ich habe die Stelle korrigiert.
Gruß,
Jörn
Hallo Jörn,
ich habe eine Frage zu den bedingten Sprüngen (beq), die ja “nur” -127 oder +127 Stellen springen kann.
Wenn ich die Grenze überspringe will, wie kann ich das schön lösen. Mir fällt nur ein eine Weiterleitung innerhalb der Grenze einzubauen (was ich aber unschön empfinde). Also beq springt dann erst +120 Stellen und von dort via jsr dann noch mal + 80 Stellen.
Ich nutze C64Studio und bekomme den Fehler “Relative jump too far”.
LG Jens
in der Regel wirst du einfach die Bedingung umkehren und einen
jmp
verwenden.Also in etwa so…
cmp #12
beq zu_weit_weg
falls nicht gleich, hier weitermachen
cmp #12
bne weiter
jmp zu_weit_weg ;aber nicht für einen jmp
weiter
falls nicht gleich, hier weitermachen
Der Vollständigkeit halber:
Die Sprungweite bei bedingten Sprüngen, liegt zwischen -128 und +127.
Gruß,
Jörn
Hallo Jörn,
danke für den Tipp. Er ist aber in meinem Fall nicht so einfach einzusetzen.
Ich hatte eine Joystikabfrage auszuwerten, nach der ich bedingt springe um meine Sprites zu bewegen. Also so was hier:
joyauswerten
lda JoyEingabe
cmp #JOY_UP
beq hoch
cmp #JOY_DOWN
beq runter
und so weiter für 8 Richtungen
Ich habe allerdings meine Auswertung einkürzen können und komme inzwischen mit dem Sprungweiten zurecht. Außerdem bin ich mit meinen Sprüngen nur in die PLUS Richtung gesprungen.
LG Jens
Früher oder später kommst du um andere Lösungen aber evtl. nicht herum.
Dann ist die beschriebene Methode eine sehr gebräuchliche. Beim Einsatz eines Macro-Assemblers (wie beim C64 Studio) kannst du dir auch ein passendes Macro erstellen, das spart dann Tipparbeit.
Andere Lösungen, um solche Sprung-Probleme zu lösen, sind z. B. sich selbstverändernder Code oder indirekte Sprünge. Unter Zuhilfenahme von Tabellen kann man den Code dann auch etwas übersichtlicher halten.
Hallo Jörn,
zuerst mal vielen Dank für das Tutorial im Ganzen (ich wünschte, so etwas hätte es vor gut 30 Jahren gegeben).
Allerdings fällt mein CBM prg Studio (3.10.0) hier auf die Nase:
[Error ] Line 79:Invalid operand, label or variable “lda “%” ;Prozent-Zeichen in den Akku” – S:\store\CBM\CBM prg Studio\test2\test-7.asm
[Error ] Line 87:Invalid operand, label or variable “ldx “0” ;Zeichen “0” ins X-Register” – S:\store\CBM\CBM prg Studio\test2\test-7.asm
[Error ] Line 116:Invalid operand, label or variable “lda “$” ;Dollar-Zeichen in den Akku” – S:\store\CBM\CBM prg Studio\test2\test-7.asm
Woran liegt’s?
Grüsse,
Chris
das liegt daran, dass Arthur laufend die Kompatibilität zerstört und ich einfach keine Lust mehr habe, alles zu überarbeiten.
Daher empfehle ich mittlerweile auch das C64 Studio von Endurion. Eigentlich müssten mal sämtliche Beispiele auf das C64 Studio umgestellt werden, aber dazu fehlt mir einfach die Zeit.
Schreib einfach
(beachte die Raute #) und das „RUN ( )“ in Kleinbuchstaben „run ( )“, dann sollte es wieder klappen.lda #"%"
ldx #"0"
lda #"$"
Jawohl, läuft damit wieder. Vielen Dank!
Ich werde Deinen Rat beherzigen und auf C64 Studio umsteigen.
Grüsse,
Chris
Ich finde es für Einsteiger allerdings suboptimal, wenn die auch noch die Beispiele anpassen müssen. Die Beispiele sollten direkt laufen. Daher denke ich gerade aktiv darüber nach, wie ich die Überarbeitung am besten auf die Reihe kriege.
Hallo Jörn,
ich “arbeite” gerade ebenfalls gerade sämtliche Deiner Tutorials ab und kann Dir sagen, dass ich den gleichen Fehler, wie Chris, hatte. Es war allerdings bis dahin auch der erste und einzige. Von daher kann man, zumindest bis hier hin, auch noch ganz gut beim CBM prg Studio bleiben. Ich denke, ich werde den Umstieg aufs C64 Studio erst dann vornehmen, wenn es in Deinen Tutorials auch geschieht.
Vielen Dank für diese wunderbare Seite. Sie sucht – vergeblich – ihresgleichen.
Grüße Carsten
wer weiß, was passiert, sobald der Countdown abläuft.