CIA-Kurs: "Die Geheimnisse des Secret-Service" (Teil 3)
Hallo zusammen zum dritten Teil des
CIA-Kurses. Diesen Monat geht' s ran an
die Bulletten, wir wollen uns endlich
einmal um die konkrete Programmierung
der CIAs und somit von Interrupts kümmern! Letztes Mal hatten wit ja den System- IRQ behandelt, dessen Funktionsaufbau wir heute brauchen werden. Ich werde
Ihnen anhand eines Beispielprogramms
einmal zeigen, wie wir den System-IRQ
für uns benutzen können. Also los
geht' s. . .
Wie Sie nun ja wissen, läuft im C64 im
Normalfall ja schon ein IRQ, der Systeminterrupt nämlich. Er wird 60 Mal pro
Sekunde aufgerufen und arbeitet eine
Jobroutine im Betriebssystem-ROM ab, die
gewisse interne Aufgaben ( die wir im letzten Monat ja schon besprochen hatten) abarbeitet. Wollen wir einen eigenen IRQ schreiben, so ist die einfachste
Methode hierfür ein " Einklinken" in den
System-IRQ. Es hat den Vorteil, daß wir
uns ( wenn es sich um zyklisch wiederkehrende Aufgaben handelt) nicht noch
umständlich um das Programmieren eines
CIA-Timers kümmern müssen, sondern einfach die vorgegebene Timerprogrammierung
übernehmen.
Sie erinnern sich ja bestimmt noch daran, daß beim Auftreten eines IRQ-Ereignisses, der Prozessor über einen
Vektor in $ FFFE/$ FFFF auf eine kleine
Jobroutine verzweigt, die feststellt, ob
der Interrupt, der aufgetreten ist, ein
IRQoder ein BRK-Interrupt war. Diese
Routine verzweigte dann wiederum über
zwei Vektoren im RAM auf verschiedene
Jobroutinen für die beiden Interrupts.
Diese waren:
* $0314/$0315 (dez. 788/789) für den IRQ * $0316/$0317 (dez. 790/791) für den BRK
Wollen wir also, daß der Prozessor jetzt
auf eine eigene Routine verzweigt, dann
müssen wir einfach den entsprechenden
Vektor hier dahingehend verändern ( im
Fachjargon spricht man auch von " verbiegen"), daß er anschließend auf unsere
eigene IRQbzw. BRK-Routine zeigt.
Ich habe Ihnen, wie schon erwähnt, einmal ein kleines Beispiel vorbereitet, das dies verdeutlichen soll. Das Problem
das ich lösen wollte, war folgendes:
Stellen Sie sich vor, Sie programmierten
gerade eine Anwendung und Sie wollten, daß Ihr Programm von Zeit zu Zeit Fehler- oder Benutzungshinweise auf dem
Bildschirm ausgibt. Damit das ganze auch
noch optisch gut ins Auge fällt, wäre es
angebracht, daß diese Mitteilung längere
Zeit aufblinkt, so daß sie dem Benutzer
buchstäblich " ins Gesicht springt" .
Dies ist eine Aufgabe, die sich hervorragend über einen IRQ lösen läßt. Es hat sogar zusätzlich noch den Vorteil, daß
das Hauptprogramm vollkommen unberührt
von dem wäre, was da angezeigt werden
soll. Es genügt also, eine Interrupt-Routine zu aktivieren, die dann so ganz
nebenher zum Beispiel die Mitteilung
" Das Programm rechnet!" ausgibt, wärend
das Hauptprogramm tatsächlich gerade mit
irgendeiner Berechnung beschäftigt ist.
Wollen wir uns ansehen, wie man eine
solche Routine nun realisiert. Zunächst
einmal brauchen wir natürlich eine eigene IRQ-Routine. Sie soll nachher die
Nachricht auf dem Bildschirm ausgeben.
Ich habe mich da einmal auf die letzte
Bildschirmzeile festgelegt. Das ist ein
Randbereich, den man gut nutzen kann.
Zur eigentlichen Textausgabe brauchen
wir zwei kleine Unterroutinen - eine, die den Text schreibt, und eine die ihn
wieder löscht, damit wir somit ein Blinken erzeugen.
Der Einfachheit halber, habe ich mich dazu entschieden, den auszugebenden Text
im Bildschirmcode irgendwo im Speicher
abzulegen. Dann genügt es nämlich ( im
Gegensatz zum ASCII-Code), den Text einfach in den Bildschirmspeicher einzukopieren, was durch eine kleine, aber feine Schleife sehr schnell erledigt wird.
Der Aufbau dieser Routine verlangt es
mitunter auch, daß das letzte Zeichen
des Textes den binären Wert 0 hat. Im
Bildschirmcode ist dies der Klammeraffe
("") .
Zum Löschen der Mitteilungszeile genügt
es, diese mit dem Bildschirmcode für
" SPACE" aufzufüllen. Das wäre gleich dem
Vorgang, wenn Sie mit dem Cursor in die
unterste Zeile des Bildschirms fahren und nun 40 Mal die SPACE-Taste drücken
würden. Der Bildschirmcode für das Zeichen SPACE ist 32($20) .
Hier nun also die beiden Routinen, die
diese Aufgaben übernehmen. DOMSG gibt
den Bildschirmtext aus, und BLANK löscht die Mitteilungszeile. Zu DOMSG sei noch
zu sagen, daß die Anfangsadresse des
auszugebenden Textes, vorher schon von
der Interrupt-Initialierungsroutine in
die beiden Adressen nach dem LDA-Befehl
geschrieben wurde. Das Programm hat sich
also selbst verändert. Dies ist ( für
uns) die einfachste und sinnvollste Lösung, die man in einem solchen Fall anwedet. Zu jener Initialisierungsroutine
kommen wir später. Ich möchte übrigens
darauf hinweisen, daß alle hier erwähnten Routinen und Programme mit dem
HYPRA-ASS- Assembler aus der Computerzeitschrift "64' er" erstellt wurden.
Leser, die das Eigabeformat und die Bedienungsweise dieses Assemblers kennen, können sich also glücklich schätzen.
Trotzdem werde ich die auftauchenden
Assembler-Besonderheiten hier erklären, damit Sie die Programme auch mit jedem
anderen Assembler eingeben können. Sie
sollten auf jeden Fall wissen, daß in
diesem Assembler anstatt absoluter Sprungadressen sogenannte " Labels" benutzt werden. Das sind Sprungmarken, die
einfacher zu handhaben sind, da man so
nur auf einen Namen springen muß, dessen
absolute Adresse der Assembler berechnet. Assemblerprogrammier sollten sich
aber sowieso mit Labels auskennen, da
sie heutzutage in jedem Assembler
Verwendung finden.
----------------- DOMSG LDY #00 Y-Reg. als Zeiger initialisieren. LOOP1 LDA $C000,Y Zeichen holen (An- fangsadresse Text plus Y-Offset). BEQ L3 War das letzte Zei- chen gleich 0 (= ), dann ENDE. STA $07C0,Y Ansonsten Zeichen in Bildschirmspecher schreiben. INY Zähler erhöhen. BNE LOOP1 Unbedingter Sprung. ------------------ BLANK LDY #39 Y-Reg. als Zeiger initialisieren (39+1=40 Zeichen fül- len). LDA #32 Bildschirmcode für SPACE in Akku holen. STA $07C0,Y Akku in Bildschirm- speicher entleeren. DEY Zähler erniedrigen. BPL LOOP2 Solange wiederholen, bis 0 unterschritten wird (Y ist dann ne- gativ). RTS Und Tschüß! ------------------
Die Anfangsadresse der Mitteilungszeile
( im Folgenden MSG-Zeile; MSG = Message = Mitteilung) ist logischerweise die
Adresse des ersten Zeichens in der 25 .
und letzten Zeile des Bildschirms. Sie
errechnet sich aus der Basisadresse des
Bildschirmspeichers ( normalerweise=1024)
plus der Zeilenanzahl-1 multipliziert
mit 40 . Da unsere Zeilenanzahl 25 ist, lautet die Rechunug für uns:
1024+(25-1)*40=1024+24*40=1984 (=$07C0)
Was der Adresse in unseren beiden Routinen entspricht!
Desweiteren muß ich noch auf eine Besonderheit in DOMSG hinweisen. Die Schleife
wird erst dann verlassen, wenn das zuletzt gelesene Zeichen 0 ist ( wir Erinnern uns - das ist die Endmarkierung) .
Über BEQ springen wir dann auf den RTS-Befehl der BLANK-Routine. Der Branch-Befehl BNE am Ende der Routine ist ein
sogenannter unbedingter Sprung. Die hier
abgefragte Bedingung ist immer erfüllt, weil das vorher inkrementierte Y-Register nie die 0 erreichen wird, da
die maximale Zeichenanzahl ja 40 ist.
Mehr wäre unsinnig, denn man sähe diese
Zeichen ja gar nicht auf dem Bildschirm.
Versierte Assembler-Programmierer kennen
diese Art des Springens. Sie hat den
Vorteil der Speicherersparnis ( ein JMP-Befehl belegt immer 3 Bytes, ein
Branch-Befehl, wie BNE nur 2) und ist
zusätzlich ganz sinnvoll, wenn man Routinen relokatibel halten möchte. Verschiebt man ein Assembler-Programm im
Speicher, so ist es nur dann auch an
anderer Stelle lauffähig, wenn keine
absoluten Zugriffe auf programminterne
Adressen stattfinden. Der JMP-Befehl
springt ja immer absolut und somit auf
die alte Adresse. Der BNE-Befehl jedoch
ist relativ adressiert. Er merkt sich
nur um wieviele Bytes im Speicher er
nach vorne, oder nach hinten springen
muß.
Wird der Sprungbereich eines Branchbefehls nicht überschritten (+127 und -128 Bytes vom Befehl selbst entfernt), so
ist der Einsatz von unbedingten Sprüngen
sehr sinnvoll ( insofern möglich, also
wenn man über den Inhalt eines bestimm- ten Prozessorflags eine eindeutige Aussage machen kann) .
Doch zurück zu unserer Message-Ausgabe- Routine. Außer den beiden Routinen zur
Textausgabe, brauchen wir auch die IRQ-Routine selbst, die den Aufruf dieser
beiden Routinen steuert.
Legen wir nun also einmal fest, daß der
Message-Text jeweils einmal pro Sekunde
blinken soll. Das heißt im Klartext, daß
wir zunächst einmal den Text auf den
Bildschirm schreiben müssen, wo er eine
halbe Sekunde stehen bleibt und ihn anschließend wieder für eine halbe Sekunde
löschen. Dieser Vorgang wiederholt sich
nun beliebig oft. Damit das Ganze aber
auch irgendwann einmal ein Ende hat, müssen wir nach einer bestimmten Anzahl
von Blinkzyklen die Interruptroutine
auch wieder abschalten.
Da immer alle halbe Sekunde eine der
beiden Ausgaben getätigt werden soll, müssen wir so logischerweise alle 30
Interrupts eine solche tätigen ( wir wollen ja den Systeminterrupt benutzen, der
wie gesagt 60 Mal pro Sekunde auftritt) .
Für all diese Funktionen brauchen wir
insgeamt 3 verschiedene Zwischenspeicher:
1) Eine Speicherzelle, die als Interruptzähler dienen soll. Sie zählt 30 Interrupts mit und veranlaßt dann die
IRQ-Routine eine der beiden Ausgaben
zu tätigen. Dieser Speicherzelle wollen wir den schlichten Namen COUNTER
(=" Zähler") geben.
2) Damit die IRQ-Routine auch immer
weiß, ob sie nun Text ausgeben, oder
Text löschen soll, brauchen wir auch
noch eine Speicherzelle, in der vermerkt ist, welcher Ausgabemodus als
nächstes benötigt wird. Ich nenne
diese Speicherzelle einmal MSGMODE.
3) Zuletzt brauchen wir noch eine Speicherzelle, die mitzählt, wie oft der
Text nun geblinkt hat. Ist eine ge- wisse Anzahl erreicht, so kann die
IRQ-Routine in eine Routine verzweigen, die diese selbst beendet. Diese
Adresse habe ich PULSE genannt.
Die drei soeben erwähnten Namen werden
alle in dem nun folgenden Source-Listing
für die IRQ-Routine verwendet, so daß
Sie jetzt also über deren Verwendungszweck informiert sind. Ich habe diesen
Namen natürlich auch schon absolute
Adressen zugewiesen. HYPRA-ASS benutzt
dafür den Pseudo-Opcode " . EQ" für " is
EQual"(=" ist gleich") . Schauen Sie sich
hierzu auch einmal den Sourcecode zu
MSGOUT an ( auf der Vorderseite dieser MD
als " MSGOUT-ROM. SRC") .
Die Zwischenspeicher belegen in der oben
angezeigten Reihenfolge die Adressen
$ FB,$ FC,$ FD. Diese sind allesamt aus
der Zeropage, und ( wie die fleißigen
Assembler-Programmierer unter Ihnen sicher wissen) vom Betriebssystem nicht
genutzt, weshalb wir sie für unsere Zwecke verwenden können.
Jetzt brauchen wir eine Initialisierungsroutine, die die neue IRQ-Routine
in den System-IRQ einbindet und sie somit im System installiert. Das hier ist
sie ( im Source-Listing auf dieser MD
steht sie ganz am Anfang und heißt MS-GOUT) :
------------ SEI Alle weiteren IRQs sperren (wegen, des Verbiegens der Vektoren). STA PULSE Im Akku steht die Blinkan- zahl; also merken wir sie uns. STX LOOP1+1 LO-Byte der Anfangsadresse des Textes wird in DOMSG eingesetzt (LOOP1 ist ein Label davon, siehe oben). STY LOOP1+2 Dasselbe mit dem HI-Byte. ------------ LDX #<(IRQ) LO-Byte der Anfangsadresse
der neuen IRQ-Routine laden
( siehe unten) .
LDY #>( IRQ) HI-Byte laden.
STX $0314 LO-Byte des Vektors auf unsere Routine ausrichten.
STY $0315 HI-Byte des Vektor auf unsere Routine ausrichten.
------------ LDA #01 Initialisierungswert in Akku laden. STA COUNTER Zählregister damit initia- lisieren. STA MSGMODE Mode-Register damit initia- lisieren. CLI Alle Voreinstellungen getä- tigt; wir können den IRQ wieder freigeben. RTS Und Tschüß! ------------
Die Routine ist in 3 Abschnitte gegliedert. Im ersten Abschnitt werden
zunächst die Aufruf-Parameter gemerkt.
Diese sind:
* Die Anzahl der Blinkvorgänge steht im
Akku.
* Die Anfangsadresse des auszugebenden
Textes steht in LO/ HI-Byte- Darstellung
in Xund Y-Register.
Der SEI-Befehl am Anfang ist sehr wichtig. Wir müssen nämlich davon ausgehen, daß gerade dann ein Interrupt auftreten
könnte, wenn das LO-Byte der neuen IRQ-Adresse schon gesetzt ist, das HI-Byte
jedoch noch nicht. Dann zeigt der Vektor
irgendwo in den Speicher hinein. Tritt
jetzt ein IRQ auf, dann springt der Prozessor in die Pampas und verabschiedet
sich meistens danach. Um dem vorzubeugen, muß man einfach alle IRQs verhindern, was ja mit dem SEI-Befehl erzielt
wird. Der Prozessor ignoriert jetzt die
Tatsache, daß da die CIA1 gerade einen
Interrupt meldet und wir können in Ruhe
den Vektor neu setzen.
Diese Aufgabe erledigt der zweite Teil unserer Routine. Die Anfangsadresse der
neuen IRQ-Routine wird in Xund Y-Register geholt und in die Speicheradressen unseres Zeigers geschrieben.
Im dritten und letzten Teil initialisieren wir noch zwei der drei Variablen
( PULSE wurde durch das Speichern am Anfang der Routine schon gesetzt) . Den
COUNTER laden wir mit 1, damit gleich
beim nächsten Interrupt eine Ausgabe
erfolgt ( siehe auch unten) . MSGOUT kann
zwei verschiedene Zustände haben. Entweder es steht dort 0, dann soll bei der
nächsten Ausgabe der Text gedruckt werden, oder wir haben dort eine 1, dann
soll die MSG-Zeile gelöscht werden. Ich
initialisiere hier mit 1, damit beim
ersten Aufruf die Zeile erst einmal
gelöscht wird. Würden wir zuerst den
Text schreiben, könnte es uns passieren, daß in den verbleibenden Zeichen ( vorausgesetzt der Text ist weniger als 40 Zeichen lang) noch alter " Zeichenmüll"
im Bildschirmspeicher steht.
Kommen wir nun endlich zur Interrupt- rountine selbst. Hier einmal das Li- sting: ----------------- IRQ DEC COUNTER Zähler herunterzäh- len. BEQ L1 Wenn Zähler=0, dann verzweigen auf Ausga- be. JMP SYSIRQ Sonst springen wir auf den System-IRQ. ----------------- L1 LDA MSGMODE Welcher Ausgabemodus? BNE L2 Ungleich 0, also verzweigen auf "Zeile löschen"! ----------------- INC MSGMODE Schon mal den MSGMODE auf 1 schalten. JSR DOMSG MSG ausgeben. JMP PRP Und Ausgaberoutine
verlassen.
------------------
L2 DEC MSGMODE Schon mal den MSGMODE auf 0 schalten. JSR BLANK Und MSG-Zeile lö- schen. ------------------ DEC PULSE Wenn Zeile gelöscht, ist ein Blinkvorgang abgeschlossen. Also Zähler für Blinks erniedrigen. BPL PRP Wenn noch nicht die 0 unterschritten wurde, Ausgaberoutine ver- lassen. ------------------ LDX #<(SYSIRQ) Wenn ja, dann haben wir oft genug ge- blinkt... LDY #>(SYSIRQ) ...also LO/HI-Byte der Adresse vom Sy- stem-IRQ laden. STX $0314 Und den IRQ-Vektor... STY $0315 ...wieder auf den System-IRQ setzen. JMP SYSIRQ Eigenen IRQ mit Sprung auf System-IRQ beenden. ------------------ PRP LDA #30 Akku laden... STA COUNTER ...und den IRQ-Zähler neu initialisieren. JMP SYSIRQ Auch hier beenden wir den eigenen IRQ mit einem Sprung auf den System-IRQ. ------------------
( Anm. d. Red. : Bitte Laden Sie jetzt den zweiten Teil des CIA-Kurses aus dem Kursmenü!)