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 ]