Interrupt-Kurs "Die Hardware ausgetrickst..." (Teil 2) ----------------------------------------
Im zweiten Teil unseres Interruptkurses wollen wir uns um die Programmierung von IRQ- und NMI-Interrupts kümmern. Hierbei soll es vorrangig um die Auslösung der Beiden durch die beiden CIA-Chips des C64 gehen. 1) DER BETRIEBSSYSTEM-IRQ Um einen einfachen Anfang zu machen, möchte ich Ihnen zunächst eine sehr sim- ple Methode aufzeigen, mit der Sie einen Timer-IRQ programmiereren können. Hier- bei machen wir uns zunutze, daß das Be- triebssystem selbst schon standardmäßig einen solchen Interrupt über den Timer des CIA-A direkt nach dem Einschalten des Rechners installiert hat. Die Routi- ne die diesen Interrupt bedient, steht bei Adresse $EA31 und ist vorrangig für das Cursorblinken und die Tastaturabfra- ge verantwortlich. Wichtig ist, daß der Timer der CIA diesen IRQ auslöst. Hier- bei handelt es sich um eine Vorrichtung, mit der frei definierbare Zeitintervalle abgewartet werden können. In Kombination mit einem Interrupt kann so immer nach einer bestimmten Zeitspanne ein Inter- ruptprogramm ausgeführt werden. Die Funktionsweise eines Timers wollen wir etwas später besprechen. Vorläufig genügt es zu wissen, daß der Betriebs- system-IRQ von einem solchen Timer im sechzigstel-Sekunden-Takt ausgelöst wird. Das heißt, daß 60 Mal pro Sekunde das Betriebssystem-IRQ-Programm abgear- beitet wird. Hierbei haben wir nun die Möglichkeit, den Prozessor über den IRQ-Vektor bei $0314/$0315 auf eine ei- gene Routine springen zu lassen. Dazu muß dieser Vektor ledeiglich auf die Anfangsadresse unseres eigenen Programms verbogen werden. Hier einmal ein Bei- spielprogramm:
1000: SEI ;IRQs sperren 1001: LDX #$1E ;IRQ-Vektor bei 1003: LDY #$10 ; $0314/$0315 auf eigene 1005: STX $0314 ; Routine bei $101E 1008: STY $0315 ; verbiegen 100B: LDA #00 ;Interruptzähler in Adr. 100D: STA $02 ; $02 auf 0 setzen 100F: CLI ;IRQs wieder erlauben 1010: RTS ;ENDE --- 1011: SEI ;IRQs sperren 1012: LDX #$31 ;IRQ-Vektor bei 1014: LDY #$EA ; $0314/$0315 wieder 1016: STX $0314 ; auf normale IRQ-Rout. 1019: STY $0315 ; zurücksetzen. 101C: CLI ;IRQs wieder erlauben 101D: RTS ;ENDE --- 101E: INC $02 ;Interruptzähler +1 1020: LDA $02 ;Zähler in Akku holen 1022: CMP #30 ;Zähler=30? 1024: BNE 102E ;Nein, also weiter
1026: LDA #32 ;Ja, also Zeichen in 1028: STA $0427 ; $0427 löschen 102B: JMP $EA31 ;Und SYS-IRQ anspringen ---
102E: CMP #60 ;Zähler=60? 1030: BNE 103B ;Nein, also weiter 1032: LDA #24 ;Ja, also "X"-Zeichen in 1034: STA $0427 ; $0427 schreiben 1037: LDA #00 ;Zähler wieder auf 1039: STA $02 ; Null setzen 103B: JMP $EA31 ;Und SYS-IRQ anspringen
Sie finden dieses Programm übrigens auch als ausführaren Code auf dieser MD unter dem Namen "SYSIRQ-DEMO". Sie müssen es mit "...,8,1" laden und können es sich mit einem Disassembler anschauen. Ge- startet wird es mit SYS4096 (=$1000). Was Sie daraufhin sehen, ist ein "X", das in der rechten, oberen Bildschirmek- ke im Sekundentakt vor sich hin blinkt. Wollen wir nun klären wie wir das zu- stande gebracht haben:
Bei Adresse $1000-$1011 wird die Inter-
ruptroutine vorbereitet und der Inter- ruptvektor auf selbige verbogen. Dies geschieht durch Schreiben des Low- und Highbytes der Startadresse unserer eige- nen IRQ-Routine bei $101E in den IRQ- Vektor bei $0314/$0315. Beachten Sie bitte, daß ich vor dieser Initialisie- rung zunächst einmal alle IRQs mit Hilfe des SEI-Befehls gesperrt habe. Dies muß getan werden, um zu verhindern, daß wäh- rend des Verbiegens des IRQ-Vektors ein solcher Interrupt auftritt. Hätten wir nämlich gerade erst das Low-Byte dieser Adresse geschrieben, wenn der Interrupt ausgelöst wird, so würde der Prozessor an eine Adresse springen, die aus dem High-Byte des alten und dem Low-Byte des neuen Vektors bestünde. Da dies irgend- eine Adresse im Speicher sein kann, wür- de der Prozessor sich höchstwahrschein- lich zu diesem Zeitpunkt verabschieden, da er nicht unbedingt ein sinnvolles Programm dort vorfindet. Demnach muß also unterbunden werden, daß solch ein unkontrollierter Interrupt auftreten kann, indem der IRQ mittels SEI einfach gesperrt wird. Bei $100B-$100F setzen wir nun noch die Zeropageadresse 2 auf Null. Sie soll der IRQ-Routine später als Interruptzähler dienen. Anschließend werden die IRQs wieder mittels CLI-Befehl erlaubt und das Programm wird beendet. Durch das Verbiegen des Interruptvektors und dadurch, daß schon ein Timer-IRQ von Betriebssystem installiert wurde, wird unser Programm bei $101E nun 60 Mal pro Sekunde aufgerufen. Die Anzahl dieser Aufrufe sollen nun zunächst mitgezählt werden. Dies geschieht bei Adresse $101E, wo wir die Zähleradresse bei $02 nach jedem Aufruf um 1 erhöhen. Unser "Sekunden-X" soll nun einmal pro Sekunde aufblinken, wobei es eine halbe Sekunde lang sichtbar und eine weitere halbe Sekunde unsichtbar sein soll. Da wir pro Sekunde 60 Aufrufe haben, müssen wir logischerweise nach 30 IRQs das "X"- Zeichen löschen und es nach 60 IRQs wie- der setzen. Dies geschieht nun in den folgenden Zeilen. Hier holen wir uns den IRQ-Zählerstand zunächst in den Akku und vergleichen, ob er schon bei 30 ist. Wenn ja, so wird ein Leerzeichen (Code 32) in die Bildschirmspeicheradresse $0427 geschrieben. Anschließend springt die Routine an Adresse $EA31. Sie liegt im Betriebssystem-ROM und enthält die ursprüngliche Betriebssystem-IRQ- Routine, die ja weiterhin arbeiten soll. Ist die 30 nicht erreicht, so wird nach $102E weiterverzweigt, wo wir prüfen, ob der Wert 60 im Zähler enthalten ist. Ist dies der Fall, so wird in die obig ge- nannte Bildschirmspeicheradresse der Bildschirmcode für das "X" (=24) ge- schrieben. Gleichzeitig wird der Zähler in Speicherstelle 2 wieder auf 0 zurück- gesetzt, damit der Blinkvorgang wieder von Neuem abgezählt werden kann. Auch hier wird am Ende auf die Betriebs- system-IRQ-Routine weiterverzweigt. Ebenso, wenn keiner der beiden Werteim Zähler stand. Dieser nachträgliche Auf- ruf des System-IRQs hat zwei Vorteile: zum Einen werden die Systemfunktionen, die von dieser Routine behandelt werden, weiterhin ausgeführt. Das heißt, daß obwohl wir einen eigenen Interrupt lau- fen haben, der Cursor und die Tastatu- rabfrage weiterhin aktiv sind. Zum Ande- ren brauchen wir uns dabei auch nicht um das zurücksetzen der Timerregister (mehr dazu weiter unten) oder das Zurückholen der Prozessorregister (sie wurden ja beim Auftreten des IRQs auf dem Stapel gerettet - sh. Teil1 dieses Kurses) küm- mern, da das alles ebenfalls von der System-Routine abgehandelt wird. Bleiben nun nur noch die Zeilen von $1011 bis $101E zu erläutern. Es handelt sich hierbei um ein Programm, mit der wir unseren Interrupt wieder aus dem System entfernen. Es wird hierbei wie bei der Initialisierung des Interrupts vorgegangen. Nach Abschalten der IRQs wird die alte Vektoradresse $EA31 wieder in $0314/$0315 geschrieben. Dadurch wer- den die IRQs wieder direkt zur System- IRQ-Routine geleitet. Sie werden nun mittels CLI erlaubt und das Programm wird beendet. 2) DIE PROGRAMMIERUNG DER TIMER Einen Interrupt auf die obig genannte Weise in das System "einzuklinken" ist zwar eine ganz angenehme Methode, jedoch mag es vorkommen, daß Sie für spezielle Problemstellungen damit garnicht auskom- men. Um zum Beispiel einen NMI zu pro- grammieren, kommen Sie um die Initiali- sierung des Timers nicht herum, da das Betriebssystem diesen Interrupt nicht verwendet. Deshalb wollen wir nun einmal anfangen, in die Eingeweide der Hardware des C64 vorzustoßen um die Funktionswei- se der CIA-Timer zu ergründen. Zunächst einmal sollte erwähnt werden, daß die beiden CIA-Bausteine einander gleichen wie ein Ei dem Anderen. Unter- schiedlich ist lediglich die Art und Weise, wie sie im C64 genutzt werden. CIA-A ist haupsächlich mit der Tastatur- abfrage beschäftigt und übernimmt auch die Abfrage der Gameports, wo Joystick, Maus und Paddles angeschlossen werden. Sie kann die IRQ-Leitung des Prozessors ansprechen, weswegen sie zur Erzeugung solcher Interrupts harangezogen wird. CIA-B hingegen steuert die Peripherie- geräte, sowie den Userport. Zusätzlich hierzu erzeugt sie die Interruptsignale, die einen NMI auslösen. Je nach dem ob wir nun IRQs oder NMIs erzeugen möchten, müssen wir also entweder auf CIA-A, oder CIA-B zurückgreifen. Hierbei sei ange- merkt, daß wir das natürlich nur dann tun müssen, wenn wir einen timerge- steuerten Interrupt programmieren möch- ten. Innerhalb der CIAs gibt es zwar noch eine ganze Reihe weiterer Möglich- keiten einen Interrupt zu erzeugen, je- doch wollen wir diese hier nicht anspre- chen. Hier muß ich Sie auf einen schon vor längerer Zeit in der MD erschienenen CIA-Kurs, in dem alle CIA-Interrupt- quellen ausführlich behandelt wurden, verweisen. Wir wollen uns hier aus- schließlich auf die timergesteuerten CIA-Interrupts konzentrieren. Beide CIAs haben nun jeweils 16 Regi- ster, die aufgrund der Gleichheit, bei beiden Bausteinen dieselbe Funktion ha- ben. Einziger Unterschied ist, daß die Register von CIA-A bei $DC00, und die von CIA-B bei $DD00 angesiedelt sind. Diese Basisadressen müssen Sie also zu dem entsprechenden, hier genannten, Re- gisteroffset hinzuaddieren, je nach dem welche CIA Sie ansprechen möchten. Von den 16 Registern einer CIA sind insge- samt 7 für die Timerprogrammierung zuständig. Die anderen werden zur Daten- ein- und -ausgabe, sowie eine Echtzeit- uhr verwandt und sollen uns hier nicht interessieren. In jeder der beiden CIAs befinden sich nun zwei 16-Bit-Timer, die man mit Timer A und B bezeichnet. Beide können ge- trennt voneinander laufen, und getrennte Interrupts erzeugen, oder aber zu einem einzigen 32-Bit-Timer kombiniert werden. Was tut nun so ein Timer? Nun, prinzi- piell kann man mit ihm bestimmte Ereig- nisse zählen, und ab einer bestimmten Anzahl dieser Ereignisse von der dazu- gehörigen CIA einen Interrupt auslösen lassen. Hierzu hat jeder der beiden Ti- mer zwei Register, in denen die Anzahl der zu zählenden Ereignisse in Low/High- Byte-Darstellung geschrieben wird. Von diesem Wert aus zählt der Timer bis zu einem Unterlauf (Zählerwert=0), und löst anschließend einen Interrupt aus. Hier eine Liste mit den 4 Zählerregistern:
Reg. Name Funktion ----------------------------------------
4 TALO Low-Byte Timerwert A 5 TAHI High-Byte Timerwert A 6 TBLO Low-Byte Timerwert B
7 TBHI High-Byte Timerwert B Schreibt man nun einen Wert in diese Timerregister, so wird selbiger in ein internes "Latch"-Register übertragen und bleibt dort bis zu nächsten Schreibzu- griff auf das Register erhalten. Auf diese Weise kann der Timer nach einmali- gem Herunterzählen, den Zähler wieder mit dem Anfangswert initialisieren. Liest man ein solches Register aus, so erhält man immer den aktuellen Zähler- stand. Ist der Timer dabei nicht ge- stoppt, so bekommt man jedesmal ver- schiedene Werte. Zusätzlich gibt es zu jedem Timer auch noch ein Kontrollregister, in dem fest- gelegt wird, welche Ereignisse gezählt werden sollen. Weiterhin sind hier Kon- trollfunktionen untergebracht, mit denen man den Timer z.B. starten und stoppen kann. Auch hier gibt es einige Bits, die für uns irrelevant sind, weswegen ich sie hier nicht nenne. Das Kontrollregi- ster für Timer A heißt "CRA" und liegt an Registeroffset 14, das für Timer B heißt "CRB" und ist im CIA-Register 15 untergebracht. Hier nun die Bitbelegung von CRA: Bit 0 (START/STOP) Mit diesem Bit schalten Sie den Timer an (=1) oder aus (=0). Bit 3 (ONE-SHOT/CONTINOUS) Hiermit wird bestimmt, ob der Timer nur ein einziges Mal zählen, und dann anhal- ten soll (=1), oder aber nach jedem Un- terlauf wieder mit dem Zählen vom An- fangswert aus beginnen soll. Bit 4 (FORCE LOAD) Ist dieses Bit bei einem Schreibvorgang auf das Register gesetzt, so wird das Zählregister, unabhängig, ob es gerade läuft oder nicht, mit dem Startwert aus dem Latch-Register initialisiert. Bit 5 (IN MODE) Dieses Bit bestimmt, welche Ereignisse Timer A zählen soll. Bei gesetztem Bit werden positive Signale am CNT-Eingang der CIA gezählt. Da das jedoch nur im Zusammenhang mit einer Hardwareerweite- rung einen Sinn hat, lassen wir das Bit gelöscht. In dem Fall zählt der Timer nämlich die Taktzyklen des Rechners. Dies ist generell auch unsere Arbeits- grundlage, wie Sie weiter unten sehen werden. Kommen wir nun zur Beschreibung von CRB (Reg. 15). Dieses Register ist weitge- hend identisch mit CRA, jedoch unter- scheiden sich Bit 5 und 6 voneinander. Diese beiden Bits bestimmen nämlich ZU- SAMMEN, die Zählerquelle für Timer B (IN MODE). Aus den vier möglichen Kombina- tionen sind jedoch nur zwei für uns in- teressant. Setzt man beide Bits auf 0, so zählt Timer B wieder Systemtaktimpul- se. Setzt man Bit 6 auf 1 und Bit 5 auf 0, so werden Unterläufe von Timer A gezählt. Auf diese Art und Weise kann man beide Timer miteinander koppeln, und somit Zählerwerte verwenden, die größer als $FFFF sind (was der Maximalwert für ein 16-Bit-Wert ist). Nun wissen wir also, wie man die beiden Timer initialisieren kann, und zum Lau- fen bringt. Es fehlt nun nur noch ein Register, um die volle Kontrolle über die CIA-Timer zu haben. Es heißt "Inter- rupt-Control-Register" ("ICR") und ist in Register 13 einer CIA untergebracht. Mit ihm wird angegeben, welche CIA- Ereignisse einen Interrupt erzeugen sol- len. Auch hier sind eigentlich nur drei Bits für uns von Bedeutung. Die Restli- chen steuern andere Interruptquellen der CIA, die uns im Rahmen dieses Kurses nicht interessieren sollen. Es sei angemerkt, daß der Schreibzugriff auf dieses Register etwas anders funk- tioniert als sonst. Will man nämlich bestimmte Bits setzen, so muß Bit 7 des Wertes, den wir schreiben möchten, eben- falls gesetzt sein. Alle anderen Bits werden dann auch im ICR gesetzt. Die Bits, die im Schreibwert auf 0 sind, beeinflussen den Registerinhalt nicht. So kann z.B. Bit 0 im ICR schon gesetzt sein. Schreibt man nun den Binärwert 10000010 (=$81) in das Register, so wird zusätzlich noch Bit 1 gesetzt. Bit 0 bleibt davon unberührt, und ebenfalls gesetzt (obwohl es im Schreibwert gelöscht ist!). Umgekehrt, werden bei gelöschtem 7. Bit alle gesetzten Bits des Schreibwertes im ICR gelöscht. Um also Bit 0 und 1 zu löschen müsste der Binärwert 00000011 geschrieben werden. Näheres dazu finden Sie in einem Bei- spiel weiter unten. Die nun für uns relevanten Bits sind die schon angesprochenen Bits 0, 1 und 7. Die Funktion des 7. Bits sollte Ihnen jetzt ja klar sein. Bit 0 und 1 geben an, ob Timer A oder Timer B (oder beide) einen Interrupt auslösen sollen. Sie müssen das entsprechende Bit lediglich auf die oben beschriebene Art setzen, um einen entsprechenden Interrupt zu erlau- ben. Um z.B. einen Timer-A-Unterlauf als Interruptquelle zu definieren, müssen Sie den Wert $81 in das ICR schreiben. Für einen Timer-B-Unterlauf $82. Für beide Timer als Interruptquelle $83. Das ICR hat nun noch eine weitere Funk- tion. Tritt nämlich ein Interrupt auf, so wissen wir als Programmierer ja noch nicht, ob es tatsächlich ein CIA- Interrupt war, da es auch moch andere Interruptquellen als nur die CIA gibt. Um nun zu überprüfen, ob der Interrupt von einer CIA stammt, kann das ICR aus- gelesen werden. Ist in diesem Wert nun das 7. Bit gesetzt, so heißt das, das eines der erlaubten Interruptereignisse eingetreten ist. Wenn wir wissen möch- ten, um welches Ereignis es sich dabei genau handelt, brauchen wir nur die Bits zu überprüfen, die die Interruptquelle angeben. Ist Bit 0 gesetzt, so war es Timer A, der den Interrupt auslöste, ist Bit 1 gesetzt, so kam die Unterbrechung von Timer B. Das Auslesen des ICR hat übrigens noch eine weitere Funktion: solange in diesem Register ein Interrupt gemeldet ist, werden weitere Interrupt- ereignisse ignoriert. Erst wenn das Re- gister ausgelesen wird, wird der CIA signalisiert, daß der Interrupt verar- beitet wurde und neue Unterbrechungen erlaubt sind. Auf diese Weise kann ver- hindert werden, daß während der Abarbei- tung eines Interrupts noch ein zweiter ausgelöst wird, was womöglich das gesam- te Interruptsystem durcheinander bringen könnte. Sie müssen also, egal ob Sie sicher sind, daß der Interrupt von der CIA kam, oder nicht - das ICR immer ein- mal pro Interrupt auslesen, damit der Nächste ausgelöst werden kann. Beachten Sie dabei auch, daß Sie das Register mit dem Auslesen gleichzeitig auch löschen! Sie können den gelesenen Wert also nicht zweimal über das Register abfragen! Nach all der trockenen Theorie, wollen wir einmal in die Praxis übergehen und uns einem Programmbeispiel widmen. Wir wollen einmal ein Sprite mittels Joy- stick in Port 2 über den Bildschirm be- wegen. Die Abfrage desselben soll im NMI geschehen, wobei wir CIA-B 30 Mal einen Timerinterrupt pro Sekunde auslösen las- sen. Timer A soll für diese Aufgabe her- halten. Hier das Programmlisting des Beispiels, das Sie auf dieser MD auch unter dem Namen "NMI-SPR-DEMO" finden: (Anm.d.Red.: Bitte laden Sie jetzt Teil 2 dieses Artikels)