SuperCPU durchleuchtet - Folge 7
IRQ-Programmierung mit der SuperCPU
Wo ist der Vektor?
Erinnern wir uns an einen ganz fruehen Kursteil: Es ging um Native und
Emulation Mode. Der Native Mode wird nicht etwa, wie man zunaechst annehmen
koennte, fuer die neuen Befehle benoetigt - die funktionieren naemlich auch im
Emulation Mode (das ist auch der Grund, warum illegale Opcodes nicht
funktionieren koennen). Doch um 16 Bit-Akku und Indexregister nutzen zu
koennen, muss man auf jeden Fall in den Native Mode schalten. Hier haben die
Entwickler des 65816 neue Vektoren fuer die Interrupts vorgesehen. Der
wichtigste, der fuer den IRQ liegt im Native Mode nicht mehr bei $FFFE/$FFFF,
sondern bei $FFEE/$FFEF. Doch was soll das ganze? Offenbar hat man sich bei
der Entwicklung im voraus Gedanken gemacht (schade, dass sowas heute nur noch
selten der Fall ist): Im Native Mode kann man jederzeit den Akku sowie auch
das X- und Y-Register zwischen 8 und 16 Bit umschalten. Eine
Interrupt-Routine muss hier in der Lage sein, mit Registern in beiden
Zustaenden fertigzuwerden - schliesslich muessen diese gerettet werden, wenn sie
in der IRQ-Routine benutzt werden! Darum gibt es fuer den Native Mode einen
eigenen IRQ-Vektor, somit kann man, falls man zwischen beiden Modi
umschaltet, je eine entsprechend auf den jeweiligen Mode abgestimmte
IRQ-Routine bereithalten.
Ja, aber...?
Doch was zum Geier ist mit dem guten alten Vektor bei $0314/$0315, den man
doch sonst immer fuer IRQ-Programmierung benutzt hat? Den kann man getrost
vergessen. In Wahrheit naemlich springt der 6510 bei einem IRQ immer an die
Adresse, die in $FFFE/$FFFF abgespeichert ist. Und die zeigt ins ROM - dort
werden die Register gerettet und erst dann wird ueber $0314/$0315 gesprungen.
Sicher erinnert Ihr Euch: Vor jedem Umschalten in den Native Mode haben wir
bisher immer einen SEI ausgefuehrt, und der CLI kam erst wieder, wenn der
Prozessor wieder im Emulation Mode war. Doch das wurde nicht etwa
durchgezogen, weil im Native Mode keine Interrupts moeglich sind, sondern aus
einem anderen Grund: Wie bereits beschrieben liegt der IRQ-Vektor im Native
Mode bei $FFEE/$FFEF. Gewoehnlich ist das ROM eingeblendet, und - Ihr ahnt es
wahrscheinlich schon - was steht bei $FFEE/$FFEF im ROM? Die Adresse $E505,
ein Operand zu einem JMP-Befehl, der eine Betriebssystemroutine zur
Screen-Spalten- und Zeilenermittlung anspringt. Kurz gesagt: Bestimmt keine
IRQ-Routine - wie denn auch? Commodore konnte 1982 doch noch nichts vom 65816
wissen (der wurde erst 1985 released), geschweigedenn ahnen, dass dieser
Prozessor mal in einem Geraet namens SuperCPU an ihrem Original C64 verwendet
werden wuerde. Somit muessen wir im Native Mode entweder den IRQ sperren, oder
aber das ROM mit LDA #$35 STA $01 ausblenden und eine eigene IRQ-Routine
schreiben. Wo diese liegt, schreiben wir dann nach $FFEE/$FFEF.
Es geht los
Das Hauptproblem bei IRQ-Routinen im Native Mode ist die Tatsache, dass der
Interrupt jederzeit das Hauptprogramm unterbrechen kann, egal ob dieses
gerade mit 8 oder 16 Bit arbeitet. Die IRQ-Routine rettet die Register - die
Push-Befehle arbeiten ja immer gleich, egal ob man 8 oder 16 Bit breite
Register pusht, oder diese mit STA irgendwohin speichert. Doch dann will man
natuerlich auch irgendwelche Operationen ausfuehren, und da stoert es dann doch,
nicht zu wissen, ob der Akku und/oder die Index-Register gerade 8 oder 16 Bit
sind - schliesslich wuerde ein LDX #$00, ausgefuehrt bei geloeschtem X-Flag
(Indexregister sind 16 Bit) zu einem Chaos fuehren, da das Befehlsbyte nach
der $00 noch mit hinzugenommen wuerde, um den 16 Bit-Wert zu bilden. Danach
wird es einen mehr oder weniger fatalen Programmabsturz geben. Doch es ist
doch nichts einfacher, als die Register so zu schalten, wie man sie haben
moechte - schliesslich gibt es die Befehle REP und SEP mit denen man die 16
Bit-Flags loeschen und setzen kann. So weit, so gut - die IRQ-Routine wird
abgearbeitet und man schaltet munter zwischen den Modi hin- und her. Langsam
naehert sich die Interrupt-Routine ihrem Ende und wir einem Problem. Denn die
Register muessen wiederhergestellt werden, und zwar entweder als 8- oder als
16 Bit-Register!
Umgehen?
Natuerlich gibt es eine einfach Moeglichkeit, das ganze Problem zu umgehen -
gerade fuer Demos oder Intros ist dies sicher auch ein akzeptabler Weg: Man
verzichtet einfach auf ein Hauptprogramm und laesstt alles im Interrupt laufen.
Nach dem Initialisierungsteil folgt dann nur noch ein JMP auf sich selbst -
der braucht keine Register, somit muessen wir sie auch im IRQ nicht retten,
geschweigedenn uns um 8 oder 16 Bit-Modi Gedanken machen.
Die direkte Loesung
Aber angenommen, wir lassen im Hauptprogramm irgendetwas nicht
frame-orientiertes berechnen oder was auch immer, so dass wir das Problem
loesen statt umgehen wollen. Wie koennen wir nun die Register 100%ig wieder
herstellen? Um die Modi-Wiederherstellung brauchen wir uns aber nicht zu
kuemmern, denn die Flags werden ja bei einem IRQ automatisch auf den Stack
gerettet und beim Interrupt-Ende (RTI-Befehl) wieder zurueckgeholt. Fuer uns
aber leider zu spaet, denn wir haetten vorher schon gern gewusst, welchen
Zustand die M- und X-Flags (fuer Akku und Indexregister) hatten. Und hier ist
der Schluessel zur Loesung: Am Anfang der IRQ-Routine, nachdem die Register
bereits auf dem Stack sind (entsprechend ihrer Modi auch korrekt), retten wir
uns auch einfach nochmal das Status-Register mit saemtlichen Flags - Befehl
PHP. Am Ende der IRQ-Routine nun, bevor wir die Register mit den
Pull-Befehlen wieder restaurieren, ziehen wir diesen geretteten Status mit
PLP wieder vom Stack - und schon befinden sich M- und X-Flag wieder in dem
Zustand, die sie zur Zeit der IRQ-Ausloesung hatten. So koennen wir sicher
sein, dass die nachfolgenden Pull-Instruktionen genau die richtige Anzahl Bytes
fuer das entsprechende Register vom Stack holen! Ja, und das war's auch schon
- das Problem ist geloest. Nun kann man im Hauptprogramm beliebig zwischen 8
und 16 Bit umschalten, ohne sich Sorgen machen zu muessen, dass nach einem IRQ
vielleicht einige Highbytes von 16 Bit-Registern verlorengegangen sind.
Raster-Interrupt
Fuer gewoehnlich wird man den IRQ nicht, wie vom Betriebssystem vorgeschlagen,
ueber einen CIA-Timer ausloesen lassen, sondern stattdessen vom VIC - an einer
bestimmten Rasterzeile. Beim CIA muss man mit einem LDA $DC0D bestaetigen, dass
der IRQ empfangen und abgearbeitet wurde - genau das macht das Kernal bei
$EA7E. Beim VIC funktioniert dies anders: Man muss den Interrupt mit LDA $D019
STA $D019 bestaetigen. Bestaetigt man einen Interrupt - egal ob von CIA oder
VIC - nicht, wird gleich nach dem IRQ-Routinen-Ende sofort wieder in die
IRQ-Routine gesprungen, weil der C64 denkt, der Interrupt muesste noch
abgearbeitet werden. Statt des LDA $D019 STA $D019 benutzen viele Coder
allerdings ein DEC $D019, ASL $D019 oder LSR $D019 - die bewirken dasselbe,
nur ist es kuerzer. Im Native Mode des 65816 allerdings zeigt sich eine
Merkwuerdigkeit, fuer die auch wir keine Erklaerung finden konnten: Die DEC, ASL
oder LSR-Variante zeigt auf $D019 keine Wirkung mehr - man muss schon LDA-STA
nehmen. Haargenau dieselbe Routine, die im Emulation Mode noch klappte, geht
bei eingeschaltetem Native Mode und IRQ-Vektor bei $FFEE/$FFEF nicht mehr.
Mit dem "saubereren" LDA $D019 STA $D019 ist man die Sorgen allerdings wieder
los.
Bitte warten
Viele Interrupt-Routinen sollen eine bestimmte Operation an einer bestimmten
Rasterzeile ausfuehren. Nehmen wir als simples Beispiel eine Rasterbar. Hier
warten wir auf eine Rasterzeile, timen mit einer kurzen Schleife aus, bis wir
genau an deren Ende sind, und aendern dann schnell $D020 und $D021, damit,
wenn der VIC die naechste Rasterzeile zu zeichnen beginnt, diese in einer
anderen Farbe erscheint. Dann warten wir wieder bis kurz vor Ende der Zeile
und aendern wieder, entsprechend einer Farbtabelle, die Rahmen- und
Hintergrundfarbe. Als besonderes uebel hat sich die Charline (Badline) dabei erwiesen,
da hier der VIC Daten aus dem Screen- und Farbram liest, dafuer etwas Zeit
braucht und wir deshalb etwas weniger als sonst in der Warteschleife
haengen muessen, bis die naechste Rasterzeile erreicht ist. Fast jedem
Assembler-Einsteiger sind diese Rasterbalken ein Greuel, denn bis man
verstanden hat, wieso man wann wieviel warten muss, vergeht manchmal einige
Zeit. Zeit vergeht uebrigens auch, wenn wir den C64 warten lassen, bis die
Rasterzeile vorbei ist. Ueberzieht man den gesamten Bildschirm mit
Rasterbalken, bleibt nicht mehr viel Rasterzeit uebrig, um noch grosse Spruenge
zu machen - und die Warteschleife waehrend des Balken-Aufbaus ist jeweils so
kurz, dass nur wirkliche Profi-Coder dadrin noch Routinen unterbringen.
Mehr als 20 mal schneller?
Mit der SuperCPU koennte man nun natuerlich die 20fache Menge an
Schleifendurchlaeufen warten, und haette auch wunderschoene Rasterbalken. Doch
es gibt noch eine andere Moeglichkeit, und mit der bekommen wir sogar einen
sehr grossen Anteil der Rechenzeit, die sonst gewartet wird, frei fuer das
Hauptprogramm! Die SuperCPU ist naemlich so schnell, dass sie am Beginn einer
Rasterzeile die Farben rechtzeitig aendern kann. Man muss also nicht, wie sonst
ueblich, schon am Ende der vorigen Rasterzeile mit seinem LDA aus der
Farbtabelle beginnen, damit man noch rechtzeitig zum Beginn der neuen
Rasterzeile den Wert nach $D020/21 geschrieben hat. Wir lassen den Interrupt
einfach an der Zeile kommen, wo wir die erste Farbe haben wollen, und
schreiben diese in die VIC-Register. Danach haben wir noch sehr, sehr viel
Rechenzeit, bis die naechste Rasterzeile erreicht und die naechste Farbe
gesetzt werden muss. Also sagen wir dem VIC einfach, dass er in der naechsten
Rasterzeile uns wieder Bescheid sagen soll (einen IRQ ausloesen), damit wir
dann die naechste Farbe setzen koennen. Wir stellen die Register wieder her und
verlassen mit RTI die Interrupt-Routine! Nun stehen dem Hauptprogramm
Unmengen an Taktzyklen zur Verfuegung, die fuer Berechnungen oder aehnliches
genutzt werden koennen. Inzwischen zeichnet der VIC in Ruhe die Rasterzeile.
Beginnt die naechste, wird die IRQ-Routine wieder aufgerufen, wir aendern
schnell die Farbe und geben sogleich die Kontrolle wieder zurueck an's
Hauptprogramm. Das Warten auf die naechste Rasterzeile entfaellt komplett,
somit wird Rechenzeit frei. Waerde man saemtliche existenten Rasterzeilen des
C64 mit Rasterbalken ueberziehen, so wuerde bei 1 MHz keine Zeit mehr
uebrigsein, irgendetwas anderes zu machen, da der arme Computer meistens auf
die naechste Rasterzeile wartet. Mit der SuperCPU haben wir aber im selben
Fall noch ca. 80% der Rechenzeit oder mehr uebrig (nur eine Schaetzung)!
Das Programmbeispiel
Im Listing 7.1 koennt Ihr alles hier erklaerte nocheinmal sehen. Zunaechst wird
der Native Mode angeschaltet und der IRQ initialisiert. Dann wird in einer
Endlosschleife als Hauptprogramm staendig zwischen 8 und 16 Bit hin- und
hergeschaltet. Zur Kontrolle wird im 8-Bit-Mode die Adresse $D800, das ist
die Farbe des Zeichens oben links, erhoeht. Da dies im 8 Bit-Mode geschieht,
duerfte nie $D801 beeinflusst werden, was aber im 16 Bit-Modus der Fall waere.
Auf diesen schalten wir danach naemlich um und erhoehen $0400 - ist diese
Adresse auf $FF, wird automatisch $0401 incrementiert. Der IRQ kann also
jederzeit ausgeloest werden - im 8- oder im 16 Bit-Modus. Im IRQ retten wir
dann die Register, die die Routine selbst verwendet, sowie den Status, um
spaeter die Register wieder ordnungsgemaess zu restaurieren. Dann schalten wir
die Index-Register auf 8, den Akku auf 16 Bit. Aus einer Farbtabelle holen
wir uns mit einem LDA auf einen Schlag zwei Werte, die wir nach $D020 (und
natuerlich nach $D021) schreiben. Zur Erinnerung: Ein LDA #$0102 STA $D020
bewirkt, dass die $02 nach $D020 geschrieben wird, waehrend die $01
logischerweise in $D021 landet. Danach erhoehen wir $D012, dies ist die
Rasterzeile, wo der naechste IRQ ausgeloest werden soll. Natuerlich schalten wir
vorher den Akku wieder auf 8 Bit, sonst wuerde auch $D013 erhoeht, was wir
sicherlich nicht unbedingt wollen. Dann wird noch abgefragt, ob wir am Ende
unserer Rasterbar angelangt sind - wenn ja, lassen wir den naechsten Interrupt
erst wieder am Beginn der Rasterbar (fuer den naechsten Frame) auslaesen. Zum
Schluss wird mit LDA $D019 STA $D019 dem VIC mitgeteilt, dass wir seinen IRQ
empfangen und abgearbeitet haben - warum z.B. ASL $D019 nicht funktioniert,
bleibt ungeklaert. Uebrigens: Wenn man im Hauptprogramm haeufig mit 16 Bit
arbeitet, kaennte der IRQ etwas zu spaet kommen, da die 16 Bit-Befehle (obwohl
sie die doppelte Arbeit machen) in der Regel einen Taktzyklus mehr brauchen.
Doch durch geschickte Programmierung der IRQ-Routine laesst sich das
ausgleichen - unser Beispiel stellt keineswegs die ultimative Loesung dar.
(w) Malte Mundt
©1999 Go64 Redax! & Count Zero/SCS*TRC for all HTML Stuff
[ Zum 6. Teil ][ Zum Index ][ Zum 8. Teil ]