Magic Disk 64

home to index to html: MD9312-KURSE-IRQ-KURS_2.1.html
             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)        
Valid HTML 4.0 Transitional Valid CSS!