SuperCPU durchleuchtet - Folge 9

16 MByte am 20fach beschleunigten C64 - der Brotkasten der Superlative.
Wie man Ihn programmiert, erklaert unser Kurs zur Turbokarte - wir sind mittlerweile
schon sehr weit fortgeschritten, doch gerade bei der SuperRAM-Card gibt's noch einiges
zu entdecken...

Im vorigen Kurstell haben wir gelernt, auf den Speicher der SuperRAM-Card mittels
der langen Adressierung zuzugreifen. Statt zwei Byte nimmt man einfach drei Byte,
um die Speicherstelle zu bestimmen. Ein LDA $050000 holt ein Byte (oder zwei, wenn
man im 16 Bit Modus ist) von genau dieser Adresse. Dann lernten wir noch eine andere
Moeglichkeit kennen, an den Speicher ranzukommen: Das Data Bank Register.
Dieses wird bei allen 2 Byte-Adressen immer als Bank-Byte davorgehaengt und ist
normalerweise $00, somit fuehrt ein LDA $2000 auch zur Adresse $002000.
Aendert man den lnhalt des Registers (der Befehl PLB ist hier angesagt) z.B. auf $07,
so fuehrt derselbe LDA nun zu Adresse $072000.
Dadurch war es moeglich, schnell und unkompliziert auf jede Speicherstelle innerhalb
des 16 MByte-Adressraumes zuzugreifen. Je nach Anwendung bleibt es dem Coder freigestellt,
die 24-Bit-Adressierung zu benutzen und das Data Bank Register links liegen zu lassen,
oder (bei haeufigem Speicherzugriff auf eine andere Bank als 0) dieses Register dorthin
zeigen zu lassen, wobei man dann natuerlich bei Bank 0-Zugriffen achtgeben muss, auch z.B.
STA $00D020 zu schreiben.  Ausserdem hatten wir die Moeglichkeit kennengelernt, ueber einen
3-Byte-Pointer in der Zeropage (z.B. LDA (FC),Y) zuzugreifen, wobei Low- und Highbyte in
$FC/$FD abgelegt sind, das Bankbyte logischerweise in $FE.


