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 ]