Erstellt: 24. Februar 2013 (zuletzt geändert: 5. April 2021)

Nach unten und zurück

Unterprogramme

C64 Studio, AMCE & TASM

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.

Unsere „Bildschirmeingabe“
;<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:

Unsere Eingabe findet zwischen den Klammern statt.

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.

ASL
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.

Binärzahl der Nummer des Zeichen „A“ im Char-ROM.

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.

Unser fertiges Programm…
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.


Schrott!!Naja...Geht so...Ganz gut...SUPER! (11 Bewertungen | Ø 5,00 von 5 | 100,00%)

Loading...


ZurückWeiter

14 Gedanken zu „Nach unten und zurück“

  1. 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

  2. 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

  3. 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

    1. Hi 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

      1. 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

        1. Schön, dass du für Platz sorgen konntest und so direkte Sprünge verwenden kannst. Das ist natürlich immer am besten.

          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.

  4. 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

    1. Hallo 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
      lda #"%"
      ldx #"0"
      lda #"$"
      (beachte die Raute #) und das „RUN ( )“ in Kleinbuchstaben „run ( )“, dann sollte es wieder klappen.

        1. Denk daran, dass du dann auch die Sourcen ändern musst.

          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.

          1. 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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Protected by WP Anti Spam