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üm- mern! Letztes Mal hatten wit ja den Sy- stem-IRQ behandelt, dessen Funktionsauf- bau 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 Syste- minterrupt 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 hat- ten) abarbeitet. Wollen wir einen eige- nen 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 wiederkeh- rende Aufgaben handelt) nicht noch umständlich um das Programmieren eines CIA-Timers kümmern müssen, sondern ein- fach die vorgegebene Timerprogrammierung übernehmen. Sie erinnern sich ja bestimmt noch da- ran, 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 IRQ- oder 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 "verbie- gen"), daß er anschließend auf unsere eigene IRQ- bzw. BRK-Routine zeigt. Ich habe Ihnen, wie schon erwähnt, ein- mal 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 Feh- ler- 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 hervor- ragend ü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 eige- ne 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 Blin- ken 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 ein- fach in den Bildschirmspeicher einzuko- pieren, was durch eine kleine, aber fei- ne 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 Zei- chen 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 an- wedet. Zu jener Initialisierungsroutine kommen wir später. Ich möchte übrigens darauf hinweisen, daß alle hier erwähn- ten Routinen und Programme mit dem HYPRA-ASS-Assembler aus der Computer- zeitschrift "64'er" erstellt wurden. Leser, die das Eigabeformat und die Be- dienungsweise 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" be- nutzt werden. Das sind Sprungmarken, die einfacher zu handhaben sind, da man so nur auf einen Namen springen muß, dessen absolute Adresse der Assembler berech- net. 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 Routi- nen entspricht! Desweiteren muß ich noch auf eine Beson- derheit in DOMSG hinweisen. Die Schleife wird erst dann verlassen, wenn das zu- letzt gelesene Zeichen 0 ist (wir Erin- nern 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 Rou- tinen relokatibel halten möchte. Ver- schiebt 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 Branchbe- fehls 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 Aus- sage 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 an- schließ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 wol- len ja den Systeminterrupt benutzen, der wie gesagt 60 Mal pro Sekunde auftritt). Für all diese Funktionen brauchen wir insgeamt 3 verschiedene Zwischenspei- cher: 1) Eine Speicherzelle, die als Inter- ruptzä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 wol- len 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 ver- merkt ist, welcher Ausgabemodus als nächstes benötigt wird. Ich nenne diese Speicherzelle einmal MSGMODE. 3) Zuletzt brauchen wir noch eine Spei- cherzelle, die mitzählt, wie oft der Text nun geblinkt hat. Ist eine ge- wisse Anzahl erreicht, so kann die IRQ-Routine in eine Routine verzwei- gen, 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 Verwendungs- zweck 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 si- cher wissen) vom Betriebssystem nicht genutzt, weshalb wir sie für unsere Zwecke verwenden können. Jetzt brauchen wir eine Initialisie- rungsroutine, die die neue IRQ-Routine in den System-IRQ einbindet und sie so- mit 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 un- sere 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 geglie- dert. 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 X- und Y-Register. Der SEI-Befehl am Anfang ist sehr wich- tig. 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 Pro- zessor in die Pampas und verabschiedet sich meistens danach. Um dem vorzubeu- gen, muß man einfach alle IRQs verhin- dern, 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 X- und Y- Register geholt und in die Speichera- dressen unseres Zeigers geschrieben. Im dritten und letzten Teil initialisie- ren wir noch zwei der drei Variablen (PULSE wurde durch das Speichern am An- fang 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. Entwe- der es steht dort 0, dann soll bei der nächsten Ausgabe der Text gedruckt wer- den, 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 (vo- rausgesetzt 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ü!)