2. Unterprogramme
Bis jetzt können Sie
Ihre Programme nur Schritt für Schritt abarbeiten, oder aber mit Hilfe von
bedingten Sprüngen kleinere Schleifen ausführen. In der Tat können Sie auf
diese Weise beliebig komplexe Programme erzeugen, aber irgendwann wird die
Sache unübersichtlich und fehleranfällig. Wenn es so weit ist, und Sie den
Überblick verloren haben, können (und sollten) Sie versuchen, Dinge, die Sie
immer wieder in identischer Weise eintippen, in Unterprogramme auszulagern. Ein
populäres Beispiel ist z.B. die Ausgabe eines Zeichens an der Cursorposition,
die bereits in den Indexregistern steht. Im nächsten Beispiel sollen die
Koordinaten in den Indexregistern stehen (X=Bildschirmspalte, Y=Bildschirmzeile),
und das Zeichen selbst als PETSCII-Wert im Akkumulator. Wenn Sie nun mehrere
Zeichen (z.B. einen String) ausgeben wollen, dann ist es sinnvoll, ein
Unterprogramm wie CHAROUT zu verwenden. Das BASIC-Pendant sieht
beispielsweise so aus:
10 X=10:Y=10:A=1
20
GOSUB 10000
30 END
10000
REM *** CHAROUT ***
10010 AD=1024+(40*Y)+X
10020 POKE AD,A
10030
RETURN
2.1 Eigene
Unterprogramme erstellen
Im Endeffekt dürfte die
Assembler-Variante ähnlich einfach zu programmieren sein, denn wie man mit dem
Akkumulator rechnet, und wie man die Bildschirm-Basisadresse durch Zeiger
darstellt, wissen Sie bereits. Leider kann der 6510-Prozessor die
Multiplikation in der BASIC-Zeile 10010 nur durch zeitaufwendige
Algorithmen (die sogenannte ägyptische Multiplikationsmethode)
berechnen, und diese ist (da sie bitweise arbeitet) in Assembler nicht
schneller, als in BASIC. Sie müssen sich also an dieser Stelle (zumindest, wenn
Sie Assembler als Programm-Beschleuniger sehen) einen anderen Weg ausdenken.
Zum Glück greift Ihnen auch hier Hypra-Ass unter die
Arme. Sie können nämlich in einfacher Weise eine Tabelle erstellen, die
sämtliche 25 Zeigerwerte für den Anfang der einzelnen Bildschirmzeilen in Form
eines Hi- und Lo-Bytes enthält. Auf diese Werte können Sie dann (da die Tabelle
an einer absoluten Adresse steht) auch direkt durch ein Indexregister zugreifen
(Sie benutzen also hier die indizierte absolute Adressierung). Dies geht wie
folgt (die Werte in X, Y und A seien bereits korrekt
zugewiesen):
PHA
TYA
ASL
TAY
LDA LINETAB+1,Y
STA 249
LDA LINETAB,Y
STA 248
TXA
TAY
PLA
STA
(248),Y
Da der Akkumulator-Inhalt
sich während der Berechnungen ändert, muss das darin enthaltene Zeichen, das
Sie am Ende ausgeben wollen, zunächst auf dem Stack zwischengespeichert werden.
Anschließend muss der Zeiger, der die Startadresse auf die Zeile enthält, den Sie
im Y-Register übergeben haben, zusammengesetzt werden. Hierzu muss der
richtige Zeigerindex aus dem Array LINETAB ausgewählt werden, und
anschließend in die Adressen 248 und 249 übertragen werden. Leider haben
Zeiger, die Sie für die Y-indizierte indirekte Adressierung verwenden, 2 Bytes,
das heißt, Sie müssen den Inhalt des Y-Registers um 1 Bit nach links
verschieben, um den richtigen Address-Offset in das
Array LINETAB zu erhalten. Da es den Befehl YSL (Y shift left) nicht gibt, müssen Sie zunächst mit TYA (transfer Y to A) Y nach A
kopieren. Anschließend können Sie ASL ausführen. Allerdings müssen Sie
danach den Akkumulator wieder mit TAY (transfer
A to Y) in das Y-Register zurückkopieren. Nun
enthält Y den korrekten Offset und Sie können den richtigen 2-Byte-Zeiger
durch die absolute indizierte Adressierung aus dem Array LINETAB
auswählen. Der Zeiger in den Adressen 248/249 enthält nun den Anfang der
gewünschten Bildschirmzeile Nr. 10. Nun müssen Sie nur noch die X-Position in
das Y-Register schreiben, um mittels
STA
(248),Y
auf die Adresse
zuzugreifen, in der Sie das gewünschte Zeichen ablegen wollen. Leider steht der
Offset für die X-Position auch im X-Register, und natürlich gibt es
keinen Befehl der Form TXY. Sie müssen also auch hier zunächst mittels TXA
das X-Register in den Akkumulator kopieren, und anschließend den
Akkumulator in das Y-Register. Nun können Sie endlich das Zeichen, das
Sie eigentlich auf dem Bildschirm ausgeben wollten, vom Stack ziehen und an die
korrekte Position schreiben. Wenn Sie nun noch zusätzlich das Label CHAROUT
definieren, und Ihr Unterprogramm korrekt mit RTS beenden, dann können
Sie Ihr Unterprogramm auch mit
JSR
CHAROUT
aufrufen. Leider hat CHAROUT
die unangenehme Eigenschaft, den Inhalt Ihrer Index-Register zu verändern - und
diese bleiben auch nach Rückkehr durch RTS verändert. Sie müssen die
Index-Register also direkt beim Eintritt in Ihr Unterprogramm sichern, und
direkt vor der Rückkehr wieder herstellen. Dies können Sie auf unterschiedliche
Art und Weise erledigen, z.B. über den Stack:
PHA
TXA
PHA
TYA
PHA
PHP
…
Ihre
Routine CHAROUT in der ursprünglichen Form ohne das abschließende RTS
…
PLP
PLA
TAY
PLA
TAX
PLA
RTS
Hier werden zusätzlich zu
den Standardregistern auch die Flags gesichert (durch PHP und PLP).
Dies kann wichtig sein, wenn Sie CHAROUT innerhalb einer Schleife mit
bedingten Sprüngen und Vergleichen aufrufen. Dass Sie hier den Akkumulator
zweimal sichern, ist kein so großer Nachteil. Allerding kann es zu Problemen
kommen, wenn Sie den Stack am Ende falsch aufräumen,
denn dann findet der Prozessor nach dem RTS nicht mehr nach Hause zurück:
Er verirrt sich quasi und stürzt auch irgendwann ab. Da der 6510 nicht so viele
wichtige Register hat, können Sie die Registerinhalte auch in bestimmten
Speicherstellen sichern, z.B. so:
STA 1017
STX 1018
STY 1019
PHP
…
PLP
LDY 1019
LDX
1018
LDA 1017
Auf diese Weise
benötigen Sie nur noch zwei zusätzliche Stack-Aufrufe. Wie Sie vorgehen, ist im
Endeffekt Geschmackssache. BASIC verfährt übrigens beim SYS-Befehl auch so, wie
im letzten Beispiel, allerdings werden hier A, X und Y in
den Adressen 780, 781 und 782 gesichert. Nun haben Sie alles zusammen, um ein
laufendes Programm zu erstellen, das CHAROUT benutzt, um an der Position
{10,10} das Wort „HALLO“ auszugeben.
05-STRINGOUT
10
- .BA 49152
20
- LDX #10
30 - LDY #10
40
- LDA #0
50
- STA 1000
60
-READCHAR STX 1001
70
- LDX 1000
80
- LDA HALLO,X
90
- CMP #0
100
- BEQ EXIT
110
- LDX 1001
120
- JSR CHAROUT
130
- INX
140
- INC 1000
150
- JMP READCHAR
160
-EXIT LDX 1001
170
- RTS
180 -CHAROUT STA 1017
190
- STX 1018
200
- STY 1019
210
- PHP
220
- PHA
230
- TYA
240
- ASL
250
- TAY
260
- LDA LINETAB+1,Y
270
- STA 248
280
- LDA LINETAB,Y
290
- STA 249
300
- TXA
310
- TAY
320
- PLA
330
- STA (248),Y
340
- PLP
350
- LDA 1017
360 - LDX 1018
370 - LDY 1019
380 - RTS
1000 -LINETAB
.BY $04,$00,$04,$28,$04,$50,$04,$78,$04,$A0,$04,$C8
1010 - .BY
$04,$F0,$05,$18,$05,$40,$05,$68,$05,$90,$05,$B8
1020 -
.BY $05,$E0,$06,$08,$06,$30,$06,$58,$06,$80,$06,$A8
1030 -
.BY $06,$D0,$06,$F8,$07,$20,$07,$48,$07,$70,$07,$98
1040 -
.BY $07,$C0
1050 -HALLO .TX “HALLO“
1060 - .BY 0
Wie Sie im letzten
Beispiel sehen, können Sie die Byte-Werte für das Zeiger-Array LINETAB
mit .BY in den Speicher schreiben. Hypra-Ass
setzt automatisch die richtige absolute Adresse ein, wenn Sie das Label LINETAB
im Programmcode benutzen. Den Text „HALLO“ können Sie mit .TX
Byte für Byte im Programmspeicher ablegen, allerdings wird in dem letzten
Beispiel der auszugebende String „HALLO“ noch durch ein Nullbyte
abgeschlossen (dies nennt man auch einen nullterminierten String).
Leider benutzt Hypra-Ass die ASCII-Codierung, was
bedeutet, dass der Text in Groß/Kleinschrift ausgegeben wird (also vor SYS
49152 bitte POKE 53272,22 eingeben, oder aber noch vor Zeile 90 ein AND
#$3F einfügen).
Der Rest ist nun nicht
mehr so schwer zu verstehen. Das Hauptprogramm initialisiert zunächst die
Ausgabeposition mit Bildschirmzeile 10 und Bildschirmspalte 10 in den
Indexregistern. Nun muss noch die aktuelle Leseposition im String HALLO
auf 0 gesetzt werden, allerdings kann hierfür keines der Indexregister mehr
benutzt werden. Deshalb wird die aktuelle Leseposition im String HALLO
in der Adresse 1000 abgelegt. Zusätzlich wird das Indexregister, das in Zeile
80 verändert wird, in der Adresse 1001 zwischengespeichert, da das Sichern
auf dem Stack den Inhalt des Akkumulators verändern würde, in dem stets das
zuletzt aus dem String gelesene Zeichen stehen muss. Dieses Zeichen ist in den
ersten 4 Fällen ungleich 0, deshalb führt der CMP-Befehl und der
anschließende BEQ-Befehl in Zeile 100 auch nicht zum Label EXIT,
sondern zum Aufruf von CHAROUT. Allerdings muss in diesem Fall das X-Register
wieder hergestellt werden, da dieses die aktuelle X-Position der Zeichenausgabe
enthält. Nach der Zeichenausgabe muss sowohl der Inhalt des X-Registers
um 1 erhöht werden (mit INX), als auch der Inhalt der Speicheradresse
1000. Auch hierfür gibt es einen Befehl, nämlich den Befehl INC (increment). INC (increment)
und DEC (decrement) sind die einzigen Befehle,
die Speicherinhalte direkt verändern können, allerdings funktionieren INC
und DEC nur zusammen mit absoluten Adressen oder absoluten Adressen
zusammen mit dem X-Register. Folgendes ist also erlaubt:
INC
1000
INC 1000,X
Folgendes ist dagegen
nicht erlaubt:
INC 1000,Y
INC (248,X)
INC (248),Y
2.2
Assembler-Unterprogramme in BASIC einbinden
Manchmal wollen Sie
einfach Assembler verwenden, um Ihre langsamen BASIC-Programme an einigen
Stellen zu „tunen“. Vielleicht wollen Sie ja nur einen einfachen Texteditor
erstellen, stellen aber fest, dass das Scrolling zu langsam arbeitet. Wenn dies
die einzige Sache ist, die Sie wurmt, dann kommen Sie vielleicht auf die Idee,
den eigentlichen Text in einem bestimmten Offscreen-Speicherbereich
abzulegen, und für das Scrolling einfach eine Assembler-Routine zu verwenden,
die einen Teil des Textes ausliest, und in korrekter Weise neu in den
Bildschirmspeicher kopiert.
Inzwischen wissen Sie
schon, wie man unter BASIC Maschinenprogramme in den Speicher schreibt, nämlich
mit READ und DATA. Zunächst müssen Sie aber die DATA-Zeilen
erzeugen. Bei kurzen Programmen geht dies auch ganz einfach, Sie müssen nur
unter Hypra-Ass RUN eingeben, und anschließend
den Resetschalter drücken. Dadurch wird nur BASIC neu gestartet, Ihr
Maschinenprogramm bleibt jedoch im Speicher erhalten. Anschließend können Sie
sich Ihre Bytes in der folgenden Weise auf dem Bildschirm ausgeben lassen
(SA=Startadresse, EA=Endadresse):
FOR I=SA TO EA:PRINT PEEK(I);:NEXT
I
Leider müssen Sie nun
die Zahlen, die auf dem Bildschirm stehen, per Hand in DATA-Zeilen
umwandeln. Bei kleinen Programmen ist dies auch wie gesagt kein großes Problem,
aber bei längeren Programmen kann man sich schon leicht vertun, besonders, wenn
die Ausgabe mehrere Bildschirmseiten füllt (Sie müssen dann quasi stückweise
arbeiten). Sie können nun entweder den schon aus dem Kapitel „Datei-I/O“
bekannten „Saver“ dazu benutzen, um Ihre Module auf
Diskette auszulagern und später einfach nachzuladen. Vielleicht wollen Sie aber
auch erreichen, dass Ihre Anwendung aus nur einer einzigen Datei besteht, die
Sie dann unter Umständen auch mit einem Compiler wie Austro-Comp
beschleunigen wollen. In diesem Fall können Sie mein Programm DATAGEN
aus dem Kapitel „Datei-I/O“ des BASIC-Kurses benutzen. DATAGEN erzeugt
automatisch aus den Daten im Speicher ein BASIC-Listing, das die richtigen DATA-Zeilen
enthält.