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)