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)