Magic Disk 64

home to index to text: MD9312-KURSE-IRQ-KURS_2.1.txt
             Interrupt-Kurs             
     "Die Hardware ausgetrickst..."     
                (Teil 2)                

Im zweiten Teil unseres Interruptkurses wollen wir uns um die Programmierung von IRQund 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 simple Methode aufzeigen, mit der Sie einen Timer-IRQ programmiereren können. Hierbei machen wir uns zunutze, daß das Betriebssystem selbst schon standardmäßig einen solchen Interrupt über den Timer des CIA-A direkt nach dem Einschalten des Rechners installiert hat. Die Routine die diesen Interrupt bedient, steht bei Adresse $ EA31 und ist vorrangig für das Cursorblinken und die Tastaturabfrage verantwortlich. Wichtig ist, daß der Timer der CIA diesen IRQ auslöst. Hierbei 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 Interruptprogramm ausgeführt werden. Die Funktionsweise eines Timers wollen wir etwas später besprechen. Vorläufig genügt es zu wissen, daß der Betriebssystem- IRQ von einem solchen Timer im sechzigstel-Sekunden- Takt ausgelöst wird. Das heißt, daß 60 Mal pro Sekunde das Betriebssystem-IRQ- Programm abgearbeitet wird. Hierbei haben wir nun die Möglichkeit, den Prozessor über den IRQ-Vektor bei $0314/$0315 auf eine eigene 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 102 B: 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. Gestartet wird es mit SYS4096(=$1000) .
Was Sie daraufhin sehen, ist ein " X", das in der rechten, oberen Bildschirmekke im Sekundentakt vor sich hin blinkt.
Wollen wir nun klären wie wir das zustande gebracht haben:

Bei Adresse $1000-$1011 wird die  Inter-

ruptroutine vorbereitet und der Interruptvektor auf selbige verbogen. Dies geschieht durch Schreiben des Lowund Highbytes der Startadresse unserer eigenen IRQ-Routine bei $101 E in den IRQ-Vektor bei $0314/$0315 . Beachten Sie bitte, daß ich vor dieser Initialisierung zunächst einmal alle IRQs mit Hilfe des SEI-Befehls gesperrt habe. Dies muß getan werden, um zu verhindern, daß während 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 irgendeine Adresse im Speicher sein kann, würde der Prozessor sich höchstwahrscheinlich 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 $100 B-$100 F 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 $101 E nun 60 Mal pro Sekunde aufgerufen. Die Anzahl dieser Aufrufe sollen nun zunächst mitgezählt werden. Dies geschieht bei Adresse $101 E, 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 wieder 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 $102 E weiterverzweigt, wo wir prüfen, ob der Wert 60 im Zähler enthalten ist. Ist dies der Fall, so wird in die obig genannte Bildschirmspeicheradresse der Bildschirmcode für das " X"(=24) geschrieben. Gleichzeitig wird der Zähler in Speicherstelle 2 wieder auf 0 zurückgesetzt, damit der Blinkvorgang wieder von Neuem abgezählt werden kann. Auch hier wird am Ende auf die Betriebssystem- IRQ-Routine weiterverzweigt.
Ebenso, wenn keiner der beiden Werteim Zähler stand. Dieser nachträgliche Aufruf 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 laufen haben, der Cursor und die Tastaturabfrage weiterhin aktiv sind. Zum Anderen 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ümmern, da das alles ebenfalls von der System-Routine abgehandelt wird.
Bleiben nun nur noch die Zeilen von $1011 bis $101 E 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 werden 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 auskommen. Um zum Beispiel einen NMI zu programmieren, kommen Sie um die Initialisierung 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 Funktionsweise 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 Tastaturabfrage 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 Peripheriegerä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 angemerkt, daß wir das natürlich nur dann tun müssen, wenn wir einen timergesteuerten Interrupt programmieren möchten. Innerhalb der CIAs gibt es zwar noch eine ganze Reihe weiterer Möglichkeiten einen Interrupt zu erzeugen, jedoch wollen wir diese hier nicht ansprechen. 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 ausschließlich auf die timergesteuerten CIA-Interrupts konzentrieren.
Beide CIAs haben nun jeweils 16 Register, die aufgrund der Gleichheit, bei beiden Bausteinen dieselbe Funktion haben. 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, Registeroffset hinzuaddieren, je nach dem welche CIA Sie ansprechen möchten. Von den 16 Registern einer CIA sind insgesamt 7 für die Timerprogrammierung zuständig. Die anderen werden zur Datenein- und - ausgabe, sowie eine Echtzeituhr 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 getrennt voneinander laufen, und getrennte Interrupts erzeugen, oder aber zu einem einzigen 32- Bit-Timer kombiniert werden.
Was tut nun so ein Timer? Nun, prinzipiell kann man mit ihm bestimmte Ereignisse zählen, und ab einer bestimmten Anzahl dieser Ereignisse von der dazugehörigen CIA einen Interrupt auslösen lassen. Hierzu hat jeder der beiden Timer 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 Schreibzugriff auf das Register erhalten. Auf diese Weise kann der Timer nach einmaligem Herunterzählen, den Zähler wieder mit dem Anfangswert initialisieren.
Liest man ein solches Register aus, so erhält man immer den aktuellen Zählerstand. Ist der Timer dabei nicht gestoppt, so bekommt man jedesmal verschiedene Werte.
Zusätzlich gibt es zu jedem Timer auch noch ein Kontrollregister, in dem festgelegt wird, welche Ereignisse gezählt werden sollen. Weiterhin sind hier Kontrollfunktionen 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 Kontrollregister 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 anhalten soll (=1), oder aber nach jedem Unterlauf wieder mit dem Zählen vom Anfangswert 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 Hardwareerweiterung 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 Arbeitsgrundlage, wie Sie weiter unten sehen werden.
Kommen wir nun zur Beschreibung von CRB ( Reg.15) . Dieses Register ist weitgehend identisch mit CRA, jedoch unterscheiden 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 Kombinationen sind jedoch nur zwei für uns interessant. 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 Laufen bringt. Es fehlt nun nur noch ein Register, um die volle Kontrolle über die CIA-Timer zu haben. Es heißt " Interrupt- Control-Register"(" ICR") und ist in Register 13 einer CIA untergebracht.
Mit ihm wird angegeben, welche CIA-Ereignisse einen Interrupt erzeugen sollen. Auch hier sind eigentlich nur drei Bits für uns von Bedeutung. Die Restlichen 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, ebenfalls 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 Beispiel 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 erlauben. 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 Funktion. 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 ausgelesen 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öchten, 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 Interruptereignisse ignoriert. Erst wenn das Register ausgelesen wird, wird der CIA signalisiert, daß der Interrupt verarbeitet wurde und neue Unterbrechungen erlaubt sind. Auf diese Weise kann verhindert werden, daß während der Abarbeitung eines Interrupts noch ein zweiter ausgelöst wird, was womöglich das gesamte 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 einmal 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 Joystick in Port 2 über den Bildschirm bewegen. Die Abfrage desselben soll im NMI geschehen, wobei wir CIA-B 30 Mal einen Timerinterrupt pro Sekunde auslösen lassen. Timer A soll für diese Aufgabe herhalten. 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)

Valid HTML 4.0 Transitional Valid CSS!