Blitz-Transfer
Wie versprochen gibt's diesmal was ganz feines: Die Block-Move-Befehle. Damit ist es moeglich, bis zu 64K auf einen Schlag durch die Gegend zu transferieren. Das funktioniert innerhalb einer Speicherbank, aber auch von einer Bank zur anderen. Es handelt sich sozusagen um eine hardwaremaessig implementierte Transferier-Routine. Dabei braucht die SuperCPU fuer ein Byte lediglich 7 Taktzyklen also weit schneller als eine normale TransferSchleife! Einziger Haken: Diese Befehle transferieren nicht ueber Bankgrenzen - vermutlich waere ein Ueberschreitungstest auf Kosten der Geschwindigkeit gegangen. Nun aber zur Anwendung: Irgendwie muss man dem Prozessor ja sagen, ab wo wieviel nach wo transferiert werden soll. Dazu werden Akku, X- und Y-Register benutzt, alle werden auf 16 Bit geschaltet (REP #$30). In den Akku schreibt man dann rein, wieviele Bytes man transferieren moechte. In's X-Register kommt die Quell-Adresse, in's Y-Register die Ziel-Adresse. Das sieht also beispielsweise folgendermassen aus: LDA #$2000 ; Anzahl Bytes LDY #$8000 ; von dort LDX #$1000 ; noch da Angenommen. wir wollen diesen Transfer innerhalb der normalen 64K, also Bank 0, durchfuhren, so kaeme nun der Block-MoveBefehl: MVN $00,$00 ; nach Bank 0. von Bank 0 Hinter dem Opcode kommt also erst die ZielBank, dann die Quell-Bank. MVN steht fuer 'Move Block Next', was nichts weiter heisst, als dass das erste Byte zuerst transferiert wird, dann das naechste, usw. Dadurch kann man auch sich ueberschneidende Bereiche transferieren, und zwar solche, wo das Ziel vor der Quelle liegt (also z.B. $7000 nach $4000). Natuerlich gibt es als Gegenstueck noch den MVP - 'Move Block Previous', wo beim Transfer mit dem letzten Byte begonnen wird falls sich die Transferbereiche an der gegenueberliegenden Seite ueberschneiden (z.B. Transfer von $4000 Bytes von $3000 nach $3200). Beim MVP muss man unbedingt beachten, die END-Adresse des zu movenden Blocks anzugeben, sowohl bei Source als auch Destination! Desweiteren ist grundsaetzlich im Akku ein Byte weniger anzugeben, als transferiert werden soll: Steht $0005 drin, werden 6 Bytes transferiert. WIll man nun von einer in eine andere Bank transferieren, geht dies einfach mit MVN $03,$06 ; nach Bank 3, von Bank 6 Die Register muss man natuerlich vorher auch entsprechend setzen. Ausserdem veraendert der Prozessor im dem Augenblick, in dem der BlockMove-Befehl ausgefuehrt wird, auch das Data Bank Register - danach steht darin naemlich le Ziel-Bank. Also sollte man vorher das Data Bank Register retten (mit PHB auf den Stack) und danach wieder zurueckholen. So, nun wollen wir also mal $5000 Bytes von $08A000 nach $022000 transferieren (warum auch immer): LDA #$5000 ; Anzahl Bytes LDX #$A000 ; Low/High Source LDY #$2000 ; Low/High Dest. PHB ; Push Data Bank Reg. MVN $02,$08 ; nach Bank 2, von Bank 8 PLB ; Data Bank Register zurueckholen Das Ganze muss natuerlich im Native Mode passieren, damit man ueberhaupt auf 16 Bit schalten kann. In der Regel wird man dann auch einen SEI abgesetzt haben, aber was wenn nicht? Fuer gewoehnlich wird bei einem lnterrupt der aktuelle Befehl zu Ende ausgefuehrt, dann wird in die IRQ-Routlne gesprungen. Der Block-Move-Befehl kann aber, wenn man richtig viel transferiert, sogar laenger als einen Frame dauern. Man beachte: Ohne Optimization schafft er ueber $4000 Byles pro Frame, mit Optimization sogar ueber $D000 Bytes pro Frame! So lange koennen wir mit dem IRQ auch nicht warten. Also in so einem Fall doch wieder ein normaler Transfer-Loop? Nein, denn die Entwickler haben auch daran gedacht. So kann erstmalig mitten im Befehl der Interrupt auftreten - das aktuelle Byte wird noch transferiert, dann wird der Block-MoveBefehl unterbrochen und zur lnterrupt-Routine verzweigt! Wer die 1764 REU kennt weiss, dass diese soetwas nicht zulaesst (ihr Controller arbeitet ja auch mit solchen Block-Moves), und man somit den Transfer stueckeln' musste. um z.B. noch Musik zu spielen oder andere IRQ-basierte Sachen laufenzulassen. Das ist bei der SuperCPU alles nicht noetig! Nur muss man auch hoellisch aufpassen, denn gelangt der Computer in die IRQ-Routine, ist das Data Bank Register ja auf einem vom Block-Move gesetzten Wert. Diesen muss man retten, den eigenen, fuer die IRQ-Routine gewuenschten hineinschreiben (meistens wohl $00), und am Ende der IRQ-Routine das Data Bank Register wieder restaurieren - sonst kann der Block-Move seine Arbeit ja nicht ordnungsgemaess wieder aufnehmen. Listing 1 zeigt die Benutzung der Block-Move Befehle, In Listing 2 laeuft dazu noch eine IRQ-Routine; das gerade Geschilderte wird noch einmal anhand der Kommentare erlaeutert. Routinen ausserhalb der 64K
Es ist problemlos moeglich, Programmteile oder das ganze Programm ausserhalb des gewohnten Speicherbereiches laufenzulassen. Schliesslich ist der Zusatzspeicher ja nicht bloss eine Ram-Disk, sondern es koennen auch Befehle verarbeitet werden. Wie das ganze geht? Eigentlich ist nichts Besonderes dabei. Jeder Befehl funktioniert z.B. bei $025000 genauso wie bei $005000. Mit einem 'long'-JMP oder JSR kann man beliebige Routinen direkt anspringen. Verwendet man aber den long'-JSR, muss die Subroutine auch mit einem RTL enden, was nichts mit einem gewissen Fernsehsender zu tun hat, sondern fuer ReTurn Long steht - schliesslich muessen als Ruecksprungadresse drei Byte auf dem Stack gehalten werden. Innerhalb des 65816 existiert, analog zum Data Bank Register. das Program Bank Register, welches eigentlich nichts anderes ist als das Bank-Byte des Program Counters. Defaultmaessig steht $00 drin, und nur ueber einen 'long'-JMP/JSR Ist es zu aendern - direkte Befehle gibt es dafuer nicht. Andere Sorgen braucht man sich nicht zu machen: Bei geaenderter Program Bank liegt der Stack nach wie vor in Bank 0, ebenfalls die Zero-/Direct Page. Ein STA $50 schreibt also, vorausgesetzt die Direct Page liegt bei $0000, noch $000050, auch wenn Data Bank und Program Bank ganz woanders sind! Auch die Vektoren (IRQ usw.) sind immer in Bank 0. Ansonsten ist es ueberhaupt kein Problem, Routinen an beliebigen Speicherstellen laufenzulassen. Nur Betriebssystem-Routinen (z.B. Bildschirm loeschen. Tastatur abfragen) kann man von da aus nicht benutzen - sie enden mit einem RTS, dieser holt nur zwei Byte als Ruecksprungadresse vom Stack, wodurch das beim 'long'-JSR gerettete Program Bank Register zuruckbleibt und der C64 nicht weiss, wo er hin soll. Alles andere funktioniert wie gewohnt! Natuerlich muss man nun immer ein Auge auf das Data Bank Register haben, legt man z.B. Variablen hinter dem Code ab, und dieser liegt ab $020000 im Speicher, kommt man an diese Variablen nur ueber die 3-Byte Adressierung heran, oder man muss den Wert $02 in das Data Bank Register packen. Uebrigens, eine Sache die man noch wissen sollte: Im Gegensatz zum Daten-Zugriff, wo die Indexreglster ueber Bank-Grenzen problemlos hinauskommen, springt der Program Counter beim Erreichen der Adresse $FFFF nicht zur naechsten Bank, sondern bleibt in derselben und geht dort bei $0000 wieder los. Um das Programm in der naechsten Bank welterlaufenzulassen, genuegt aber ein einfacher 'long'-JMP. Angenommen, die Routine geht von $02F000 bis $03017A, so muss bei $02FFFC ein JMP $030000 stehen - fertig. Das handlen von Routinen in Baenken ungleich null und von Spruengen seht Ihr nochmal in den Listings. In Listing 3 springen wir kraeftig hin und her, Listing 4 ist die dazugehoerige Routine In der anderen Bank. Alte Routinen, neue Location
Um also Programme ausserhalb der normalen 64K laufenzulassen, muss man beim Coden einiges beachten, hat aber den Vorteil, beispielsweise Bank 0 fast vollstaendig fuer Grafik freizuhaben. Wie sieht's denn nun aber aus, wenn man schon existente Routinen in einer anderen Ram-Bank laufenlassen will? Auch das stellt kein Problem dar, obwohl doch ueberall nur 2-Byte-Adressierung verwendet wird! Wir lassen unsere Routine trotzdem in, sagen wir, Bank 2 laufen. Wir stellen einfach das Data Bank Register auf $02 ab jetzt findet unsere Routine auch ihre Daten wieder - und springen dann mit einem 'long'-JMP hin. Da die Routine aber mit einem RTS endet (nicht RTL), bleibt der Computer haengen - doch auch das ist schnell geloest: in Bank 2 kommt ein normaler JSR auf unsere Routine, direkt dahinter ein RTL. Mit einem 'long'-JSR (also 3-Byte-Adresse) springen wir auf den normalen JSR, die Routine wird ausgefuehrt, kehrt zurueck, der Prozessor findet unseren RTL und wir sind wieder in Bank 0. Musik!
Als kleinen kroenenden Abschluss wollen wir nun mal versuchen, einen herkoemmlichen Sound, der bei $1000 liegt, in Bank 2 ablaufen zu lassen, bei $021000. Init und Play springen wir ueber oben erklaerte Loesung an, sodass wir damit schonmal keine Probleme haben, Das Data Bank Register haben wir auch auf $02 gesetzt - die Routine scheint zu laufen. Doch zu hoeren ist von der Musik rein gar nichts! Eigentlich auch ganz logisch, oder? Das Data Bank Register steht auf Bank 2. Die Musik hat alle Pattern- und Notendaten gefunden, alle Einstellungen zu Wellenformen und Filtern und-.. hat diese ab $02D400 In den Speicher geschrleben! Der SID, wie immer ab $D400, oder besser $00D400, sitzend, wartet noch immer auf die Daten, die da kommen sollen, Natuerlich liesse sich eine komplett neue Abspielroutine coden, klar, mit Zeropage-Verlagerung, 16 Bit und anderen feinen Sachen, der Geschwindigkeitsgewinnn waere selbst im 1 MHz-Modus sicher deutllch sichtbar (weniger Rasterzeit). Doch das ist im Moment nicht das Ziel - wir wollen diese Routine, so wie sie ist, dazu bringen, Musik zu spielen! Doch nichts einfacher als das: Der Bereich $02D400-$02D41C ist ja bereits mit allen Daten vollgeschrieben. In einer kleinen Schleife noch dem Play-Aufruf kopieren wir das Zeug einfach nach $00D400-$00D41C fertig! Feinste SID-Klaenge dringen an unsere Ohren. Und der normale C64-Speicher? Komplett frei fuer andere Daten, fuer Grafik, fuer Code. Dafuer liegt die Musik ja nun in Bank 2, hat dort allerdings nicht nur den Bereich ihrer eigenen Laenge eingenommen, sondern auch noch die paar besagten Bytes ob $02D400 - aber das kann man leicht verschmerzen. Listing 5 ist alles, was Im normalen 64K-Speicher liegt, Listing 6 ist die kleine angesprochene Routine in Bank 2, die alles uebrige erledigt. Um uebrigens die Routinen und den Sound in Bank 2 zu bekommen. koennt ihr entweder kleine Block-Move-Routinen schreiben oder mit dem 65816-Monitor, der beim Flash8-Assblaster V1.2 dabei ist, diese direkt dorthin laden. Auf unserer Disk findet Ihr neben den Source-Codes auch die fertige Routine, einzeln als File und bereits mit einer Musik gelinkt (nach dem RUN wird alles transferiert und gestartet). Uebrigens klingt durch das Umkopieren mancher Sound nicht mehr ganz originalgetreu. Wen dies stoert und auch keine neue PlayRoutine schreiben will: Die SuperCPU hat auch genug Power, die Musik kurz per Block-Move reinzuswappen, anzuspielen und danach den urspruenglichen Speicherinhalt wieder zu restaurieren. Aber das waere ja schon fast PC-maessig, oder?


(w) Malte Mundt
©1999 Go64 Redax! & Count Zero/SCS*TRC for all HTML Stuff. Special Thanks to Matthias Hensler for Scanning this part of the tutorial.

[ Zum 8. Teil ][ Zum Index ][ Zum 10. Teil ]