CIA-Kurs: "Die Geheimnisse des Secret-Service..." (Teil 1)
1) Einleitung:
Herzlich willkommen zu unserer neuen
Kurs-Serie. Nachdem Sie mein Kollege IVO
HERZEG die letzten Monate ja eingehend
in die Matierie der Raster-Interrupts
eingeführt hat, will ich Ihnen nun ein
weiterführendes Thema anbieten: die
CIA-Bausteine des C 64 .
Diese steuern ( wie im Raster-IRQ- Kurs
schon angedeutet) die übrigen Interruptfunktionen unseres Rechners und sind für
den Kontakt mit der Außenwelt des 64 ers
verantwortlich. Ohne sie könnten wir ihn
in keinster Weise bedienen - die Bedienung der Tastatur und des Joysticks oder die Datenspeicherung auf Massenmedien wie Kassette oder Diskette wäre gar nicht möglich. Sie sehen also, daß in
den nächsten Monaten einige ganz interessante Themen auf Sie zukommen werden.
Ich möchte mich zunächst einmal um die
Interruptprgrammierung kümmern und in
fortführenden Folgen Anleitungen zur
Bedienung der Ein-/ Ausgabeeinheiten des
64 ers geben. Wir werden dann den Joystick einmal genauer unter die Lupe nehmen und auch den Anschluß einer Maus
durchführen, ganz abgesehen von den
vielfältigen Möglichkeiten die uns der
Userport bietet, um Harware-Erweiterungen zu bedienen.
Im übrigen sollte ich noch darauf hinweisen, daß Sie zum vollen Verständnis
dieses Kurses doch schon tiefergreifende
Kenntnisse von der Programmierung in
Maschinensprache haben sollten, sowie in
der Handhabung eines Maschinensprache-Assemblers und eines Speichermonitors.
Nichts desto trotz können auch BASIC- Programmierer einiges hier lernen, was
eventuell auch von BASIC aus genutzt
werden kann, jedoch mit Sicherheit nicht
in der komplexen und vielfältigen Art
und Weise, wie dies von Maschinensprache
aus möglich ist.
2) Die Hardware:
Zunächst jedoch einmal eine kleine Beschreibung, mit was für Bausteinen wir
es überhaupt zu tun haben. Die beiden
CIAs des C64 sind zwei unscheinbare 40- polige Microchips mit vielfältigen Möglichkeiten. Man könnte sie quasi als
" Manager" unseres Computersystems bezeichnen, die die Verbindung zwischen
den einzelnen Einund Ausgabeeinheiten herstellen und deren Zusammenwirken erst
richtig möglich machen.
Beide CIAs sind baugleich und können
somit also problemlos miteinander vertauscht werden ( was oft bei einer Prü- fung auf Funktionsstörungen schon zu
einer eindeutigen Analyse führen kann - trotzdem sei von einer Nachahmung ohne
Vorkenntnisse abgeraten) . Sie tragen die
Bezeichnung MOS 6526 und befinden sich
in der Ecke links oben auf der Mutterplatine unseres Rechners.
Soviel zur harwaremäßigen Einordnung
dieser kleinen Helfer, jedoch möchten
wir uns hier ja mit der softwaremäßigen
Bedienung befassen, weshalb ich nun also
zu den für uns intressanten Fähigkeiten
komme.
Die CIAs beinhalten jeweils:
* Zwei 16- Bit-Timer, mit denen man hervorragend besonders zeitkritische Programm- Probleme lösen kann.
* Eine 24- Stunden Echtzeituhr, die die
Zeit im Gegensatz zur interruptgesteuerten BASIC-Uhr TI$ extrem genau
geht.
* Zwei freiprogrammierbare Datenports, mit denen auch komplexe Datenübertra- gungen über den Userport möglich werden.
Wir unterscheiden die beiden CIAs im
Folgenden mit CIA1 und CIA2 . Sie werden
vom Betriebssystem für unterschiedliche
Aufgaben genutzt, die nun ebenfalls beschrieben werden sollen:
* CIA1 ist mit der Tastatur und den beiden Joystickports verbunden und ist
somit für die Eingabe über Tastatur, Joysticks, Maus oder Paddles zuständig.
Desweiteren wird von ihm der Systeminterrupt gesesteuert, der zyklische
Aufgaben, wie das Empfangen von Tastencodes oder das Weiterzählen der
BASIC-Uhr TI$ erledigt ( dazu später
mehr) .
* CIA2 ist für die komplette Daten Ein-/ Ausgabe zuständig. Er steuert den
IEC-Bus, mit dem bis zu 4 Diskettenlaufwerke und maximal 2 Drucker angesteuert werden können. Desweiteren ist er am Kasettenport angeschlossen und
seine Datenleitungen sind am Userport
herausgeführt, wodurch auch PC-Standard- Schnittstellen wie RS-232( seriell) oder CENTRONICS ( parallel) softwaremäßig emuliert werden können.
Das Wichtigste, was wir zur Interruptprogrammierung wissen müssen ist, daß
der CIA1 mit der IRQ-Leitung und der
CIA2 mit der NMI-Leitung des Prozessors
verbunden ist. Je nach Aufgabengebiet
einer Interruptroutine müssen wir also
unterscheiden, von welchem CIA die Interrupts ausgelöst werden. In der Speicherkonfiguation des 64 ers sind die beiden Chips getrennt an zwei verschiedenen
Basisadressen eingebunden. Ihre Register
sind jedoch aufgrund der Baugleichheit
für die gleichen Funktionen zuständig, weshalb wir auch nur EINE Registertabelle benötigen. Es kommt halt nur drauf
an, welchen der beiden Zwillinge wir
ansprechen wollen. Die Basisadresse für CIA1 ist $ DC00(= dez.56320), für CIA2$ DD00(= dez.56576) . Wollen wir also
Timer A ( dazu später) von CIA1 mit dem
Grundwert 16384 initialisieren, so müssen wir die Register 4 und 5 ab $ DC00($ DC04 und $ DC05) mit dem LO/ HI-Byte von
16384 beschreiben, bei Timer A von CIA2 ebenfalls Register 4 und 5, jedoch diesmal ab Basisadresse $ DD00($ DD04 und
$ DD05) .
3) Was ist ein Interrupt?
Nun zu einigen grundlegenden Informationen zu Interrupts. Ich benuzte dieses
Wort die ganze Zeit schon, ohne zu erklären was es überhaupt bedeutet ( obwohl
Sie sich darin vielleicht schon durch
den Raster-IRQ- Kurs auskennen) .
Interrupt ist englisch und heißt wörtlich übersetzt " Unterbrechung" . Der Prozessor des C64 besitzt, wie jeder andere Prozessor auch, sogenannte Interrupt-Eingänge. Beim Prozessortyp 6510( wie er
in unserem Rechner Verwendung findet) sind dies insgesamt drei Leitungen, womit er zwischen drei verschiedenen Interrupts ( rein hardwaremäßig - softwaremäßig sind es sogar noch mehr) unterscheiden kann. Diese sind IRQ, NMI und
RESET. Diese drei Leitungen können nun
extern, von anderen Bausteinen, wie zum
Beispiel ( und vor allem) von den CIAs
angesprochen werden um dem Prozessor das
Eintreten eines bestimmten Ereignisses
zu signalisieren. Der Prozessor bemerkt
dies und kann nun durch ganz bestimmte
Maßnahmen auf die Bearbeitung eines
Ereignisses eingehen.
Der Clou an der Sache ist, daß der Prozessor so nicht ständig auf das Eintreten eines Ereignisses warten muß und
deshalb beispielsweise nicht ständig in
einer Endlosschleife prüfen muß, ob in
irgendeiner Speicherzelle irgendwann
einmal ein bestimmter Wert steht, son- dern er bekommt diese Arbeit von den
CIAs abgenommen, die ihn schlichtweg nur
noch darauf aufmerksam machen, daß er
nun seine Achtung etwas anderem schenken
sollte - dem Interruptereignis. So kann
er also auch gerade mit ganz anderen
Dingen beschäftigt sein - nämlich mit
der Abarbeitung eines Programmes - und
trotzdem zwischendurch ganz gezielten
Aufgaben nachgehen.
In der Praxis sieht das so aus, daß er
seine momentane Arbeit - das Hauptprogramm - dann UNTERBRICHT und in ein Jobprogramm zur Bearbeitung des Interrupts
springt.
Ich möchte hier zur Verdeutlichung einmal ein Beispiel aus dem Alltag bringen.
Ich, Uli Basters, sitze hier an meinem
Rechner und bin gerade dabei, den ersten
Teil des CIA-Kurses zu schreiben. Plötzlich klingelt das Telefon. Bevor ich
aufstehe um zum Telefon zu gehen speichere ich schnell noch das bisher geschriebene ab und merke mir vor, daß ich nach dem Telefonat unbedingt weiterschreiben werde. Am anderen Ende ist
mein Kollege Ralf Zwanziger, der mir den
nächsten Redaktionsschluß durchgibt.
Nachdem ich aufgehängt habe erinnere ich
mich an mein Vorhaben, gehe wieder
zurück zum Rechner, lade den Text wieder
ein und setze meine Arbeit fort.
Diesen Vorgang kann man sehr gut mit den
Tätigkeiten des Prozessors beim Eintreten eines Interrupts vergleichen. Eine
Interruptleitung signalisiert ihm, daß ein Interruptereignis eingetreten ist
( das Telefon klingelt) . Schnell merkt er
sich noch die wichtigsten Daten, nämlich
den Inhalt der Prozessorregister ( Akku, Xund Y-Register), den Prozessorstatus
( Speicherung des Textes) und den Inhalt
des Programmzählers ( das Vormerken weiterzuarbeiten) . Danach springt er auf
eine Jobroutine, die er für das Eintreffen eines Interrupts parat hat und arbeitet diese dann ab ( ich führe ein Telefonat) . Ist er am Ende dieser Routine angelangt, so holt er sich Programmzähler, Statusund Prozessorregister wieder ins Gedächtnis zurück ( Erinnerung
weiterzuarbeiten und wiedereinladen des
Textes) und setzt seine alte Arbeit wieder fort.
Diesen ganzen Vorgang erledigt er mit
einer derart affenartigen Geschwindigkeit, daß wir meinen er würde beides
gleichzeitig tun. Das wäre dann auch der
nächste Vorteil der ganzen Geschichte.
Durch Interrupts ist man also in der
Lage mehrere Dinge, ganz unabhängig voneinander, scheinbar gleichzeitig zu
erledigen - so auch das Betriebssystem, das, während es in der Hauptschleife auf
Tasteneingaben wartet, über einen Interrupt den Cursor weiterhin blinken läßt.
Die Möglichkeiten hier sind sehr
vielfältig, wie wir noch bemerken werden.
4) Der Timer-Interrupt: -----------------------
Soviel zu den Vorgängen innerhalb unseres Rechners. Nun möchte ich mich ein
wenig mit den Unterschieden zwischen den
drei Interruptarten beschäftigen.
Wir haben also insgesamt drei verschiedene Unterbrechungen. Eine davon, nämlich der IRQ wird Ihnen vielleicht, wenn
auch unbewußt, vielleicht schon nur zu
gut bekannt sein. Er wird vom CIA1 ausgelöst und vom Betriebssystem für interne, cyklische Aufgaben verwendet. Deshalb ist er auch ein gutes Beispiel für
uns um in dieser Materie einzusteigen, da uns das Betriebssystem schon eine
koplette IRQ-Routine zur Verfügung
stellt - den System-IRQ.
Der System-IRQ nutzt die einfachste und
zugleich auch vielseitigste Funktion des
CIAs um Interrupts auszulösen - den Timerinterrupt. Bevor ich mich jetzt jedoch in unverständlichen Erklärungen
verliere erst einmal eine Registerbe- schreibung eines CIA-Registers. Hierzu
habe ich Ihnen eine Grafik vorbereitet, die eine Kurzbeschreibung der Register
liefert; ausgedruckt sollte sie Ihnen
immer parat liegen, da wir sie in Zukunft häufiger benutzen werden.
CIA-Kurs Teil 2 Wie man leicht erkennen kann sind zur
Timerprogrammierung sechs Register notwendig:
* TA-LO (Reg.4) * TA-HI (Reg.5) * TB-LO (Reg.6) * TB-HI (Reg.7) * CRA (Reg.14) * CRB (Reg.15)
Desweiteren brauchen wir auch noch das
Interrupt-Control- Register ( Reg.13) .
Ohne dieses Register läuft interruptmäßig überhaupt nichts mit dem CIA.
Der System-IRQ benutzt nun Timer A, der
für ihn den Auslöser darstellt. Deshalb
können wir die Register TB-LO, TB-HI und
CRB vorläufig einmal ausschließen und
uns nur den Registern für Timer A zuwenden.
Zunächst möchte ich jedoch den Begriff
" Timer" definieren. Ein Timer, so wie er
pro CIA ja zweimal vorhanden ist, ist nichts anderes als ein spezielles Zählregister, das ständig, nach ganz bestimmten Regeln von einem Maximalwert in
Einerschritten heruntergezählt wird. Ist
der Timer bei Null angelangt, so wird
ein Interrupt ausgelöst.
Den schon angesprochenen Maximalwert
müssen wir, wie Sie sicher schon vermuten in LO/ HI-Byte aufgespaltet in TA-LO
und TA-HI schreiben. Diese Timerregister
haben quasi ein " Doppelleben" . Sie bestehen nämlich zum Einen aus einem Speicherregister, dem sogenannten " LATCH" und einem Zählregister oder auch " COUN-TER" . Schreiben wir nun einen Wert in
die Timerregister, so wird dieser
zunächst im Latch abgelegt und gleichzeitig in den Counter geladen. Starten
wir anschließend den Timer, so beginnt
der CIA damit, den Counter herabzuzählen. Hierzu kann man verschiedene Signalquellen wählen, die ein Herabzählen
veranlassen. Diese Signalquellen, können
im Control-Register für Timer A festge- legt werden. Jedes der acht Bits in diesem Register steuert eine ganz bestimmte
Funktion. Desweiteren kann von hier auch
der Timer gestartet und gestoppt werden, sowie einige bestimmte Zählmodi eingestellt werden. Zur Erläuterung habe ich
Ihnen einmal eine Tabelle erstellt:
Bit0 (START/STOP):
Durch Löschen dieses Bits wird der Timer
gestoppt. Setzt man es, so wird begonnen, den Counter herabzuzählen.
Bit1( PB ON/ OFF) :
Dieses Bit eignet sich besonders für
Hardware-Erweiterungen. Ist es gesetzt, so wird beim Unterlauf des Timers ein
Signal auf die Portleitung PB6( Bit 6 an
Port B) gelegt. Ist es gelöscht, so werden keine Signale ausgegeben.
Bit2 (TOGGLE-PULSE):
Dieses Bit arbeitet nur in Zusammenhang
mit " PB ON/ OFF" . Ist dieses gesetzt, so gelten folgende Bestimmungen für " TOG-GLE- PULSE" :
* Wenn gelöscht, so werden wie bei " PB-ON/ OFF" beschrieben Signale auf die
Leitung PB 7 gelegt. Diese sind übrigens genau einen Prozessortaktzyklus
lang.
* Wenn gesetzt, so wird der Zustand von
PB6 bei jedem Unterlauf des Counters
in den jeweils anderen Zusand gekippt, das heißt von Gesetzt auf Gelöscht und
umgekehrt. Damit läßt sich also ganz
einfach ein Rechtecksignal erzeugen, wobei die Pulsbreite von der Laufzeit
des Counters abhängt.
Wie Sie sehen, sind die Bits 1 und 2 für
ganz spezifische Aufgaben verwendbar und
haben leider sehr wenig mit Interrupts
zu tun, zumal kein Interruptsignal an
den Prozessor gesand wird. Für manche
Harwareerweiterungen sind Sie jedoch
bestimmt sinnvoll einzusetzen, da man so
sehr einfach beliebige Taktfrequenzen für irgendewelche Schaltungen am User-Port des C64 erzeugen kann, da die Leitung PB6 an selbigem herausgeführt ist.
Hierzu jedoch in einem späteren Kursteil
mehr.
Bit3 (ONE-SHOT/CONTINOUS):
Ist dieses Bit gelöscht, so befindet
sich der Timer im CONTINOUS-Modus. Das
heißt, daß der Counter bis 0 heruntergezählt, wieder mit dem Wert im Latch
geladen wird und von neuem beginnt zu
zählen.
Bei gesetztem Bit ist der ONE-SHOT- Modus
aktiv - es wird bis 0 gezählt und neu
geladen, jedoch stoppt der Timer jetzt
automatisch, solange bis man ihn wieder
mit Bit0 des Controlregisters in Gang
setzt.
Bit4( FORCE-LOAD) :
Wird dieses Bit gesetzt, so wird der
Counter, egal ob der Timer im Moment
läuft oder nicht, mit dem Wert im Latch neu geladen.
Bit5( IN MODE) :
Hier wird die Quelle des " Timer-Triggers" festgelegt. Der Timer-Trigger
ist die Einrichtung, die den CIA dazu
veranlaßt den Counter einmal herunterzuzählen.
Ist dieses Bit gelöscht ( was bei uns
eigentlich immer der Fall ist), so wird
der Systemtakt als Trigger herangezogen
( das werden wir im nächsten Abschitt
ganz genau behandeln) . Ist es gelöscht, so ist die CNT-Leitung des CIA der Trigger. Diese ist an den Userport herausgeführt, so daß somit auch Hardware-Erweiterungen in der Lage sind die Interrupts im 64 er extern zu steuern.
Bit6 (SP-DIRECTION):
Dieses Bit hat etwas mit Register 12 der
CIA zu tun ( SDR) . Dieses Register ist
für die serielle Datenübertragung sehr
nützlich. Hier kann ein 8- Bit-Wert ge- speichert werden, der zyklisch aus dem
Register heraus, an den Pin SP des CIA
( mit dem Userport verbunden) gerollt
wird, beziehungsweise ein Wert kann über
den Pin SP hereingerollt werden ( auch
dazu in einem späteren Kursteil mehr) .
Bit6 von CRA steuert nun die Datenrichtung von SDR. Ist es gelöscht, so ist
SDR auf Eingang geschaltet ( Bits werden
hereingerollt), ist es gesetzt, so wird
SDR als Ausgang benutzt ( Bits werden
herausgerollt) .
Bit7 (POWER FREQUENCY):
Dieses Bit wird benötigt um den Triggerfrequenz für die Echtzeituhr des CIAs zu
bestimmen. Je nach dem, in welchem Land
wir unseren 64 er angeschlossen haben, beträgt die Netzfrequenz des Wechselstroms aus der Steckdose nämlich 50 oder
60 Hertz ( man spricht hier vom sogenannten " technischen Strom") . Diese Frequenz
wird nun benutzt um die Zeiteinheiten
der Echtzeituhr festzustellen. Ist die- ses Bit nun gesetzt, so gilt eine Frequenz von 50 Hz als Trigger, ist es
gelöscht, eine von 60 Hz. Da wir in der
Bundesrepublik Deutschland ja die von 50 Hz haben, sollte bei Uhr-Betrieb dieses
Bit also immer gesetzt sein, da andernfalls unsere Uhr schnell " nachgehen" könnte.
Na das ist doch schon eine ganze Menge
an Information. Doch keine Angst, die
Bits 1 und 2, die etwas komplizierter
erscheinen mögen, wollen wir vorläufig
erst einmal wegfallen lassen. Von ihnen
wird, ebenso wie von Bit 6 und 7, in
einer späteren Folge dieses Kurses mehr
die Rede sein.
Nun zum Systemtakt, der - in aller Regelals Timer-Trigger dient. Er stellt wohl
einen der wichtigsten Grundbausteine in
unserem ( wie auch in jedem anderen) Rechner dar. Ein Rechensystem, so wie es
von einem Computer verkörpert wird,
braucht schlichtweg IMMER einen Grundtakt, den alle miteinander verknüpften
Bausteine benutzen können um synchron
miteinander zu arbeiten. Er dient sozusagen als " Zeitmaß" für die Geschäftswelt in einem Rechner. Ein Taktzyklus
stellt die elementare Zeiteinheit innerhalb eines Rechner dar, an den sich alle
Bausteine halten müssen um mit ihren
Signalen nicht zu kollidieren. Solche
Takte werden von Quarz-Bausteinen erzeugt, die, je nach chemischer Zusammensetzung, eine ganz bestimmte Eigenfrequenz haben, die extrem genau ist ( wie
von Quartz-Uhren her ja allgemein bekannt) .
Ein Systemtakt ist von Rechner zu Rechner verschieden. Beim AMIGA beträgt er
zum Beispiel 7 .16 MHz ( Megahertz), beim
ATARI ST 8 MHz, bei PCs mittlerweile
zwischen 4 .77 und 33 MHz ( und mehr) . Je
höher ein Rechner " getaktet" ist, desto
mehr Befehle kann er pro Sekunde abarbeiten und desto schneller ist er; wes- halb die Taktfrequenz häufig auch als
ein Maß für die Rechengeschwindigkeit
eines Rechners herangezogen wird.
Der C64 schneidet hier, aufgrund seiner
schon etwas älteren Entwicklung und dem
Fakt, daß er halt einfach nur ein Homecomputer ( der für jeden erschwindlich
sein soll) ist, relativ schlecht ab. Mit
etwa 1 MHz ist er vergleichsmäßig langsam, was jedoch immer noch affenschnell
ist! Um genau zu sein sind es 985248 .4 Hz ( also knapp ein Mhz) . So zumindest
bei der europäischen Version, die Sie ja
alle haben sollten. Die amerikanischen
64 er sind sogar noch ein wenig schneller
( nämlich 1022727 .1 Hz), was für uns jedoch unerheblich ist.
Doch was bedeutet diese Zahl nun eigentlich für uns. Sie bedeutet schlichtweg, daß wir pro Sekunde genau 985248 .4 Taktzyklen haben; und die brauchen wir
ja als Timer-Trigger. Damit wird es einfach, die Dauer zwischen zwei Counter-Unterläufen zu berechnen. Angenommen,
Sie wollten ( aus welchem Grund auch immer), daß jede 1/16- Sekunde ein Interrupt ausgelöst würde. Demnach müßten Sie
den Systemtakt einfach durch 16 dividieren und das Ergebnis in LO/ HI-Byte aufgespalten in die Register TA-LO und TA-HI schreiben. Konkret wäre das:
985248.4 / 16 = 61578.025 (dez.) $F08A (hex.) LO: $8A = 138 HI: $F0 = 240
Schreiben Sie diese beiden Werte nun in
die Register 4 und 5 des CIA1, so wird
der Systeminterrupt, der ja durch Timer
A der CIA1 gesteuert wird, eindeutig
verlangsamt ( normalerweise tritt er nämlich jede 1/60- Sekunde auf) . Erkennen
können Sie dies am langsameren Cursorblinken, da auch das vom Systeminterrupt
erledigt wird. Probieren Sie es doch
einfach einmal, also:
POKE 56320+4,138:POKE 56320+5,240 (56320 ist die Basisadresse von CIA1!)
So. Nun wissen Sie also, wie man den
Timer A eines CIA programmiert. Da dieser für IRQs jedoch schon vom Betriebsystem benutzt wird, und es da möglicherweise Timing-Probleme gibt, wenn wir
eine eigene IRQ-Routine schreiben wollen, die zwar parallel zum Betriebsystem- IRQ läuft, aber öfter oder weniger
oft als dieser auftreten soll, so gibt
es für uns auch die Möglichkeit auf Timer B auszuweichen. Dieser ist absolut
analog zu bedienen, nur, daß wir die
Timerwerte diesmal in die Register 6 und
7 der CIA schreiben müssen ( TB-LO und
TB-HI) . Desweiteren wird er vom Control-Register- Timer-B ( CRB) gesteuert - Register 15 der CIA also. Bis auf kleinere
Unterschiede, ist der Aufbau der Register CRA und CRB identisch. Hier die
Unterschiede:
1) Grundsätzlich sind die Funktionen der
Bits 0 bis 4 gleich, jedoch mit dem
Unterschied, daß sich die Bits 1 und
2 nicht mehr auf PB6 sondern auf PB7 beziehen.
2) Bit 5 und 6 steuern den IN-MODE von
Timer B ( bei CRA war das NUR Bit 5) .
Hierzu ergeben sich 4 verschiedene
Timer-Trigger- Modi:
Bit 5 6 Funktion
0 0 Zähle Systemtakt 0 1 Zähle CNT-Flanken 1 0 Zähle Unterläufe von Timer A 1 1 Zähle Unterläufe von Timer A, wenn CNT=1
Somit kann Timer B bei Steuerung
durch Hardware bestens eingesetzt
werden, da mehr externe Steuermöglichkeiten ( durch CNT) vorhanden
sind.
3) Bit 7 steuert die Alarmfunktion der Echtzeituhr. Ist es gesetzt, so werden die Werte, die man in die Register der Echtzeituhr schreibt ( dazu
auch in einem späteren Kursteil) als
Alarmzeit genommen. Ist es gelöscht, so kann die normale Uhrzeit eingestellt werden.
Da die beiden Register CRA und CRB ebenfalls eine sehr wichtige Funktion bei
den vielseitigsten Anwendungen erfüllen, habe ich Ihnen einmal eine grafische
Öbersicht angefertigt:
Das Interrupt-Control-Register (ICR)
So. Nun wissen wir also, wie man die
Timer der CIAs steuert. Interrupts haben
wir nun aber noch lange nicht! Die Timer
dienen ja lediglich als Interrupt-Quellen. Wir benötigen noch ein weiteres
Register des CIA um ihm zu sagen, daß
beim Unterlauf eines Timers auch die
IRQ-Leitung ( beim CIA1, bzw. NMI-Leitung
beim CIA2) des Prozessors zu aktivieren
ist, um einen Interrupt zu signalisieren. Dieses Register ist Register 13 eines CIAs, das Interrupt Control Register ( ICR) . Es ist wohl das wichtigste
Register im CIA überhaupt, denn ohne es
könnten wir mit ihm überhaupt nichts
anfangen!
Bevor wir also einen Timer zur Interrupterzeugung starten, sollten wir also
immer im ICR auch angeben, daß dieser
Timer Interrupts erzeugen soll. Darüberhinaus gibt es auch noch eine ganze Menge anderer Interruptquellen, die dieses Register steuert. Ich gebe Ihnen hier
einmal eine tabellarische Öbersicht der
einzelnen Bits vom ICR. Die Bits müssen
gestezt sein, um einen Interrupt auszulösen, wenn das entsprechende Ereignis
eintritt:
Bit0 : Löse einen Interrupt aus, wenn
Timer A unterläuft.
Bit1 : Löse einen Interrupt aus, wenn
Timer B unterläuft.
Bit2 : Löse einen Interrupt aus, wenn die
Alarmzeit der Echtzeituhr mit der
aktuellen Zeit übereinstimmt.
Bit3 : Löse einen Interrupt aus, wenn das
SDR ( Serial Data Register) voll, bzw. leer ist ( abhängig von der
entsprechenden Betriebsart - heraus- oder hereinrollen) .
Bit4 : Löse einen Interrupt aus, wenn am
Pin FLAG ( am Userport herausgeführt) ein Signal anliegt.
Bit5 : Unbelegt.
Bit6 : Unbelegt.
Bit7 : Doppelfunktion ( siehe unten) .
Wie Sie sehen, ist es ganz einfach, bestimmte Interruptquellen zu wählen. Sogar von externer Hardware können DIREKT
Interrupts ausgelöst werden.
Nun jedoch noch zu der Sonderfunktion
von Bit 7 . Man muß bem ICR nämlich unterscheiden, ob man nun in das Register
schreibt, oder ob man es ausliest. Es
hat nämlich, wie die Timerregister auch, eine Doppelfunktion. Man unterscheidet
zwischen einem Latch-Register, in dem
die Interrupt-Maske ( INT-MASK) gespeichert wird und den Interrupt-Daten
( INT-DATA) . Schreiben wir in das ICR, so
wird der geschriebene Wert zwar zwischengespeichert, jedoch können wir ihn
nicht lesen. Denn wenn wir lesen, gibt
uns das ICR augenblickliche Informationen, ob und welches Interruptereignis
eingetreten ist, nicht aber, welchen
Wert wir vorher hineingeschrieben haben.
An diese Doppelfunktion von Registern sollten Sie sich gewöhnen, denn wir werden noch öfter damit zu tun haben.
Lesen wir nun aus dem ICR Daten aus, so
zeigt uns Bit 7 an, ob eines der zugelassenen Interruptereignisse eingetreten
ist, das heißt, daß Bit 7 immer dann
gesetzt ist, wenn mindestens ein Bit von
INT-MASK mit einem Bit von INT-DATA
übereinstimmt. Dadurch haben wir eine
einfache Kontrolle, ob ein Interrupt
auch tatsächlich vom CIA ausgelöst wurde, oder nicht doch von was anderem ( wie
zum Beispiel vom VIC, der ja die Raster-Interrupts auslöst) . Duch eine einfache
Abfrage mit dem Assembler-Befehl " BMI"( Branch on MInus), der ja den Zustand
von Bit 7 überprüft, können wir schnell
feststellen, von wo der Interrupt nun
kommt.
Schreiben wir in das ICR, so ist Bit 7 nochmal doppeldeutig:
Ist es nämlich gelöscht, so wird jedes
weitere 1- Bit sein korrespondierendes
Maskenbit in INT-MASK löschen. Die ande- ren Bits bleiben unberührt davon. Es
wird also quasi ein AND mit dem Wert den
wir schreiben und dem Wert der in INT-MASK steht vollzogen.
Ist Bit 7 jedoch gesetzt, so wird jedes
weitere 1- Bit sein korrespondierendes
Masken-Bit setzen. Die anderen bleiben
ebenfalls davon unberührt. Diesmal wird
also ein OR mit den beiden Werten vollzogen. Damit können wir also problemlos
ganz gezielt Bits im ICR setzen und löschen, ohne dabei aus Versehen andere
Bits zu verändern.
Auch hier will ich Ihnen eine grafische
Öbersicht liefern, damit Sie die Bittabelle vom ICR immer auf Papier parat
haben können.
Das wars dann mal fürs erste. Ich hoffe, ich habe Ihnen nun mit den ( zugegeben) überaus trockenen Grundlagen der
Interrupt-Programmierung, nicht das Interesse am Thema dieses Kurses genommen.
Keine Panik, im nächsten Monat werden
wir uns dann einmal um die praktische
Anwendung kümmern. Ich zeige Ihnen dann
einmal den kompletten Ablauf des Systeminterrupts, auf dem wir dann unsere ersten Schritte in Sachen Interrupt aufbauen werden. Bis dahin Servus,
Ihr Uli Basters (ub)
CIA-Kurs: "Die Geheimnisse des Secret Service..." (Teil 2)
Herzlich willkommen zur zweiten Runde
unseres CIA-Kurses. Nachdem ich Sie
letzten Monat ja lange genug mit der
trockenen Theorie von der Bedienung der
Timer der CIAs gelangweilt habe, will
ich jetzt einmal den Grundstein zur Praxis legen. Ich möchte mich diesmal mit
dem eigentlichen Ablauf eines Interrupts
beschäftigen und Ihnen einen solchen
auch haarfitzelgenau anhand des Systeminterrupts erklären.
1) Der Prozessor und ein Interrupt.
Wie ich schon im letzten Teil erwähnte, wird der Systeminterrupt vom Betriebs
system des 64 ers zur Erledigung verschiedener zyklischer Aufgaben genutzt.
Er wird 60 mal in der Sekunde aufgerufen
und von Timer A der CIA1 erzeugt. Dem- nach haben wir also einen IRQ ( wir erinnern uns: CIA1 erzeugt Impulse an der
IRQ-Leitung des Prozsessors, CIA2 an der
NMI-Leitung) . Die Timerregister TALO und
TAHI beinhalten die Werte 37 und 64 . Der
Timer zählt also von 16421(= HI*256+ LO) bis 0 herunter und löst dann einen Interrupt aus. Das ist auch ganz logisch, denn wenn wir ja 60 Interrupts in der
Sekunde haben wollen, dann müssen wir ja
den Systemtakt durch 60 dividieren. Das
Ergebnis hiervon ist 16420 .8, aufgerundet 16421 ! Ich hoffe Sie verstehen
jetzt, warum ich mich zunächst um die
Timer selbst gekümmert hatte, da Sie nun
auch besseren Einblick in den Systeminterrupt haben. Wenn Sie einmal ein bisschen rumprobieren möchten, bitte:
Schreiben Sie doch einfach einmal mittels POKE einen höheren oder niedrigeren
Wert als 64 in TAHI ( Reg.5 von CIA1) .
Das Ergebnis sehen Sie dann am Cursorblinken, was ebenfalls vom Systeminterrupt erledigt wird. Der Cursor sollte nun entweder schneller ( bei niedrigerem
Wert) oder langsamer ( bei höherem Wert) blinken. Übertreiben Sie die Werte jedoch nicht übermäßig, da auch die Tastaturabfrage vom Systeminterrupt erledigt
wird, und Sie so entweder einen Turbo-Cursor haben, mit dem bei eingeschaltetem Key-Repeat ( das ist bei meinem
Floppy-Speeder- Betriebssystem nämlich
der Fall, und ich bin eben beim Ausprobieren natürlich prompt wieder darauf
hereingefallen. . .) keine vernünftigen
Eingaben gemacht werden können, ebenso
wie bei einem ultralangsamen Cursor, wo
es noch bis morgen dauern würde, einen
rücksetzenden POKE-Befehl einzugeben.
Dieser Trick wird übrigens oft benutzt, um Programme schneller zu machen. Je
öfter nämlich ein Interrupt pro Sekunde
auftritt, desto weniger Zeit hat der
Prozessor, das momentan laufende Hauptprogramm abzuarbeiten. Setzt man jedoch
die Zahl der Interrupts pro Sekunde herunter, oder schaltet man ihn sogar ganz ab ( indem man den Timer einfach anhält, oder mittels des Assemblerbefehls SEI
Interrupts ganz sperrt), so läuft das
Hauptprogramm logischerweise schneller
ab. Dies nur ein Tip am Rande.
Was geht nun eigentlich in unserem 64 er
vor, wenn ein Interrupt abgearbeitet
werden soll? Nun, zunächst einmal haben
wir da einen Unterlauf von Timer A der
CIA1 . Diese legt sodann auch gleich ein
Signal an die IRQ-Leitung des Prozessors
an und markiert die Interruptquelle " Timer A" in ihrem ICR.
Folgende Prozesse gehen nun im Prozessor
vor:
1) Der Prozessor überprüft nun nach jedem abgearbeiteten Befehl den Zustand
der Unterbrechungsleitungen ( IRQ, NMI) . Ist eine davon gesetzt, und ist
der zugehörige Interrupt auch freigegeben, so beginnt er damit, die Unterbrechung zu bearbeiten. Hierzu
müssen zunächst die wichtigsten Daten
auf den Stapel gerettet werden. Das sind in der hier gezeigten Reihenfol- ge: * HI-BYTE des Programmzählers * LO-BYTE des Programmzählers * Prozessorstatusregister
Der Programmzähler ist ein Prozessorinternes Register, das anzeigt, an
welcher Speicheradresse, der nächste
zu bearbeitende Befehl liegt. Das
Prozessorstatusregister beinhaltet
die Flaggen, die dem Prozessor bei
Entscheidungen weiterhelfen ( ich verweise da auf die letzten Kursteile
des Assemblerkurses meines Kollegen
RALF TRABHARDT) .
2) Der Prozessor setzt sich selbst das
Interrupt-Flag und verhindert so, daß er durch weitere Interrupts
gestört wird.
3) Ganz am Ende des Speichers ( in den
Adressen $ FFFA-$ FFFF) sind 6 Bytes
für das Interrupt-Handling reser- viert. Dort stehen insgesamt 3 Vektoren, die dem Prozessor zeigen, bei
welcher Unterbrechung er wohin springen muß, um einen Interrupt zu bearbeiten. Diese Vektoren heißen im
Fachjargon übrigens auch Hardware-Vektoren. Hier einmal eine Auflistung:
Interrupt Vektor Adresse
NMI $FFFA/$FFFB $FE43 (65091) RESET $FFFC/$FFFD $FCE2 (64738) IRQ,BRK $FFFE/$FFFF $FF48 (65352)
Da wir ja einen IRQ behandeln, holt
sich der Prozessor jetzt also die
Adresse, auf die der Vektor in
$ FFFE/$ FFFF zeigt in den Programmzähler und beginnt so damit eine Jobroutine für den IRQ abzuarbeiten.
Diese Jobroutine wollen wir uns jetzt einmal genauer ansehen. Vorher jedoch
noch eine kleine Erläuterung. Wie Sie ja
sehen, wird der Vektor für den IRQ auch
als Vektor für BRK-Unterbrechungen benutzt. Der BRK-Befehl sollte Ihnen ja
vielleicht bekannt sein ( wenn Sie sich
mit Maschinensprache auskennen) . Er hat
ansich ja keinen direkten Verwendungszweck, jedoch wird er von vielen Monitor- Programmen zum Debuggen benutzt. Wie
Sie ebenfalls sehen können, hat er gleiche Priorität wie ein IRQ, und man kann
Ihn also auch als eigenen Interrupt ansehen. Vielmehr ist der BRK-Befehl die
Unterbrechungsquelle für BRK-Interrupts.
Wie man mit ihm umgeht, will ich Ihnen
später zeigen.
Werfen wir nun jedoch einmal einen Blick
auf die Jobroutine ab $ FF48- diese befindet sich natürlich im Betriebssystem-ROM, weshalb ich Ihnen hier auch einen
Auszug daraus liefern möchte ( ich habe
dies in Form einer Grafik getan, damit ich ausführlichere Kommentare zu den
einzelnen Programmschritten geben kann) .
Bitte laden Sie hierzu den 2 . Teil des
CIA-Kurses.
CIA-Kurs: "Die Geheimnisse des Secret-Service" (Teil 3)
Hallo zusammen zum dritten Teil des
CIA-Kurses. Diesen Monat geht' s ran an
die Bulletten, wir wollen uns endlich
einmal um die konkrete Programmierung
der CIAs und somit von Interrupts kümmern! Letztes Mal hatten wit ja den System- IRQ behandelt, dessen Funktionsaufbau wir heute brauchen werden. Ich werde
Ihnen anhand eines Beispielprogramms
einmal zeigen, wie wir den System-IRQ
für uns benutzen können. Also los
geht' s. . .
Wie Sie nun ja wissen, läuft im C64 im
Normalfall ja schon ein IRQ, der Systeminterrupt nämlich. Er wird 60 Mal pro
Sekunde aufgerufen und arbeitet eine
Jobroutine im Betriebssystem-ROM ab, die
gewisse interne Aufgaben ( die wir im letzten Monat ja schon besprochen hatten) abarbeitet. Wollen wir einen eigenen IRQ schreiben, so ist die einfachste
Methode hierfür ein " Einklinken" in den
System-IRQ. Es hat den Vorteil, daß wir
uns ( wenn es sich um zyklisch wiederkehrende Aufgaben handelt) nicht noch
umständlich um das Programmieren eines
CIA-Timers kümmern müssen, sondern einfach die vorgegebene Timerprogrammierung
übernehmen.
Sie erinnern sich ja bestimmt noch daran, daß beim Auftreten eines IRQ-Ereignisses, der Prozessor über einen
Vektor in $ FFFE/$ FFFF auf eine kleine
Jobroutine verzweigt, die feststellt, ob
der Interrupt, der aufgetreten ist, ein
IRQoder ein BRK-Interrupt war. Diese
Routine verzweigte dann wiederum über
zwei Vektoren im RAM auf verschiedene
Jobroutinen für die beiden Interrupts.
Diese waren:
* $0314/$0315 (dez. 788/789) für den IRQ * $0316/$0317 (dez. 790/791) für den BRK
Wollen wir also, daß der Prozessor jetzt
auf eine eigene Routine verzweigt, dann
müssen wir einfach den entsprechenden
Vektor hier dahingehend verändern ( im
Fachjargon spricht man auch von " verbiegen"), daß er anschließend auf unsere
eigene IRQbzw. BRK-Routine zeigt.
Ich habe Ihnen, wie schon erwähnt, einmal ein kleines Beispiel vorbereitet, das dies verdeutlichen soll. Das Problem
das ich lösen wollte, war folgendes:
Stellen Sie sich vor, Sie programmierten
gerade eine Anwendung und Sie wollten, daß Ihr Programm von Zeit zu Zeit Fehler- oder Benutzungshinweise auf dem
Bildschirm ausgibt. Damit das ganze auch
noch optisch gut ins Auge fällt, wäre es
angebracht, daß diese Mitteilung längere
Zeit aufblinkt, so daß sie dem Benutzer
buchstäblich " ins Gesicht springt" .
Dies ist eine Aufgabe, die sich hervorragend über einen IRQ lösen läßt. Es hat sogar zusätzlich noch den Vorteil, daß
das Hauptprogramm vollkommen unberührt
von dem wäre, was da angezeigt werden
soll. Es genügt also, eine Interrupt-Routine zu aktivieren, die dann so ganz
nebenher zum Beispiel die Mitteilung
" Das Programm rechnet!" ausgibt, wärend
das Hauptprogramm tatsächlich gerade mit
irgendeiner Berechnung beschäftigt ist.
Wollen wir uns ansehen, wie man eine
solche Routine nun realisiert. Zunächst
einmal brauchen wir natürlich eine eigene IRQ-Routine. Sie soll nachher die
Nachricht auf dem Bildschirm ausgeben.
Ich habe mich da einmal auf die letzte
Bildschirmzeile festgelegt. Das ist ein
Randbereich, den man gut nutzen kann.
Zur eigentlichen Textausgabe brauchen
wir zwei kleine Unterroutinen - eine, die den Text schreibt, und eine die ihn
wieder löscht, damit wir somit ein Blinken erzeugen.
Der Einfachheit halber, habe ich mich dazu entschieden, den auszugebenden Text
im Bildschirmcode irgendwo im Speicher
abzulegen. Dann genügt es nämlich ( im
Gegensatz zum ASCII-Code), den Text einfach in den Bildschirmspeicher einzukopieren, was durch eine kleine, aber feine Schleife sehr schnell erledigt wird.
Der Aufbau dieser Routine verlangt es
mitunter auch, daß das letzte Zeichen
des Textes den binären Wert 0 hat. Im
Bildschirmcode ist dies der Klammeraffe
("") .
Zum Löschen der Mitteilungszeile genügt
es, diese mit dem Bildschirmcode für
" SPACE" aufzufüllen. Das wäre gleich dem
Vorgang, wenn Sie mit dem Cursor in die
unterste Zeile des Bildschirms fahren und nun 40 Mal die SPACE-Taste drücken
würden. Der Bildschirmcode für das Zeichen SPACE ist 32($20) .
Hier nun also die beiden Routinen, die
diese Aufgaben übernehmen. DOMSG gibt
den Bildschirmtext aus, und BLANK löscht die Mitteilungszeile. Zu DOMSG sei noch
zu sagen, daß die Anfangsadresse des
auszugebenden Textes, vorher schon von
der Interrupt-Initialierungsroutine in
die beiden Adressen nach dem LDA-Befehl
geschrieben wurde. Das Programm hat sich
also selbst verändert. Dies ist ( für
uns) die einfachste und sinnvollste Lösung, die man in einem solchen Fall anwedet. Zu jener Initialisierungsroutine
kommen wir später. Ich möchte übrigens
darauf hinweisen, daß alle hier erwähnten Routinen und Programme mit dem
HYPRA-ASS- Assembler aus der Computerzeitschrift "64' er" erstellt wurden.
Leser, die das Eigabeformat und die Bedienungsweise dieses Assemblers kennen, können sich also glücklich schätzen.
Trotzdem werde ich die auftauchenden
Assembler-Besonderheiten hier erklären, damit Sie die Programme auch mit jedem
anderen Assembler eingeben können. Sie
sollten auf jeden Fall wissen, daß in
diesem Assembler anstatt absoluter Sprungadressen sogenannte " Labels" benutzt werden. Das sind Sprungmarken, die
einfacher zu handhaben sind, da man so
nur auf einen Namen springen muß, dessen
absolute Adresse der Assembler berechnet. Assemblerprogrammier sollten sich
aber sowieso mit Labels auskennen, da
sie heutzutage in jedem Assembler
Verwendung finden.
----------------- DOMSG LDY #00 Y-Reg. als Zeiger initialisieren. LOOP1 LDA $C000,Y Zeichen holen (An- fangsadresse Text plus Y-Offset). BEQ L3 War das letzte Zei- chen gleich 0 (= ), dann ENDE. STA $07C0,Y Ansonsten Zeichen in Bildschirmspecher schreiben. INY Zähler erhöhen. BNE LOOP1 Unbedingter Sprung. ------------------ BLANK LDY #39 Y-Reg. als Zeiger initialisieren (39+1=40 Zeichen fül- len). LDA #32 Bildschirmcode für SPACE in Akku holen. STA $07C0,Y Akku in Bildschirm- speicher entleeren. DEY Zähler erniedrigen. BPL LOOP2 Solange wiederholen, bis 0 unterschritten wird (Y ist dann ne- gativ). RTS Und Tschüß! ------------------
Die Anfangsadresse der Mitteilungszeile
( im Folgenden MSG-Zeile; MSG = Message = Mitteilung) ist logischerweise die
Adresse des ersten Zeichens in der 25 .
und letzten Zeile des Bildschirms. Sie
errechnet sich aus der Basisadresse des
Bildschirmspeichers ( normalerweise=1024)
plus der Zeilenanzahl-1 multipliziert
mit 40 . Da unsere Zeilenanzahl 25 ist, lautet die Rechunug für uns:
1024+(25-1)*40=1024+24*40=1984 (=$07C0)
Was der Adresse in unseren beiden Routinen entspricht!
Desweiteren muß ich noch auf eine Besonderheit in DOMSG hinweisen. Die Schleife
wird erst dann verlassen, wenn das zuletzt gelesene Zeichen 0 ist ( wir Erinnern uns - das ist die Endmarkierung) .
Über BEQ springen wir dann auf den RTS-Befehl der BLANK-Routine. Der Branch-Befehl BNE am Ende der Routine ist ein
sogenannter unbedingter Sprung. Die hier
abgefragte Bedingung ist immer erfüllt, weil das vorher inkrementierte Y-Register nie die 0 erreichen wird, da
die maximale Zeichenanzahl ja 40 ist.
Mehr wäre unsinnig, denn man sähe diese
Zeichen ja gar nicht auf dem Bildschirm.
Versierte Assembler-Programmierer kennen
diese Art des Springens. Sie hat den
Vorteil der Speicherersparnis ( ein JMP-Befehl belegt immer 3 Bytes, ein
Branch-Befehl, wie BNE nur 2) und ist
zusätzlich ganz sinnvoll, wenn man Routinen relokatibel halten möchte. Verschiebt man ein Assembler-Programm im
Speicher, so ist es nur dann auch an
anderer Stelle lauffähig, wenn keine
absoluten Zugriffe auf programminterne
Adressen stattfinden. Der JMP-Befehl
springt ja immer absolut und somit auf
die alte Adresse. Der BNE-Befehl jedoch
ist relativ adressiert. Er merkt sich
nur um wieviele Bytes im Speicher er
nach vorne, oder nach hinten springen
muß.
Wird der Sprungbereich eines Branchbefehls nicht überschritten (+127 und -128 Bytes vom Befehl selbst entfernt), so
ist der Einsatz von unbedingten Sprüngen
sehr sinnvoll ( insofern möglich, also
wenn man über den Inhalt eines bestimm- ten Prozessorflags eine eindeutige Aussage machen kann) .
Doch zurück zu unserer Message-Ausgabe- Routine. Außer den beiden Routinen zur
Textausgabe, brauchen wir auch die IRQ-Routine selbst, die den Aufruf dieser
beiden Routinen steuert.
Legen wir nun also einmal fest, daß der
Message-Text jeweils einmal pro Sekunde
blinken soll. Das heißt im Klartext, daß
wir zunächst einmal den Text auf den
Bildschirm schreiben müssen, wo er eine
halbe Sekunde stehen bleibt und ihn anschließend wieder für eine halbe Sekunde
löschen. Dieser Vorgang wiederholt sich
nun beliebig oft. Damit das Ganze aber
auch irgendwann einmal ein Ende hat, müssen wir nach einer bestimmten Anzahl
von Blinkzyklen die Interruptroutine
auch wieder abschalten.
Da immer alle halbe Sekunde eine der
beiden Ausgaben getätigt werden soll, müssen wir so logischerweise alle 30
Interrupts eine solche tätigen ( wir wollen ja den Systeminterrupt benutzen, der
wie gesagt 60 Mal pro Sekunde auftritt) .
Für all diese Funktionen brauchen wir
insgeamt 3 verschiedene Zwischenspeicher:
1) Eine Speicherzelle, die als Interruptzähler dienen soll. Sie zählt 30 Interrupts mit und veranlaßt dann die
IRQ-Routine eine der beiden Ausgaben
zu tätigen. Dieser Speicherzelle wollen wir den schlichten Namen COUNTER
(=" Zähler") geben.
2) Damit die IRQ-Routine auch immer
weiß, ob sie nun Text ausgeben, oder
Text löschen soll, brauchen wir auch
noch eine Speicherzelle, in der vermerkt ist, welcher Ausgabemodus als
nächstes benötigt wird. Ich nenne
diese Speicherzelle einmal MSGMODE.
3) Zuletzt brauchen wir noch eine Speicherzelle, die mitzählt, wie oft der
Text nun geblinkt hat. Ist eine ge- wisse Anzahl erreicht, so kann die
IRQ-Routine in eine Routine verzweigen, die diese selbst beendet. Diese
Adresse habe ich PULSE genannt.
Die drei soeben erwähnten Namen werden
alle in dem nun folgenden Source-Listing
für die IRQ-Routine verwendet, so daß
Sie jetzt also über deren Verwendungszweck informiert sind. Ich habe diesen
Namen natürlich auch schon absolute
Adressen zugewiesen. HYPRA-ASS benutzt
dafür den Pseudo-Opcode " . EQ" für " is
EQual"(=" ist gleich") . Schauen Sie sich
hierzu auch einmal den Sourcecode zu
MSGOUT an ( auf der Vorderseite dieser MD
als " MSGOUT-ROM. SRC") .
Die Zwischenspeicher belegen in der oben
angezeigten Reihenfolge die Adressen
$ FB,$ FC,$ FD. Diese sind allesamt aus
der Zeropage, und ( wie die fleißigen
Assembler-Programmierer unter Ihnen sicher wissen) vom Betriebssystem nicht
genutzt, weshalb wir sie für unsere Zwecke verwenden können.
Jetzt brauchen wir eine Initialisierungsroutine, die die neue IRQ-Routine
in den System-IRQ einbindet und sie somit im System installiert. Das hier ist
sie ( im Source-Listing auf dieser MD
steht sie ganz am Anfang und heißt MS-GOUT) :
------------ SEI Alle weiteren IRQs sperren (wegen, des Verbiegens der Vektoren). STA PULSE Im Akku steht die Blinkan- zahl; also merken wir sie uns. STX LOOP1+1 LO-Byte der Anfangsadresse des Textes wird in DOMSG eingesetzt (LOOP1 ist ein Label davon, siehe oben). STY LOOP1+2 Dasselbe mit dem HI-Byte. ------------ LDX #<(IRQ) LO-Byte der Anfangsadresse
der neuen IRQ-Routine laden
( siehe unten) .
LDY #>( IRQ) HI-Byte laden.
STX $0314 LO-Byte des Vektors auf unsere Routine ausrichten.
STY $0315 HI-Byte des Vektor auf unsere Routine ausrichten.
------------ LDA #01 Initialisierungswert in Akku laden. STA COUNTER Zählregister damit initia- lisieren. STA MSGMODE Mode-Register damit initia- lisieren. CLI Alle Voreinstellungen getä- tigt; wir können den IRQ wieder freigeben. RTS Und Tschüß! ------------
Die Routine ist in 3 Abschnitte gegliedert. Im ersten Abschnitt werden
zunächst die Aufruf-Parameter gemerkt.
Diese sind:
* Die Anzahl der Blinkvorgänge steht im
Akku.
* Die Anfangsadresse des auszugebenden
Textes steht in LO/ HI-Byte- Darstellung
in Xund Y-Register.
Der SEI-Befehl am Anfang ist sehr wichtig. Wir müssen nämlich davon ausgehen, daß gerade dann ein Interrupt auftreten
könnte, wenn das LO-Byte der neuen IRQ-Adresse schon gesetzt ist, das HI-Byte
jedoch noch nicht. Dann zeigt der Vektor
irgendwo in den Speicher hinein. Tritt
jetzt ein IRQ auf, dann springt der Prozessor in die Pampas und verabschiedet
sich meistens danach. Um dem vorzubeugen, muß man einfach alle IRQs verhindern, was ja mit dem SEI-Befehl erzielt
wird. Der Prozessor ignoriert jetzt die
Tatsache, daß da die CIA1 gerade einen
Interrupt meldet und wir können in Ruhe
den Vektor neu setzen.
Diese Aufgabe erledigt der zweite Teil unserer Routine. Die Anfangsadresse der
neuen IRQ-Routine wird in Xund Y-Register geholt und in die Speicheradressen unseres Zeigers geschrieben.
Im dritten und letzten Teil initialisieren wir noch zwei der drei Variablen
( PULSE wurde durch das Speichern am Anfang der Routine schon gesetzt) . Den
COUNTER laden wir mit 1, damit gleich
beim nächsten Interrupt eine Ausgabe
erfolgt ( siehe auch unten) . MSGOUT kann
zwei verschiedene Zustände haben. Entweder es steht dort 0, dann soll bei der
nächsten Ausgabe der Text gedruckt werden, oder wir haben dort eine 1, dann
soll die MSG-Zeile gelöscht werden. Ich
initialisiere hier mit 1, damit beim
ersten Aufruf die Zeile erst einmal
gelöscht wird. Würden wir zuerst den
Text schreiben, könnte es uns passieren, daß in den verbleibenden Zeichen ( vorausgesetzt der Text ist weniger als 40 Zeichen lang) noch alter " Zeichenmüll"
im Bildschirmspeicher steht.
Kommen wir nun endlich zur Interrupt- rountine selbst. Hier einmal das Li- sting: ----------------- IRQ DEC COUNTER Zähler herunterzäh- len. BEQ L1 Wenn Zähler=0, dann verzweigen auf Ausga- be. JMP SYSIRQ Sonst springen wir auf den System-IRQ. ----------------- L1 LDA MSGMODE Welcher Ausgabemodus? BNE L2 Ungleich 0, also verzweigen auf "Zeile löschen"! ----------------- INC MSGMODE Schon mal den MSGMODE auf 1 schalten. JSR DOMSG MSG ausgeben. JMP PRP Und Ausgaberoutine
verlassen.
------------------
L2 DEC MSGMODE Schon mal den MSGMODE auf 0 schalten. JSR BLANK Und MSG-Zeile lö- schen. ------------------ DEC PULSE Wenn Zeile gelöscht, ist ein Blinkvorgang abgeschlossen. Also Zähler für Blinks erniedrigen. BPL PRP Wenn noch nicht die 0 unterschritten wurde, Ausgaberoutine ver- lassen. ------------------ LDX #<(SYSIRQ) Wenn ja, dann haben wir oft genug ge- blinkt... LDY #>(SYSIRQ) ...also LO/HI-Byte der Adresse vom Sy- stem-IRQ laden. STX $0314 Und den IRQ-Vektor... STY $0315 ...wieder auf den System-IRQ setzen. JMP SYSIRQ Eigenen IRQ mit Sprung auf System-IRQ beenden. ------------------ PRP LDA #30 Akku laden... STA COUNTER ...und den IRQ-Zähler neu initialisieren. JMP SYSIRQ Auch hier beenden wir den eigenen IRQ mit einem Sprung auf den System-IRQ. ------------------
( Anm. d. Red. : Bitte Laden Sie jetzt den
zweiten Teil des CIA-Kurses aus dem
Kursmenü!)
Hier die Erklärung:
Nachdem wir die Initialisierungsroutine
von vorhin aufgerufen haben, zeigt der
IRQ-Vektor jetzt also auf die Routine
" IRQ" . Die CIA1 signalisiert nun einen
Timerunterlauf in Form eines Signals an
den Prozessor. Dieser springt daraufhin
auf die Jobroutine ab $ FF47, wo die Prozessorregister auf den Stapel gerettet
werden und über den IRQ-Vektor unsere
Routine angesprungen wird.
Diese erniedrigt nun also den COUNTER
und prüft, ob er schon 0 ist. Das ist
der Fall, da wir den COUNTER ja mit 1 initialisiert hatten, und er soeben auf
0 abgezählt wurde. Das Programm verzweigt deshalb also auf das Label " L1" .
Dort wird jetzt geprüft, welcher Ausgabemodus eingestellt ist. Da MSGMODE auf
1 steht gehts jetzt also gleich weiter
zu " L2", wo zunächst einmal MSGMODE auf
0 gezählt wird. Durch einen Aufruf von
" BLANK" wird die MSG-Zeile gelöscht.
Dies heißt für uns auch, daß einmal geblinkt wurde. Also müssen wir jetzt den
Zähler für die Anzahl der Blinks um 1 erniedrigen. Gehen wir einmal davon aus, daß wir die Initialisierungsroutine mit
einer 10 im Akku aufgerufen hatten. Somit ist der Inhalt von PULSE jetzt, nach
dem Herunterzählen 9 . Das heißt, daß die
0 noch nicht unterschritten wurde und
deshalb wird beim folgenden Branch-Befehl auch gleich auf das Label PRP
verzweigt. Dort steht eine kleine Jobroutine, die unseren IRQ wieder beendet.
Der COUNTER wird hier mit 30 neu geladen
und das Programm verzweigt anschließend
auf den System-IRQ, der nun regulär abgearbeitet wird, und der den Interrupt
wieder beendet, indem er die alten Prozessorregister zurückholt und den Prozessor mittels RTI wieder in das alte
Programm, das bearbeitet wurde, als der
Interrupt auftrat, zurückschickt.
Das Label SYSIRQ beinhaltet also die
Sprungadresse des System-IRQs, wie Sie anhand des Source-Codes erkennen können.
Ich habe dort nämlich wieder mittels
" . EQ" eine Zuweisung an diesen Labelnamen gemacht.
Bei dem folgenden IRQ, zählt unsere Routine wieder den COUNTER um 1 herunter.
Diesmal jedoch, ist diese Speicherzelle
noch nicht 0, weshalb die Routine auch
nicht in die Ausgaberoutine ab " L1" verzweigt, sondern gleich auf den System- IRQ springt. Dies geht nun 30 Interrupts lang so weiter, erst dann gibt
es wieder eine 0 im COUNTER. Unsere Routine verzweigt jetzt wieder in die Ausgaberoutine. Dort wird wieder der Ausgabemodus geprüft, der diesmal jedoch 0, also " Text ausgeben" ist. Dort müssen
wir jetzt MSGMODE dann auf 1 hochzählen
und dann mittels DOMSG unsere Mitteilung
auf dem Bildschirm ausgeben. Anschließend können wir den Interrupt wieder
über den System-IRQ verlassen.
Diese Vorgänge werden nun solange wiederholt, bis PULSE die 0 unterschreitet.
Dann nämlich wird nicht auf PRP verzweigt, sondern es werden gleich die
Befehle hinter dem BPL-Befehl abgearbeitet. Sie setzen den IRQ-Vektor wieder
auf den System-IRQ zurück, so daß also
unsere eigene Routine nicht mehr angesprungen wird. Ihre Aufgabe ist nun
erfüllt.
Diesmal brauchen wir das Interrupt-Flag
übrigens nicht zu setzen, da innnerhalb
eines Interrupts dieses Flag ja schon
durch den Prozessor gesetzt wurde ( letzten Monat hatte ich das ja genauer
erklärt) .
Auch jetzt verzweigen wir wieder auf den
System-IRQ um unseren Interrupt zu beenden.
Das wäre nun also eine Routine, die in
den System-IRQ eingebunden ist. Das Betriebssystem springt sie direkt an, und
sie selbst fährt nach ihrer eigenen Arbeit gleich mit dem System-IRQ fort. So
daß dieser also auch weiterhin arbeitet.
Der Vorteil ist schnell ersichtlich.
Laden Sie doch einfach einmal das Programm " MSGOUT. CODE" auf der Vorderseite
dieser MD. Es ist ein Assembler-Programm, das mit " SYS 4096*8", oder
" SYS 32768" aufgerufen wird. Der MSG-Text " DAS IST EIN IRQ UEBERS BETRIEBS-SYSTEM!" blinkt nun in der untersten
Bildschirmzeile. Währenddessen haben wir
aber immer noch den Cursor auf dem Bildschirm, den wir auch weiterhin benutzen
können. Würden wir unsere IRQ-Routine
nicht über den System-IRQ wieder verlassen, wäre das auch nicht der Fall. Dadurch können Sie also während Ihren eigenen IRQs die Tastatur weiterhin verwenden!
Kommen wir nun zu einem weiteren Problem. Angenommen, Sie wollten eine Mitteilung ausgeben, wärend zum Beispiel
gerade eine Routine damit beschäftigt
ist, im RAM unter dem ROM Daten zu verschieben. In dem Fall können Sie ja nicht mehr über den System-IRQ springen, da das Betriebssystem-ROM ja abgeschaltet wäre. Man kann dies tun, indem man
einige Bits im Prozessorport verändert.
Dieser wird durch die Speicherzelle
$0001 repräsentiert. Dort steht normalerweise der Wert 55(=$37), was für den
Prozessor die Speicherkonfiguration:
* BASIC-ROM bei $ A000-$ BFFF eingeschaltet.
* I/ O-Bereich bei $ D000-$ DFFF ( wo auch
die Register der beiden CIAs liegen) eingeschaltet.
* Betriebssystem-ROM bei $ E000-$ FFFF
eingeschaltet.
Wollen wir nun auf das RAM unter dem
BASICund dem Betriebssystem-ROM zugreifen, so kann man letztere mit dem
Schreiben des Wertes 53(=$35) in den
Prozessorport abschalten. Das I/ O-ROM, das wir ja noch brauchen ( wegen der
CIA1), bleibt dabei eingeschaltet.
Der System-IRQ ist somit nicht mehr für uns vorhanden und ebenso auch nicht die
Jobroutine, die über den IRQ-Vektor
$0314/$0315 auf entsprechende IRQ-Routinen springt.
In dem Fall müssen wir die Steuerung des
Interrupts selbst bewältigen. Das heißt
zunächst einmal, daß wir diesmal die
Prozessorregister selbst retten müssen
( was ja normalerweise die Jobroutine bei
$ FF47 macht - siehe Teil 2 des CIA-Kurses), und sie auch entsprechend wieder zurückholen müssen. Als IRQ-Vektor
zählt jetzt auch nicht mehr der bei
$0314/$0315, sondern wir benutzen den
Hardware-Vektor direkt. Da das ROM dort
ja abgeschaltet ist, können wir also
problemlos die Speicherzellen
$ FFFE/$ FFFF mit einem Vektor auf unsere
IRQ-Routine beschreiben.
Zur Demonstration habe ich Ihnen unsere
MSGOUT-Routine einmal umgeschrieben, so
daß sie auch ohne Betriebssystem auskommt. Der Source-Code hierzu ist ebenfalls auf dieser MD zu finden, unter dem Namen " MSGOUT-RAM. SRC" . Im Prinzip brauchen wir nur ein paar Befehle zu der
ROM-Version von MSGOUT hinzuzufügen, um
die RAM-Version zu erhalten. Das wichtigste ist hierbei die Initialisierungsroutine, die ich Ihnen hier nun aufführen möchte:
------------ SEI Interrupts wie immer speren.
STA PULSE Blinkzähler merken.
STX LOOP1+1 Anfangsadresse des. . .
STY LOOP1+2 . . . Textes merken.
------------
LDX #<( IRQ) Anfangsadresse der neuen. . .
LDY #>( IRQ) . . . IRQ-Routine laden.
STX $ FFFE Und den Hardware-Vektor. . .
STY $ FFFF . . . darauf ausrichten.
------------
LDA #$35 Wert für " ROM aus" laden. . .
STA $01 . . . und ab in Prozessorport.
------------
LDA #01 Initialisierungswert laden.
STA COUNTER Zähler initialisieren.
STA MSGMODE Modus initialisieren.
CLI IRQs wieder freigeben.
------------
LOOP3 :
LDA $01 Prozessorport laden. CMP #$37 Vergleiche mit "ROM an". BNE LOOP3 Ungleich, also weiter prü- fen. RTS Ansonsten Tschüß! ------------
Viel hat sich hier ja nicht geändert.
Den ersten Abschnitt kennen wir ja noch
von der alten MSGOUT-Routine. Diesmal
müssen wir jedoch noch aus einem zweiten
Grund die Interrupts sperren. Indem wir
nämlich später noch das Betriebssystem-ROM abschalten, nehmen wir dem Prozessor
die Grundlage für IRQs. Zum Einen verschwindet somit nämlich der Hardware-Vektor des Betriebssystems, zum Anderen
auch alle Jobroutinen für den System-IRQ. Der Prozessor springt dann irgendwo
im undefinierten RAM rum und hängt sich dann unweigerlich auf. Also jetzt geht
nix mehr ab mit IRQs!
Der zweite Abschnitt ist uns auch nicht
so unbekannt. Diesmal setzen wir jedoch
nicht den IRQ-Vektor $0314/$0315, sondern den Hardware-Vektor für IRQs bei
$ FFFE/$ FFFF. Das können wir getrost auch
bei eingeschaltetem ROM tun ( wie das
hier der Fall ist), denn die geschriebenen Daten landen auf jedem Fall im RAM, da der Prozessor ins ROM ja nicht
schreiben kann. Weil er aber irgendwo
hin muß mit seinen Daten, schickt er sie
automatisch ins RAM. Nur der Lesezugriff
kommt aus dem ROM!
Um auch dies zu ändern, verändern wir im
dritten Abschnitt der Initialisierungsroutine dann auch noch den Prozessorport
so, daß BASICund Betriebssystem-ROM
abgeschaltet werden.
Im vierten Abschnitt werden jetzt noch
die variablen Register unserer IRQ-Routine initialisiert. Hier hat sich
nichts geändert.
Wichtig ist nun noch der letzte Abschnitt. Wir können nämlich unsere Initialisierungsroutine nicht einfach so
verlassen - zumindest nicht in diesem
Beispiel. Denn normalerweise, wenn Sie
sich im Eingabemodus des 64 ers befinden, wird eine Eingabeschleife des BASICs
durchlaufen, die ständig auf Eingaben
prüft und dann bei entsprechenden BA-SIC- Befehlen, diese aufruft. Wenn Sie
also mit SYS unsere IRQ-Routine starten, dann wird die Initialisierngsroutine
nach ihrer Arbeit wieder in die BASIC-Eingabeschleife zurückkehren wollen. Die
ist jetzt jedoch nicht mehr verfügbar, weil wir ja das BASIC-ROM abgeschaltet
haben. Auch hier springt der Prozessor
dann mitten ins leere RAM, verläuft sich
dort und stürzt vor lauter Kummer einfach ab. Da ich die IRQ-Routine nun aber
so programmiert habe, daß sie automatisch, wenn sie genug geblinkt hat, BA-SIC und Betriebssystem wieder einschaltet, können wir dies als Kennzeichen dafür nehmen, daß die Grundvoraussetzungen für ein Verlassen der Initialisierungsroutine wieder gegeben sind. Deshalb also, habe ich eine Warteschleife
hier eingebaut, die immer nur prüft, ob
die ROMs mittlerweile wieder da sind.
Erst wenn dieser Fall eintritt, wird
zurückgesprungen!
Soviel zur Initialisierung für eine Arbeit unter dem ROM. Kommen wir nun zur
Interrupt-Routine selbst. Auch sie muß
leicht modifiziert werden. Auch hier
will ich einen kurzen Abriß der hinzugefügten Befehle geben:
------------ IRQ PHA Akku retten. TXA X-Reg. in Akku schieben... PHA ...und retten. TYA Y-Reg. in Akku schieben... PHA ...und retten. ------------ (etc...)
So fängt nun die neue IRQ-Routine an.
Anschließend folgen genau die Befehle, die auch in MSGOUT-ROM verwedet wurden.
Bis auf einen Unterschied: wenn es nämlich darum geht, den Interrupt wieder
abzuschalten, weil wir oft genug geblinkt haben, lautet die Abschaltroutine
folgendermaßen:
------------ LDA #$37 Alte Speicherkonfiguration STA $01 wieder einschalten.
JMP SYSIRQ Und IRQ beenden,------------
Hier wird einfach das ROM wieder eingeschaltet. Ein Zurückbiegen von Vektoren
entfällt, da das ROM ja nun wieder da
ist, und von nun an der System-IRQ wieder treu seine Dienste leistet, so, als
wäre nichts geschehen.
Nach dieser Änderung des Prozessorports
ist auch die Bedingung der Warteschleife der Initialisierungsroutine erfüllt, womit diese sogleich wieder zum guten
alten BASIC zurückspringt.
Eins muß ich jedoch noch hinzufügen. Wie
sie ja noch wissen, verzweigt die ganze
Routine ja noch öfter auf den System-IRQ, der dann ja gar nicht da ist! Demnach hätte ich diese Verzweigungen, die
ich vorhin so leichtfertig übersprungen
habe, ja erwähnen müssen!
Nun, ich habe dieses Problem anders
gelöst. Ich habe nämlich den " . EQ"- Pseudo-Opcode von " HYPRA-ASS", mit dem
ich dem Label " SYSIRQ" die Adresse
"$ EA31" zuwies aus dem Source-Code entfernt, und dafür eine eigene SYSIRQ-Routine geschrieben. Der Name entspricht
zwar nicht mehr dem, was vorher die Bedeutung war ( SYStem-IRQ), aber so ging
es halt am einfachsten.
Diese neue Routine tut nun nichts anderes, als den Interrupt ordnungsgemäß zu beenden. Wie wir ja noch aus dem letzten
CIA-Kurs wissen, tut dies der System-IRQ
am Ende auch. Die entsprechenden Befehle
hierzu stehen ab Adresse $ EA7 E. Genau
die habe ich nun in die neue " IRQ-Beenden"- Routine übernommen:
----------------- SYSIRQ LDA $DC0D ICR von CIA1 löschen. PLA Altes Y-Reg. vom Sta- pel in Akku holen... TAY ...und zurück in Y- Reg. schieben. PLA Altes X-Reg. vom Sta- pel in Akku holen... TAX ...und zurück in X- Reg. schieben. PLA Alten Akkuinhalt vom Stapel holen. RTI Und Interrupt beenden. -----------------
Die Bedeutung dieser Befehle sollte Ihnen ja noch bekannt sein. Zunächst müs- sen wir weitere IRQs durch Löschen des
ICR-Registers der CIA1 wider ermöglichen
( dadurch werden ja die Interrupt-Quellen- Flags gelöscht, wie wir aus Teil
1 dieses Kurses noch wissen) . Dann holen
wir uns in umgekehrter Reihenfolge die
Prozessorregister wieder vom Stapel runter, bevor wir den Interrupt mit RTI
beenden.
So. Das war' s dann mal wieder für diesen
Monat. Noch einen Hinweis zu den Programmen bezüglich dieses Kurses:
* Die beiden Source-Codes der MSGOUT-Routine können Sie übrigens auch lesen, wenn sie nicht den HYPRA-ASS besitzen. Laden Sie hierzu ein Source-Code- File einfach an den BASIC-Anfang
( also mit ",8" am Ende) und geben Sie
LIST ein. Jetzt wird der Text zwar
nicht automatisch formatiert, so wie
HYPRA-ASS das normalerweise tut, aber
lesen kann man das ganze schon. Zur
Anschauung genügt es zumindest.
* Das File " MSGOUT-CODE" beinhaltet beide Versionen von MSGOUT. Laden Sie es
bitte absolut ( also mit ",8,1") und
starten Sie die einzelnen Routinen
mit:
- SYS 32768 für MSGOUT-ROM - SYS 32777 für MSGOUT-RAM
Ich will mich jetzt von Ihnen verabschieden. Nächsten Monat wollen wir uns
dann einmal um die Kupplung von Timer A
und Timer B einer CIA kümmern und auch
noch den BRK-Interrupt behandeln. Bis
dahin noch viel Spaß beim Herumprobieren
mit IRQs.
( ub)
CIA-Kurs: "Die Geheimnisse des Secret-Service" (Teil 4)
Herzlich Willkommen zum 4 . Teil unseres
CIA-Kurses. Diesen Monat möchte ich dann
doch vorgreifen und Ihnen zunächst einmal die NMI-Interrupts erklären. Dann
können wir nämlich anhand eines einfachen Beispiels auch eine sehr nützliche
Anwendungsweise von Timerkopplung behandeln.
Mit dem IRQ kennen Sie sich mittlerweile
ja gut aus. Wir haben diese Interruptart
in Zusammenhang mit dem Systeminterrupt
ja schon eingehendst kennengelernt und
auch schon eigene IRQ-Routinen geschrieben, die sowohl eigenständig, als auch
im System-IRQ eingebunden arbeiteten.
Kommen wir nun also auch zu der anderen
für uns wichtigen Interruptart, dem NMI.
Zunächst: Was ist der Unterschied zwischen einem IRQ und einem NMI? Da haben
wir zum einen schon einmal den Unterschied, daß beide Interruptarten von
jeweils einem CIA angesteuert werden.
Das hatte ich Ihnen ja schon zu einem
früheren Zeitpunkt erläutert. CIA2 löst
also NMIs aus, CIA1 IRQs.
Doch es gibt da noch einen weitgehendst
wichtigeren Punkt, in dem sich der NMI
vom IRQ unterscheidet. Ich hatte Ihnen
damals bei der Erklärung der Hardwareverbindungen der CIAs und des Prozessors
untereinander ja schon erklärt, daß jede
der CIAs nicht nur verschiedenartige
Interrupts auslöst, sondern daß vielmehr
der Prozessor über zwei verschiedene
Eingänge verfügt, an denen der jeweilige
Interrupt ausgelöst werden kann. Das
bedeutet aber auch, daß er einen Unterschied zwischen beiden Interruptarten
macht, und das ist ganz wichtig für uns
zu wissen!
IRQ ist die Abkürzung für " Interrupt-ReQuest", was soviel bedeutet, wie " Anfrage auf eine Unterbrechung" . Das Wort
" Anfrage" möchte ich hier ganz deutlich
herausstellen, denn wie Sie mittlerweile
ja ebenfalls wissen sollten, können wir
den Prozessor durch den SEI-Befehl dahingehend manipulieren, daß er Signale
am IRQ-Eingang ignoriert. Im Fachjargon
sagt man auch, man kann einen Interrupt
" maskieren"- durch Setzen des Interruptflags können wir also softwaremäßig
IRQs sperren und das ist dann auch der
Punkt, bei dem der Unterschied zum NMI
in Erscheinung tritt." NMI" ist nämlich
ebenfalls eine Abkürzung und steht für
" Non-Maskable- Interrupt", was mit
" Nichtmaskierbare- Unterbrechung" den
Nagel auf den Kopf trifft. Und schon
hätten wir das Kind im Brunnen. NMIs
sind softwaremäßig nicht sperrbar und
das kann enorme Vorteile gegenüber dem
IRQ haben!
Hier einmal ein einfaches Beispiel: die
Routinen des Betriebssystems müssen in
der Regel aus dem einen oder anderen
Grund von Zeit zu Zeit IRQs verhindern.
Zum Einen aus Zeitersparnis und somit
zur Geschwindigkeitssteigerung, zum Anderen bei komplizierten Synchronisationsvorgängen mit der Peripherie des
Computers, wobei auftretende Interrupts
Zeitwerte verfälschen und somit stören
könnten, benutzt das Betriebssystem nun
ebenfalls den SEI-Befehl. Die Folge des
Ganzen wird schnell klar: soll der IRQ
nun ganz zeitkritische Arbeiten erldedigen, so kommt er schnell aus dem Takt
und ist somit oft viel zu ungenau. Glänzendes Beispiel ist die BASIC-Uhr TI$ .
Sie wird nämlich über den System-IRQ
gesteuert, der ja normalerweise 60 Mal
pro Sekunde auftritt. Rein theoretisch
braucht die Routine für TI$ also nur bis
60 zu zählen, um zu wissen, daß jetzt
eine Sekunde verstrichen ist. Praktisch
sieht es aber so aus, daß zum Beispiel die Ein-/ Ausgaberoutinen des Betriebssystems oft den IRQ unterbinden. Es genügt
also, ein längeres Programm von Diskette
zu laden um die TI$- Uhr extrem zu bremsen, so daß sie die eine oder andere
Sekunde nachgeht. Aus den Sekunden werden Minuten, je mehr man lädt und irgendwann kann man die Zeitwerte der Uhr
komplett vergessen: dadurch, daß IRQs
zwischendurch nicht mehr auftreten können, aber die Zeit weiterhin unerbittlich verstreicht, zählt die TI$- Routine
zwar weiterhin 60 IRQs, diese jedoch
dauerten länger als eine Sekunde.
NMIs hingegen werden IMMER bearbeitet, sobald sie auftreten. Sogar dann, wenn
sich der Prozessor gerade innerhalb eines IRQs befindet. Er rettet dann einfach die Daten des IRQs ( Programmzeiger, Prozessorstatus etc.) und bearbeitet den
NMI. Umgekehrt jedoch, kann kein IRQ
während eines NMIs auftreten, da der
Prozessor ja dann das Interruptflag ja schon von selbst gesetzt hat ( sie erinnern sich. . .) . Es sei denn wir lassen
dies ausdrücklich zu, indem wir innerhalb der NMI-Routine das Flag durch CLI
wieder löschen.
Sie sehen also, man muß immer Unterschiede machen, wofür ein Interrupt benötigt wird. Einfache Probleme lassen
sich schnell mit dem IRQ bewältigen ( und
das ist bei den meisten der Fall), da er
bei Bedarf auch sehr einfach abgeschaltet werden kann. Bei zeitkritischen Problemen benutzt man besser einen NMI. Er
funktioniert genau und zuverlässig, wobei man allerdings in Kauf nehmen muß, daß man diesen nicht so einfach wieder
verhindern kann.
Das ist nämlich der Grund warum man bei
der Programmierung eines NMIs mehr Aufwand hat. Für ihn existiert, ebenso wie
für den IRQ, auch ein Vektor, der verändert werden muß, wenn man die NMIs auf
eigene Interruptroutinen umleiten will.
Wir hatten ja letzten Monat schon gelernt, daß man dabei sichergehen muß, daß während dieser Veränderung in gar
keinem Fall ein Interrupt ausgelöst werden darf, da so schon während das LO-Byte, jedoch noch nicht das HI-Byte des
Vektors verändert ist, der Rechner unkontrolliert in die Pampas springen
könnte, was so unangenehme Folgen hätte, wie zum Beispiel einen Rechnerabsturz.
Aus diesem Grund müssen wir zusehen, daß
alle eventuell in Frage kommenden NMI-Quellen so geschaltet sind, daß sie keinen Interrupt auslösen, während wir den
NMI-Vektor verändern.
Im Normalfall ist dieses Problem eigentlich relativ einfach zu handhaben, denn
das Betriebssystem benutzt den Timer-NMI
ausschließlich nur bei Betrieb der
RS232- Schnittstelle, also bei der seriellen Datenübertragung per Modem. In
aller Regel können wir diesen Fall jedoch ausklammern und davon ausgehen, daß
alle Funktionen der CIA2, die den NMI betreffen, funktionslos ihr Dasein fristen. Nur im Falle einer eigenen Benutzung sollten wir uns immer im Klaren
darüber sein, was für eine Aufgabe der
NMI gerade behandelt und wie sie gesteuert wird. Im Regelfall genügt es
jedoch, einfach alle Bits des ICR-Registers der CIA2 zu löschen, so daß
von dort keine Interrupts mehr an den
Prozessor gelangen. Dies geschieht durch
ein Schreiben des Wertes 127(=$7 F) in
selbiges Register ($ DD0 D = dez.56589) .
Eine weitere Besonderheit des NMIs ist, daß die RESTORE-Taste hardwaremäßig DI-REKT an die NMI-Leitung des Prozessors
angeschlossen ist, daß also auch von
dort Interrupts ausgelöst werden können.
Dieses macht sich das Betriebssystem
zunutze, denn bei einem Druck auf RUN/- STOP-RESTORE, was den C64 ja wieder in
einen einigermaßen definierten Zustand
zurückbringt, wird immer ein NMI ausgelöst. Was nun allerdings wirklich da- bei geschieht und wie es mit Sprungvektoren für NMIs aussieht, wollen wir uns
jetzt einmal näher anschauen.
Dazu ist wieder einmal eine kleine Reise
in die tieferen Gefilde des Betriebssystems angesagt. Beginnen wir mit den
elementaren Grundvoraussetzungen:
* Zunächst also wird ein NMI ausgelöst, indem der Benutzer auf die RESTORE-Taste drückt.
* Der Prozessor hält seine momentane
Arbeit jetzt unverzüglich an, rettet
wie bei jedem Interrupt die wichtigsten Daten auf den Stapel ( das hatten
wir ja schon), setzt das Interruptflag, so daß ihn keine IRQs mehr stören können und macht sich daran, wieder in einem eigenen Vektor für NMIs, am Ende seines Adressbereichs nachzusachauen, wo er jetzt weiterfahren
soll. Dieser Vektor liegt bei
$ FFFA/$ FFFB und zeigt auf eine Jobrou- tine des Betriebssystems bei $ FE43 .
Schauen wir uns einmal an, was dort so
läuft:
---------------- NMI-Anspringen
FE43 SEI IRQs sperren.
FE44 JMP ($0318) Öber NMI-Vektor springen.
----------------
Da hätten wir auch schon den angesprochenen NMI-Vektor, der für uns veränderbar ist. Er belegt die Speicherstellen
$0318/$0319( dez.792/793) und zeigt
normalerweise auf die Adresse gleich
hinter der soeben aufgelisteten Routine, auf $ FE47 .
Was übrigens anzumerken ist, ist die
Tatsache, daß wir beim Einbinden von
eigenen NMIs in den System-NMI darauf
achten müssen, daß wir auch die Prozessorregister quasi " von Hand" auf den Stapel retten müssen. Die IRQ-Vorbereitungsroutine hatte dies ja noch
VOR dem Sprung über den RAM-Vektor gemacht, weshalb wir uns nicht mehr darum
kümmern mußten. Beim NMI macht das Betriebssystem das erst NACH dem Sprung
über den Vektor, in der nun folgenden
Routine:
---------------- NMI vorbereiten. FE47 PHA Akku auf Stapel. FE48 TXA X nach Akku... FE49 PHA ...und auf Stapel. FE4A TYA Y nach Akku... FE4B PHA ...und auf Stapel. FE4C LDY #$7F Wert laden... FE4E STA $DD0D ...und damit alle NMI-Quellen von der CIA2 kommend sperren. ---------------- Auf RS232-Betrieb prü- fen. FE51 LDY $DD0D Interruptquellen- Anzeige aus ICR lesen und somit löschen um weitere NMIs freizuge- ben. FE54 BMI $FE72 Wenn die RS232- Schnittstelle aktiv ist, verzweigen. ----------------
Anmerkung: Mit dem letzten Befehl wurde
abgefragt, ob eines der Interruptquellenbits des ICR gesetzt ist. Da das Betriebssystem ja CIA2- gesteuerte NMIs nur
dann benutzt, wenn die RS232 Schnittstelle läuft, genügt es, nur zu prüfen, ob der NMI überhaupt von der CIA2 kommt.
In diesem Fall ist Bit 7 des ICR auf 1 .
Wenn das nicht der Fall ist, dann kann
der Auslöser nur die RESTORE-Taste gewesen sein, und es wird wiefolgt fortgefahren:
---------------- Auf ROM-Modul prüfen FE56 JSR FD02 Prüft ob ein ROM-Modul im Expansions-Port steckt. FE59 BNE $FE5E Wenn nein, dann ist das Zero-Flag gelöscht und wir überspringen den folgenden Befehl. FE5B JMP ($8002) Ja, wir haben ein Mo- dul, also springen wir auf den Modul-NMI (siehe unten). ---------------- Prüfen, ob R-S/RESTORE FE5E JSR $F6BC Flag für STOP-Taste in der Zeropage ($91 = dez. 145) berechnen und setzen. FE61 JSR $FFE1 STOP-Taste abfragen. FE64 BNE $FE72 Wenn nicht gedrückt verzweigen, um den NMI zu beenden. ---------------- R-S/RESTORE ausführen FE66 JSR $FD15 Standard-Vektoren für Interrupts und Ein-/ Ausgabevektoren ini- tialisieren. FE69 JSR FDA3 Ein-/Ausgabebausteine initialisieren. FE6C JSR E518 Bildschirm löschen. FE6F JMP ($A002) BASIC-Warmstart ausführen. ----------------
Der Teil von $ FE47-$ FE59 rettet nun
zunächst einmal die drei Prozessorregister auf den Stapel. Desweiteren werden alle Interruptquellen die der CIA2 geben könnte, gesperrt.
Dann, von $ FE51-$ FE55 wird das ICR der
CIA2 ausgelesen und somit für neue Interrupts freigeben ( ist im Moment zwar
nicht möglich, da wir ja die Interrupts vorher sperrten, wird aber für die
RS232- Behandlung gebraucht!) . Gleichzeitig wird geprüft, ob der Befehl von der
CIA2 kam, und wenn ja zur RS232- Unterroutine verzweigt.
Im nun folgenden Teil von $ FE56-$ FE5 D
wird geprüft, ob ein ROM-Modul im Expansionsport steckt. Wie so etwas funktioniert, will ich Ihnen nächsten Monat
erklären. Wenn ein Modul da ist, dann
wird auf einen moduleigenen NMI verzweigt, andernfalls wissen wir nun
endgültig, daß der Benutzer wahrscheinlich den Computer zurücksetzen will, und
wir können in den folgenden Teil verweigen.
Dieser geht von $ FE5 E-$ FE65 und prüft
nach, ob die RUN/ STOP-Taste gleichzeitig
auch noch gedrückt ist. Hierzu wird eine
Unterroutine ab $ F6 BC benutzt, die direkt die Tastatur abfragt und in Speicherzelle $91 der Zeropage anzeigt, ob
die RUN/ STOP-Taste gedrückt ist. Steht dort eine 0, so war dies der Fall. Andernfalls verzweigt das Programm nun
doch in die RS232- Routine. Ehrlich gesagt, weiß ich nicht warum dies so ist, denn es läge näher, den NMI direkt zu
beenden, aber die Wege des C64- Betriebssystems sind manchmal halt auch
unergründlich. . .
Jetzt sind wir aber endlich im letzten
Teil angelangt. R/ S-RESTORE wurde
gedrückt, was heißt, daß wir einen " Mini- Reset" ausführen sollen. Es werden
nun drei Unterroutinen aufgerufen, die
die wichtigsten Voreinstellungen im System vornehmen, nämlich das Zurücksetzen
der Sprungvektoren von ( inclusive)$0314-$333( Routine ab $ FD15), das Rücksetzen des Grafikchips ( VIC) und des
Soundchips ( SID), sowie der CIAs ( Routine ab $ FDA3) und das Löschen des Bildschirms ( Routine ab $ E518) . Zum Schluß
wird dann mit einem indirekten Sprung
auf den NMI-BASIC- Warmstart in den" READY"- Modus des BASICs verzweigt.
Hierbei wird der NMI nicht wie üblich
mit RTI beendet, sondern die Warmstartroutine stetzt einfach den Stackpointer
wieder zurück und springt dann in die
BASIC-Eingabe- Warteschleife.
Soviel zum System-NMI. Wir werden in den
nächsten Kursteilen auch noch auf die
RS232- Schnittstelle und deren Bedienung, sowie auf die ROM-Modul- Behandlung näher
eingehen, weshalb ich diese Themen diesmal aussparen möchte.
Kommen wir nun zu der Programmierung von
NMIs. Die Anwendungsbereiche für diese
Interruptart sind vielfältig. Sie läßt
sich gut bei zeitkritischen Problemen
einsetzen, wie zum Beispiel das zyklische Lesen von Daten, in einer fest vorgeschriebenen Geschwindigkeit ( Digitizer
und Scanner arbeiten oft mit NMIs) . Am
eindrucksvollsten ist aber bestimmt das
Abspielen von Musik über den NMI. Viele Sound-Editoren benutzen ja schon häufig
die Möglichkeit, ein Musikstück via Interrupt spielen zu lassen, jedoch gehen
diese meist über den IRQ.
Ich habe Ihnen einmal ein Beispielprogramm auf dieser MD mit abgespeichert.
Es heißt " NMI/ IRQ-DEMO" und beinhaltet
drei kleine Unterprogramme. Das Programm
tut nichts anderes, als einen Interrupt
zu initialisieren, der ständig einen Ton
über Stimme 1 des SID spielt. Hierbei
wird bei jedem Aufruf das HI-Byte der
Tonfrequenz um 1 erhöht. Das Ergebnis
ist ein ganz lustiger Soundeffekt, der
nicht unähnlich dem Geräusch ist, das
entsteht, wenn Scotty die Besatzung der
" Enterprise" rumbeamt.
Eigentliche Aufgabe dieses Beispielprogramms ist nun aber, Ihnen den Unterschied zwischen IRQ und NMI zu verdeutlichen. Deshalb gibt es zwei Möglichkeiten, es aufzurufen. Zum Einen können Sie es mit " SYS 4096*9" starten. Dann initialisieren Sie einen NMI. Der Ton wird
ständig über den NMI ausgegeben. Nun
bietet sich zusätzlich noch die Möglichkeit, daß Sie durch " SYS 4096*9+3" eine
kleine Unterroutine aufrufen, die alle
IRQs sperrt. Zu erkennen ist dies daran, daß nach dem Aufruf der Cursor nicht
mehr blinkt. Trotzdem aber hören Sie
weiterhin den Soundeffekt - dies also
als Beweis, daß der NMI unabhängig vom
IRQ arbeitet.
Die zweite Möglichkeit den Effekt zu
starten ist die mit " SYS 4096*9+6" . Sie
initialisieren dann einen IRQ, der jedoch genau dasselbe tut wie der NMI zuvor. Sie können nun nocheinmal mit
" SYS 4096*9+3" die IRQs sperren, und
schon hören Sie nichts mehr.
Als Beispiel zu den Problematiken, diesich mit dem IRQ und dem Betriebssystem
ergeben, empfehle ich Ihnen, während der IRQ läuft einmal ein Programm von Diskette zu laden. Sie werden merken, daß
die Tonausgabe zwischenzeitlich desöfteren stockt. Wenn das passiert, dann hat
gerade wieder einmal eine Routine des
Betriebssystems den IRQ mittels SEI abgeschaltet. Leider können Sie dieses
Problem nicht mit einem laufenden NMI
untersuchen. Der stört nämlich dann die
anfangs schon erwähnten Synchronisationsvorgänge, die beim Laden benötigt
werden, wobei der 64 er nur Mist anstellt. Probieren können Sie es einmal.
Manchmal hat man Glück, machmal nicht.
Bitte laden Sie nun den zweiten Teil des
IRQ-Kurses aus dem Kurs-Menü.
IRQ-Kurs Teil 4 .2
Wollen wir uns nun einmal anschauen, wie
unser NMI-Programm aufgebaut ist. Die
NMIs werden übrigens über Timer A der
CIA2 ausgelöst, der denselben Wert wie
der Timer des System-IRQs als Startwert
bekommt ( das wäre der Wert 16420=$4024) .
Hier also das Programm:
---------- NMI vorbereiten LDA $7F "NMI-Quellen sperren" laden, STA DD0D und in ICR von CIA2. LDX #$2B Zeiger auf eigene... LDY #$90 ...Routinen laden, STX 0318 und NMI-Vektor... STY 0319 ...setzen. LDX #$24 Timerwert LO-Byte. LDY #$40 Timerwert HI-Byte. STX DD04 In TALO und... STY DD05 ...in TAHI schreiben. LDA #$81 Wert laden... STA DD0D Timer A als Interruptquelle festlegen. STA DD0E Timer A starten. ---------- SID einstellen.
LDA #$0 F Wert 15 für volle Lautstärke
STA D418 . . . ins Lautstärkeregister.
LDA #$00 Zählregister für Tonfrequenz
STA 02 initialisieren.
RTS Und zurück.
---------- NMI-Routine CLI IRQs wieder freigeben. PHA Akku, TXA X-, PHA TYA und Y-Register auf Stapel PHA retten.
LDA #16 Wert für " Dreieckswelle aus" STA D404 . . . in SID schreiben
LDA 02 Zähler für Frequenz lesen. . .
STA D401 und in Frequenz-HI schreiben
INC 02 Zähler um 1 erhöhen.
LDA #17 Wert für " Dreieckswelle an" STA D404 . . . in SID schreiben.
LDA DD0D NMIs wieder freigeben PLA Akku, TAY X-, PLA TAX und Y-Register wieder vom PLA Stapel zurückholen. RTI Und Interrupt verlassen
----------
Im ersten Teil dieses Listings haben wir
die Initialisierungsroutine für unseren
NMI. Hier werden zunächst auf die schon
beschriebene Art und Weise alle Interruptquellen die von der CIA2 kommen, gesperrt. Anschließend wird der NMI-Vektor
bei $0318/$0319 auf unsere eigene Routi- ne verbogen ( die Routine beginnt bei
$9000 im Speicher, weshalb die eigentliche Interruptroutine bei $902 B beginnt) .
Ist dies getan, müssen wir als nächstes
den Timerwert in die Timerregister für
Timer A laden ( wie bei CIA1 sind dies
die Register 4 und 5- TALO und TAHI) .
Dies ist der wie oben schon beschriebene
Wert $4024 . Jetzt müssen wir nur noch
den Timer A als NMI-Interruptquelle setzen und ihn anschließend starten. Dies
geschieht in den folgenden 3 Zeilen. Was
der Wert $81 für Register 13( ICR) und
14( CRA) bedeutet wissen Sie ja schon
aus Teil 1 dieses Kurses, als ich Ihnen
die Funktionen dieser Register genauer
erläutert habe.
Zum Abschluß der Initialisierungsroutine
müssen wir auch noch den SID darauf vorbereiten, Sound auszugeben. Dazu haben
wir auch noch genug Zeit, da der schon
laufende Timer zum nächsten Interrupt
noch lange genug zählen wird ( ich hätte die SID-Initialisierung auch vorher anbringen können) . Also wird erst einmal
die Lautstärke des Soundchips eingeschaltet, sowie den Anfangsfrequenzwert
für unseren Soundeffekt in Adresse $02 in der Zeropage geschrieben.
Diese Adresse wird als Zählregister benutzt, da man auf die Register des SID
leider nicht zum Lesen zugreifen kann.
Somit sind sie also auch nicht mittels
INC hochzählbar. Nun sind alle Voreinstellungen getätigt, und wir können wieder zum aufrufenden Programm zurückverzweigen.
Im zweiten Teil des Listings sehen Sie
nun die NMi-Routine selbst. Als erstes
erlauben wir hier wieder das Auftreten
von IRQs ( sie erinnern sich, das Betriebssystem hatte sie ja gesperrt) . Nun
werden nach der mittlerweile schon altbekannten Methode die Prozessorregister
auf den Stapel gerettet, was wir bei
NMIs ja IMMER von Hand machen müssen, da das Betriebssystem uns diese Arbeit
leider nicht abnimmt.
Nun kommt der Teil, in dem der nächste
Ton gespielt wird. Hierzu wird erst einmal die Stimme 1 des SID abgeschaltet.
Dies ist notwendig, weil wir keine Hüllkurve vorher festgelegt hatten, die einen Ton möglicherweise dauerhaft spielen
würde. Deshalb befinden sich in den
Hüllkurvenregistern die Werte 0, was
bedeutet, daß ein Ton nur ganz kurz angeschlagen wird und gleich wieder verstummt. Damit man aber die nächste Frequenz nun hört müssen wir die Stimme
also erst noch ausschalten. Anschließend
wird der Inhalt des Zählregisters $02 in
das HI-Byte- Frequenzregister von Stimme
1 geschrieben ($ D401), und der Zähler
für den nächsten Interrupt um 1 erhöht.
Nun schalten wir Stimme 1 wieder an, und
zwar mit einer Dreieckswellenform - der
Ton wird nun gespielt.
Die Arbeit des NMIs ist getan, machen
wir uns also daran, den Interrupt zu
beenden. Ebenso altbekannt werden also
das ICR wieder freigegeben, die Prozessorregister zurückgeholt und mittels RTI
der NMI beendet.
So. Nun wissen Sie also alles wissenwerte über NMIs. Bis auf einige kleine Ausnahmen, können Sie diese Interruptart
genauso behandeln, wie einen IRQ. Da die
CIAs ja baugleich sind, fällt die CIAgesteuterte Programmierung von NMIs ja
ebenso aus, wie beim IRQ.
Ein ebenfalls ganz interessantes Anwendungsgebiet von NMIs ist die Steuerung
von gewiseen Funktionen über einen Druck
auf die RESTORE-Taste. Ich habe dies
einmal bei einem Apfelmännchenprogramm
benutzt. Diese Programme berechnen ja
bekanntermaßen Grafiken aus der Mandelbrotmenge, die zwar ganz ansehlich sind, deren Berechnung jedoch oft Stunden,
wenn nicht sogar Tage dauern kann. Ich
wollte nun eben jenes Programm beschleunigen, indem ich den Bildschirm abschalte. Wie Sie vielleicht wissen, kann
durch diese Maßnahme eine Geschwingigkeitssteigerung von 5% erzielt werden, da der VIC bei abgeschaltetem Bildschirm
micht mehr auf den Speicher des Computers zugreifen muß, um die Daten für
Grafiken, Zeichen, Sprites und ähnliches
zu holen. Dadurch stört er den Prozessor
nicht mehr beim Zugriff, wodurch dieser
schneller arbeiten kann. Das Problem war
nun jedoch, daß ich weiterhin sehen
wollte, wie weit der Rechner nun mit der
Grafikberechnung fortgefahren ist. Mit
einer einfachen NMI-Routine war dies
möglich. Ohne noch zeitraubend die Tastatur abzufragen, habe ich einfach einen neuen NMI " eingekoppelt", der nichts
anderes tut, als den Bildschirm aus-, bzw. einzuschalten, wenn man die RESTO-RE- Taste drückt. Dieser Trick läßt sich
vielfältig anwenden und ist einfach zu programmieren, hier das kleine Programm:
----------------- Initialisierung MAIN LDA #$7F CIA2-NMIs... STA CIA2+13 sperren. LDX #<(NMI) NMI-RAM-Vektor LDY #>(NMI) ...auf eigene STX $0318 ...NMI-Routine STY $0319 ...verbiegen. RTS Tschüß! ----------------- NMI-Routine NMI PHA Akku retten. LDA $D011 Register laden, EOR #16 Bildschirmbit inver- tieren. STA $D011 Und wieder speichern. PLA Akku zurückholen. RTI NMI-Ende. -----------------
Das ist tatsächlich alles! Das Programm
ist im " Hypra-Ass"- Quellcode angegeben, dessen Sonderfunktionen ich Ihnen letzen
Monat ja schon erklärte. Hier eine Dokumentation:
Im ersten Teil wird zunächst einmal die
CIA2 als Interruptquelle gesperrt. Dies
ist nicht unbedingt notwendig, da sie
sowieso ausgeschaltet sein sollte, jedoch habe ich es hier zur Sicherheit
einmal gemacht. Desweiteren wird der
NMI-Vektor auf unseren eigenen NMI verbogen, und die Initialisierung ist beendet.
Nun zum zweiten Teil: Zunächst einmal
rette ich hier nur den Akku. Xund Y-Register werden in der NMI-Routine sowieso nicht benutzt, weshalb wir sie
nicht unbedingt auch noch retten müssen.
Als nächstes laden wir den Inhalt von
Register 17 des VIC ($ D011= dez.53265) in
den Akku, da mit dem 4 . Bit dieses Regi- sters der Bildschirm einund ausgeschaltet wird. Der Inhalt dieses Registers wird nun einfach mit dem Wert des
4 . Bits geEORt. Dabei wird der Wert des
Bits immer invertiert. Ist es 1(= Bildschirm an), so wird es nach dem EOR-Befehl 0(= Bildschirm aus) sein und umgekehrt. Der neue Wert muß nun nur noch
wieder in $ D011 zurückwandern, und wir
können den NMI beenden.
Das Programm finden Sie übrigens auch
auf dieser MD unter dem Namen " NMI-SCREENOFF" . Es muß absolut (",8,1") geladen werden und wird mit SYS 49152 gestartet. Ab dann können Sie per Tastendruck auf RESTORE den Bildschirm nach
Belieben einund ausschalten.
Das war es dann man wieder für diesen
Monat. Ich wünsche Ihnen noch viel Spaß
beim herumexperimentieren mit den NMIs
und seien Sie nicht enttäuscht, wenns mal nicht auf Anhieb klappen sollte, denn: Wer noch nie einen Rechnerabsturz
erlebt hat, ist kein wahrer Programmierer!
In diesem Sinne bis nächsten Monat,
Ihr Uli Basters (ub).
CIA-Kurs: "Die Geheimnisse des Secret Service..." (Teil 5)
Hallo zusammen, zum 5 . Teil dieses Kurses. Nachdem Sie nun ja ausgiebig über
Interrupts Bescheid wissen, wollen wir
uns diesen Monat noch einmal ein wenig
intensiver um die CIA-Bausteine ansich
kümmern, denen dieser Kurs ja gewidmet
ist.
Diesmal wollen wir nämlich das Thema der
Timerkopplung behandeln. Darunter versteht man die Verkettung von Timer A und
Timer B einer CIA zu einem großen 32- Bit
Timer ( je ein Timer verfügt ja über je
16 Bit) .
Wozu das gut ist, werden Sie spätestens
dann gemerkt haben, als Sie einmal einen
Timer-Interrupt programmieren wollten, der weniger oft als 15 mal pro Sekunde
auftritt. Dann reicht nämlich ein 16-
Bit-Timer nicht mehr aus, insofern er
Systemtakte zählt, was ja eigentlich die
häufigste Anwendung der Timer-Triggerung
ist ( Triggerung gibt den auslösenden
Faktor an, der den Timer dazu veranlaßt, den Wert, den er beinhaltet um 1 zu erniedrigen - ich erwähnte Timer-Trigger
schon zu Anfang dieses Kurses) .
Sie können in einen 16- Bit-Timer ja einen maximalen Wert von 2↑16-1=65535 laden. Bei 985248 .4 Taktzyklen, die der
64 er pro Sekunde bekommt, heißt das also, daß der Timer genau 985248 .4/65535=15 .03392691 mal pro Sekunde unterlaufen
kann, wenn er am langsamsten läuft.
Langsamer ( oder besser: weniger häufig) geht es nicht.
Zu diesem Zweck besteht nun aber auch
die Möglichkeit Timer A und Timer B einer CIA zu koppeln. Öber Timer B hatten
wir bisher ja wenig gesprochen, da er
vom Betriebssystem sowohl in CIA1, als
auch in CIA2 nicht benutzt wird. Jedoch
ist es ebenso möglich ihn als Timer zu verwenden, wobei analog zu Timer A vorgegangen wird.
Nun jedoch zu jener Kopplung. Es gibt
nämlich eine Möglichkeit, mit der wir
Timer B anstelle von Systemtakten die
Unterläufe von Timer A zählen lassen
können. Das heißt also, daß jedesmal, wenn Timer A bei 0 angekommen ist, Timer
B um 1 erniedrigt wird. Schaltet man nun
einen Interrupt so, daß er dann von einer CIA ausgelöst wird, wenn Timer B
unterläuft, so hat man einen vollen 32- Bit-Zähler, mit dem wir schon ganz andere Dimensionen in Sachen Häufigkeit von
Unterläufen erreichen können. Mit 32 Bit
können wir nämlich maximal 2↑32-1=42949672995( in Worten: über zweiundvierzigmilliarden) Werte zählen, was
bedeutet, daß wir auch dementsprechend
lange Pausen zwischen zwei Timerinterrupts haben. Mal kurz durchgerechnet
sind das alle 42949672955/985248 .2=4359 .273556 Sekunden. Das sind mehr als72 Minuten, also eine ganze Menge!
Die dabei anfallenden Interrupts werden
dann von Timer B ausgelöst, weshalb wir
dann auch darauf achten müssen, das wir
ihn als Interruptquelle im ICR ( Register
13) setzen.
Kommen wir nun zu einer Anwendung. Ich
muß gestehen, viele Möglichkeiten hierzu
bieten sich mir nicht, jedoch könnte
eine Timerkopplung durchaus zur Lösung
des einen oder anderen speziellen Problems nützlich sein. Ich habe mir da
eine ganz sinnvolle Anwendung einfallen
lassen und Ihnen gleich einmal ein Beispielprogramm vorbereitet, anhand dessen
ich Ihnen die Timerkopplung erläutern
möchte. Es ist ein kleines Programm, das
den Takzyklenverbrauch eines anderen
Programms stoppen kann. Es heißt EVAL
und ist auf dieser MD in zwei Versionen
gespeichert. Zum einen habe ich da den
ausführbaren Code, den Sie absolut laden
müssen ( mit ",8,1") und der ab Adresse
$9000( dez.36864) gestartet wird. Hierzu jedoch später mehr. Desweiteren finden
Sie auch noch den Quell-Code von EVAL
unter dem Namen " EVAL. SRC" . Er ist wie
immer im Hypra-Ass- Format und kann auch
ohne HYPRA-ASS mit ",8" zum Anschauen in
den Basicspeicher geladen werden.
Doch nun zu EVAL selbst. Zunächst einmal
wollen wir uns fragen, was nun genau
geleistet werden soll. EVAL soll
zunächst einmal ganz einfach die Taktzyklen zählen, die ein anderes Programm
verbraucht. Das ist die Problemstellung.
Die Lösung wollen wir - na, Sie werden es
nicht glaubenüber die Timer einer CIA
bewerkstelligen. Ich habe zu diesem
Zweck die CIA2 ausgesucht, deren Timer
normalerweise, solange die RS232- Schnittstelle des C64 nicht genutzt
wird, unbenutzt sind.
Zur Ermittlung der verstrichenen Zeiteinheiten, sprich Taktzylen, müssen wir
nun einfach nur einen bestimmten Grundwert in beide Timer laden, sie starten und anschließend das zu prüfende Programm aufrufen. Dies wollen wir mittels
eines " JSR"- Befehls tun. Springt das
aufgerufene Programm nun zurück, so müssen wir den Timer direkt anhalten und
anschließend den in ihm enthaltenen Wert
von unserem Anfangswert subtrahieren.
Dadurch erhalten wir die Anzahl der
Taktztyklen, die verstrichen sind, zwischen Start und Stop des Timers.
Soviel zum theoretischen Programmablauf
von EVAL. Kommen wir nun zu den Timern
selbst. Zunächst müssen wir zusehen, daß
wir eine richtige Triggerung für Timer A
und Timer B wählen. Timer B soll ja die
Unterläufe von Timer A zählen und dieser
widerum die Systemtakte. Zu diesem Zweck
schreiben wir also erst einmal den Wert
$81 in das Control-Register von Timer A
(= CRA, Reg.14), wie wir das ja auch
schon von der Interruptprogrammierung
her kennen. Weil bei diesem Wert Bit 5 gelöscht ist zählt Timer A also System- takte.
Für Timer B wird das schon schwieriger.
Ich hatte Ihnen ja schon einmal bei der
Beschreibung der CIA-Register aufgelistet, welche Möglichkeiten es hier gibt.
Timer B kann nämlich in Gegensatz zu
Timer A vier ( anstelle von zweien) verschiedene Triggerquellen haben. Dies
wird von den Bits 5 und 6 gesteuert, deren Kombinationen ich Ihnen noch einmal auflisten möchte:
Bit 5 6 Timer B zählt...
0 0 Systemtakte. 0 1 steigende CNT-Flanken. 1 0 Unterläufe von Timer A. 1 1 Unterläufe von Timer A, wenn CNT=1 ist.
Für uns kommt da die Kombination "10" in
Frage. Bit 5 ist also gesetzt und alle
anderen gelöscht. Wie bei Timer A müssen
wir jedoch auch Bit 0 setzen, weil wir beim Laden dieses Wertes in das Control-Register von Timer B ( CRB, Reg.15) den
Timer auch gleich starten wollen. Demnach brauchen wir diesmal den Wert $41 .
Das war dann auch schon alles, was wir
zur Timerkopplung brauchen. Timer A
zählt nun Systemtakte und löst bei jedem
Unterlauf ein Herabzählen von Timer B
aus. Einen Interrupt wollen wir diesmal
nicht erzeugen, doch könnte man auch
durchaus im ICR festlegen, das einer
erzeugt werden soll, wenn Timer B dann
unterläuft.
Somit hätten wir also einen 32- Bit Timer
der Systemtakte zählt. Die Reihenfolge
der LO/ HI-Zählbytes sieht nun folgendermaßen aus:
TimerB-HI TimerB-LO TimerA-HI TimerA-LO
Sie müssen also nicht nur ein High und
Lowbytepaar berechnen, sondern gleich
zwei. Hier wechselt man dann auch in die nächsthöhere Ebene der " Bits und Bytes" .
Eine 16- Bit-Zahl bezeichnet man nämlich
als ein " Word"( engl. : wörd = Wort) und
eine 32- Bit-Binärzahl als ein " Longword"( engl. : Langwort) . Um eine Dezimalzahl
nun in ein Longword umzuwandeln müssen
Sie folgendermaßen vorgehen:
1) Zunächst teilen wir unsere Zahl durch
2↑16(=65536) und nehmen den Ganzzahlanteil des Ergebnisses als höherwertiges Word. Dieses wird nun wie gewohnt in Lowund Highbyte aufgespalten.
2) Nun multiplizieren wir das High-Word
mit 2↑16 und subrahieren den Wert von
unserem Anfangswert. Das Ergebnis was
wir hier erhalten ist das niederwertige Word, das ebenfalls, wie gewohnt, in Lowund Highbyte umgewandelt wird.
Als Beispiel habe ich einmal die Zahl 2000000 in ein Langword umgewandelt:
1 ) 2000000/65536=30.51757813 1a) HI-Word=30 --> LO=30, HI=0 2 ) 2000000-30*65536=33920 2a) LO-Word=33920 --> LO=128, HI=132
Longword:
0 30 132 128 Binär: ------ 00000000 00011110 10000100 10000000
Soviel also hierzu. Nun können wir also
schon einmal beliebig lange 32- Bit-Timerwerte berechnen. Für EVAL habe ich
übrigens nicht irgendeine Zahl genommen, sondern schlichtweg die größte ( also die
42 Milliarden von oben) . Länger als 72 Minuten sollte eine zu testende Assemblerroutine sowieso nicht sein.
Kommen wir nun also zu der Programmierung von EVAL. Hier möchte ich Ihnen
einmal den Anfang des Programms auflisten:
------------------- EVAL LDA #$7F Zunächst alle Inter- STA CIA2+13 rupts sperren. LDA #00 Und... STA CIA2+14 Timer A und STA CIA2+15 Timer B anhalten. ------------------- LDA #$FF Nun den Maximalwert STA CIA2+4 in Timer A STA CIA2+5 und STA CIA2+6 in Timer B STA CIA2+7 schreiben. ------------------- LDA #$41 Timer B soll Timer A STA CIA2+15 zählen LDA #$81 Timer A soll System- STA CIA2+14 takte zählen. -------------------
JSR $ C000 Testprogramm aufrufen
-------------------
Im ersten Teil dieses Listings sperren
wir zunächst einmal alle Interruptquellen durch Löschen des ICR. Anschließend
werden durch das Schreiben von 0 in die
Control-Register der Timer selbige gestoppt. Diese Maßnahmen sind nur für den
Fall gedacht, daß in diesen Timern ganz
gegen unsrer Erwartung doch etwas laufen
sollte. Zum korrekten Ablauf von EVAL
ist es absolut notwenidig, daß die Timer
stehen, da sonst die Testwerte
verfälscht würden.
Anschließend werden die Register TALO, TAHI, TBLO, TBHI mit dem Wert 255(=$ FF) geladen und so auf den Maximalwert
2↑32-1 gesetzt.
Im dritten Teil des Listings werden nun
die beiden Timer wieder gestartet. Hierbei MUß Timer B unbedingt VOR Timer A
aktiviert werden, da er zum Einen von
diesem abhängig ist, und deshalb zuerst
aktiv sein sollte ( würde Timer A nämlich unterlaufen BEVOR Timer B zählbereit
ist, wäre das Testergebnis ebenfalls
nicht in Ordnung), und zum Anderen müssen wir so spät wie möglich den 32- Bit-Timer starten, damit auch tatsächlich
die Taktzyklen der zu messenden Routine
gezählt werden und nicht etwa noch einige Taktzyklen die EVAL benötigt.
Im letzten Teil wird nun noch die zu
testende Routine aufgerufen, die ich
hier einmal bei $ C000( dez.49152) angesiedelt habe.
Jetzt ist also unser 32- Bit-Timer aktiv
und zählt schön brav von 4294967295 Richtung 0 hinab. Währenddessen läuft
unser Testprogramm ab und jeder Taktzyklus der dabei verstreicht wird mitgezählt.
Wird das Programm dann mittels " RTS" beendet, so kehrt der Rechner wieder in
EVAL zurück. Nun müssen wir uns um die
weitere Behandlung der Timer kümmern.
Damit wieder keine unnötige Rechnerzeit mitgezählt wird stoppen wir also
zunächst beide Timer:
------------------- LDA #$80 Wert für "Timer-Stop" STA CIA2+14 Timer A und STA CIA2+15 Timer B anhalten. -------------------
Da der unser 32- Bit-Timer nun also nur
während des Ablaufs des zu testenden
Programms lief, müsste nun in den Timerregistern logischerweise haargenau die
Anzahl der verbrauchten Taktzyklen stehen, jedoch in einem invertierten Format. Das heißt, daß dadurch, daß der
Timer rückwärts lief, die Taktzyklenanzahl mit folgender Formel berechnet werden muß:
(2↑32-1)-( Longword in Timerregistern)= tatsächlich verbrauchte Taktzyklen.
Na aber holla, das könnte ja schwer wer- den, die 4- Byte Werte voneinander zu
subtrahieren! Doch keine Panik, auch das
läßt sich ganz einfach lösen. Dadurch
nämlich, daß wir beim Start den absoluten Maximalwert in die Timer geladen
hatten, können wir diese Umrechnung
durch schlichtes invertieren der einzelnen Bytes vornehmen. Dabei wird das sogenannte Einerkomplement unserer Zahl
gebildet, was ebenfalls eine Art ist, Bytes voneinander zu subtrahieren. Zum
Beweis möchte ich Ihnen hier einmal ein
Beipiel geben:
Wir hatten den Startwert $ FF $ FF $ FF $ FF
im Timer stehen. Eine Invertierung dieses Werts mittels des " EOR"- Befehls
ergäbe dann:$00$00$00$00- mit anderen Worten, der Startwert war 0 .
Angenommen, nun war der Wert, den wir
nach Ablauf eines Testprogramms in den
Timern vorfanden folgender:
$ FF $ FD $03$ AE. Dies entspricht dem
Dezimalwert 4294771630 . Subrtrahieren wir diesen Wert von 2↑32-1, so ergibt
sich folgendes Ergebnis:195665 . Das
wäre also die Anzahl der Taktzyklen, die
das aufgerufene Programm verbrauchte.
Nun wollen wir doch testweise einfach
einmal das Einerkomplement der Ergebniszahl bilden ( ebenfalls mittels " EOR") :
$FF $FD $03 $AE --> $00 $02 $FC $51.
Dieses Longword umgerechnet ergibt nun
aber ebenfalls die Zahl 195665-" quod
erat demonstandum!"
Kommen wir nun also zum nächsten Teil
von EVAL, dem Retten der Timerwerte und
dem gleichzeitigen Umwandeln in die absolute Zahl an Taktzyklen:
------------------- LDY #03 Quellzähler initia- lisieren. LDX #00 Zielzähler initiali- sieren. LOOP1 LDA CIA2+4,Y Akku mit dem hinter- sten Timerregister laden EOR #$FF Komplement bilden STA $62,X und umgekehrt si- chern. INX Zielzähler +1. DEY Quellzähler -1. BPL LOOP1 Wenn noch nicht un- tergelaufen, dann wiederholen. -------------------
Diese Schleife nimmt sich nun nacheinander die CIA-Register 7,6,5 und 4 vor, bildet ihr Komplement und speichert sie
umgekehrt in die Zeropageadressen $62,$63,$64 und $65 . Damit hätten wir dann
auch gleichzeitig das Problem gelöst, daß die Bytes ja in der Reihenfolge TBHI
TBLO TAHI TALO aufeinander folgen müssen, damit Sie einen Sinn ergeben. Das
hat aber auch noch einen anderen, weitaus wichtigeren Grund:
Ich habe die Adressen $62-$65 nämlich
nicht etwa willkülich als Zwischenspeicher gewählt, sondern in diesen Adressen
befinden sich nämlich die Mantissenbytes
des Floatingpoint-ACcumualtors ( kurz:
" FAC") des 64 ers. Damit wir unsere 32- Bit-Zahl nämlich auch richtig in dezimaler Schreibweise ausgeben können, brauchen wir nämlich den FAC. Der FAC ist
ein bestimmter Speicherbereich in der
Zeropage der vom Betriebssystem dazu
genutzt wird, Fließkommazahlen aufzunehmen und mit ihnen herumzurechnen. Ebenso
gibt es auch eine Routine des Betriebssystems, mit deren Hilfe wir die
32- Bit-Zahl nachher in Dezimalschreibweise umwandeln, um sie ausgeben zu können. Dazu jedoch später.
Zunächst einmal hätten wir also die absolute Anzahl der vergangenen Taktzyklen, die zwischen Start und Stop des
Timers verstichen sind im FAC stehen.
Leider haben wir nun aber doch noch ei- nige Taktzyklen, die durch EVAL verbraucht wurden, in unserer Rechnung. WO?
Werden Sie jetzt fragen, na ganz einfach
- schauen wir uns nocheinmal die Zeilen
vor und hinter dem Aufruf des Testprogramms an:
... LDA #$81 Timer A soll System- STA CIA2+14 takte zählen. ------------------- JSR $C000 Testprgramm aufrufen. -------------------
LDA #$80 Wert für " Timer-Stop" STA CIA2+14 Timer A und STA CIA2+15 Timer B anhalten.
-------------------
. . .
Na? Wissen Sie wo? Natürlich! Ab dem
Befehl " STA CIA2+14" war der Timer aktiv. Der anschließende JSR-Befehl gehört
nun aber noch nicht in die zu testende
Routine, wurde aber trotzdem mitgezählt.
Ebenso wie die Befehle " LDA #$80" und
" STA CIA2+14" . Erst dann war der Timer
wieder aus. Die hier erwähnten Befehle
wurden also alle mitgezählt. Deshalb
müssen wir nun noch von unserem Gesamtergebnis die Anzahl der Takte die sie
verbrauchten subtrahieren. Ein JSR-Befehl braucht immer 6 Taktzyklen, das
direkte Laden des Akkus 2 und das absolute Entleeren des Akkus 4 . Demnach haben wir also 6+2+4=12 Taktzyklen zuviel, die nun von der folgenden Subtraktionsroutine von dem Gesamtwert subtrahiert
werden:
------------------- SEC Carry setzen. SBC #12 Vom Akku (=TALO, in $65) 12 subtrahieren. BCS L1 Wenn kein Unterlauf, dann auf Ende ver- zweigen. DEC $64 Sonst nächsthöheres Byte -1. LDX $64 Prüfen, ob dieses CPX #$FF untergelaufen ist, BNE L1 Nö, also zum Ende. DEC $63 Ja, also nächsthöhe- res Byte -1 LDX $63 Prüfen, ob dieses CPX #$FF untergelaufen ist, BNE L1 Nö, also zum Ende. DEC $62 Sonst, das höchste Byte -1 (ohne Prü- fung, weil es nicht unterlaufen kann!) L1 STA $65 Alles klar, also Akku im niedrigsten Byte sichern. -------------------
Ich erspare mir hier einen Kommentar, weil Sie bestimmt schon wissen, wie man
zwei Integerzahlen in Assembler voneinander subtrahiert. Ich will nur noch
darauf hinweisen, daß am Anfang dieses
Programmteils der Inhalt von Speicherzelle $65 ja immer noch im Akku steht, da die Komplementierungsschleife von
vorhin ihn dort noch hat stehen lassen. . .
So. Nun hätten wir also die tatsächliche
Anzahl der Taktzyklen als Longword im
FAC stehen. Nun müssen wir sie nur noch
ausgeben. Hierzu möchte ich eine Routine
des Betriebssystems benutzen, die den
Inhalt des FAC in ASCII-Code umwandelt
und ihn anschließend ab $0100 ablegt.
Nach dem letzten ASCII-Zeichen fügt sie
noch ein Nullbyte ein, was später von
Bedeutung sein wird.
Zunächst haben wir noch ein kleines Problem - die erwähnte Umwandlungsroutine
(" FACTOASC" ist übrigens ihr Name), ver- langt nämlich reine Floating-Point- Zahlen im FAC. Diese haben ein eigenes
Format, nämlich das MFLPT-Format. Leider
entspricht unser 32- Bit-Integer aber
noch nicht diesem Format, weshalb wir
ihn erst " normieren" müssen. Was dabei
passiert, und wie das MFLPT-Format aussieht möchte ich hier auslassen, weil es
den Rahmen dieses Kurses sprengen würde.
Geben Sie sich einfach zufrieden damit, daß der nun folgende Programmabschnitt
von EVAL einfach eine 32- Bit-Zahl im FAC
normiert:
----------------- LDA #$A0 Exponent initialisie- STA $61 ren. LOOP2 LDA #$62 Höchstes Byte laden, und prüfen, ob höchstes Bit gesetzt. BMI L2 Ja, also fertig! DEC $61 Nein, also die ganze ASL $65 Mantisse um 1 ROL $64 nach ROL $63 links ROL $62 rollen. BCC LOOP2 Wenn Carrybit gelöscht, wiederholen -----------------
L2 . . . Sonst weiter. . .
So. Jetzt können wir aber endlich die Taktzyklenanzahl ins ASCII-Format umwandeln, also rufen wir FACTOASC auf. Die Einsprungsadresse dieser Routine lautet übrigens:$ BDDD ( dez.48605) .
Nun haben wir also den ASCII-Text unserer Zahl im Speicher ab $0100 stehen.
Jetzt müssen wir ihn nur noch auf den
Bildschirm bringen. Dies geschieht mittels der Routine STROUT. Sie ist ebenfalls eine Betriebssystemroutine und
steht ab $ AB1 E. Als Parameter müssen wir
ihr die Anfangsadresse des auszugebenden
Textes in LO/ HI im Akku und im Y-Register übergeben. Das Textende erkennt
sie an einem Nullbyte am Ende des Tex- tes, das FACTOASC ja freundlicherweise
schon eingefügt hat. Kommen wir nun also
zum letzten Teil von EVAL:
-------------------- L2 JSR FACTOASC Erst FAC in ASCII umwandeln. LDA #<(TXT1) Jetzt geben wir zu- LDY #>(TXT1) nächst den Text "BE- NOETIGTE TAKTZY- KLEN:" JSR TXTOUT aus. LDA #00 Nun kommt die Takt- LDY #01 zyklenanzahl auf den JSR TXTOUT Bildschirm. LDA #13 Zum Schluß Cursor in JMP BSOUT die nächste Zeile setzen. -------------------- TXT1 .TX "BENOETIGTE TAKTZYKLEN:" .BY 0 --------------------
So. Das wars. Die letzten beiden Befehle
am Ende des Listings geben nun noch ein
" Carriage Return" aus, damit der Cursor
nicht am Ende der Taktzyklenzahl stehenbleibt. Mit dem JMP auf BSOUT ( bei
$ FFD2) beenden wir dann EVAL. Der Rechner kehrt direkt von dort in den Eingabemodus zurück.
Damit sind wir dann auch wieder am Ende
eines CIA-Kurses angelangt. Ich hoffe, Sie kennen sich jetzt mit der Timerkopplung aus. Wenn Sie wollen, können Sie
ja einmal versuchen, einen Interrupt von
einem 32- Bit-Timer auslösen zu lassen, das Know-How sollten Sie aus den vorhergehenden Kursteilen schon haben.
In diesem Sinne möchte ich mich nun bis
nächsten Monat von Ihnen verabschieden, wenn wir uns einmal die Echtzeituhren in
den CIAs anschauen wollen.
Bis dann, Ihr Uli Basters (ub).
PS: Zum Test von EVAL laden Sie den Assemblercode bitte absolut in den Speicher und sorgen Sie dafür, daß ihr Testprogramm bei Adresse $ C000 beginnt. Nun rufen Sie EVAL einfach mit " SYS 36864" auf.
CIA-Kurs: "Die Geheimnisse des Secret Service..." (Teil 6)
Willkommen zum 6 . Teil unseres CIA-Kurses. Diesmal wollen wir, wie letzten
Monat schon versprochen, die Echtzeituhren der CIAs programmieren. Hierbei handelt es sich pro CIA um je eine 24- Stunden-Uhr, die weitaus genauer als die
von BASIC bekannte TI$- Uhr gehen. Sie
werden nämlich über die Frequenz des
Wechselstroms der aus der Steckdose
kommt getriggert. Da diese Frequenz in
der Regel immer 50 Hertz beträgt ( dafür
sorgt das Elektrizitätswerk, das ihn in
das Stromnetz einspeist), gehen die Uhren der CIA so gut wie nie falsch!
Zudem haben wir die Möglichkeit, eine
Alarmzeit zu programmieren. Stimmt irgendwann die aktuelle Uhrzeit mit der
angegebenen Alarmzeit überein, so löst
die jeweilige CIA einen Interrupt aus.
Dieser wird uns im ICR dann auch ge- trennt als Alarm-Interrupt angezeigt.
Auch dieses Mal habe ich Ihnen bezüglich
unseres Schwerpunktes ein Programm geschrieben, daß sich auf dieser MD befindet. Es heißt " CLOCK" und wird mit RUN
gestartet. Desweiteren finden Sie wie
immer auch den Source-Code von CLOCK
unter dem Namen " CLOCK. SRC" im Hypra-Ass- Format auf dieser Diskette. Mit einem Laden an den BASIC-Anfang (",8") können Sie ihn sich mit LIST anschauen.
Kommen wir nun jedoch erst einmal zur
Theorie. Wie programmiert man denn einen
CIA-Timer?
Das gestaltet sich eigentlich als relativ einfach. In den Registern 8-11 einer
jeden CIA werden Zehntelsekunden, Sekunden, Minuten und Stunden abgelegt. Aus
diesen Registern kann ebenso die aktuelle Uhrzeit ausgelesen werden. Dennoch
gibt es einige Besonderheiten, die wir
beachten müssen. Hier zunächst einmal eine Auflistung der besagten Register
mit ihrer Funktion:
Register Funktion
8 Zehntelsekunden 9 Sekunden 10 Minuten 11 Stunden
Erfreulicherweise sind die Uhren der
CIAs so ausgelegt, daß sie im BCD-Format
arbeiten. Dies ist ein spezielles Darstellungsformat für Zahlen, das uns die
Umwandlung der Ziffern für eine Bildschirmausgabe erheblich vereinfacht. BCD
steht für " Binary Coded Decimal", was
übersetzt " Binär kodierte Dezimalzahl" bedeutet. Dieser Code ist unter anderem
auch deshalb so vorteilhaft, weil der
Prozessor des C64, der 6510, es uns erlaubt, mit diesen Zahlen zu rechnen.
Vielleicht kennen Sie ja die Assemblerbefehle SED und CLD. Mit ihnen kann man das Dezimal-Flag des Prozessorstatusregisters setzen oder löschen, um dem 6510 mitzuteilen, daß man nun mit BCD-Zahlen
rechnen möchte.
Ich will Ihnen das einmal anhand einiger
Beispiele erläutern. Kommen wir zunächst
zu dem Zahlenformat selbst. Im BCD-Format wird ein Byte nicht wie sonst
anhand seiner gesetzten, oder gelöschten
Bits codiert, sondern ein Byte wird in
zwei 4- Bit Bereiche aufgepalten. Einen
solchen 4- Bit Abschnitt bezeichnet man
im Fachjargon als Nibble. Hier einmal
eine keine Verdeutlichung:
Nibble1 Nibble2
10010011
Nibble1 ist hierbei das höherwertige, Nibble2 das niederwertige Nibble eines
Bytes. Im BCD-Format stellt nun jedes
Nibble eine Zahl zwischen 0 und 9 dar, die dem normalen Binärformat entspricht( deshlb auch " binär kodiert") . Hier einmal eine Tabelle mit den Werten für die
Ziffern:
Wert Binär
0 0000 1 0001 2 0010 3 0011 4 0100 5 0101 6 0110 7 0111 8 1000 9 1001
Sie sehen, die Werte entsprechen also
denselben Werten die sie im Binärformat
haben. Die Besonderheit des BCD-Formates
ist nun, daß, wie oben schon erwähnt, jedes Nibble eines Bytes eine Dezimalziffer kodiert. Die Binärzahl aus dem
obigen Beipiel kann also folgendermaßen interpretiert werden:
Binär : 1001 0011 BCD-Format : 9 3 = 93 Dezimal umgewandelt: 147
Das höherwertige Nibble der Zahl hat den
Binärwert 9, das niederwertige 3, weshalb die BCD-Zahl 93 lautet! So kann nun
jede Zahl zwischen 0 und 99 kodiert werden. Die sonst 256 verschienden Binärzahlen werden auf 100 Kombinationen reduziert. Zahlen wie 11001010 gibt es im
BCD-Format nicht. Hier wäre die Wertigkeit der Nibbles 12 und 10 . Im BCD-Format gibt dies jedoch keinen Sinn, weshalb Bytewerte wie diese wegfallen.
In der Informatik spricht man dann von
einem " redundanten Code"- man kann mit
ihm weniger Elemente kodieren, als es
Möglichkeiten gibt.
Welchen Vorteil gibt uns nun das BCD-Format in Bezug auf die Timerprogrammierung? Nun aufgrund der einzelnen Nibble- codierung können wir sehr leicht die
Ziffern der Minuten, Stunden, etc. herausfinden. Eine komplizierte Binär-Dezimal- Umwandlung fällt weg.
Desweiteren können wir mit dem Prozessor
im BCD-Format rechnen. Hierzu setzt man
zunächst das Dezimal-Flag mit dem Assembler- Befehl SED. Hiernach verhalten sich
die Befehle ADC und SBC so, daß sie immer BCD-Werte liefern, vorausgesetzt, man addiert auch BCD-Werte miteinander.
Hierzu drei kleine Beispiele:
1) $11 + $12 = $23 2) $11 + $0C = $1D 3) $12 + $19 = $31
Ich habe in den Beispielen Hexadezimalzahlen verwendet, weil mit Ihnen BCD-Zahlen einfacher anzuzeigen sind. Hexzahlen stellen ja ebenfalls die zwei
Nibbles eines Bytes dar, nur daß man
alle Möglichkeiten eines Nibbles berücksichtigt ( von 0 bis F) . Beachten wir nun, daß Zahlen wie $1 C gar keine BCD-Zahlen sind, und verwenden wir sie auch
nicht, so können durch das Hexadezimalsystem sehr einfach BCD-Zahlen dargestellt werden. Hierbei entspricht die
Zahl $12 nicht wie sonst dem Dezimalwert
18, sondern tatsächlich dem Wert 12 ! ! !
Das erste und dritte Beispiel soll Ihnen
nun verdeutlichen, wie im BCD-Format
gerechnet wird.11+12 ergibt tatsächlich
23 . Das wäre nun jedoch nichts neues, da
$11+$12 auch $23 ergäbe. Deshalb zeigt
Beispiel 3 das Aufreten eines Öberlaufs.
Ist der BCD-Modus eingeschaltet, so erhalten wir aus der Addition 12+19 das
Ergebnis 31, was auch richtig ist. Bei
abgeschaltetem BCD-Modus wäre $12+$19- gleich $2 B! Beispiel 2 dient als Gegenbeispiel für eine BCD-Rechung. Weil wir
hier mit $0 C addierten, was ja keine
BCD-Zahl ist, kommt auch keine BCD-Zahl
als Ergebnis heraus.
Soviel zum BCD-Format. Kommen wir nun
zurück zu den Uhren der CIAs. Um die Uhr einer CIA zu Setzen müssen wir also nur
BCD-Zahlen in die jeweiligen Register
schreiben um die aktuelle Zeit einzustellen. Hierbei müssen wir jedoch noch
einige Dinge beachten:
1) Beim Setzen der Uhrzeit sollte IMMER
zunächst das Register für die Stunden
( Reg.11) beschrieben werden. Wird
nämlich auf dieses Register zugegriffen, so hält die entsprechende CIA
die komplette Uhr an. Dies ist deshalb so wichtig, da die Uhr ja gerade
in dem Moment, in dem wir schreiben
auf die nächste Stunde umspringen
könnte. Würden wir eine 10 in das
Stundenregister schreiben und die
restlichen Register stünden auf der
Zeit " :59 :59 .9", so würde noch während wir die Minuten schreiben die
Uhr die volle Stunde erreichen und
unsere 10 wäre eine 11, und das
ist nicht die aktuelle Uhrzeit!
Umgekehrt verhält es sich mit dem Register für die Zehntelsekunden
( Reg.8) . Erst, wenn es beschrieben
wurde, wird die Uhr wieder in Gang
gesetzt. Deshalb müssen wir also immer gleich die komplette Uhrzeit setzen, damit die Uhr auch wirklich
läuft!
Ahnlich verhält es sich auch beim
Lesen. Hier sollten wir ebenfalls
IMMER zuerst die Stundenzahl lesen.
Dies veranlaßt die CIA zum Zwischenspeichern der Uhrzeit in den 4 Uhrregistern zum Zeitpunkt des Zugriffs.
Intern läuft die Uhr allerdings weiter, sie wird also NICHT angehalten.
So können wir immer die richtige
Zeit, nämlich die, die zum Zeitpunkt
des Zugriffs in der Uhr stand, auslesen. Auch hier muß das Zehntelsekundenregister als letztes ausgelesen
werden, damit die tatsächliche Uhrzeit wieder in die 4 Uhrregister
übertragen wird.
( Weiter geht' s im zweiten Teil. . .)
Zweiter Teil:
Die CIA-Uhren sind zwar echte 24 h-Uhren, jedoch zählen sie nicht, wie
wir es gewohnt sind von 0 bis 23 Uhr, sondern sie arbeiten nach dem amerikanischen Zeitsystem, das eine Unterscheidung von Vorund Nachmittag
berücksichtigt und nur die Stunden
von 0 bis 12 kennt. Sie haben das
sicher schon einmal bei einer Digitaluhr beobachtet - sobalt es 12 Uhr
mittags ist springt die Anzeige auf
" PM" um. Das steht für " post meridian" und bedeutet nichts anderes als
" nach Mittag" . Ebenso erscheint auf
dem Display ein " AM" für " ante meridian"(=" vor Mittag") wenn die Uhr
von 11 :59 PM auf 12 Uhr nachts umschaltet. Dieses Verfahren wird ebenso von den CIA-Uhren beutzt. Das 7 .
Bit des Stundenregisters gibt an, ob
es Vor-, oder Nachmittag ist. Ist es
gelöscht, so haben wir " AM", ist es gesetzt, so ist " PM" . Dies müssen wir
also ebenfalls bei der Programmierung
berücksichtigen - sowohl beim Lesen, als auch beim Schreiben. CLOCK ist
übrigens so ausgelegt, daß es beide
Arten der Zeitdarstellung berücksichtigt. Dazu später mehr.
3) Bei den CIA-Uhren ist mir noch eine
kleine Besonderheit aufgefallen, von
der ich nicht weiß, ob sie absichtlich ist und einem Standard entspricht, oder ob sie eine Fehlfunktion darstellt.
Es ist nämlich möglich, wie sollte es
auch anders sein, die Stunde 0 Uhr
( also 12 Uhr nachts) in das Stundenregister einzutragen. Die CIA liefert
dabei keine Fehler sondern übernimmt
die Zeit wie sie ist. Sie zählt nun
bis 12 Uhr PM und springt dann auf 1 Uhr PM (=13 Uhr) um. Soweit nichts
besonderes. Der Witz ist nun, daß
nachts um 11 :59 PM nicht auf 0 :00 AM
geschaltet wird, sondern auf 12 :00
Uhr AM. Hier scheint also ein kleiner
Fehler zu sein ( vielleicht begründet
durch den internen Aufbau der CIAs), oder haben Sie schon einmal eine Uhr
gesehen, die diese Zeit anzeigt? Ich
nicht. Deshalb ist CLOCK auch so programmiert, daß es bei 12 Uhr AM
nicht 12 :00 Uhr sondern 0 :00 anzeigt.
Dies nur als Hinweis.
So. Nun wissen Sie also, wie man die
aktuelle Uhrzeit in den 4 Uhrregistern
einer CIA unterbringt, und wie man sie
dort wieder herausholt. Das ist jedoch
noch nicht alles, was man zu einer korrekten Uhr-Programmierung braucht. Wir
müssen nämlich vor dem Einstellen der
Uhr noch 2 Dinge beachten.
1) Zunächst müssen wir der CIA, die unsere Uhr steuert, mitteilen, mit welcher Netzfrequenz der 64 er arbeitet, in dem sie drinsteckt. Dies liegt
daran, daß in Amerika eine andere Stromnorm benutzt wird, als hier bei
uns in Europa. Dort beträgt die Frequenz des Wechselstroms aus der
Steckdose nämlich 60 Hertz und nicht
etwa 50, wie das bei uns üblich ist.
Aus diesem Grund kann man der CIA
auch mitteilen, welche der beiden
Frequenzen nun benutzt wird, damit
sie in beiden Stromnetzen richtig
arbeitet kann. Diese Funktion legt
Bit 7 des CRA-Registers ( Reg.14) fest. Steht es auf 1, so beträgt der
Echtzeituhrtrigger 50 Hz, steht es
auf 0, so ist er 60 Hz. Weil wir hier
in Europa eine Netzfrequenz von 50 Hz
haben, müssen wir auch dementsprechend das 7 . Bit von CRA setzen!
2) Beim Einstellen der Uhrzeit verlangt
die CIA noch eine weitere Information
von uns. Wie ich anfangs ja schon
erwähnte, kann der Echtzeituhr auch
eine Alarmzeit mitgeteilt werden, zu
der ein Interrupt auftreten soll.
Diese Alarmzeit wird nun aber in die- selben Register geschrieben, wie die
Uhrzeit, also ebenfalls in die Register 8 bis 11 . Bit 7 von CRB ( Reg.
15) ist nun dafür zuständig zu unterscheiden, ob gerade die Alarm-, oder
die Uhrzeit gesetzt wird. Steht es
auf 1, so setzen wir die Alarmzeit, steht es auf 0 so wird die Uhrzeit
geschrieben. Dies müssen wir also
berücksichtigen, wenn wir eine der
beiden Zeiten einstellen wollen.
Dies wäre alles, was Sie zur Programmierung der Echtzeituhr wissen müssen. Lassen Sie mich nun zum praktischen Beispiel schreiten und Ihnen die Funktionsweise von CLOCK erklären.
Hierzu wollen wir uns erst einmal überlegen, wie CLOCK überhaupt arbeiten
soll:
1) Zunächst soll CLOCK in den Systeminterrupt eingebunden werden, von wo aus es ständig die Ausgabe aktualisiert.
2) CLOCK soll eine Alarmfunktion beinhalten, die bei erreichen der Alarmzeit einen Piepton ausgibt.
3) Es soll möglich sein zwischen der
24 hund der AM/ PM-Darstellung der
Uhrzeit zu wählen. Hierzu habe ich
mit die Speicherzelle 3 als Modusregister ausgesucht, die normalerweise
vom Basic-Befehl USR benutzt wird. Da
dieser Befehl jedoch wenig Anwendung
findet, kann man bedenkenlos diese
Speicherzelle für eigene Zwecke nutzen. Wenn sie 0 ist, so soll die
AM/ PM-Darstellung verwendet werden.
Ist sie ungleich 0, so wird die 24 h-Darstellung gewünscht.
Egal, in welchem Modus CLOCK laufen
soll, die Eingabe der Uhrzeit soll
immer in der 24 h-Darstellung geschehen. Diese wird in der Form " HHMMSS" angegeben.
4) Zur optischen Aufmachung habe ich die Ziffern 0 bis 9 als Sprites in den
Spriteblöcken 33 bis 42( inklusive) untergebracht. Desweiteren befinden
sich den den Blöcken 43 und 44 je ein
Sprite für die AMund PM-Anzeige.
Die Uhrzeit wird durch die Sprites
des 64 ers auf dem Bildschirm angezeigt.
Kommen wir nun also zum Source-Code
Listing von CLOCK. Zunächst wollen wir
uns einmal die IRQ-Initialierung anschauen:
======================================== init lda #$81 Uhr-Trigger auf 50Hz und Timer A starten sta cia1+14 in CRA festlegen lda #<(txt1) LO-Byte Text1 ldy #>(txt1) HI-Byte Text1 ldx #$00 Wert für CRA (=Uhrzeit setzen) jsr setit Unterroutine für Zeit einlesen und setzen aufrufen lda #<(txt2) LO-Byte Text2 ldy #>(txt2) HI-Byte Text2 ldx #$80 Wert für CRA (=Alarm setzen) jsr setit Alarmzeit Lesen und Setzen sei IRQs sperren ldx #<(newirq) ..und die neue ldy #>(newirq) IRQ-Routine stx $0314 im IRQ-Pointer sty $0315 setzen ldx #15 Kopiert Liste mit loop1 lda xtab,x Sprite Koordina- sta v,x ten in die ent- dex sprechenden VIC- bpl loop1 register. ldx #07 Setzt die Farben lda #01 für alle loop2 sta v+39,x 7 Sprites dex auf bpl loop2 "Weiß" lda #$85 Alarm- und Timer- IRQs erlauben sta cia1+13 und im ICR fest- legen lda #15 Hüllkurve für Pie ton... sta sid+5 ...festlegen. lda #$c0 Frequenz von Piepton sta sid+1 festlegen. ldx #<(end+1) CLOCK-Endadr. LO ldy #>(end+1) CLOCK-Endadr. HI stx $2b und BASIC-Anfang sty $2c ...setzen jsr $a642 "NEW" ausführen cli IRQs wieder frei- rts geben und Ende. ========================================
Dies wäre also die Initialisierungsroutine für CLOCK. Als Erstes wird der Wert
$81(= bin.10000001) in CRA geschrieben.
Damit setzen wir die Echtzeituhrtriggerung auf 50 Hz ( Bit 7=1) . Gleichzeitig
müssen wir aufpassen, daß wir nicht versehens Timer A anhalten, der ja den System- IRQ steuert. Deshalb muß also auch
Bit 0 gesetzt sein, was für " Timer A
starten" steht. Er läuft zwar bereits, jedoch würden wir ihn mit einer 0 anhalten. Die 1 ändert also nichts an dem
momentanen Zustand von Timer A, sie verhindert nur eine Änderung.
( Noch weiter geht' s im dritten Teil. . .)
Dritter Teil:
Als Nächstes wird die absolute Anfangsadresse eines Textes, den ich im Speicher
abgelegt habe, in LO/ HI-Darstellung in
A/ Y geladen und der Wert 0 in das X-Register. Dies dient der Parameterübergabe für die Routine " SETIT", die anschließend aufgerufen wird. Sie gibt den
Text, dessen Anfangsadresse in A/ Y steht
aus und liest anschließend eine Uhrzeit
ein, die sie in den Uhrregistern von
CIA1 speichert. Zuvor schreibt sie den
übergebenen Wert des X-Registers in CRB
und setzt somit den Alarmoder Uhzeit-Setzen- Modus. Der erste Aufruf von SETIT
setzt die aktuelle Uhrzeit, der zweite
die Alarmzeit.
Anschließend wird der neue Interrupt
eingestellt - alle IRQs werden gesperrt
und der IRQ-Zeiger des Betriebssysems
wird auf unsere neue Interruptroutine
mit Namen " NEWIRQ" verbogen.
Nun folgen zwei Schleifen, die die Spri- tes, die wir benutzen wollen um die Uhrzeit auszugeben vorbereiten. Die erste
Schleife kopiert eine Liste, die ich
etwas weiter hinten im Source-Code abgelegt habe in den Bereich von $ D000 bis
$ D00 F. Diese Register des VIC sind für
die Koordinaten der Sprites verantworlich, die nun entsprechend gesetzt sind.
Die zweite Schleife schreibt in die VIC-Register 39 bis 46 den Wert 1, und setzt
somit die Farbe für alle Sprites auf
Weiß.
Nun müssen wir die CIA1 noch darauf vorbereiten, daß Echtzeituhr-Alarm- IRQs
ausgelöst werden sollen. Dies geschieht, indem wir den Wert $85 in das ICR ( Reg.
13) von CIA schreiben. Damit lassen wir
zum Einen die immer noch gültigen Timer-IRQs zu die von Timer A erzeugt werden
zu ( für den System-IRQ), zum Anderen
haben wir mit dem Wert $85 auch noch das
2 . Bit gesetzt, das Alarm-IRQs als Interruptquelle festlegt. Bit 7 muß wie
immer gesetzt sein, damit die übrigen Bits als IRQ-Quelle übernommen werden.
Nun müssen wir noch den SID darauf vorbereiten, einen Piepton auszugeben, wenn
ein IRQ auftritt. Deshalb wird das HI-Frequenzregister von Stimme1( Reg.1) mit dem Wert $ C0 geladen und eine Hüllkurve in Reg.5 des SID geschrieben.
Register 6, das ja ebenfalls Attribute
der Hüllkurve angibt, wird nicht neu
beschrieben, da es in der Regel den Wert
0 enthalten sollte, den ich dort auch
haben wollte.
Zum Abschluß folgen nun noch 5 Befehle, die den Anfang des BASIC-Speichers höhersetzen. Dies ist erforderlich, da ich
nämlich am normalen Anfang die Sprites
und den Code von CLOCK untergebracht
habe. Würden Sie nun ein BASIC-Programm
eingeben, so würden Sie CLOCK überschreiben, was nicht sein sollte. In den
Adressen $2 B und $2 C hat das Betriebssystem die Adresse des BASIC-Starts gespeichert. Wir setzen diese Adressen nun
einfach auf die Endadresse von CLOCK und rufen anschließend die Routine bei $ A642 auf. Dort steht die BASIC-Routine für
den Befehl " NEW" . Dies ist notwendig, damit die neue Anfangsadresse des BA-SIC- Speichers auch vom Betriebssystem
angenommen wird.
Die Initialisierungsroutine gibt nun die
IRQs wieder frei und springt zurück.
Kommen wir zu der neuen IRQ-Routine, die
die Uhr steuert. Sie muß zunächst prüfen, ob der ausgelöste Interrupt ein
Timer-, oder ein Alarm-IRQ war und dementsprechend reagieren. Bei Alarm gibt
sie einen Piepton aus, bei einem Timer-IRQ wird nur die aktuelle Zeit aus CIA1 ausgelesen und mit Sprites auf dem Bildschirm dargestellt. Hierbei muß sie unterscheiden, ob die Zeit nun in 24 hoder AM/ PM-Darstellung auf dem Bildschirm erscheinen soll. Werfen wir doch
einfach einmal einen Blick auf diese
Routine:
======================================== NEWIRQ ist die IRQ-Routine von CLOCK. Hier wird zunächst einmal die vom Dar- stellungsmodus abhängige Spriteanzahl eingeschaltet. ======================================== newirq lda #$7f Wert für Sprites 0-6 einschalten (für 24h) ldx mode Uhr-Modus prüfen. bne l5 ungleich 0, also 24h lda #$ff Sonst AM/PM, des- halb Wert für "Sprites 0-7 ein- schalten" laden l5 sta v+21 und einschalten === Hier wird geprüft ob ein Alarm-IRQ (Bit2 von ICR=1) Auslöser war. Wenn ja, so wir ein Piepton ausgegeben. === lda cia1+13 ICR auslesen and #$04 Bit2 isolieren beq timeref Wenn =0, dann kei Alarm und weiter lda #15 Sonst Lautstärke sta sid+24 einschalten lda #33 Stimme 1 als "Sä- gezahn" sta sid+4 einschalten ldx #$ff Und warten... loop5 dey (dies... bne loop5 ...ist... dex ...eine... bne loop5 ...Warteschleife) lda #00 Ton gespielt, deshalb sta sid+4 Stimme1 aus sta sid+24 Lautstärke aus ======================================== Dies ist der Teil von NEWIRQ, der die Zeit neu ausgibt. ======================================== timeref ldx #43 Spriteblock "AM" in X laden lda cia1+11 Stunden in Akku bmi l6 Wenn 7.Bit=1 habe wir "PM", dehalb weiter cmp #$12 Akku=12 (in BCD)? bne l2 Nein, also weiter lda #00 12 Uhr AM gibts nicht, deshalb 00 Uhr AM beq l2 Unbedingter Sprun === Hier wird hinverzweigt, wenn PM ist. === l6 inx X-Reg enthält Spriteblock für "AM" - jetzt für "PM" and #$7f Bit7 (=PM) der Stunden löschen ldy mode Modus prüfen beq l2 Wenn AM/PM dann weiter cmp #$12 Akku = 12? beq l3 Ja, dann nicht 12 addieren sed BCD-Mode an clc Weil PM, 12 ad- adc #$12 dieren (für 24h) cld BCD-Mode aus l2 stx sprpoi+7 AM/PM-Sprite schalten
======================================== Nun folgt der Teil von NEWIRQ der die aktuelle Zeit ausgibt.
======================================== l3 jsr getnum Akkuinhalt in Spritepointer umwandeln stx sprpoi+0 Sprites für Stunsta sprpoi+1 den setzen
lda cia1+10 Minuten laden jsr getnum ...wandeln stx sprpoi+2 ...und sta sprpoi+3 ...setzen lda cia1+9 Sekunden laden jsr getnum ...wandeln stx sprpoi+4 ...und sta sprpoi+5 ...setzen lda cia1+8 Zehtelsek. laden clc Spritepointer der adc #sprbas Ziffer 0 addiern sta sprpoi+6 und setzen jmp $ea31 IRQ beenden ========================================
Zunächst einmal möchte ich Ihnen die In
halte und die Bedeutung verschiedene
Labels in diesem Teil des Listings erläu
tern:
* MODE enthält den Wert 3 und steht fü
die Speicherzelle 3, in der wir j
festgelegt hatten in welchem Darstel
lungsmodus CLOCK arbeiten soll.
* SPRPOI enthält den Wert 2040, die Basi
sadresse der Spritepointer. In diese
Pointern wird angegeben, welcher Spri
teblock in einem Sprite dargestell
werden soll.
* SPRBAS enthät den Wert 33 und steht fü
den ersten Spriteblock, den wir verwen
den. In ihm steht die Ziffer 0 al
Sprite. Addieren wir zu einem Wert i
Akku ( zwischen 0 und 9) den Wert SPRBA
hinzu, so erhalten wir den Zeiger au
den Spriteblock mit der entsprechende
Ziffer des Wertes, den wir im Akku ste
hen hatten.
* GETNUM ist eine Routine, die eine BCD
Zahl im Akku in die zwei Ziffernwert
aufspaltet und SPRBAS zu diesem Wer
hinzuaddiert. In X-Register und Akk
werden die Spritepointer der beide
Ziffern zurückgegeben.
( Endgültig Schluß ist erst bei Teil vier. . .)
Vierter Teil:
Im ersten Teil von NEWIRQ wird der Darstellungsmodus geprüft und, abhängig da
von, entweder sieben oder acht Sprite
eingeschaltet. Im AM/ PM-Modus wird das 8 Sprite nämlich zur Darstellung von A
oder PM verwendet.
Als Nächstes prüft NEWIRQ, was die Inter
ruptquelle des IRQs war. Ist Bit 2 im IC
von CIA1 gesetzt, so war es der Alarm
IRQ. Wenn dieser der Auslöser war, s
wird der voreingestellte Piepton einge
schaltet und für die Dauer der folgende
Warteschleife gespielt. Anschließend wir
er wieder abgeschaltet und NEWIRQ fähr
mit der Zeitausgabe fort. Sicherlich is
die Tonausgabe mit einer Warteschleif
nicht unbedingt die eleganteste Art un
Weise dieses Problem zu lösen, jedoc
genügt Sie für den Zweck als Beispielpro
gramm. Sehen Sie sich doch einfach gefor
dert, und versuchen Sie eine eigen
Alarm-Routine zu schreiben, die läuft ohne den IRQ zu verzögern.
Es folgt nun die Routine TIMEREF, die di
Ausgabe der Uhrzeit erledigt. Hierzu müs
sen wir zunächst einmal herausfinden, o
die Stunden in AM/ PModer in 24 h
Darstellung erscheinen soll. Demensprech
end muß nämlich dann die Stundenzahl mo
difiziert werden. Das geschieht am Anfan
von TIMEREF. Das Sprite für AM/ PM wir
übrigens immer mitgesetzt, selbst wen
die 24 h-Darstellung gewählt wurde. I
diesem Fall ist es jedoch nicht zu sehen
weil es am Anfang von NEWIRQ abgeschalte
wurde.
Wenn das Programm herausfindet, daß di
Uhr 12 Uhr AM anzeigt, was ja nach unse
rer Definition keine logische Uhrzei
ist, so lädt es 0 in den Akku, um späte
bei der Ausgabe ein "00" als Stunde er
scheinen zu lassen.
Wenn es Nachmittag ist, stellt TIMERE
zunächst einmal den Spritepointer für da
AM/ PM-Sprite auf " PM", indem es das X
Register um 1 erhöht. Anschließend lösch es das 7 . Bit der Stundenzahl, da diese
ja nur angibt, daß Nachmittag ist, un
später bei der Umwandlung in eine Ziffe
stören würde. Wenn die AM/ PM-Darstellun
aktiv ist, wird direkt zu dieser Umwand
lung weiterverzweigt. Andernfalls prüf
TIMEREF, ob es nicht gerade 12 Uhr P
ist, da bei der 24 h-Darstellung in diese
Fall ja NICHT der Wert 12 zu der Stunden
zahl addiert werden soll. Ist dies nich
der Fall, so müssen wir den Wert 12 doc
noch zur Stundenzahl hinzuaddieren, wobe
wir den BCD-Modus des Prozessors benut
zen. Nun ist die Stundenzahl berechne
und kann an die Umwandlungsroutine wei
tergegeben werden.
Der restliche Teil von NEWIRQ erklär
sich von selbst.
Damit wissen Sie nun, wie man eine CIA
Echtzeituhr programmiert. Die übrige
Programmteile von CLOCK ( die schon erwän
ten Routinen SETIT und GETNUM) möchte ic
hier aussparen, da sie mit dem Thema un seres Kurses wenig zu tun haben und all
gemeingültige Probleme lösen. Nichts de
sto trotz können Sie sie sich ja einma
im Source-Code- Listing auf dieser MD an
schauen.
Probieren Sie CLOCK doch einmal aus. Wen
Sie es starten so werden Sie zunächs
nach Uhr und Alarmzeit gefragt. Anschlie
ß end können Sie die Uhr am unteren Bild
schirmrand beobachten. Wenn Sie CLOC
starten, so müßte die Zeit in der 24 h
Darstellung erscheinen, da die Speicher
stelle 3 in der Regel einen Wert ungleic
0 enthält. Schalten Sie dann doch einfac
einmal mit " POKE 3,0" und " POKE 3,1" zwi
schen den beiden Modi um. Sie werden se
hen, daß CLOCK durch die IRQ
Programmierung immer direkt auf Ihre Ein
gaben reagiert.
So. Das war es dann mal wieder für diese
Monat. Sie können ja einmal versuche
eine noch komfortablere Uhr zu program mieren - wie es geht wissen Sie jetzt ja
Wie wäre es zum Beispiel mit einer Funk
tion, die stündlich einen Piepton aus
gibt? Oder sogar gleich eine ganze Reih
von Intervall-Tönen, wie man es vo
Quartz-Weckern her kennt?
Ich hoffe, Ihnen damit einige Öbungsanre
gungen gegeben zu haben und verabschied
mich nun bis nächsten Monat. Dann werde
wir uns mit einem der Hauptanwendungsge
biete der CIAs beschäftigen: der Einun
Ausgabe.
Bis dahin wünsche ich Ihnen viel Zeit un
Piep,
Ihr Uli Basters (ub)
CIA-Kurs: "Die Geheimnisse des Secret Service..." (Teil 7)
Hallo und willkommen zum 7 . Teil des
CIA-Kurses. Diesen Monat wollen wir uns
mit der wichtigsten Funktion der CIA-Bausteine beschäftigen, durch die sie
erst so richtig leitungsfähig werden.
Die beiden kleinen Chips steuern nämlich
den kompletten Verkehr mit der Außenwelt
des C64 . Sei das nun die Bedienung eines
Diskettenlaufwerks, eines Druckers, der
Tastatur oder sogar die Kommunikation
mit eiener Hardware-Erweiterung am Userport, alles geht nur mit den CIAs. Und
das sogar relativ einfach. Kommen wir
zunächst einmal zu den Grundeinheiten in
den CIAs, die die für Ein-/ Ausgabe bestimmt sind, den Portregistern.
Jede CIA verfügt nämlich über jeweils
zwei frei programmierbare 8- Bit-Ports.
Jeder dieser Ports wird von je einem
Register der CIA repräsentiert. Die Bits, die Sie dort auslesen, beziehungsweise hineinschreiben, kommen von, oder
erscheinen an den jeweiligen Portleitungen der entsprechenden CIA und sind aus
ihr herausgeleitet.
" Frei programmierbar" heißt, daß diese
Ports sowohl zur Ein-, als auch zur Ausgabe benutzt werden können. Die jeweilige Funktion, die man benutzen möchte, kann softwaremäßig festgelegt werden.
Und das nicht nur für einen ganzen Port, sondern wir können sogar die einzelnen
Bits eines Ports, je nach Bedarf auf
Einoder Ausgabe schalten.
Die Portleitungen erscheinen an den verschiedensten Stellen wieder. So sind zum
Beispiel die Portbits der CIA2 am Userport zu finden, oder die der CIA1 an den
beiden Control Ports für Joysticks, beziehungsweise intern an der Tastatur.
Sie sehen also, daß die Ports auch mehrfach benutzt werden. Daher erklärt es
sich auch, daß Sie, wenn Sie den Joystick in Port1 ein wenig hin und her bewegen, wirre Zeichen auf dem Bildschirm erscheinen. Das Betriebssystem
des C64 glaubt nämlich, die Tastatur
würde bedient und gibt die entsprechenden Zeichen aus. Doch dazu wollen wir
später noch einmal kommen. Zunächst will
ich Ihnen erst einmal erläutern, wie die
Abfrage dieser Geräte funktioniert.
Kommen wir also zu der Funktionsweise
der Portprogrammierung. Man unterscheidet die Ports einer CIA mit den Bezeichnungen " Port A" und " Port B" . Die Register dieser Ports finden sich in den
Registern 0 und 1 der entsprechenden CIA
wieder. Sie heißen Portregister A und B.
Zu jedem dieser Ports gibt es nun auch
die sogenannten Datenrichtungsregister.
Hier wird bestimmt, welches Bit eines
Ports auf " Eingabe", und welches auf
" Ausgabe" steht. Die Datenrichtungsregister sind in den Registern 2 und 3 einer
CIA zu finden. Hier nochmal eine kleine
Öbersicht ( die Abkürzungen werden wir der Einfachheit ab jetzt immer benutzen) :
Reg. Abk. Name
0 PRA Portregister A 1 PRB Portregister B 2 DDRA Datenrichtungsregister Port A 3 DDRB Datenrichtungsregister Port B
Ist ein Bit im Datenrichtungsregister
eines Ports gelöscht, so ist das entsprechende Bit des Ports auf " Eingang" geschaltet; ist es gesetzt, so steht das
Bit des Ports auf " Ausgang" . Dem Programmierer sind hier keine Grenzen gesetzt. Schreibt man einen 8- Bit-Wert in
einen Port, dessen Bits unterschiedlich, also in beide Richtungen geschaltet sind
( z. B. Bits 0-3 auf " Ausgang"=1, Bits 4-7 auf " Eingang"=0), so werden auch nur die
Bits des entsprechenden Portregister an
den Ausgang gelegt, die als solcher geschatet sind ( im Beispiel die Bits 0-3) .
Die übrigen Bits werden ignoriert, beziehungsweise vom Eingang überschrieben.
Im Handling mit den Portregister müssen
wir übrigens dringend darauf auchten, daß in der CIA ein Inverter eingebaut
ist der die Einund Ausgangssignale
invertiet. Fragen Sie mich nicht warum, für die Probrammierung ist es jedoch
unablässig dies zu wissen.
So müssen wir beim Lesen eines Datenports darauf achten, daß die Eingangsbits immer in invertierter Schreibweise
im Register erscheinen. Ist dabei also
ein Port, der auf " Eingang" geschaltet
ist, unbelegt, das heißt, daß an ihm
kein Signal anlegt, so sind die Bits im
Datenportregister auf 1 . Liegt eine
Spannung an einem der Bits an, so erscheint es im Portregister als 0 ! Das
ist wichtig zu wissen, da wir den gelesenen Wert dann nämlich erst einmal wieder invertieren müssen, wenn wir ihn als
normalen Zahlwert lesen wollen. Dazu benutzt man dann den EOR-Befehl. Mit EOR
#$ FF kann man den Inhalt des Akkus invertieren.
Umgekehrt müssen wir beim Beschreiben
der Portregister darauf achten, daß wir
das was wir an Eins-Bits erscheinen lassen wollen umgekehrt schreiben müssen.
Wenn Sie also zum Beipsiel die Bitfolge
11110000 am Port A der CIA2 ausgeben
wollen ( dieser ist am Userport herausgeführt), dann müssen Sie die Bitfolge
00001111 in das Portregister ( Reg.0 von
CIA2=$ DD00) schreiben!
Kommen wir nun zur Tastaturabfrage. Die
Tastatur wird mit Hilfe der Datenports A
und B der CIA1 abgefragt. Dies geschieht
auf eine besonders pfiffige Art und Weise. Da jeder Port über jeweils 8 Bits
verfügt, könnte man eigentlich nur 16 Tasten abfragen. Mit diesen 2 x8 Bits
kann man aber auch eine Matrix bilden, mit der 2↑8=64 Kombinationen abgefragt
werden können. Die Abfrage dieser Kombi- nationen ist nun etwas kompliziert. Deshalb gibt es jetzt erst einmal eine Grafik mit der Belegung der Matrix. Drucken
Sie sie sich am besten aus, oder malen
Sie sie ab, da ich sie später bei der
Erklärung dringend benötige. . .
Die Entwickler des C64 haben nun 2 Vorraussetzungen geschaffen, die eine Matritzenabfrage der Tastatur ermöglichen:
1) Jede einzelne Taste der Tastatur kann
man sich als einen Ein/ Aus-Schalter
vorstellen, der nur solange einen
Strom durchschaltet, wie die Taste
gedrückt ist.
2) Der Strom, der durchgeschaltet wird
kommt - jetzt halten Sie sich fest - von Port A der CIA1 . Die Bits dieses
Ports sind alle auf " Ausgang" geschaltet und gesetzt. Wie Sie aus der
obigen Grafik erkennen konnten, sind
die Leitungen der einzelnen Bits von
Port A in der Waagerechten für jeweils acht Tasten durchgeschleift.
Wird nun eine Taste gedrückt, so
schaltet sie ein Signal senkrecht
durch, wo widerum acht Tasten miteinander verbunden sind. Port B dient
nun als Eingang für widerum acht solcher Signale. Mit Port A sind also die waagerechten, mit Port B die senkrechten Tasten vesorgt. Öber eine
Kreuzpeilung kann man nun genau feststellen, welche Taste gerade gedrückt
wird.
Hierzu eine genaue Bescheibung:
Die Tastaturabfrage des Betriebssystems
legt nun zunächst an allen Bits von
Port A Eins-Signale an. Wird keine Taste
gedrückt, so ist auch nichts in Port B
zu sehen. Alle seine Bits sind auf Null
( das heißt für uns auf 1, weil die Eingangsbits ja von der CIA invertiert werden) .
Nun brauchen Sie die Tabelle von eben.
Wird jetzt nämlich eine Taste gedrückt, als Beipiel nehme ich mal die Taste " J", so wird das Eins-Signal von Port A, Bit4, an Port B, Bit2 durchgeschaltet.
Die Tastaturabfrageroutine erkennt so, daß überhaupt eine Taste gedrückt wurde, da der Inhalt von Port B jetzt ja nicht
mehr Null ist. Nun beginnt sie, alle Bits einzeln durchzutesten, indem sie
der Reihe nach die Bits von 0 bis 7 von
Port A auf Eins setzt und prüft, ob Bit2 von Port B immer noch gesetzt ist. Bei
Bit0 ist das nicht der Fall, ebenso bei
Bit1,2, und 3 . Wenn sie jetzt Bit4 von
Port A auf Eins legt, so erscheint es an
Bit2 von Port B wieder. Die Taste wäre
lokalisiert! Nun sucht die Abfrageroutine sich noch aus einer Tabelle im Betriebssystem den ensprechenden Tastencode heraus, speichert ihn zwischen und schreibt ihn zusätzlich in den Tastaturpuffer, von wo aus die Ausgaberoutine
des Betriebssystems das Zeichen auf dem
Bildschirm ausgibt.
Als Beipiel habe ich Ihnen einmal ein
kleines Programm vorbereitet. Es wartet, bis eine der SHIFT-Tasten gedrückt wird
und kehrt dann wieder zum aufrufenden
Programm zurück. Dabei müssen wir jedoch
darauf achten, daß die SHIFT-Tasten bei
der Tastatur unterschieden werden in linke und rechte Taste. Unser kleines
Progrämmchen muß also zwei Abfragen machen, damit es erkennt, wann wir SHIFT
gedrückt haben. Zunächst möchte ich Ihnen jedoch den Source-Code des Programms
hier auflisten. Sie finden ihn auch wie
immer auf dieser MD unter dem Namen
" WAITSHIFT. SRC" . Das lauffähige Assemblerprogramm heißt " WAITSHIFT. OBJ" und
muß absolut (",8,1") geladen werden. Es
liegt ab Adresse $ C000( dez.49152) und
muß mit SYS49152 gestartet werden. Nun
aber zum Listing:
**************************************** start sei System-IRQ sperren lda #$42 waagerechte Reihen mit SHIFT left und right (Bits 1 und sta cia1+2 auf "Ausgang" scha ten. lda #$00 Alle Bits von Port sta cia1+3 auf "Eingang" scha ten. loop1 lda #$fc Bit 1 auf "Eins" legen sta cia1+0 und ab ins PRA. lda cia1+1 Jetzt PRB prüfen.. cmp #$7f ...wenn SHIFT-LEFT gedrückt, dann ist Bit7 gelöscht! beq end Jau, is so, also Ende. lda #$bf Sonst Bit 6 auf "Eins" setzen sta cia1+0 und wieder ins PRA lda cia1+1 PRB holen... cmp #$ef ...wenn SHIFT-RIGH gedrückt, dann ist Bit4 gelöscht! bne loop1 Is nich, also noch mal von vorne!
end lda #$ ff DDRA muß für Sys-I
sta cia1+2 rückgesetzt werden cli System-IRQs wieder frei geben. rts Unn Tschüß! ****************************************
Kommen wir nun zur Dokumentation:
Wie Sie aus der Tastentabelle entnehm
können, sind die SHIFT-Tasten folgenderm
ß en codiert: SHIFT-LEFT wird durch Bi
von PRA angesprochen und schaltet zu Bi
von PRB durch. Ebenso bekommt SHIFT-RIG
sein Signal von Bit6 von PRA und schalt
nach Bit4 von PRB durch. Möchten wir n
also ganz gezielt diese Tasten abfrage
so müssen wir zunächst genau das Bit v
PRA setzen, das die entsprechende Tas
ansteuert. Dann muß geprüft werden, ob d
Bit, das von dieser, und NUR von dies
Taste durchgeschaltet wird auch auf 1 is
Ist dies der Fall, so war die Tas
tatsächlich gedrückt.
Zuvor müssen wir jedoch noch ein paar Vo
bereitungen treffen. Dies geschieht in d ersten 5 Zeilen von WAITSHIFT. Zunäch
müssen wir den System-IRQ sperren, da d
ja sonst die Tastatur für uns abfragt, u
uns nur stören würde. Da er über CI
läuft, genügt es, mit SEI alle IRQs unterbinden. Anschließend müssen die D
tenrichtungsregister der beiden Ports f
unsere Zwecke ausgerichtet werden. Hier
schreiben wir zunächst den Wert $42 DDRA.$42 entspricht dem Binärwe
01000010 . Hier sind die Bits 1 und 6 g
stetzt, womit wir sie auf Ausgang scha
ten. Alle anderen sind Eingang. Dadur
legen wir fest, daß Signale nun nur no
von den Tasten die in diesen Reihen ( der Tabelle) liegen kommen. In DDRB kom
eine 0 . Es würde zwar genügen, wenn w
nur die Bits 4 und 7 als Eingang schalt
ten, jedoch habe ich mich der Einfachhe
halber für diese Kombination entschiede
da sie nicht zuletzt einfacher und eben
effektiv ist.
Nun beginnt erst die eigentliche Abfrag
Begonnen wird mit der linken SHIFT-Tast Durch den Wert $ FC, den wir in PRA schre
beb, legen wir Bit2 auf Eins (000000 invertiert ergibt 11111101) . Es liegt n
ein Pegel dort an, der zu den Tast
SHIFT-LEFT, E, S, Z,4, A, W und 3 durc
geschaltet ist. Wird nun eine dieser T
sten gedrückt, so erscheint die Eins v
Bit2 an einem der 8 Bits von PRB wiede
Für die SHIFT-LEFT- Taste wäre das Bit7 .
wir die Signal-Invertierung beachten mü
sen, muß in PRB beim Auslesen also d
Wert $7 F ($7 F=01111111, das ist ein inve
tiertes 10000000=$80) stehen. Das wi
sogleich durch den CMP-Befehl überprüf
War es tatsächlich der Fall, so wird a
Ende der Routine verzweigt, wenn nich
dann müssen wir nun die rechte SHIFT-Tas
abfragen.
Dies erfolgt auf demselben Wege wie vo
her. Nur legen wir diesmal Bit6 von P
auf 1 und untersuchen, ob es an PRB wied
erscheint. Ist dies auch nicht der Fal
so werden beide Tasten nochmals überprüf
solange, bis eine Taste gedrückt wird.
Zum Schluß muß WAITSHIFT noch den alt
Wert ( ALLE Bits als Ausgang) in DD
zurückschreiben, damit die Tastaturabfra
des System-IRQs auch wieder alle Tast
abfragen kann. Nun können wir die IRQs m
CLI wieder freigeben und die Routine bee
den.
Nun wissen Sie also, wie man die Tastat
hardwaremäßig abfragt. Das kann sehr hi
freich bei der Programmierung von ein
Spielsteuerung sein, da diese Art der A
frage es einem ermöglicht mehrere Tast
gleichzeitig abzufragen. Bei einem Rennw
genspiel zum Beispiel, oder bei einer ei
fachen Auto-Simulation kann so zum Be
spiel eine Taste für die Kupplung des A
tos herhalten und eine andere für den Ga
der eingelegt werden soll. Nur wenn d
Kupplung gedrückt ist, kann der Gang ei
gelegt werden. Dadurch bekommt das Spi
gewissermaßen einen " seriösen Touch" . . .
Damit soll es dann für diesen Monat gen sein in Sachen Geheimdienst. Nächstea M
geht es weiter mit der Ein-/ Ausgab
Programmierung. Wir wollen uns dann eine Joystickabfrage kümmern und noch e
was über ein spezielles Ein-/ Ausgab
register der CIA erfahen. Bis dahin vi
Spaß beim Tastendrücken,
Ihr Uli Baster
CIA-Kurs (Teil 8) "Die Geheimnisse des Secret Service..."
Hallo und Willkommen zum 8 . Teil unseres
Kurses. Diesen Monat soll es weitergehen, mit der Ein-/ Ausgabe über die CIA-Bausteine. Wir werden eine Joystickabfrage programmieren und darüber hinaus
die Funktionsweise von anderen Eingabegeräten einmal etwas genauer unter die
Lupe nehmen.
Von der letzten Ausgabe her sollten Sie
ja noch wissen, daß jede CIA über zwei
Ein-/ Ausgabeports verfügt. Diese befinden sich jeweils in den ersten beiden
Registern einer CIA. Desweiteren finden
wir zu jedem Port ein Datenrichtungsregister ( Register 2 und 3 einer CIA), in
denen wir jeweils festlegten, in welcher
Richtung Daten verarbeitet werden sollen
( Eingabe, oder Ausgabe) . Dies soll uns
nun für die folgenden Themen als Grund- lage dienen. Kommen wir zum ersten Kernthema, der Joystickabfrage:
Wahrscheinlich ist dies für Sie nichts
neues mehr; Sie haben sicherlich schon
einmal so etwas programmiert, jedoch
werden Sie jetzt vielleicht auch die
hardwaremäßigen Hintergründe verstehen.
Desweiteren will ich Ihnen hiermit auch
die Joyports und ihre Verbindungen zu
den CIAs und anderen Bausteinen innerhalb unseres " Brotkastens" erläutern.
Zunächst einmal können wir CIA2 für dieses Thema ausklammern, weil nämlich ausschließlich die CIA1 für die Bedienung
der Joyports verantwortlich ist. An jedem der beiden Ports sind jeweils fünf
Leitungen ( für die 4 Joystickrichtungen
und den Feuerknopf) mit entsprechenden
Portleitungen der CIA1 verbunden. Für
den Joyport1 sind das die Leitungen
PB0- PB4, für Joyport2 die Leitungen
PA0- PA4 . Desweiteren sind pro Joyport
auch noch weitere Signale zu finden, die ich Ihnen in der folgenden Grafik einmal
aufführen möchte:
Hier Grafik 1 . . .
Die eigentliche Joystickabfrage gestaltet sich nun als sehr einfach. Wir müssen lediglich darauf achten, daß die
Bits 0-4 eines Ports auf " Eingang" geschaltet sind. Da wir in der Regel
nichts ausgeben, genügt es also, wenn
wir einfach eine 0(= alle Pins auf " Eingang") in das entsprechende Datenrichtungsregister schreiben. Richtiger wäre
natürlich der Wert 224(= bin.11100000), da wir damit nur die Bits 0-4 als Eingang setzen. Die Bits 5-7 sind Ausgang.
Wie SIE es nun letztendlich handhaben
ist Ihre Sache, nur müssen Sie darauf
achten, daß die Werte, die wir in den
beiden Fällen lesen verschieden voneinander sind ( bei Datenrichtungsregister=0--> Bits 5-7 immer gesetzt, bei DDR
=224, Bits 5-7 abhängig vom Wert im
Datenregister) . Betrachtet man also 8- Bit-Werte beim Auslesen, sollte man sich
darüber im Klaren sein, welchen Wert die
drei unbenutzten Bits haben.
Nun brauchen Sie einfach nur einen Wert
aus dem entsprechenden Datenregister
auszulesen und müssen prüfen, ob, und
wenn ja, welche der Bits gelöscht sind.
Sie erinnern sich ja vielleicht daran, daß die Signale der Datenports invertiert werden, was bedeutet, daß bei keiner Joystickbewegung alle Bits auf 1 sind ( keine Signale liegen an) . Wird der
Joystick bewegt, so legt er ein Signal
an die entsprechende Portleitung an.
Dieses erscheint für uns im Datenregister als ein 0- Bit!
Hier möchte ich Ihnen noch einmal eine
Öbersicht über die Zuständigkeiten der
einzelnen Bits geben, damit Sie auch wissen, welches Bit gelöscht ist, wenn
der Joystick in welche Richtung gedrückt
wird:
Richtung Joy1 Joy2
oben PB0 PA0 unten PB1 PA1 links PB2 PA2 rechts PB3 PA3 Knopf PB4 PA4
Natürlich ist es auch möglich, mehrere
Richtungen gleichzeitig abzufragen. Wird
der Joystick z. B. nach rechts oben
gedrückt, so sind die Portleitungen für
" oben" und " rechts"( PB0 und PB3 für
Joyport1, bzw. PA0 und PA3 für Joyport2) auf 1, d. h. die Bits 0 und 3 im jeweiligen Datenregister sind gelöscht! Achten
Sie für solche Fälle also immer darauf, daß Ihre Joystickabfrage dynamisch ist
und mehrere Richtungen auch erkennen kann. Ich möchte da mit gutem Beispiel
vorangehen und habe Ihnen einmal eine
Abfrage des Joyport1 als Beipspielprogramm programmiert. Sie finden es, wie
immer, als Source-Code ( mit " . SRC"- Extension) und als ausführbares Maschinenprogramm ( mit " . OBJ"- Extension) unter
dem Namen " JOYTEST" auf dieser MD.
Ich habe deshalb den Joyport1 gewählt, weil wir bei ihm das Datenrichtungsregister nicht zu ändern brauchen. Er läuft
ja über Port B der CIA1) und wie wir aus
dem letzten Kursteil wissen, ist dieser
schon vom Betriebssystem her komplett
auf " Eingang" geschaltet. Natürlich können Sie das auch bei Joyport2 tun ( erscheint in Port A der CIA1), jedoch müssen Sie dabei berücksichtigen, daß Sie
ihn nach der Abfrage wieder auf " Ausgang" schalten, weil sonst die Tastatur
nicht mehr ansprechbar ist!
Kommen wir nun aber zu dem Beipspielprogramm. Es steht ab $ c000(= dez.49152) und wird auch dort gestartet ( SYS49152) .
**************************************** start lda #01 Zeichenfarbe auf sta 646 "weiß" setzen.
loop1 lda #147 Code für " CLR" laden jsr $ ffd2 und ausgeben.
lda $dc01 Datenport B laden. lsr "oben"-Bit in Carry. bcs l1 Gesetzt, also nix "oben". jsr pup Gelöscht, also "oben" ausgeben. l1 lsr "unten"-Bit in Carry. bcs l2 Gesetzt, also nix "un- ten". jsr pdown Gelöscht, also "unten" ausgeben.
l2 lsr " links"- Bit in Carry.
bcs l3 Gesetzt, also nix
"links". jsr pleft Gelöscht, also "links" ausgeben. l3 lsr "rechts"-Bit in Carry. bcs l4 Gesetzt, also nix "rechts". jsr prigh Gelöscht, also "rechts" ausgeben. l4 lsr "Knopf"-Bit in Carry. bcs l6 Gesetzt, kein Knopf gedrückt. lda #02 Sonst, Farbe "rot" in Akku. bne l5 Unbedingt verzweigen...
l6 lda #00 Kein Knopf, also Farbe " schwarz" in Akku.
l5 sta $ d020 und Bildschirmfarbe sta $ d021 setzen.
jmp loop1 Schleife wiederholen ****************************************
JOYTEST tut nun nichts anderes, als eine
Schleife zu durchlaufen, die zunächst
den Bildschirm löscht, dann prüft, welche der Joystickrichtungen gedrückt sind
und den dazu passenden Text " oben"," unten"," links" oder " rechts" ausgibt.
Zusätzlich berücksichtigt sie dabei, daß
zwei Richtungen gleichzeitig aktiv sein
können und gibt auch dementsprechende
Texte aus (" oben links"," unten rechts", etc. . .) . Wird der Feuerknopf gedrückt, so erscheint der ganze Bildschirm rot.
Beachten Sie bitte, daß auch in diesem
Fall alle 8 Joystickrichtungen abgefragt
werden!
Die Funktionsweise der Routine ist so
simpel, daß sie keiner großen Erklärung
bedarf. Am Anfang der Schleife wird der
Inhalt des Datenports B in den Akku geholt und nun Bit für Bit nach rechts
herausgeschoben. Dabei gelangen nacheinander die fünf Joystick-Bits in das Carrybit, von wo aus man prüfen kann, ob
die einzelnen Bits nun gesetzt, oder
gelöscht sind. Ist eines der Bits
gelöscht, so wird eine entsprechende
Routine aufgerufen, die einen passenden
Text ausgibt (" pup"," pdown"," pleft"," prigh") . Diese Routinen habe ich hier
nicht aufgeführt, jedoch können Sie sie
sich im Source-Code ja einmal anschauen.
Soviel zur Joystickabfrage. Wie Sie jedoch sicherlich in der Grafik von oben
gesehen haben, gibt es noch weitere
Anschlüsse am Joyport, über die man gewisse Geräte betreiben kann. Diese sind
zum einen die Paddles und zum anderen
der Light-Pen. Von beiden Eingabegeräten
haben Sie bestimmt schon einmal gehört.
Ich möchte Ihnen nun kurz erläutern, wie
sie Funktionieren und wie man sie abfragen kann.
Ein Paddle, ist ein Eingabegerät, bei dem prinzipiell nur zwei Werte übertragen werden, nämlich die Xund die Y-Position eines Grafikcursors. Sicherlich
einnern Sie sich noch an diese kleinen
" Magic-Tables", wie man sie als Kind oft
gehabt hat. Mit Hilfe zweier Drehknöpfe
konnte man da auf einen grauen Glasschirm malen, wobei man mit dem einen
Knopf den Zeichenstift nach links und
rechts bewegte, mit dem anderen nach
oben und unten. So in etwa kann man sich
auch Paddles vorstellen, wobei ein Paddle einem der beiden Drehknöpfe entspricht. Grundsätzlich können ( oder müssen) also ZWEI Paddles an EINEM Joyport
angeschlossen werden, wobei diese aufgeteilt werden in Xund Y-Richtungspaddle
( Pin 9 und 5 am Joyport) . Wie kann man
nun aber eine Position von nur einem Pin
ablesen? Nun das ist ganz pfiffig: ein
Paddle ist nämlich nichts anderes als
ein Potentiometer, wie man es aus der
Elektronik kennt. Also ein Stufenlos
verstellbarer elektrischer Widerstand,
der je nach Drehrichtung größer oder
kleiner wird. Er wird über das Potentiometer in den Eingang an Pin 5 oder 9 eingeleitet. Diese Pins sind nun mit dem
SID, dem Soundchip des 64 ers, verbunden, der über zwei Analog/ Digital-Wandler
verfügt. Da Widerstand eine analoge Größe ist ( er kann unendlich fein aufgelöst
werden), muß er zur Verarbeitung mit dem
Rechner erst in einen digitalen Wert
gewandelt werden, was über jene A/ D-Wandler geschieht. Sie haben je eine
Auflösung von 8 Bit und legen den digitalen Wert in den Registern 25 und 26( Adressen 54297 und 54298) des SID ab.
Durch Auslesen der Werte dieser Register
erhalten wir einen Digitalwert des Widerstandes des Potentiometers, wobei wir
256 verschiedene " Positionen" unterscheiden können. Zu beachten ist jedoch, daß die A/ D-Wandler des SID nur einen
bestimmten Bereich abtasten können, nämlich von 200 Ohm ( Wert 0) bis 200000 Ohm
(=200 Kiloohm, Wert 255) .
Zum Lightpen gibt es nicht viel zu sagen. Er kann ja ebenfalls am Joyport
angeschlossen werden, wobei dies ausschließlich nur bei Joyport2 der Fall
ist. Pin 6 dieses Ports, an dem normalerweise der Joystickfeuerknopf hängt, ist für den Lightpen zuständig. Zur Abfrage eines Lightpens sollte man aber
gewisse Grundkenntnisse über den VIC und
den Bildschirmaufbau ansich haben.
Ein Lightpen ist im Prinzip nichts anderes, als eine einfache und schlichte
Fotozelle, wie man sie im Fachhandel für
wenig Geld erstehen kann. Sie ist in der
Lage, Licht, das auf sie einfällt zu
registrieren und in diesem Fall einen
Strom zu erzeugen. Dieser Strom nun wird
an Pin 6 von Joyport2 angelegt und veranlaßt somit ein Löschen des Bits 4 vom
Datenport A ( dasselbe Bit, wie für den
Feuerknopf) . Dieses Ereignis tritt genau
dann ein, wenn der Rasterstrahl des Mo- nitors ganz genau an der Stelle des
Bildschirms vorbeifährt, an dem der
Lightpen positioniert ist. In dem Fall
muß nun ein pfiffiges Programm feststellen, an genau welcher Position sich der
Rasterstrahl nun befindet um die Position des Lightpens zu ermitteln. Dabei
kann einem der VIC helfen, der ein solches Lightpen-Signal als Interruptquelle
vorgesehen hat, jedoch müssen Sie
berücksichtigen, daß Sie über den VIC
lediglich die aktuelle Rasterzeile, nicht aber die Rasterspalte abfragen
können. Die muß man kleinlich berechnen, was nur geht, wenn man weiß, wie lange
es dauert, bis der Rasterstrahl eine
Zeile gezeichnet hat, und vor allen Dingen wann er damit begonnen hat. Da das
alles sehr schnell geht (25 Mal pro Sekunde läuft der Rasterstrahl über den
GESAMTEN Bildschirm), bekommt man meist
sehr ungenaue Ergebnisse, was ein hinund herspringen des Grafikcursors bewirkt.
Obwohl einige Lightpens für den 64 er
schon auf dem Markt waren, hat sich diese Eingabeart auf unserem Rechner wohl
nie so richtig durchgesetzt. Schade eigentlich, aber wenn Sie wollen, können
Sie es ja einmal versuchen, das nötige
Wissen dazu sollten Sie jetzt ja haben.
Wie man mit Raster-Interrupts richtig
umgeht, sollte Ihnen mein Kollege Ivo
Herzeg, dessen Kurs vor diesem hier
lief, hinreichend erklärt haben.
In diesem Sinne möchte ich mich nun wieder von Ihnen verabschieden. Nächsten
Monat geht es, ab dann wieder in gewohnter Länge, um eine Mausabfrage. Ich habe
Ihnen als Leckerbissen ein Programm vorbereitet, mit dem Sie eine AMIGA-Maus am
64 er anschließen und betreiben können.
Desweiteren wollen wir uns dann auch
noch ein wenig mit dem Userport befassen. Bis dahin Servus,
Ihr Uli Basters ( ub) age. Wie Sie jedoch sicherlich in der Grafik von oben
gesehen haben, gibt es noch weitere
Anschl
CIA-Kurs (Teil 9) "Die Geheimnisse des Secret Service..."
Hallo zusammen zum 9 . Teil des CIA-Kurses. Wie schon versprochen, wollen
wir uns heute mit einer " echten" Mausabfrage befassen.
Die Maus - sicher haben Sie schon vieles
von diesem Eingabegerät gehört, durch
das die einfache und komfortable Benutzung eines Computers erst richtig möglich wurde. Die " neuen" Rechner wie
AMIGA, ATARI ST, oder der MAC werden
standardmässig mit diesen kleinen viereckigen Kästchen ausgeliefert. Auch das
vielgerühmte C64- System GEOS prahlt damit, über Maus-Steuerung benutzbar zu
sein. Doch hier wollen wir gleich einmal
eine Unterscheidung machen:
1) Die Maussteuerung von GEOS ist nichts
besonderes. Für GEOS ist die Maus nichts anderes als ein " getarnter" Joystick. Bewegt man sie, so werden
absolut dieselben Signale erzeugt, wie der Joystick sie liefert, womit
GEOS-Mäuse eigentlich unbrauchbar
sind, da sie nicht die Vorteile von
" echten" Mäusen bieten.
2) Die " Echten", wie sie an den oben
genannten Rechnern zu finden sind, sind nämlich nur deshalb so gut, weil
sie jede Bewegung, die die Hand des
Benutzers vollzieht absolut naturgetreu wiedergeben. Das heißt im Klartext, daß wenn die Maus nur um ein
paar Millimeter bewegt wird, sich
auch der Mauscursor nur um ein paar
Pixel über den Bildschirm bewegt;
wird die Maus jedoch um mehrere Zentimeter bewegt, so bewegt sich auch
der Mauscursor um eine äquivalent
größere Anzahl von Pixeln weiter.
Damit wird es also möglich mit Hilfe
einer passenden Maus Bilder naturgetreu abzuzeichnen.
Desweiteren bewegt sich der Mauscursor auch immer mit der Geschwindigkeit, mit der die Hand die Maus bewegt. Wenn Sie die Maus schnell bewegen, so bewegt sich der Mauscursor
auch schnell; und ebenso bewegt er
sich langsam, wenn die Maus langsam
bewegt wird. Eine " falsche" Maus bewegt sich immer nur so schnell, wie
sie abgefragt wird - egal wieviel
Meter Sie sie " über den Tisch ziehen" .
Uns soll es hier nun um den zweiten Typ
von Mäusen gehen. Um eine Abfrage zu
programmieren, sollten Sie zunächst einmal wissen, wie eine " echte" Maus funktioniert.
Dazu möchte ich Ihnen nun erklären, welche " Hardware" in einer Maus so drinsteckt:
Jede Maus verfügt an der Unterseite über
eine, aus dem Gehäuse herausschauende, Stahlkugel, die zur besseren Haftung mit einer Gummischicht überzogen ist. Diese
Kugel kann man über eine Klappe zu Reinigungszwecken entfernen. Sieht man nun
in die Ausparung hinein, so erkennt man
dort zwei kleine Walzen, auf die die
Bewegungen der Maus übertragen werden.
Die Walzen stehen in einem rechten Winken zueinander, so daß die horizontale
und die vertikale Richtung der Maus erfasst wird. Je nach dem, wie stark die
Maus nun in eine Richtung bewegt wird, drehen sich auch die Walzen mehr oder
weniger schnell, wobei bei Schrägbewegungen die Bewegung durch die Stahlkugel
immer in die horizontale und die vertikale Bewegungsrichtung aufgespalten
wird.
An jeder der Achsen der beiden Walzen
sind nun kleine Lochscheiben angebracht, die sich mit der Walze drehen. An jeder
Lochscheibe wird über eine Lichtschranke
festgestellt, ob die Maus in einer der
beiden Bewegungsachsen bewegt wird. Jedesmal, wenn ein Loch in der Scheibe an der Lichtschranke vorbeifährt, wird ein
Impuls an den Rechner gegeben, der so
erkennt, daß die Maus in der entsprechenden Bewegungsachse bewegt wurde. Je
höher die Frequenz dieser Impulse ist, desto schneller war die Bewegung.
Nun wissen wir also, daß eine Bewegung
stattfand, nicht aber, in welche Richtung bewegt wurde, weil die Impulse ja
in beiden möglichen Richtungen dieselben
sind. Um nun die richtige Richtung herauszufinden, befindet sich noch eine
zweite Lichtschranke an jeder Lochscheibe. Beide Schranken sind um ein halbes
Loch voneinander versetzt, so daß immer
eine der beiden Schranken vor der anderen ein Signal liefert. Je nach dem, welche der Schranken zuerst einen Impuls
gibt, wird die Maus in die eine, oder in
die andere Richtung bewegt ( bei der horizontalen Achse nach links oder rechts, bei der vertikalen Achse nach oben oder
unten) .
Zur besseren Erläuterung hier einmal
eine Grafik. Das Signal " H" ist das Signal der ersten Lichtschranke." H" steht
für " Horitontal Pulse" und zeigt dem
Rechner, daß überhaupt eine Bewegung in
der Horizontalen Achse geschieht. Die
zweite Lichtschranke erzeugt das " HQ"- Signal, was für " Horizontal Quadrature
Pulse" steht. Durch dieses Signal kann
die Bewegungsrichtung festgestellt werden. Ich habe mich bei der Grafik auf
die horizontale Bewegungsachse
beschränkt. Bei der vertikalen Achse ist
der Ablauf jedoch derselbe, nur daß hier
die Signale " V" und " VQ" heißen.
Zunächst habe ich Ihnen den Aufbau der
Mausmechanik einmal aufgezeichnet. Sie
sehen, wie durch die Versetzung um ein
halbes Loch, die zweite Lichtschranke, die das HQ-Signal erzeugt, in dieser
Position der Lochscheibe noch keinen
Impuls liefert, während die erste Lichtschranke schon aktiv ist. Beachten Sie
bitte auch die Oszillator-Darstellung der Signale im unteren Bereich des Bildes.
( Anm. d. Red. : Bitte wählen Sie jetzt
den nächsten Kursteil aus dem Menu!)
Soviel also zum Aufbau der Mausmechanik.
Nun wollen wir uns einmal überlegen, was
wir zu einer Mausabfrage beachten müssen. Zunächst einmal wollen wir die einzelnen Zustände der Maussignale untersuchen um herauszufinden, in welche Richtung die Maus bewegt wird. Hierbei
beschränke ich mich wieder auf die Horizontalbewegung, da der Ablauf für die
Vertikalbewegung identisch ist.
Betrachtet man einmal die Oszillatorkurven in der Grafik oben und " übersetzt" man sie in Bitmuster (0 für " low",1 für
" High"), dann erhält man für die einzelnen Bewegungen folgende Bilder:
Linksbewegung : H - ...0011 0011... HQ - 0110 0110
Rechtsbewegung: H - . . .01100110 . . .
HQ -00110011
Nach jeweils 4 Signalen wiederholt sich
das Ganze immer wieder. Wie Sie sehen
kommen in beiden Bewegungen die H/ HQ-Kombinationen 00,01,10 und 11 vor. Die
Unterschiede der beiden Signale, an denen wir dann die Richtung erkennen können sind folgende:
1) Ist die Bedingung H= HQ=0 erfüllt, und
sind sind folgenden Signale H=0 und
HQ=1, so fand eine Bewegung nach
LINKS statt.
2) Ist die Bedingung H= HQ=0 erfüllt, und
sind die folgenden Signale H=1 und
HQ=0( also genau umgekehrt), so fand
eine Bewegung nach RECHTS statt.
3) Ist die Bedingung H= HQ=1 erfüllt, und
sind die folgenden Signale H=1 und
HQ=0, so fand eine Bewegung nach
LINKS statt.
4) Ist die Bedingung H= HQ=1 erfüllt, und
sind die folgenden Signale H=0 und
HQ=1, dann fand eine Bewegung nach
RECHTS statt.
Zur genauen Bestimmung der Richtung müssen wir also ZWEI zeitlich voneinander
versetzte Signale GLEICHZEITIG auswerten! Das kann unter Umständen sehr kompliziert werden, da mehrere Vergleiche
ineinander verschachtelt werden müssen, um die genaue Richtung herauszufinden.
Aus diesem Grund habe ich eine sinnvolle
Vereinfachung durchgeführt, die durch
folgende Grafik erklärt werden soll:
Durch eine Exklusiv-Oder- Verknüpfung der
Signale OldH ( entspricht dem alten Wert
von H, wobei H= HQ war) und H erhält man
folgende Aussagetabelle:
OldH H Ergebnis
0 0 0 0 1 1 1 0 1 1 1 0
Vergleichen wir nun diese Werte mit den
oben aufgezählten Richtungsbedingungen, so sieht man, daß das Ergebnis immer
dann 0 wird, wenn eine Linksbewegung
durchgeführt werden muß, und ebenso daß
es immer 1 wird, wenn eine Rechtsbewegung durchzuführen ist. Dadurch haben
wir uns eine Menge Vergleichsbefehle
gespart. Wir müssen jetzt lediglich das
Ergebnis der EOR-Operation prüfen, um
die Bewegungsrichtung zu bestimmen. Ent- sprechend wird mit dem Vertikal-Signal
verfahren.
Kommen wir nun zu der fertigen Steuerroutine. Hierbei müssen wir auf vier
wichtige Dinge achten:
1) Das Programm zur Mausabfrage MUSS in
jedem Fall in der Hauptschleife des
Rechners laufen, da sich die Signale
der Maus jederzeit ändern können.
Eine zyklische Abfrage aus dem IRQ, so wie das z. B. beim Joystick möglich
ist, wäre also undenkbar, da sie unter Umständen das eine oder andere
Signal verpassen würde und somit die
Interpretation desselben von unserer
Mausabfrage falsch sein könnte. Die
Maus würde ruckeln und sich gegebenenfalls sogar in die umgekehrte
Richtung bewegen.
2) Desweiteren sollte ich Sie darauf
aufmerksam machen, daß die Tastatur
in der Zeit, in der die Maus ange- schlossen ist nicht zu benutzen ist, da sie IMMER ein Signal sendet ( es
sei denn V, VQ, H und HQ sind zufällig gerade alle auf 0) und somit die
Tastaturabfrage stört ( wie wir wissen
benutzt diese ja die selben Datenregister der CIA wie die Joyports) .
Wenn Sie also das Beispielprogram
" AMIGA-MAUS1" auf dieser MD ausprobieren wollen, sollten Sie immer daran denken, das Programm ZUERST zu
laden und mit RUN zu starten und dann
erst die Maus einzustecken!
3) Durch die ständige Abfrage müssen wir
ebenfalls daran denken, das Maussignal zu " entprellen" . Das heißt, daß
wir prüfen müssen, ob die Maus nun
gerade bewegt wird oder nicht. In
jedem Fall werden wir nämlich ein und
dasselbe Signal mehrmals lesen. Für
uns sind jedoch immer nur die Signaländerungen von Bedeutung, weshalb
wir prüfen müssen, ob das neu gelesene Signal nun gleich, oder verschie- den vom letzten Signal ist. Bei
Gleichheit wird der Wert einfach ignoriert und wieder zur Leseschleife
zurückverzweigt.
4) Als Letztes will ich Ihnen nun noch
die Belegung der Maussignale erklären. Die Maus hat ja, wie der Joysick auch, einen 9- poligen Gameportstecker, dessen Signale sich glücklicherweise so auf die 9 Pins verteilen, daß wir sie ( fast) alle über die
CIA abfragen können. Die Belegung ist
hierbei wiefolgt:
Pin Funktion
1 V-Impluse 2 H-Impulse 3 VQ-Impulse 4 HQ-Impulse 5 Knopf 3 (unbenutzt, da nicht vor- handen) 6 Knopf 1 (links) 7 +5 Volt (Betriebsspannung) 8 GND (Masse) 9 Knopf 2 (rechts)
Die Pins 1-4 und 6 der Joyports sind
ja, wie Sie aus dem letzten Kursteil
noch wissen sollten, mit den Bits 0-4 der Datenports der CIA1 verbunden.
Demnach können wir also problemlos
die Richtung der Maus abfragen, sowie
die linke Maustaste. Die rechte Maustaste hängt an einem der A/ D-Wandler
des SID und soll uns deshalb hier
nicht interessieren.
Der Einfachheit halber habe ich die
Abfrage über Joyport2 programmiert, die Signale der Maus erscheinen also
im Datenregister A der CIA1( Reg.0- Adresse $ DC00) . Dies ist deshalb einfacher, weil dieser Port schon von
der Tastaturabfrage her auf " Eingang" geschaltet ist.
Die benötigten Signale finden wir an
folgenden Bitpositionen im Datenregister A:
Bit Signal
0 V 1 H 2 VQ 3 HQ 4 Linke Maustaste
Kommen wir nun zu unserem Beispielprogramm " AMIGA-MAUS1", das zusammen mit
seinem Quellcode (" AMIGA-MAUS1 . SRC") auf
dieser MD zu finden ist. Starten Sie es
bitte mit RUN und schließen Sie dann
eine orginal AMIGA-Maus am Joyport2 an
um den Mauspfeil über den Bildschirm zu
bewegen. Mit der linken Maustaste wird
das Programm abgebrochen.
Hier nun die Dokumentation des Programms:
**************************************** start lda #00 Bildschirm- sta 53280 farben lda #11 auf schwarz/grau sta 53281 setzen. lda #<(text) Text ab Label ldy #>(text) "TEXT" jsr strout ausgeben. lda #01 Spritefarbe auf sta vic+39 weiß setzen. lda #150 Sprite in den sta vic Bildschirm sta vic+1 positionieren. lda #36 Sprite-Pointer sta 2040 setzen. lda #01 Und Sprite 0 sta vic+21 einschalten **************************************** loop2 lda $dc00 Joyport2 lesen eor #$ff und invertieren and #$1f Bits 0-4 isolie- ren cmp #16 Vergleiche mit 1 bcc l1 Wenn kleiner, dann Bewegung rts Sonst wurde Knop gedrückt, also Ende *** l1 ldx #00 Alle stx h Puls- stx hq Register stx v werden stx vq gelöscht lsr V-Bit in V-Reg rol v hineinrollen lsr H-Bit in H-Reg rol h hineinrollen lsr VQ-Bit in VQ-Reg rol vq hineinrollen lsr HQ-Bit in HQ-Reg rol hq hineinrollen **************************************** horizon lda h H mit HQ eor hq verknüpfen cmp hmem und prüfen, ob gleich altem Wer beq vertical Ja, also entprel len sta hmem Sonst merken und cmp #00 prüfen, ob =0 bne l2 Nein, also Bewe- gung ausführen lda h Sonst war H=HQ, deshalb Bitmuste sta oldh in OLDH merken jmp vertical und weiter *** l2 lda oldh OLDH und H verknüfpfen, eor h um die Richtung herauszufinden bne l7 =1, also Rechts! jmp moveleft =0, also Links! l7 jmp moveright **************************************** vertical lda v V mit VQ eor vq verknüpfen cmp vmem und prüfen, ob gleich altem Wer beq loop2 Ja, also entprel len sta vmem Sonst merken und cmp #00 prüfen, ob =0 bne l8 Nein, also Bewe- gung ausführen lda v Sonst war V=VQ, deshalb Bitmuste sta oldv in OLDV merken jmp loop2 und zurück *** l8 lda oldv OLDV und V verknüpfen eor v um die Richtung herauszufinden bne l9 =1, also Hoch! jmp moveup =0, also Runter! l9 jmp movedown ****************************************
Zum besseren Verständnis noch einig
Erklärungen zum Source-Code:
1) Die Routinen MOVELEFT, MOVERIGHT, MO VEUP und MOVEDOWN sind Unterroutinen
die das Sprite 0, das den Mauspfei
repräsentiert, entprechend der gefor
derten Richtung bewegen. Ich habe si
hier nicht aufgeführt, da sie nicht
mit der CIA zu tun haben. Sie könne
Sie sich jedoch im Source-Code vo
" AMIGA-MAUS1" ansehen.
2) Die Labels H, HQ, V, VQ, OLDH, OLDV
HMEM, und VMEM stehen für Speicherzel
len, in denen bestimmte Werte zwi
schengespeichert werden.
3) Beim Vergleich, ob H gleich HQ ( bzw.
gleich VQ) ist habe ich ebenfalls di
EOR-Verknüfung benutzt, da so das Ent
prellen der Maussignale vereinfach
wird. HMEM ( bzw. VMEM) ändern sic
immer abwechselnd von 0 auf 1 und um
gekehrt.
4) Ab dem Label " TEXT" steht der Text
der beim Aufruf von " AMIGA-MAUS1" aus
gedruckt wird.
5) Beachten Sie bitte, daß die Eingangs
werte immer invertiert werden, da de
CIA sie uns invertiert im Datenregi ster angibt ( sollten Sie noch vo
letztem Kursteil her wissen) Desweiteren wird der Wert dann noc
AND-Verknüpft um die Bits 0-4 zu iso
lieren. Dadurch wird es möglich di
Maustaste durch einen einfachen Ver
gleich mit dem Wert 16 abzufragen
Sonst würde das Programm nämlich auc
bei dem Druck auf eine Taste, die di
Bits über dem 4 . Bit setzt abbrechen!
Das war es dann mal wieder für diese
Monat. Behalten Sie die AMIGA-Maus, je
doch noch bis nächsten Monat. Wir werde
uns dann um den Userport kümmern und ein
Mausabfrage programmieren, die die Nach
teile der oben aufgeführten ( nämlich da
die Tastatur unbenutzbar ist, und da
nicht aus dem Interrupt heraus abgefrag
werden kann) verbessert.
Bis dahin, wünsche ich ein " funny Mou
sing-Around",
Ihr Uli Basters (ub
CIA-Kurs (Teil 10) "Die Geheimnisse des Secret Service..."
Herzlich willkommen zum 10 . und letzten
Teil dieses Kurses. Wir wollen uns heute
weiterhin mit der Mausabfrage beschäftigen, wobei wir in Zusammenhang mit dem
Userport - einem der wichtigsten Themen, wenn es um die CIA geht - die Abfrage
von letztem Monat noch verbessern wollen. Also los. . .
Wie Sie sich bestimmt noch erinnern, hatten wir in der letzten MD eine Mausabfrage für eine AMIGA-Maus am Joyport
programmiert. Hardwaremäßig war das auch
am einfachsten, da wir die Maus direkt
an den 64 er anschließen konnten. Doch es
ergaben sich aber auch diverse Nachteile:
1) Solange die Maus angeschlossen war,
war es uns nicht möglich die Tastatur
zu benutzen.
2) Eine Abfrage aus dem Interrupt war
nicht möglich, da die Maussignale
ständig überwacht und ausgewertet
werden mußten.
3) Die rechte Maustaste konnte leider
nicht Abgefragt werden.
Diese drei Nachteile wollen wir nun elegant beseitigen, indem wir die Maus
nicht am Joyport, sondern am Userport
anschließen. Sicher haben Sie schon einmal von diesem Anschluß Gebrauch gemacht, ist er doch der vielseitigste von
allen Anschlüssen am C64 . Drucker, Modems, Eprommer, oder Digitizer werden
über ihn angeschlossen und bedient - kurzum, ohne ihn wäre der 64 er nicht das
was er ist!
Deshalb soll er uns nun interessieren.
Am Userport liegt eine Vielzahl von Signalen und internen Leitungen an, die es
uns ermöglichen, direkt in die Hardware unseres Rechners eingreifen zu können.
Und gerade weil fast alle Leitungen des
Userports mit den beiden CIAs etwas zu
tun haben, passt er hervorragend in diesen Kurs.
Kommen wir also zum Aufbau, dieser Unscheinbaren Schnittstelle, an der Rückseite unseres " kleinen Brotkastens" .
Insgesamt 24 Leitungen sind dort herausgeführt, die alle eine bestimmte Bedeutung haben. Hierzu gibt es jetzt erst
einmal eine Grafik:
( Anm. d. Red. : Bitte wählen Sie jetzt den
2 . Teil des CIA-Kurses 10 aus dem Menu.)
Ich möchte Ihnen nun die einzelnen Signale genauer erklären:
* Wie Sie sehen ist Masse ( GND) viermal
am Userport zu finden nämlich an den
Pins 1,12, A und N. Auf diese Weise ist
sichergestellt, daß man immer an den
äußeren Pins den Gegenpol einer
Gleichspannung findet.
* Die oben genannte Gleichspannung sind
wohl in der Regel die +5 Volt an Pin
2, die man als Betriebsspannung für
Hardwareerweitungen benutzen kann. Die
dort herausgeführte Leitung ist mit
maximal 100 mA belastbar.
* Pin 3 enthält die RESET-Leitung des
Rechners. Durch sie kann der gesamte
C64 wieder in den Einschaltzustand
zurückversetzt werden. Da das Signal
invertiert ist ( Strich über dem Wort
" RESET"), muß kein Pegel angelegt werden (+5 V), um den Reset auszulösen, sonden man muß diesen Pin mit Masse
verbinden.
* Die Leitungen CNT1( Pin4) und CNT2
( Pin6) sind die CNT-Leitungen von CIA1 und CIA2 . Wir haben diese Leitungen
schon bei den Timerinterrupts kennengelernt, da man sie zur Timersteuerung
heranziehen kann. Sie spielen eine
große Rolle bei unserer Mausabfrage
nacher.
* Die Leitungen SP1( Pin 5) und SP2( Pin
7) sind die SP-Leitungen von CIA1 und
CIA2 . Diese Leitungen werden zur interaktiven seriellen Datenübertragung
benutzt und sollen uns ebenfalls nachher noch beschäftigen.
* Die Leitungen PC2( Pin 8) und FLAG2( Pin B) sind negierte Signale ( deshalb
auch der Strich über den beiden Worten) . Sie sind die Handshake-Leitungen
von CIA2 . Ein " Handshake" ist wichtig, um bei einer Datenübertragung eine
Verbindung korrekt aufzubauen. PC2 ist
dabei der Handshake-Ausgang, FLAG2 der
Handshake-Eingang.
* Die Leitung " ATN IN" ist ein hier
ebenfalls zu findendes Signal vom IEC-Bus, an dem die Floppy angeschlossen ist. Dieses Signal stammt von der
Portleitung 3, Port A ( PA3) der CIA2 .
* An den Pins 10 und 11 liegt eine Wechselspannung von 9 Volt an. Da es sich
um Wechselspannung handelt benötigt
man natürlich 2 Anschlüsse ( Wechselspannung wird im Gegensatz zu Gleichspannung nicht mit Masse am Gegenpol
angeschlossen) .
* Die Pins C-L entsprechen dem gesamten
Port B der CIA2 . Dadurch wird es möglich eine " echte" parallele Datenübertragung zu realisieren ( CENTRO-NICS), wie sie von vielen Druckern
verlangt wird. Gleichzeitig dienen
diese Leitungen dem Datenaustausch
verschiedenster Art zwischen dem C64 und einer angeschlossenen Hardware.
Auch wir werden diese Leitungen später
benutzen.
* Pin M beinhaltet ebenfalls eine Portleitung, nämlich das 2 . Bit von Port
A. Auch diese Leitung kann als I/ O- Leitung genutzt werden ( z. B. für
Steuersignale einer Parallelschnittstelle)
Soviel also zu den Leitungen am Userport. Wie Sie sehen bedient Haputsächlich die CIA2 den Datenverkehr an dieser
Schnittstelle. Von CIA1 finden wir nur
zwei Leitungen ( CNT1 und SP1) . Doch gerade diese Leitungen, zusammen mit den
äquivalenten Leitungen von CIA2( CNT2 und SP2) spielen bei unserer Mausabfrage
später noch eine große Rolle.
Kommen wir nun also zu der Abfrage
selbst. Wie versprochen, wollen wir ja
die drei oben genannten Nachteile unterbinden. Um es einmal langsam anzugehen
wollen wir zunächst einmal die Tastatur
wieder benutzbar machen. Wie Sie ja nun
wissen, sind die Portleitungen der CIA2 am Userport herausgeführt. Diese sind
glücklicherweise vom Betriebssystem ungenutzt, weshalb wir an diesen Leitungen getrost Signale einleiten können, ohne
dabei den die " normale" Rechnerumgebung
zu stören. Da die Tastaturabfrage von
CIA1 erledigt wird stören die am Userport angeschlossenen Maussignale sie
also in keinster Weise. Zudem können wir
haargenau dieselbe Abfrageroutine wie
beim letzten Mal benutzen, jedoch mit
dem Unterschied, daß sie sich nun die
Mausdaten aus Port B von CIA2 holen muß.
Wir müssen also lediglich einen Befehl
des Programms von letztem Monat ändern, nämlich das ehemalige " LDA $ DC00" in ein
LDA "$ DD01" und schon funktioniert die
Abfrage!
Zusätzlich brauchen wir jetzt natürlich
noch einen Adapterstecker, der jedoch
einfach nachzubauen ist. Alles was Sie
dazu brauchen ist:
1) Ein Userportstecker 2) Eine Joyportbuchse 3) Etwas Kabel 4) und ein kleines bisschen Erfahrung
mit dem Lötkolben.
Bis auf Punkt 4 ist alles für ein paar
Mark im Elektronikfachhandel erhältlich.
Wenn Sie alles zusammen haben, verbinden
Sie bitte Pins der beiden Stecker wiefolgt:
Joyport Userport Signal
1 --> C Vertical Pulse 2 --> D Horizontal Pulse 3 --> E Vertical Quadrature 4 --> F Horizontal Quadrature 6 --> H Linker Mausbutton 7 --> 2 Betriebsspannung +5V 8 --> 1 GND 9 --> J Rechter Mausbutton
Die Signalnamen sollten Sie noch aus
letztem Kursteil kennen.
Achten Sie beim Löten bitte darauf, daß Sie die richtige Symmetrie benutzen.
Beim Userport sind die Leitungen näm- lich, wenn man von vorne auf den STECKER
schaut genau spiegelverkehrt ( Pins 1-12 und A-N von rechts nach links) . Schauen
Sie hinten auf die Lötpins des Steckers, so stimmt die Belegung wieder, so wie
Sie in der obigen Grafik aufgeführt war.
Ähnliches gilt für die Joyportbuchse - wenn Sie von vorne auf sie draufsehen
ist Pin 1 links oben und Pin 9 rechts
unten. Von hinten ist Pin 1 rechts oben
und Pin 9 links unten. In der Regel
sollten aber beide Stecker auch mit Pinnummern versehen sein, so daß man keine
Fehler machen kann. Achten Sie aber bitte doch darauf und prüfen Sie vor dem
ersten Anschluß nocheinmal alle Pins
nach, da Sie sich bei einer falschen
Belegung durchaus den Rechner kaputt
machen können! ! ! Wir übernehmen in diesem Fall keinerlei Ersatzansprüche!
Worauf Sie ebenfalls achten sollten ist
das sie eine Joyport-BUCHSE benötigen - nicht etwa einen STECKER. Das heißt, daß
das Ding, welches Sie sich kaufen genau denen entsprechen muß, die an den Joyports des C64 herausschauen!
Wenn dann alles geklappt hat können Sie
Ihren Adapterstecker ausprobieren. Stekken Sie ihn am Userport, bei abgeschaltetem Rechner, ein ( bitte wieder darauf
achten, daß Sie ihn nicht verkehrt hineinstecken, da sonst derselbe Effekt
wie oben auftritt) und schließen Sie
eine orginal AMIGA-Maus an der anderen
Seite an. Wie Sie sehen, können Sie die
Tastatur immer noch ganz normal benutzen. Laden Sie nun das Programm " AMIGA-MAUS2" von dieser MD und starten Sie es
mit RUN. Sie können jetzt den Mauspfeil
über den Bildschirm bewegen. Das Programm kann mit der linken UND - im Gegensatz zu unserer alten Abfrage - mit
der rechten Maustaste abgebrochen werden. Damit hätten wir also schon einmal
zwei Nachteile beseitigt, nämlich der, daß die Tastatur unbenutzbar war und der
daß die rechte Maustaste nicht abgefragt werden konnte. Letztere hatten wir ja
beim Bau des Adaptersteckers mit Pin J
des Userports verbunden, womit ihr Signal an der Leitung PB5 von CIA2 anliegt.
Diese Leitung entspricht nun dem 5 . Bit
von PortB der CIA2, womit wir die Taste
problemlos abfragen könnten!
Falls es Sie interessiert - der Source-Code von " AMIGA-MAUS2" ist ebenfalls auf
dieser MD unter dem Namen " AMIGA-MAUS2 . SRC" enthalten. Er entspricht jedoch, abesehen von der oben beschiebenen
Änderung haargenau dem von " AMIGA-MAUS1" Nun gut - über den Userport haben wir
die Maus und die Tastatur vollständig
benutzbar gemacht, jedoch ist es schon
hinderlich, wenn die Abfrage immer in
der Hauptschleife des Rechners läuft. So
muß bei einem Programm, das mit der Maus
bedient wird, bei jeder einzelnen Routine, die etwas anderes tun soll als die
Maus abzufragen, die Abfrage unterbrochen werden. Und das ist verbunden mit hohem organisatorischem Aufwand.
Deshalb wollen wir nun versuchen, eine
Abfrage über Interrupts zu programmieren. Es IST möglich - ich habe mir da
eine pfiffige Routine für Sie ausgedacht.
Wie ich oben schon erwähnte sollen dabei
die Leitungen CNT1 und CNT2 am Userport
eine wichtige Rolle spielen. Vielleicht
erinnern Sie sich ja noch an die ersten
Teile des CIA-Kurses, in denen ich Ihnen
die Timerprogrammierung erklärte. Damals
hatten wir festgestellt, daß die Timer
der CIAs Interrupts auslösen können. Bei
CIA1 waren das IRQs, bei CIA2 die NMIs.
Dabei konnte man verschiedene Ereignisse
als Timertrigger einstellen. Im Regelfall war das der Systemtakt; bei der
Timerkopplung war es der Unterlauf von
Timer A. Und nun kommts: wir konnten
ebenso ein Signal an der CNT-Leitung
einer CIA als Timertrigger einstellen.
Und genau das ist der Punkt an dem wir
ansetzen wollen.
Genauer gesagt wird ein Timer, wenn er
die CNT-Leitung als Timertrigger programmiert hat, immer dann um 1 heruntergezählt, wenn an der CNT-Leitung eine
steigende Flanke anliegt. Wenn also der
Pegel an dieser Leitung gerade von 0 auf
1 umspringt. Danach nicht mehr, solange
die Leitung auch auf 1 liegen bleiben
sollte. Sie muß erst wieder auf 0 abfallen, damit der Timer durch eine neue
Flanke ein weiteres Mal erniedrigt werden kann. Vergleichen wir das einmal mit
den Signalen, die uns die Maus liefert
( letzten Monat hatte ich das ja genauer
erklärt), so stellen wir fest, daß die
Maussignale auch immer von 0 auf 1 wechseln, egal ob es nun das H-, HQ-, Voder VQ-Signal ist. Wir können also diese Signale als Timertrigger verwenden, und zwar so, daß der Timer immer alle
Flanken der Maus mitzählt. Dadurch können wir genau erfahren, um wieviele Einheiten die Maus bewegt wurde! Das einzige Problem dabei ist die Richtungsbe- stimmung, weil ja in beiden Richtungen
dieselben Signale erzeugt werden
( beschränkt man sich auf nur EIN Signal, z. B. das H-Signal) . Das heißt, daß wir
bei JEDER Bewegung, die stattfindet, auch das entsprechende Quadrature-Signal
überprüfen müssen um die genaue Richtung
bestimmen zu können. Das reine Zählen
der Impulse nutzt uns also nichts, dennoch reicht es zum Auslösen eines Interrupts bei jeder Bewegung.
Wenn wir einen Timer nämlich mit dem
Wert 0 initialisieren, genügt ein einziger Impuls von der CNT-Leitung, um einen
Interrupt auszulösen. Dieser muß nun
feststellen, welche Bewegungsrichtung
ausgeführt wurde. Der Witz ist, daß diese Abfrage sogar noch einfacher ist, als
die von der ersten und zweiten Version
von AMIGA-MAUS. Weil wir nämlich nicht
zwei zeitlich voneinander versetzte Signale überprüfen müssen. Zur besseren
Erläuterung will ich Ihnen nocheinmal
die Signalfolgen der Bewegungsrichtungen auflisten:
Linksbewegung : H - ...11001100... HQ - 10011001 ↑ ↑ Interrupt Rechtsbewegung: H - ...11001100... HQ - 01100110 ↑ ↑ Interrupt
Gehen wir nun davon aus, daß wir das
H-Signal an der CNT-Leitung anliegen
haben, und daß einer der Timer der dazugehörigen CIA diese Leitung als Trigger
hat und zudem mit 0 initialisiert ist, so wird jedesmal, wenn das H-Signal auf
1 springt ein Interrupt ausgelöst, weil
der Timer unterläuft. Diese Stellen habe
ich in obiger Auflistung markiert ("↑") .
Wenn Sie genauer hinsehen, so erkennen
Sie, daß das HQ-Signal bei einer Linksbewegung zum Zeitpunkt des Interrupts immer 1, bei einer Rechtsbewegung immer
0 ist. Um nun die Bewegungsrichtung zu
bestimmen brauchen wir lediglich das
HQ-Signal an einem der Porteingänge von
PortB am Userport anzuschließen und bei
Auftreten eines Interrupts auszulesen.
Ist es 0, so müssen wir den Mauspfeil
nach rechts bewegen, ist es 1, so muß
der Mauspfeil nach links. Wir brauchen
also noch nicht einmal das letzte Signal
zur Bestimmung heranzuziehen. Ebenso
wird übrigens mit den vertikalen Signalen verfahren. Hierbei liegt das V-Signal dann an der anderen CNT-Leitung
an. Für unser Beispiel habe ich die Belegungen wievolgt belegt:
* V-Signal an CNT1- löst also IRQs aus
( weil an CIA1) .
* H-Signal an CNT2- löst also NMIs aus
( weil an CIA2) .
* VQ und HQ wie bei Adapterstecker1
Bei den V-Signalen müssen wir noch etwas beachten: Da der Systeminterrupt über
einen Systemtaktgetriggerten Timer A
läuft, und wir diesen ja weiterbenutzen
möchten, müssen wir die Auswertung des
V-Signals über Timer B programmieren.
Wenn jetzt ein IRQ auftritt, so müssen
wir erst anhand der gesetzten Bits im
" Interrupt-Control- Register"( ICR) feststellen, welcher Timer der Interruptauslöser war. War es Timer A, so wird
auf den Systeminterrupt weiterverzweigt, war es Timer B, so wird auf die Abfrage
der Vertikalbewegung gesprungen.
Ähnlich verhält sich dies bei den H-Signalen. Weil wir die Maustasten ja
ebenfalls noch Abfragen wollen, müssen
wir den zweiten freien Timer der NMI-CIA
zyklische Interrupts auslösen lassen, die in regelmäßigen Abständen die Maustasten abfragen. Der Einfachheit halber
habe ich die Horizontalbewegungsabfrage
ebenfalls über Timer B der CIA2 programmiert, weshalb wir also Timer A zur Abfrage der Maustasten benutzen wollen.
Kommen wir nun zu dem Programm selbst.
Hier ist der kommentierte Source-Code:
**************************************** start sei IRQs sperren ldx #<(irq) IRQ-Vektor ldy #>(irq) auf neue stx $0314 IRQ-Routine sty $0315 verbiegen. ldx #<(nmi) NMI-Vektor ldy #>(nmi) auf neue stx $0318 NMI-Routine sty $0319 verbiegen.
ldx #$83 Timer A und B als stx cia1+13 Interruptquelle für stx cia2+13 beide CIAs setzen
lda #00 Timer B von sta cia1+6 CIA1 sta cia1+7 und sta cia2+6 CIA2 mit dem Wert sta cia2+7 0 initialisieren. ldy #$90 Timer A von CIA2 sta cia2+4 initialisieren sty cia2+5 ($9000=27 IRQs/s) lda #$21 Trigger=CNT und "T mer Start" sta cia1+15 in Control-Regist sta cia2+15 für Timer B von CIA1 und CIA2. lda #$81 Timer A von CIA2 sta cia2+14 mit SysTakt als Trigger starten lda #00 Bildschirm- sta 53280 farben lda #11 setzen sta 53281 und lda #<(text) Begrüßungs- ldy #>(text) Text jsr strout ausgeben. lda #01 Sprite 0 sta vic+39 als sta vic+21 Maus- lda #150 pfeil sta vic ini- sta vic+1 tiali- lda #41 sieren. sta 2040 cli IRQs freigeben rts und ENDE. **************************************** nmi pha Alle txa Prozessorregister pha erstmal tya auf Stapel pha retten. cli IRQs freigeben. lda $dd01 Datenport sporadis eor #$ff laden und invertie sta mem merken. lda cia2+13 ICR von CIA2 holen and #$02 Bit 1 isolieren beq buttons Wenn =0, dann war Timer A der NMI- Auslöser, also Knopfabfrage. lda mem Sonst H-Bewegung.. and #$08 Bit für HQ-Signal aus Datenport iso lieren beq moveleft Wenn 0 war ->links bne moveright Wenn 1 war ->rechs buttons lda mem Aus Datenport die and #$30 Bits 4 und 5 (Mau knöpfe) isolieren beq bye Wenn =0, dann war ner gedrückt. and #$20 Sonst Bit5 isolier beq leftone Wenn =0, war der linke gedrückt lda #42 Spritepointer ldx #01 und Timerwert lad l8 sta 2040 und setzen. stx cia1+6 (Timerwert in BEI stx cia2+6 CIAs!) jmp bye NMI beenden. leftone lda #41 Spritepointer und ldx #00 Timerwert für link jmp l8 Taste setzen **************************************** irq lda cia1+13 ICR von CIA1 laden and #$02 Bit 1 isolieren bne ok Wenn =1, dann wars ein Maus-IRQ jmp sysirq Sonst auf SysIRQ springen ok cli IRQs freigeben lda $dd01 Datenport laden und eor #$ff invertieren. and #$04 Bit für VQ-Signal isolieren beq moveup Wenn 0 war -> hoch bne movedown Wenn 1 war -> runter ****************************************
Hier nun noch einige Erläuterungen:
1) Wie Sie sehen, müssen wir bei NMIs den
Prozessorregister " von Hand" auf den
Stapel retten. Bei NMIs geschieht dies
nicht durch eine Routine im Betriebssystem, so wie es bei den IRQs der
Fall war.
2) Nachdem eine der beiden Interruptroutinen aufgerufen wurde, wird so
früh wie möglich das IRQ-Flag wieder
gelöscht und die IRQs zugelassen. Das
ist deshalb so wichtig, weil die IRQs
beim Auftreten eines Interrupts ( sei
das ein IRQ oder ein NMI) gesperrt
werde. Befindet sich nun aber der Rechner gerade in einem NMI, während die
Maus der Vertikalen bewegt wird, so
wird kein IRQ ausgelöst, weil dieser
ja noch gesperrt ist. Um das doch noch
zu ermöglichen müssen wir den IRQ explizit freigeben.
Bei NMIs brauchen wir darauf nicht zu
achten, weil NMIs ja nicht maskierbar
sind. Das heißt, daß ein NMI immer einen
NMI unterbrechen kann!
3) Denken Sie bitte nach wie vor daran
daß die Daten der Datenports invertiert in den CIA-Registern stehen. Wir
müssen sie beim Lesen deshalb gleich
nochmal invertieren. Das ist gerade bei
der Maustastenabfrage sehr wichtig, weshalb der Datenport in jedem Fall
erst einmal invertiert wird, bevor eine
Entscheidung getroffen wird, woher der
NMI überhaupt kam.
Bei der Vertikalabfrage, hätte ich den
Datenport nicht unbedingt invertieren
müssen. Da hier lediglich nur ein eimziges Bit abgefragt wird, hätte ich die
Branchbefehle zu den Bewegungsroutinen
ebenso vertauschen k nnen. Der Vollständigkeit halber wird hier der Wert
jedoch ebenfalls invertiert.
4) Sicher hat Sie die merkwürdige Mausbutton abfrage etwas verwirrt. Ich habe
hier noch eine kleine Funktion eingebaut die durch die Art unserer Abfrage
ganz einfach zu programmieren wurde.
Zunächst einmal wird durch den Druck
auf einen der Mausknöpfe der Spritpointer zwischen den Spriteblöcken 00 und 42 hinund hergeschaltet. Dies
nur, um Ihnen die Mausbuttonabfrage
optisch zu signalisieren. Zusätzlich
wird, je nach dem welchen Knopf sie
noch drücken, ein anderer Wert in die
Timer-Register geschrieben. Drücken Sie
die linke Taste, so ist das der Wert 0, wie wir es für die Abfrage ja schon
vereinbart hatten. Drücken Sie jedoch
die rechte Maustaste, so wird der Wert
1 in die Timer geladen. Durch diesen
kleinen, aber effektiven Trick können
wir die Mausauflösung halbieren. Dadurch, daß nun 2 Impulse von der Maus
kommen müssen, bis ein Interrupt auftritt müssen Sie die Maus doppelt wait über den Tisch bewegen, um den Mauspfeil eine bestimmte Strecke weg zu
bewegen. Das kann oftmals kanz nützlich
sein, z. B. wenn man genau zeichnen muß.
Wenn Sie brigens den Wert 2 die Timer
schreiben, so verkleinet sich die Auflösung um ein Drittel und so fort. . .
5) Achten Sie bitte auch auf die Abfragen
in den Interruptroutinen, von welchen
Quelle der Interrupt kam. Im ICR sind
dann n mlich die entsprechenden Bits
die auch beim Einstellen der Interrupquellen benutzt werden gesetzt. So können wir also unterscheiden, von wo ein
Interrupt kam.
6) Die Routinen MOVELEFT, MOVERIGHT, MOVE-UP und MOVEDOWN sind Routinen die den
Mauspfeil bewegen und sollen hier
nicht näher erläutert werden. Sie können Sie sich aber gerne im Source-Listing " AM GA-MAUS3 . SRC" auf dieser
MD anschauen
7) Das Label ENDIRQ enthüllt die Adresse
$ EA7 E. Ab dieser Adresse stehen im Betriebssystem die Befehle, mit denen
jeder Interrupt ( auch NMIs) beendet
werden. Es werden einfach die Prozesorregister wieder vom Stapel geholt.
Um unsere eigenen Interrupts zu beenden springe ich also der Einfachheit
halber gleich diese Adresse an.
Natürlich brauchen Sie zum Betrieb der
neuen Mausabfrage einen neuen Adapterstecker. Damit das nicht allzu umständlich wird, habe ich die Belegungen im
Großen und Ganzen so gelassen wie sie
beim erst Adapterstecker waren. Sie
müssen lediglich zwei Leitungen umlöten:
* Das V-Signal ( Pin 1 an der Joyportbuchse) kommt jetzt an CNT1(= Pin 4 am
Userport - vorher Pin C) .
* Das H-Signal ( Pin 2 an der Joyportbuchse) kommt jetzt an CNT2(= Pin 6 am
Userport - vorher Pin D) .
Schließen Sie den neuen Stecker nun bei
abgeschaltetem 64 er am Userport an, und
stecken Sie eine AMIGA-Maus am anderen
Ende ein. Jetzt können Sie das Programm
" AMIGA-MAUS3" von dieser MD laden und mit
RUN starten. Wie Sie sehen, können Sie nun auch weiterhin Eingaben machen, da
der Cursor weiterhin blinkt. Denken Sie
nun daran, daß es bei Diskettenzugriffen
Probleme geben wird, da die NMIs den
Datenverkehr stören( sollten Sie noch aus
dem ersten Teilen dieses Kurses wissen) .
In solchen Fällen ist es ratsam die Abfrage doch abzuschalten ( z. B. indem man
die Timer einfach anhält, oder sie als
Interruptquellen sperrt) .
Zum Schluß möchte ich Ihnen noch eine
weitere Funktion der CIAs erklären. Nur
mit dem Userport erhält sie überhaupt
einen Sinn, weshalb ich sie bis jetzt
auslassen mußte.
Vielleicht erinnern Sie sich noch daran, daß das Register 12 einer CIA das sogenannte " Serial-Data- Register" ist. Mit
diesem Register kann über die SP-Leitung
( die ja von beiden CIAs am Userport anliegt) ein einfacher serieller Datenaustausch mit einem anderen Rechner( im einfachsten Fall ebenfalls ein 64' er stattfinden. Das kann sehr nützlich sein wenn
man z. B. ein Spiel programmieren möchte, bei dem zwei Spieler an jeweils einem
eigenen Rechner gegeneinander spielen
Wenn das Spielprogramm also Daten über
die Tätigkeiten und Bewegungen des anderen Spielers benötigt.
Über das SD-Register wird dieser Datenaustausch stark vereinfacht und ist fast
noch unkomplzierter, als wenn man sich
der normal Seriellen Schnittstelle
( RS232) des Betriebssystems bedient.
Bei der Datenübertragung müssen wir unterscheiden, ob die SP-Leitung nun auf Ein oder Ausgang geschaltet ist. Dies wird mit Bit 6 des Control-Registers- Timer A ( Reg.14) angegeben. Ist es auf 1, so ist SP auf Ausgang, ist es auf 0, so
ist SP auf Eingang geschaltet. Je nach
Betriebsart verhält sich die Benutzung
von SDR wie folgt:
* Wenn SP Ausgang ist, so wird ein Wert, der in das SDR geschrieben wird mit
der halben Unterlauffrequenz von Timer
A in den entsprechenden CIA an SP
" herausgesch ben" . Das heißt, daß der
Wert Bit für Bit, bei jedem Timerunter
lauf an SP e scheint. Nach 8 Unterläufen ist SDR wi der leer und es wird
ein Interrupt ausgelöst. Bit3 im ICR
zeigt an, daß der Interrupt von dem
leeren SDR-Regist kommt.
* Wenn SP Eingang ist, so wird mit jeder
steigenden Flanke an CNT der entsprechende CIA der Wert, der gerade anliegt
(0 oder 1) in ein internes Schieberegister übernommen. Ist dies Mal ge- schehen, so wird der Wert in das SDR
übertragen und ebenfalls ein Interrupt
ausgelöst. Hier erkennt man ebefalls an
Bit 3 im ICR, daß das SDR voll ist und
ausgelesen werden kann.
Kombiniert man das Ganze jetzt noch mit
der Möglichkeit, daß die CIA bei einem
Timerunterlauf ein Signal an PB6 anlegen
kann, so kann man sehr einfach Daten austauschen. Möchten Sie z. B. Daten an einen
anderen C64 senden, so müssen Sie sich
ein Kabel bauen, daß die Userporte der
beiden Rechner miteinander verbindet.
Dabei sollte der SP-Ausgang von Rechnern
mit dem SP-Eingang von Rechner 2 verbunden sein und die Leitung PB6 von Recher1 mit der Leitung CNT von Rechner2( ob SP1 oder SP2, bzw. CNT1 oder CNT2 hängt davon
ab mit welcher CIA sie die Daten übertragen wollen) .
Jetzt müssen Sie im Control-Register von
Timer A ( des sendenden Rechners) noch
festlegen, daß das Signal an PB6 bei
jedem Timerunterlauf in die jeweils andere Lage gekippt werden soll. Das geschieht, indem Sie die Bits 1 und 2 dieses Registers auf 1 setzen. Dadurch
erscheint nämlich ebenfalls mit der
halben Unterlauffrequenz von Timer A( des
sendenden Rechners) ein Signal an PB6 und läst somit die Datenübernahme am
empfangenden Rechner aus!
Das war es dann endgültig mit dem CIA-Kurs. Ich hoffe, daß Sie nun einen besseren Einblick in die Funktionen dieser
beiden kleinen, aber extrem mächtigen
Bausteine innerhalb unseres Rechners
haben. Wie Sie sehen lassen sich sehr
viele Probleme mit Interrupts leichter
lösen ( wie die Mausabfrage beweist) . Zusätzlich können Sie mit dem Userport
vielf ltige Har wareerweiterungen leicht
und einfach bedienen.
Ich freue mich also, wenn es Ihnen ein
wenig Spaß gemacht hat und bi n für
Kritik und Anregungen zu neuen Kursen
immer zu haben ( auch ein kleiner Kursautor kriegt gerne Leserpost) .
Bis auf Weiteres also ein letztes Mal "Servus" Ihr Uli Basters (ub)