Erschienen in 64'er Magazin, Ausgabe unbekannt · Originaldatei: ASSDATEI.TXT
Hinweis: Dies ist das an die Redaktion eingereichte Manuskript, nicht der gedruckte Endtext. Layout, Bildunterschriften, Korrekturen und Kürzungen der Redaktion können in der veröffentlichten Fassung abweichen.
Dateibearbeitung in Maschinensprache
Man braucht keinen Generalschlüssel, um in Assembler Files zu öffnen, zu schließen, zu lesen und zu beschreiben. Es sind nur ein paar kleine Programmiertricks, die im Prinzip ganz ähnlich wie in Basic funktionieren. Unser kleiner Kurs für fortgeschrittene Assemblerprogrammierer führt Sie in die Dateibearbeitung direkt vom Prozessor aus.
Sie haben gerade Ihre ersten Schritte in der Welt der Maschinensprache hinter sich, wissen, wie man in Assembler Programme eingibt und startet, und sind nun daran interessiert, in größeren Programmen auch Arbeiten wie das Laden und Speichern von Dateien, Anzeigen des Directories, Fehlerkanal auslesen und so weiter direkt in Assembler zu programmieren. Dann sind Sie hier genau richtig. Ausgehend davon, wie man solche Dinge in Basic programmiert, werden wir uns mit Hilfe der Betriebssystem-Routinen langsam an Files in Maschinensprache herantasten. In bewährter Manier stellen wir dabei erst die Programmlistings vor, und kommentieren sie dann ausführlich. Den Abschluß bietet ein Programm, das den Inhalt einer beliebigen Datei auf dem Drucker wiedergibt. Übrigens funktionieren die hier vorgestellten Verfahren keineswegs nur auf dem C 64. Sie sind, soweit nur Kernal-Routinen mit der Startadresse $FFxx verwendet werden, ohne Änderung auf allen Commodore-Homecomputern lauffähig, insbesondere C 128, VC 20, C 16 und möglicherweise sogar auf dem Amiga. Allerdings kann es notwendig sein, die Startadresse der Beispielprogramm anzupassen. In diesem Artikel wurde einheitlich 49152 gewählt, da sich dieser Speicherbereich beim C 64 anbietet. Ohne Änderungen sind alle hier vorgestellten Programmlösungen auf dem C 64 lauffähig.
Öffnen und Schließen von Files
Wie Sie das von Basic gewohnt sind, müssen wir auch in Maschinensprache dem Computer genau mitteilen, auf welche Datei zugegriffen werden soll. Als Beispiel wählen wir eine auf Diskette gespeicherte sequentielle Datei mit dem Namen »TEST«. In Basic können wir so ein File ganz einfach zum Lesen öffnen:
OPEN 1,8,2,"TEST,S,R"
Die erste Zahl, 1, gibt die logische Filenummer an. Wir brauchen sie, damit wir uns später auf diesen OPEN-Befehl beziehen können. Die 8 gibt die Gerätenummer an, sie steht für das Diskettenlaufwerk. 2 ist eine Sekundäradresse, die der Floppy die Betriebsdaten mitteilt. Dahinter steht in Stringform erst der Dateiname, dann durch Kommas getrennt ein S für SEQ-Datei und ein R für READ (Lesen aus dem File).
Ganz so einfach geht's in Maschinensprache nicht. Den OPEN-Befehl müssen wir in drei Prozeduren zerlegen. Dabei begegnen uns allerdings exakt die selben Angaben wie bei Basic. Zunächst brauchen wir drei wichtige Betriebssystem-Routinen:
SETPAR $FFBA
SETNAM $FFBD
OPEN $FFC0
Die SETPAR-Routine, die im System bereits fest ab Adresse $FFBA gespeichert ist, definiert die File- und die Gerätenummer sowie die Sekundäradresse. Diese Angaben sind im Akku, X- und Y-Register zu übergeben. Der Filename muß im Ascii-Code irgendwo im Speicher abgelegt werden. Dazu übergeben wir der SETNAM-Routine im Akku die Länge des Namens und in den Registern X und Y einen Zeiger (Low/Highbyte) auf die Adresse. Soll kein Dateiname gesetzt werden (ist ja auch in Basic bei OPEN nicht notwendig), übergeben wir als »Länge« im Akku die Null. Erst nach diesen Vorbereitungen darf die OPEN-Routine aufgerufen werden, die das File tatsächlich öffnet. Formulieren wir einmal den obigen OPEN-Befehl in Assembler. Die Funktionsweise der kurzen Routine ist dabei eher unwichtig, begreifen Sie den Ausschnitt einfach als »Kochrezept«:
* = 49152 ; Startadresse
LDA #1 ; Filenummer
LDX #8 ; Gerätenummer
LDY #2 ; Sekundäradresse
JSR $FFBA ; SETPAR
LDA #8 ; Länge des Filenamens
LDX #<NAME; Adresse
LDY #>NAME; des Filenamens
JSR $FFBD ; SETNAM
JSR $FFC0 ; OPEN Datei öffnen
Es ist also ganz einfach. Diese wenigen Zeilen haben exakt die gleiche Wirkung wie der oben vorgestellte OPEN-Befehl. Nur eines fehlt noch: Der Dateiname, den wir an einer freien Stelle im Speicher im Ascii-Code ablegen müssen:
NAME .ASC "TEST,S,R"
Das zweite, was jeder Basicprogrammierer im Zusammenhang mit Files lernt, ist, daß man jede geöffnete Datei wieder schließen muß, wenn man sie nicht mehr braucht. Das ist so, als ob Sie Ihren Computer einschalten (OPEN), dann damit arbeiten und ihn nach Beendigung der Session wieder abschalten (CLOSE). Nur, daß Sie mit mehreren Computern gleichzeitig arbeiten können: Es können viele Dateien gleichzeitig geöffnet sein. Auf welche Sie sich momentan beziehen, wird durch die Filenummer (in unserem Beispiel 1) eindeutig bestimmt. In Basic schließen wir die oben geöffnete Datei einfach mit
CLOSE 1
Das geht in Maschinensprache fast genauso einfach: Die entsprechende CLOSE-Routine erwartet im Akku die Filenummer und hat die Adresse $FFC3. Unser CLOSE 1 sieht also in Assembler so aus:
LDA #1 ; Dateinummer
JSR $FFC3 ; CLOSE Datei schließen
Das ist wirklich alles! Bemerkenswert ist, daß wir hier ebenso wie in Basic keine weiteren Angaben brauchen - die hat ja bereits alle der OPEN-Befehl bekommen.
Lesen aus der Datei
Nachdem wir das File geöffnet haben, wollen wir nun daraus lesen. In Assembler muß dazu zunächst ein Eingabekanal geöffnet werden, das heißt, wir müssen dem Computer sagen, daß wir die Datei, die vorher geöffnet wurde, nun lesen wollen. Dazu gibt es die CHKIN-Routine mit der Adresse $FFC6. Diesem Unterprogramm wird im X-Register (!) die Filenummer der vorher geöffneten Datei übergeben, aus der gelesen werden soll. In unserem Beispiel sieht das so aus:
LDX #1 ; Dateinummer 1
JSR $FFC6 ; CHKIN Eingabekanal öffnen
Ab jetzt kann man aus dieser Datei lesen. Dazu stehen im Prinzip vor allem zwei Routinen zur Verfügung:
GETIN $FFE4
CHRIN $FFCF
Die CHRIN-Routine entspricht in etwa dem INPUT# in Basic. Sie merken schon: In Basic muß hinter INPUT# noch die Filenummer angegeben werden. Das kann hier entfallen, da wir ja schon mit CHKIN festgelegt haben, auf welches File Bezug genommen wird. Die GETIN-Routine entspricht dem GET#-Befehl von Basic und ist CHRIN grundsätzlich vorzuziehen. Bitte begnügen Sie sich mit der Ausrede, GET in Basic ist ja auch sicherer als INPUT. Eine etwas detailliertere Erklärung folgt am Ende des Artikels. Die Anwendung von GETIN ist denkbar einfach: Einfach mit JSR $FFE4 aufrufen. Im Akku wird dann der Ascii-Code des gelesenen Zeichens übergeben. Auf ein Beispiel verzichten wir, da in Kürze das erste »richtige« Programm folgt.
Ebenso wie geöffnete Dateien wieder geschlossen werden, müssen auch die Eingabekanäle (später werden wir noch Ausgabekanäle kennenlernen) geschlossen werden, da sonst beispielsweise keine Eingabe von Tastatur mehr möglich ist. Unser C 64 kennt dazu die Routine CLRCHN mit der Adresse $FFCC. Rufen Sie diese Adresse auf, wird die Eingabe wieder auf die Tastatur und (falls verändert) die Ausgabe wieder auf den Bildschirm »umgelenkt« (man sollte besser sagen: »zurückgelenkt«).
Zur Sicherheit sollte man außerdem zu Beginn jedes Maschinenprogramms den CLALL-Befehl (Adresse $FFE7) anwenden. Er bewirkt, daß alle offnene Dateien und Kanäle geschlossen werden. CLALL beinhaltet also CLRCHN.
Der Vollständigkeit halber sei noch die wohl wichtigste Betriebssystem-Routine erwähnt, die es gibt: CHROUT (oft auch PRINT oder BSOUT genannt) mit der Adresse $FFD2. Diese gibt einfach das Zeichen, dessen Codenummer im Akku steht, auf dem Bildschirm oder dem Gerät aus, das momentan Ausgabegerät ist. Wie man ein anderes Ausgabegerät als den Schirm definiert (funktioniert ähnlich wie oben bei CHKIN gesehen), lernen Sie später.
Fehlerkanal auslesen
Mit dem bisher errungenen Wissen können wir schon unser erstes Programm schreiben. Es soll den Fehlerkanal der Diskettenstation auslesen und am Bildschirm anzeigen. Dazu erst das entsprechende Analogon in Basic:
10 OPEN 15,8,15:REM KEIN DATEINAME
20 GET# 15,A$
30 PRINT A$;
40 IF A$ <> CHR$(13) THEN 20
50 CLOSE 15
60 END
In Zeile 40 erfolgt eine Prüfung, ob das gelesene Zeichen ein CR (Code 13) war. In diesem Fall ist die Statusmeldung vollständig gelesen. Dieses Programm setzen wir 1:1 in Maschinensprache um.
; Beispiel 1: Fehlerkanal
49152 JSR $FFE7; CLALL zur Sicherheit
49155 LDA #15 ; Filenummer
49157 LDX #8 ; Floppy
49159 TAY ; Sekundäradresse
49160 JSR $FFBA; SETPAR
49163 LDA #0 ; kein Dateiname
49165 JSR $FFBD; SETNAM
49168 JSR $FFC0; OPEN 15,8,15,""
49171 LDX #15 ; File 15
49173 JSR $FFC6; CHKIN zur Eingabe öffnen
49176 JSR $FFE4; GETIN ein Zeichen lesen
49179 JSR $FFD2; auf Bildschirm ausgeben
49182 CMP #13 ; Code 13?
49184 BNE 49176; nein, dann weiter
49186 JSR $FFCC; sonst Kanal schließen
49189 LDA #15 ; Dateinummer
49191 JMP $FFC3; CLOSE 15
Das war auch schon alles. Wenn Sie dieses Programm mit dem Monitor oder Assembler eingegeben und ggf. assembliert haben, starten Sie es mit
SYS 49152
Sofort erscheint der Fehlerkanal, im Normalfall 00,OK,00,00. Senden Sie einfach einmal einen fehlerhaften Befehl, etwa
OPEN 1,8,15,"X":CLOSE 1
Nach SYS 49152 beschwert sich das Laufwerk mit einem 31,SYNTAX ERROR,00,00. Was bleibt ihm auch anderes übrig: Den X-Befehl gibt es nicht. Übrigens sollten Sie zur Probe im Maschinenprogramm einmal an Adresse 49189 ein RTS einbauen (POKE 49189,96) und damit den CLOSE-Befehl unwirksam machen. Der Aufruf mit SYS 49152 klappt einwandfrei, allerdings ist die Datei 15 noch offen. Das können Sie leicht nachprüfen, indem Sie jetzt von Basic aus eingeben
OPEN 15,8,15
Der Computer reagiert artig mit seinem ?FILE OPEN ERROR. In Maschinensprache haben wir es also offenbar mit exakt den gleichen Files zu tun wie in Basic. Das erleichtert die Sache erheblich.
Directory ohne Datenverlust
Kaum ein größeres Programm kommt ohne eine Routine aus, die das Inhaltsverzeichnis der Diskette lädt und listet. In Basic bietet sich dazu folgende Befehlsfolge an:
LOAD "$",8
LIST
die jedoch einen fatalen Nebeneffekt hat: Da das Directory wie ein Basicprogramm geladen wird, geht das momentan im Speicher stehende Programm rettungslos verloren. Versuchen wir daher, eine kurze Maschinenroutine zu programmierern, die das gleiche ohne Datenverlust erledigt. Wir lesen Zeichen für Zeichen der Directory und zeigen alles gleich an. Die hier vorgestellte Routine ist nicht optimiert in Bezug auf Kürze und Geschwindigkeit, aber es wird Ihnen mit etwas Einfühlungsvermögen leichtfallen, die Funktionsweise zu verstehen und das Programm vielleicht noch zu erweitern oder zu kürzen.
Zunächst öffnen wir in gewohnter Manier das »File«, in dem sich die Directory verbirgt. Aus technischen Gründen braucht man dazu die Sekundäradresse 0.
; Beispiel 2: Directory
49152 JSR $FFE7; CLALL zur Sicherheit
49155 LDA #1 ; Filenummer
49157 LDX #8 ; Geräteadresse
49159 LDY #0 ; Sekundäradresse
49161 JSR $FFBA; SETPAR
49164 LDX #$24 ; Code für Dollarzeichen
49166 STX 2 ; in freier Speicherzelle speichern
49168 LDX #2 ; Adresse low
49170 LDY #0 ; und high
49172 JSR $FFBD; SETNAM
49175 JSR $FFC0; OPEN 1,8,0,"$"
49178 LDX #1 ; Filenummer
49180 JSR $FFC6; CHKIN Eingabekanal öffnen
Da ja der Filename irgendwo im Speicher abgelegt werden muß, entscheiden wir uns für die sonst unbenutzte Speicherzelle 2. Ab 49168 richten wir den Zeiger X/Y auf diese Adresse. Sie wundern sich vielleicht, warum wir vor SETPAR nicht die Länge des Namens (1) im Akku gespeichert haben. Das ist hier nicht mehr nötig, da die 1 im Akkumulator von 49155 noch enthalten ist. Ein kleiner Trick also, den sich der Autor einfach nicht verkneifen konnte. Jetzt können wir mit GETIN Zeichen für Zeichen lesen und ausgeben. Beim Directory gibt es allerdings einige Besonderheiten zu beachten. Die ersten beiden Bytes einer jeden Zeile können ignoriert werden, die nächsten beiden Bytes geben im Format Low/Highbyte die Größe in Blocks an. Danach folgt bis zum Nullbyte die Zeileninformation. Außerdem muß getestet werden, ob das Dateiende erreicht wurde. Wie in Basic lesen wir dazu den Wert der Variablen ST aus, auf den der Maschinenprogrammierer durch Auslesen der Speicherzelle 144 (C 64, C 128 und andere) Zugriff hat. Hat diese Speicherzelle einen Inhalt ungleich Null, so ist ein Fehler aufgetreten oder das File ist beendet. Zuletzt brauchen wir eine Routine, die eine 16-Bit Integerzahl (Low/Highbyte) numerisch auf dem Bildschirm ausgibt. So etwas enthält der C 64 an Adresse $BDCD, diese Routine AXOUT wird sonst vom System zur Ausgabe von Basic-Programmzeilennummern verwendet. Sie gibt eine Zahl aus, die sich nach der Formel
WERT = X + A * 256
errechnet. High- und Lowbyte wird also im Akku und im X-Register übergeben.
49183 LDY #3 ; drei Bytes überlesen
49185 STY 3 ; als Zähler merken
49187 JSR $FFE4; GETIN Zeichen lesen
49190 STA 4 ; und merken
49192 LDY 144 ; ST Status lesen
49194 BNE 49239; ungleich Null, fertig
49196 JSR $FFE4; GETIN
49199 LDY 144 ; ST lesen
49201 BNE 49239; bei Dateiende
49203 LDY 3 ; Zähler zurückholen
49205 DEY ; und erniedrigen
49206 BNE 49185; nicht null, weitermachen
49208 LDX 4 ; Zeichen restaurieren
49210 JSR $BDCD; AXOUT Zahl ausgeben
49213 JSR $AB3F; SPACE Leerzeichen ausgeben
49216 JSR $FFE4; GETIN
49219 LDX 144 ; Status testen
49221 BNE 49239; bei ungleich Null beenden
49223 TAX ; Zeichencode
49224 BEQ 49232; bei Zeilenende
49226 JSR $FFD2; BSOUT Zeichen ausgeben
49229 JMP 49216; und weiter listen
49232 JSR $AAD7; CRLF Zeilenende ausgeben
49235 LDY #2 ; zwei Bytes für Linkpointer
49237 BNE 49185; und nächste Zeile
49239 JSR $FFCC; CLRCHN Kanal schließen
49242 LDA #1 ; Datei 1
49244 JMP $FFC3; schließen, fertig
Besitzer anderer Computer als der C 64 müssen die Befehle JSR $AAD7 (beginnt neue Zeile) und JSR $AB3F (gibt ein Leerzeichen aus) evtl. durch Ersatzkonstruktionen ersetzen. JSR $AB3F hat die gleiche Wirkung wie LDA #32 und JSR $FFD2. Außerdem müssen Sie mit Hilfe eines ROM-Listings die AXOUT-Routine, die beim C 64 an $BDCD beginnt suchen, oder auf die Ausgabe der Blocklänge verzichten.
Probieren Sie dieses Programm gleich einmal aus! Wenn alles geklappt hat, sollten Sie zur Übung einmal versuchen, beispielsweise eine Funktion einzubauen, die das Directory auf Tastendruck anhält und/oder abbricht.
Druckerausgabe
Nachdem wir gelernt haben, wie man in Maschinensprache Eingaben von Files vornimmt, beschäftigen wir uns jetzt mit der Ausgabe in eine Datei. Als erstes Beispiel mag eine Druckerausgabe dienen. Wenn Sie das bisher beschriebene verstanden haben, wird es Ihnen nicht schwerfallen, auch mit dem folgenden zurechtzukommen. Im Prinzip geht es wieder wie gehabt, nur daß wir diesmal nach dem Öffnen des eigentlichen Files keinen Eingabekanal, sondern einen Ausgabekanal öffnen werden. Dazu dient die Routine CHKOUT (Adresse $FFC9), die analog zu CHKIN funktioniert. Auch hier muß im X-Register die Filenummer übergeben werden. CHKOUT leitet dann bis zum abschließenden CLRCHN alle Ausgaben auf das betreffende Gerät um. Das funktioniert genauso wie der CMD-Befehl, den Sie vielleicht vom Basic her kennen. Und in der Tat ruft auch Basics CMD intern einfach nur die CHKOUT-Routine auf!
Es soll der Text »ICH GRUESSE DIE WELT!« auf dem Drucker ausgegeben werden. Sehen wir uns wieder zunächst die Basic-Lösung an:
10 OPEN 4,4
30 PRINT#4,"ICH GRUESSE DIE WELT!"
50 CLOSE 4
Die Zeilen 20 und 40 wurden bewußt weggelassen. Damit die Umsetzung in Assembler leichter zu verstehen ist, bauen wir das Basicprogramm etwas um und verwenden den CMD-Befehl. Dieser ergibt die noch fehlende Zeile 20. Bekanntlich muß ein CMD-Befehl mit einem PRINT#-Befehl aufgehoben werden, den wir in Zeile 40 unterbringen.
10 OPEN 4,4
20 CMD 4
30 PRINT"ICH GRUESSE DIE WELT!"
40 PRINT#4
50 CLOSE 4
Das Bemerkenswerte dabei ist, daß wir zur Druckerausgabe den normalen PRINT-Befehl (ohne Doppelkreuz) verwenden dürfen, da ja zuvor mit CMD schon der entsprechende Kanal freigegeben wurde. In Maschinensprache können wir uns die Zeile 40 sparen, wir haben zum Schließen des Kanals ja die CLRCHN-Routine zur Verfügung, die übrigens auch dem Basic-Programmierer mit SYS 65484 (hier etwa in Zeile 40) zur Verfügung steht.
Jetzt stellt die Umsetzung kein Problem mehr dar. Merken Sie sich bitte, daß beim Öffnen eines Druckerfiles grundsätzlich kein Filename angegeben wird. Eventuell kann es notwendig sein, eine andere Sekundäradresse als die Null anzugeben, das hängt von Ihrem Interface ab. Ändern Sie bei Bedarf einfach den Befehl ab Adresse 49158.
; Beispiel 3: Druckerausgabe
49152 JSR $FFE7; CLALL zur Sicherheit
49155 LDA #4 ; Filenummer
49157 TAX ; istgleich Gerätenummer
49158 LDY #0 ; Sekundäradresse
49160 JSR $FFBA; SETPAR
49163 LDA #0 ; Länge des Filenamens = 0
49165 JSR $FFBD; SETNAM
49168 JSR $FFC0; OPEN 4,4,0,""
49171 LDX #4 ; Filenummer
49173 JSR $FFC9; CHKOUT Drucker als Ausgabegerät
Jetzt ist die eigentliche Ausgabe des Textes dran. Wir verwenden dazu, damit es leichter verständlich wird, eine Schleife, in der wir Zeichen für Zeichen des im Ascii-Code abgelegten Textes über CHROUT ($FFD2) ausgeben. Im Gegensatz zu Basic wird hier bei der Ausgabe nicht automatisch das Zeilenendezeichen CR (CHR$(13)) angefügt, das müssen wir von Hand erledigen. Wird das vergessen, druckt das Gerät nichts. Der Text hat insgesamt also 22 Zeichen.
49176 LDX #0 ; Zähler initialisieren
49178 LDA 49197,X ; Text lesen
49181 JSR $FFD2 ; und Zeichen ausgeben
49184 INX ; nächstes Zeichen
49185 CPX #22 ; schon 22 Zeichen?
49187 BNE 49178 ; nein, weiter lesen
49189 JSR $FFCC ; CLRCHN Ausgabe beenden
49192 LDA #4 ; File 4
49194 JMP $FFC3 ; CLOSE 4
49197 .ASC "ICH GRUESSE DIE WELT!"
49218 .BYT 13 ; CR
Je nachdem, welchen Assembler oder Monitor Sie verwenden, kann es notwendig sein, die Pseudo-Befehle in 49197 und 49218 zu ändern. Es soll einfach der Text ab 49197 im Ascii-Code abgelegt und eine 13 in Speicherzelle 49218 gelegt werden: POKE 49218,13.
Jetzt sollten Sie den Drucker einschalten und bereitmachen. Mit
SYS 49152
kann dann der Ausdruck gestartet werden. Wenn alles richtig gelaufen ist, wird das Gerät schön brav den Text zu Papier bringen.
Beschreiben eines Disketten-Files
Jetzt wollen wir aber auch versuchen, in ein File zu schreiben. Dazu werden wir eine neue Routine des C 64 kennenlernen, die uns die Arbeit des Aufrufs von SETPAR und SETNAM abnimmt. Man kann vielmehr direkt hinter dem SYS-Befehl Filenamen und Gerätenummer angeben. Diese Routine GETPAR (Adresse beim C 64: $E1D4) wird normalerweise von den LOAD- und SAVE-Befehlen verwendet, um die Parameter zu ermitteln. Wir wollen ein kurzes Programm schreiben, mit dem eine sequentielle Datei angelegt werden kann.
Kurz zu den »Spielregeln«: Das Programm wird mit
SYS 49152,"DATEINAME,S,W",8
gestartet. Daraufhin erscheint ein Cursor, und die einzelnen Datensätze werden durch Komma getrennt eingegeben. Das Ende markiert ein Stern, der sonst nicht eingegeben werden darf - der Einfachheit halber. Doch wie programmiert man so etwas?
Zunächst holen wir das Komma hinter SYS 49152 und die Parameter. Dabei müssen die Sekundäradresse und die Filenummer immer auf 2 gesetzt werden, auch wenn der Anwender nichts oder etwas anderes vorgibt.
; Beispiel 4: File-Editor
49152 JSR $AEFD; CHKKOM Komma holen
49155 JSR $E1D4; GETPAR Parameter holen
49158 LDX $BA ; Gerätenummer G aus Vorgabe
49160 LDA #2 ; Filenummer
49162 TAY ; istgleich Sekundäradresse
49163 JSR $FFBA; SETPAR
49166 JSR $FFC0; OPEN 2,G,2,"NAME"
Zur Eingabe von Tastatur verwenden wir diesmal die oben geschmähte CHRIN-Routine. Wendet man diese auf die Tastatur an, wird beim ersten Aufruf ein Cursor ausgegeben, und der Anwender kann etwas eingeben und mit RETURN bestätigen. Im Akku steht dann das erste Textzeichen. Bei weiteren Aufrufen werden dann nacheinander die einzelnen Zeichen gelesen. Das Ganze ist von der Wirkung her mit dem INPUT-Befehl vergleichbar.
49169 LDX #2 ; Filenummer
49171 JSR $FFC9; CHKOUT Ausgabekanal in File 2 öffnen
49174 JSR $FFCF; CHRIN Eingabe holen
49177 CMP #42 ; Stern?
49179 BEQ 49187; ja, dann fertig
49181 JSR $FFD2; sonst in File ausgeben
49184 JMP 49174; und weitermachen
49187 JSR $FFCC; CLRCHN Kanal schließen
49190 LDA #2 ; Filenummer
49192 JMP $FFC3; CLOSE 2
Beim Schreiben in eine Datei auf Diskette ist es ganz wichtig, daß nach dem Beschreiben ordentlich der Kanal mit CLRCHN und das File mit CLOSE geschlossen werden, da sonst die Datei nicht ordnungsgemäß angelegt wird. Wenden wir das Programm einmal an. Legen Sie eine nicht schreibgeschützte Diskette ein, auf der sich noch Platz befindet, und starten das Programm mit
SYS 49152,"TEST,S,W",8
Wählen Sie einen Namen, den es noch nicht auf dieser Diskette gibt! Das Laufwerk beginnt kurz zu arbeiten, und ein Cursor erscheint. Geben Sie nun einige Zeilen Text ein, bestätigen Sie wie in Basic jede Zeile mit RETURN. Wenn Sie genug haben, geben Sie nur einen Stern gefolgt von RETURN ein. Die Datei wird nun geschlossen, der Computer kehrt ins Basic zurück. Wenn Sie nun das Directory einsehen, stellen Sie fest, daß eine sequentielle Datei »TEST« erzeugt wurde. Wir haben also einen richtigen kleinen Editor für sequentielle Files geschrieben! Versuchen Sie doch einmal, das Programm so auszubauen, daß auch Änderungen an bereits bestehenden Files vorgenommen werden können. Auch erkennt unsere Routine keine Diskettenfehler, man müßte dazu noch den Fehlerkanal auslesen, beispielsweise nach dem Schließen der Datei, und prüfen, ob eine andere Angabe als 00,OK,00,00 ausgegeben wurde. Es reicht in diesem Fall, nur das erste Zeichen des Fehlerkanals zu lesen und mit der Null zu vergleichen.
File-Lister
Den krönenden Abschluß stellt ein kleines Hilfsprogramm dar, mit dessen Hilfe eine Datei von Diskette gelesen werden und direkt auf den Drucker ausgegeben werden kann. Die Bedienung soll dabei wieder über Angaben nach dem SYS-Befehl erfolgen, also listet beispielsweise
SYS 49152,"TEST,S,R"
die eben erzeugte sequentielle Datei auf dem Drucker. Im Prinzip ist das gar nicht schwer. Wir beginnen damit, ein Diskettefile zu öffnen und dann einen Ausgabekanal zum Drucker anzulegen.
; Beispiel 5: File-Lister
49152 JSR $AEFD; CHKKOM Komma holen
49155 JSR $E1D4; GETPAR Parameter holen
49158 LDX $BA ; Gerätenummer G aus Vorgabe
49160 LDA #2 ; Filenummer
49162 TAY ; istgleich Sekundäradresse
49163 JSR $FFBA; SETPAR
49166 JSR $FFC0; OPEN 2,G,2,"NAME"
49169 LDA #0 ; kein Filename
49171 JSR $FFBD; SETNAM
49174 LDA #4 ; Filenummer
49176 TAX ; istgleich Gerätenummer Drucker
49177 LDY #0 ; Sekundäradresse
49179 JSR $FFBA; SETPAR
49182 JSR $FFC0; OPEN 4,4,0
Zwei Dinge haben wir dabei gelernt. Erstens spielt die Reihenfolge des Aufrufs von SETNAM und SETPAR keine Rolle. Beim Drucker haben wir erst die Angaben zum Filenamen und dann die Angaben zu den Fileparametern gemacht, anders als gewohnt. Wichtig ist nur, daß beide Routinen vor dem OPEN bereits aufgerufen wurden. Zweitens ist es wie auch in Basic problemlos möglich, mehrere Files auf einmal zu öffnen. Diese müssen sich lediglich in der Filenummer unterscheiden, damit wir uns später auf sie beziehen können.
Jetzt schalten wir File 2 (SEQ-Datei) auf einen Eingabekanal und holen uns ein Zeichen.
49185 LDX #2 ; Filenummer
49187 JSR $FFC6; CHKIN
49190 JSR $FFE4; GETIN
Planen wir im Voraus. Später wird unser Programm prüfen müssen, ob das Dateiende erreicht ist. Wie checken den Inhalt des Status. Wie das geht, wissen Sie ja schon: Speicherzelle 144 auslesen. Das Programm kann damit allerdings nicht zu lang warten, da sich der Status während späterer Operationen, beispielsweise bei der Zeichenausgabe zum Drucker noch ändern kann. Also lesen und speichern wir ST jetzt:
49193 LDX 144 ; Statusbyte
49195 PHP ; auf den Stack
Wir verwenden das X-Register, da sich im Akku ja noch das auszugebende Zeichen von GETIN befindet. Der LDX-Befehl setzt das Zero-Flag des Prozessors, wenn eine Null gelesen wurde (Dateiende nicht erreicht). PHP sichert die Prozessorflags zur späteren Untersuchung auf den Stack (PHush Processorstatus), bis wir sie brauchen.
Bevor wir jetzt allerdings das Zeichen drucken können, müssen wir den Eingabekanal mit CLRCHN wieder schließen. Fragen Sie nicht, es scheint bei Commodore-Computern einfach notwendig zu sein. Dabei müssen wir allerdings vorsichtig sein: Der CLRCHN-Aufruf ändert den Inhalt des Akkumulators, das Zeichen geht verloren. Deshalb pushen wir es auf den Stack.
49196 PHA ; Zeichen retten
49197 JSR $FFCC; CLRCHN
Der Stack enthält nun zwei Dinge: Erstens das gerade gelesene Zeichen und zweitens den Prozessor-Status (nicht mit dem Statusbyte ST verwechseln! Der PHP-Befehl legt nicht den Inhalt des X-Registers auf den Stack, sondern unter anderem eine Information darüber, ob der letzte Lesebefehl in 49193 Null ergab!). Wenn wir damit anfangen, Werte vom Stack zu holen, bekommen wir zuerst die Werte, die zuletzt dorthin geschrieben wurden - in unserem Fall das Zeichen.
Die Druckerausgabe enthält keine Geheimnisse, hier ist der Ausschnitt:
49200 LDX #4 ; Druckerfile
49202 JSR $FFC9; CHKOUT Ausgabekanal öffnen
49205 PLA ; Zeichen vom Stack retten
49206 JSR $FFD2; und zum Drucker schicken
49209 JSR $FFCC; CLRCHN Ausgabekanal schließen
Gibt es noch gültige Daten, die wir aus dem Eingabekanal lesen müssen? Der folgende Code prüft das:
49212 PLP ; Prozessorstatus zurückholen
49213 BEQ 49185; weitere Daten, dann oben weiter
Sind keine Zeichen mehr in der Datei, können wir beide Files schließen: Zum Beispiel erst das Druckerfile, dann die SEQ-Datei:
49215 LDA #4 ; Drucker
49217 JSR $FFC3; CLOSE 4
49220 LDA #2 ; Diskette
49222 JMP $FFC3; CLOSE 2 und fertig
Das war alles, was wir brauchen. Dieses kurze Utility können Sie nun allgemein verwenden. Um beispielsweise die oben angelegte Datei zu drucken, geben Sie den Befehl
SYS 49152,"TEST,S,R",8
Solche File-Lister haben in der Praxis eine große Bedeutung, wenn ohne Datenverlust Dateien ausgelesen werden sollen. Sie sind übrigens bei der Ausgabe nicht nur auf SEQ-Files beschränkt. Auch PRG- und vor allem USR-Files lassen sich listen, indem Sie die Kennung ,S im Filenamen entsprechend in ,P oder ,U ändern. Vielleicht bauen Sie ja auch noch eine Art »Schalter« ein, mit dem man die Ausgabe wahlweise auf Bildschirm und/oder Drucker legen kann.
Laden einer Datei
Bisher haben wir noch nicht versucht, ein File an einen bestimmten Speicherplatz zu laden. Natürlich gibt es das Gegenstück zum LOAD-Befehl auch in Maschinensprache, und zwar in Form der LOAD-Routine mit der Adresse $FFD5. Auch hier müssen Sie erst mit SETPAR und SETNAM oder aber GETPAR Filenamen, Gerätenummer und Sekundäradresse festlegen, die logische Filenummer spielt keine Rolle. Der Inhalt des Akkumulators beim Aufruf der Routine entscheidet darüber, ob geladen wird (A=0) oder ein VERIFY ausgeführt (A ungleich 0). Ist die Sekundäradresse nicht Null, so wird das File »absolut« geladen, also an die Stelle, die auf Diskette gespeichert ist. Sie kennen diese Betriebsart, wenn Sie in Basic mit
LOAD "NAME",8,1
etwa Tools an eine vorgegebene Stelle im Speicher laden. Man kann aber auch »relativ« laden, dem Computer also sagen: »Ignoriere die auf Diskette gespeicherte Ladeadresse und lade das File auf jeden Fall an Adresse xxx«. Diese Betriebsart findet beim Laden von Basicprogrammen Verwendung, da lassen Sie hinter LOAD die ,1 einfach weg. Für die Sekundäradresse wird Null gesetzt. In Basic wird dann immer an die Adresse geladen, die in den Speicherzellen 43/44 steht, normalerweise der Basic-Anfang 2049. In Assembler können wir der LOAD-Routine die Adresse mitgeben. Schreiben wir doch einmal ein Utility, mit dem man Hires-Grafiken unabhängig, ab wo sie gespeichert wurden immer nach 8192 laden kann. Dazu holen wir uns erst mit CHKKOM und GETPAR die Fileparameter. Dann setzen wir die Sekundäradresse (gespeichert in $B9) auf Null, um relativ zu laden. Die Ladeadresse 8192 (Highbyte: 32, Lowbyte: 0) übergeben wir im X- und Y-Register, im Akku eine Null, um zu laden, nicht zu vergleichen. Fertig sieht das so aus:
; Beispiel 6: Grafiklader
49152 JSR $AEFD; CHKKOM Komma holen
49155 JSR $E1D4; GETPAR Parameter holen
49158 LDA #0 ; LOAD-Flag
49160 TAX ; Lowbyte von 8192
49161 LDY #32 ; Highbyte von 8192 ($2000)
49163 STA $B9 ; Sekundäradresse Null
49165 JMP $FFD5; LOAD, Grafik in den Speicher laden
Mehr ist es nicht! Jetzt könnten Sie beispielsweise mit
SYS 49152,"DATEINAME",8
ein Bild (PRG-File) nach 8192 laden. Soll die auf Diskette gespeicherte Adresse berücksichtigt werden, setzen Sie einfach die Speicherzelle $B9 (Sekundäradresse, dezimal 185) auf einen Wert ungleich Null. Die Übergabe einer Adresse in X und Y kann dann entfallen.
Speichern
Was nun noch fehlt, ist die Möglichkeit, einen beliebigen Speicherbereich auf Diskette zu speichern. In Basic speichert der SAVE-Befehl das Basicprogramm. Dessen Grenzen sind in den Speicherzellen-Paaren 43/44 (Anfang) und 45/46 (Ende) erfaßt. Wir wollen ein Grafikbild, das im Speicher von 8192 ($2000) bis 16383 ($3FFF) einschließlich steht, speichern. Vor dem Aufruf der SAVE-Routine ($FFD8) müssen Sie erst wieder mit SETPAR und SETNAM oder aber in unserem Fall GETPAR die Datei-Parameter festlegen. Dann wird im X- und Y-Register das High- und Lowbyte der um eins erhöhten Endadresse übergeben. Die Startadresse des zu speichernden Bereiches übergeben wir in einem beliebigen Speicherzellen-Paar der Zeropage (in unserem Fall etwa Adressen 2 und 3), dessen Adresse wiederum im Akku übergeben wird. Die Sekundäradresse ist beim Speichern ebenso wie die Filenummer ohne Bedeutung. Das alles klingt sehr umständlich, ist es aber gar nicht, wie das folgende Beispiel beweist:
; Beispiel 7: Grafik speichern
49152 JSR $AEFD; CHKKOM Komma holen
49155 JSR $E1D4; GETPAR
49158 LDX #0 ; bis $4000 ausschließlich speichern (Lowbyte 0)
49160 LDY #64 ; Highbyte von $4000
49162 STX 2 ; ab $2000 speichern (Lowbyte 0)
49164 LDA #32 ; Highbyte von $2000
49166 STA 3 ; merken
49168 LDA #2 ; Startadresse in Speicherzellen 2 und 3
49170 JMP $FFD8; SAVE-Routine: Bild speichern
Na sehen Sie, gar keine Schwierigkeit. Die SAVE-Routine erzeugt auf Diskette ein PRG-File mit in diesem Fall 33 Blocks Länge, das im normalen Format (erst zwei Bytes Ladeadresse, hier etwa 8192, dann die Datenbytes aus dem Speicher) das Grafikbild enthält.
Das war's!
Zum Abschluß noch ein Hinweis zu den Routinen SETPAR (in der Fachliteratur auch oft SETLFS genannt) und SETNAM. Wie Sie wissen, dienen die beiden Routinen dazu, vor Dateibefehlen die File-, Gerätenummer, Sekundäradresse sowie den Dateinamen zu definieren. Werfen wir einen Blick hinter die Kulissen: Hier je ein komplettes Assemblerlisting, wie SETPAR und SETNAM intern funktionieren:
************************** SETPAR Fileparameter setzen
FFBA 4C 00 FE JMP $FE00 SETPAR-Sprung
FE00 85 B8 STA $B8 logische Filenummer
FE02 86 BA STX $BA Geräteadresse
FE04 84 B9 STY $B9 Sekundäradresse
FE06 60 RTS und fertig
************************** SETNAM Filenamen setzen
FFBD 4C F9 FD JMP $FDF9 SETNAM-Sprung
FDF9 85 B7 STA $B7 Länge
FDFB 86 BB STX $BB Adresse low
FDFD 84 BC STY $BC Adresse high
FDFF 60 RTS und fertig
Die Routinen SETPAR und SETNAM schreiben also nur die Inhalte der Register A, X und Y in je drei Speicherzellen, es werden also im Prinzip nur je drei POKEs ausgeführt. Was lernen wir daraus? Richtig, es ist nicht immer optimal, JSR $FFBA oder $FFBD aufzurufen. Betrachten Sie das Beispiel 5 (File-Lister). Hier sollte ab Adresse 49158 eigentlich nur die File- und Gerätenummer auf 2 gesetzt werden, auch wenn der Anwender etwas anderes oder gar nichts angegeben hat. Da sich beim Aufruf von SETPAR in 49163 jedoch ein definierter Wert auch im X-Register (Gerätenummer) befinden muß, mußten wir umständlich in 49158 die alte Gerätenummer in das Register laden, nur damit sie dann von SETPAR in $FE02 wieder gepoket werden kann. Einfacher wäre es also hier (und übrigens auch in Beispiel 4) gewesen, wenn wir den Teil von 49158 bis 49163 ersetzt hätten durch
LDA #2
STA $B8 ; logische Filenummer
STA $B9 ; Sekundäradresse
Dadurch hätten wir zwei Bytes gespart. Ebenso könnte man die Befehlsfolge
LDA #0 ; kein Filename
JSR $FFBD; SETNAM
die beim Eröffnen eines Druckerfiles die Länge des Filenamens auf Null setzt (vergleiche z.B. Beispiel 3 ab 49163) durch die beiden Befehle
LDA #0 ; kein Filename
STA $B7 ; Länge des Filenamens
ersetzen. Ersparnis: Ein Byte. Außerdem läuft die Alternative erheblich schneller als die »Kochrezept-Version«.
Damit wären wir am Ende unseres kleinen Streifzugs durch die gar nicht so weite Welt der Ein/Ausgabe in Assembler angelangt. Experimentieren Sie doch noch ein wenig mit dem Gelernten, das kann sicher nicht schaden! Zum Abschluß noch eine tabellarische Übersicht über die behandelten Betriebssystem-Routinen. Der Stern bei der Adreß-Angabe weist darauf hin, daß es sich nicht um eine sogenannte »Kernal-Routine« handelt, daß also die Adresse ausschließlich für den C 64 gilt.
Name Adresse Funktion
AXOUT $BDCD = 48589* 16-Bitzahl ausgeben
CHKIN $FFC6 = 65478 Kanal für Eingabe öffnen
CHKKOM $AEFD = 44797* Komma holen
CHKOUT $FFC9 = 65481 Kanal für Ausgabe öffnen
CHRIN $FFCF = 65487 Zeicheneingabe (siehe unten)
CHROUT $FFD2 = 65490 Zeichenausgabe
CLALL $FFE7 = 65511 Alle Dateien und Kanäle schließen
CLOSE $FFC3 = 65475 Datei schließen
CLRCHN $FFCC = 65484 Kanäle schließen
CRLF $AAD7 = 43735* Return ausgeben
GETIN $FFE4 = 65508 Zeicheneingabe (siehe unten)
GETPAR $E1D4 = 57812* LOAD/SAVE-Parameter holen
LOAD $FFD5 = 65493 Datei laden
OPEN $FFC0 = 65472 Datei öffnen
SAVE $FFD8 = 65496 Speicherbereich speichern
SETNAM $FFBD = 65469 Filenamen setzen
SETPAR $FFBA = 65466 File-, Gerätenummer und Sekundäradresse setzen
SPACE $AB3F = 43839* Leerzeichen ausgeben
Merke: Bei Eingabe von einem File sind GETIN und CHRIN gleichwertig, in diesem Fall ist aus technischen Gründen GETIN vorzuziehen. Bei Eingabe von Tastatur liest GETIN ein Zeichen aus dem Tastaturpuffer und liefert eine Null, wenn keine Taste gedrückt wurde, während bei CHRIN ein Eingabecursor erscheint und auch längere Texte erfaßt werden können.
Wichtige Speicherzellen (C 64):
$90 = 144 Statusvariable ST
$B7 = 183 Länge des Filenamens
$B8 = 184 logische Filenummer
$B9 = 185 Sekundäradresse
$BA = 186 Gerätenummer
$BB = 187 Adresse des Filenamens LOW
$BC = 188 Adresse des Filenamens HIGH