Projekttage: Frisieren für Kapuzineräffchen
Heute machen wir... nix!
Wir werden Veränderungen im Code vornehmen, deren Ziel es ist, am Ende dennoch nur dasselbe wie jetzt auf den Bildschirm zu zaubern. Dazu werden wir uns maßgeblich um eine Subroutine kümmern. Wir haben aus unserer letzten
praktischen Unterrichtsstunde ("Projekttage: Subroutinen und Stack") noch unser Programm, indem wir den DMA in eine Subroutine ausgelagert haben. Diese Subroutine wollen wir flexibler machen.
Wozu der Aufwand?
An dieser Stelle möchte ich gern einen Gedanken vom großen d4s anführen: Man sollte, bevor man ein größeres Projekt beginnt, sich gut organisiert haben, ansonsten wächst der Code geschwulstartig und wird selbst für den schreibenden Programmierer immer unübersichtlicher, bis sich jegliche Motivation gebunden an die Möglichkeit der Übersicht auf Nimmerwiedersehen verabschiedet. Sinngemäß sagte er mal sowas.
Folglich gilt auch hier: Vorbereitung ist alles. Wenn man sich einen guten Satz an flexiblen Subroutinen schreibt, die alle immer wiederkehrenden Wehwehchen meistern, spart man in der "heißen Phase" der Entwicklung viel Zeit; außerdem könnte man sicherlich auch solche Werkzeuge aus einem Projekt nehmen und sie in ein neues Projekt mitnehmen.
Jetzt beginnen wir mit der Automatisierung.
Zur Zeit ist es in dem Projekt so, dass die Subroutine "TilemapDMA" einen festen DMA durchführt. So oft ich sie aufrufe, transferiert sie immer wieder dieselbe Tilemap an dieselbe Stelle. Was wir jetzt tun werden ist: Wir werden Variablen einsetzen, die die Subroutine laden und in die DMA-Register speichern wird.
Da wir zur Zeit keine Variablen nutzen, können wir sie frei wählen. Aber -es tut mir leid- bevor wir ins Programmieren kommen, muss ich noch einmal einen theoretischen Teil erschlagen:
Direct Page, Direct Bank, RAM
Der Arbeitsspeicher, der RAM, liegt beim SNES zwischen #$7E0000 und #$7FFFFF. Im Englischen wird er oft als "WRAM" statt "RAM" bezeichnet, das "W" steht, glaube ich, für "Work RAM" oder "Working RAM". Vermutlich bezeichnet man ihn deshalb als "WRAM", um ihn von anderen RAMs unterscheiden können - den batteriegeschützten Speicher (SRAM), dem Palettenspeicher (CGRAM), dem Arbeitsspeicher des Videospeichers (VRAM) etc.
Jetzt könnte man bei jedem Mal, wenn man mit dem WRAM arbeitet, die 24-Bit-Adressen verwenden: LDA $7E0042.l, AND #$0F.b, STA $7F5000.l usw.
Zum Glück gibt es da zwei Vereinfachungen:
Erstens: Bei LoROM sind auf jeder Bank die Adressen $0000 bis $1FFF gleichzusetzen mit $7E0000 bis $7E1FFF. Das heißt, "LDA $0042.w" und "LDA $7E0042.l" laden dasselbe. Es gibt nur drei Unterscheide: Der erste ist: Man spart sich Tipparbeit (ganz wichtig). Der zweite ist: Man spart einen Byte im Programm (was natürlich bei der Menge der derartigen Befehle sich aufsummiert). Der dritte ist: Man spart pro Durchlauf dieses Befehls einen Cycle, was sich noch mehr aufsummiert (da mit Vorliebe viele Programmpassagen mehr als einmal durchlaufen werden).
Aber, es geht noch besser. Wir haben noch die Möglichkeit, die Direct Page Register zu nutzen. Mit der Direct Page kann man einen Bereich auf Bank $00 festsetzen, den man mit 8-Bit-Adressen ansprechen kann. In unserer InitSNES.asm finden wir "LDA #$0000.w" und "TCD", was ich auch schon im vorigen Artikel ("Projekttage: Stackbefehle (Push/Pull= und CPU-Register)") angesprochen habe. Das bedeutet, dieser Bereich beginnt bei $0000. Das heißt: "LDA $42.b" ist dasselbe wie "LDA $0042.w" ist dasselbe wie "LDA $7E0042.l" ist
nicht dasselbe wie "LDA #$42.b". Obacht Doppelkreuz. Mind the Number Sign. Gare! Croisillon! ヘルメットの法律! hãy cẩn thận! Dê ruột!
Wichtig: Der Bereich, den man mit $0000 bis $1FFF ansprechen kann, kann man nicht variieren. Der Bereich, den man mit der Direct Page ansprechen kann, bezieht sich immer auf Bank $00. Das heißt, mache ich "LDA #$1F80.w" und "TCD", kann ich mit $00 bis $7F noch WRAM-Adressen ansprechen (logischerweise $7E1F80 bis $7E1FFF), mit $80 würde man in eins der Input-/Output-Register schreiben, das,
sofern man nicht gerade ein MSU1-Programm schreibt, noch nicht mal unbedingt irgendwie belegt sein würde, und ich keine Ahnung hätte, was dann da passiert. Logischerweise wäre man mit "LDA #$8000.w" und "TCD" mit der Direct Page direkt am Anfang der eigenen ROM.
Folglich bietet es sich an, alle Werte, die man viel bearbeitet, in das Fenster zwischen $0000 und $1FFF zu packen, und die Wichtigsten der Wichtigsten im Rahmen eines dieser "Direct-Page-Fenster" zu behalten. Übrigens gibt es auch einige Spiele, die je nach Anlass die Direct Page ändern. Z. B. wird bei Squares Satellaview-Flugschiffspiel
Treasure Conflix in Soundbelangen die Direct Page auf $1E00 gesetzt. Das Switchen kostet 4 Byte und 5 Cycles, das heißt, ab mehr als zehn solcher Befehle hat sich das Hin- und Herstellen rentiert.
Soweit hierzu.
Ende theoretischer Teil.
Also fange ich mal an und kopiere den Inhalt der TilemapDMA-Subroutine (bis auf das RTS) und füge ihn überhalb des JSR-Tilemap ein. Dort fange ich jetzt an und ersetze die Register bei den STA- und STX-Befehlen durch Direct-Page-Register. Dann sieht das ganze so aus:
Code: Alles auswählen
LDX #$1000.w
STX VMADDL.w
LDA #DMAP_2_REG_WRITE_ONCE.b
STA $00.b
LDA #$18.b
STA $01.b
LDX #Tilemap.w
STX $02.b
LDA #:Tilemap.b
STA $04.b
LDX #$0800.w
STX $05.b
LDA #DMA_CHANNEL0_ENABLE.b
STA $07.b
JSR TilemapDMA
LDA #$0F.b
STA INIDSP.w
forever:
jmp forever
TilemapDMA:
LDA #DMAP_2_REG_WRITE_ONCE.b
STA DMAP0.w
LDA #$18.b
STA DMADEST0.w
LDX #Tilemap.w
STX DMASRC0L.w
LDA #:Tilemap.b
STA DMASRC0B.w
LDX #$0800.w
STX DMALEN0L.w
LDA #DMA_CHANNEL0_ENABLE.b
STA MDMAEN.w
RTS
.ENDS
Was macht das Programm jetzt? Bevor es die Subroutine aufruft und den DMA wie gewohnt durchführt, speichert es dieselben DMA-Variablen in die Direct Page Register. Natürlich ist das gerade noch vollends sinnlos, also frisieren wir noch unsere Subroutine, dass die Direct-Page-Register statt der festen Werte geladen werden. Dann sieht die Subroutine so aus:
Code: Alles auswählen
TilemapDMA:
LDA $00.b
STA DMAP0.w
LDA $01.b
STA DMADEST0.w
LDX $02.b
STX DMASRC0L.w
LDA $04.b
STA DMASRC0B.w
LDX $05.b
STX DMALEN0L.w
LDA $07.b
STA MDMAEN.w
RTS
Zunächst ein paar Fragen, die ich beantworten sollte. Erstens:
Warum benutze ich nicht Register $03 und $06?
Tue ich. Indirekt. Das sind beides 16-Bit-Werte, und die Direct-Page-Register sind alle 8 Bit groß. Folglich, wenn ich
mache, speichere ich in $02 und alles, was da nicht herein passt (50%, 8 Bit), kommt in $03. Andersrum, wenn ich
befehle, lädt er $03 gleich zusätzlich, da er sieht, dass in X noch 8 Bit Platz sind.
Wenn das aber 16-Bit-Werte, also "Words" sind, warum schreibe ich dann noch ein ".b" hinter "$02"?
Das ".b" bezieht sich niemals auf die
Größe des Inhalts, sondern auf die
Länge der Adresse.
Beispiel:
Ich könnte auch "LDX 2" statt "LDX $02.b" schreiben. Das Ergebnis wäre dasselbe. Der Punkt ist: Erstens ist die Adresse keine Hexzahl, was ich schonmal kacke finde, und zweitens überlasse ich dem Compiler zu entscheiden, was für eine Adresse er daraus bildet, ob er nun "LDX $02" als "LDX $02" oder "LDX $0002" interpretiert. Letztere Interpretation kann man z. B. erzwingen, würde man "LDX $02.w" schreiben.
In diesem Fall ist es vollends wurscht, allerdings wäre es natürlich Mist, wenn man die Direct Page auf etwas anderes als $0000 gesetzt hat, und der Compiler sich entscheidet, ein "LDX $02" als "LDX $0002" misszuinterpretieren, da er dann nämlich das falsche Register lädt. Folglich entscheidet sich das ".b", ".w" oder ".l" niemals am enthaltenen Wert, sondern immer an der Länge der vorangehenden Hexzahl.
Jetzt haben wir das umgestellt, und es ist flexibler. Wir können jetzt immer wieder dieselbe Subroutine aufrufen, wenn wir vorher unterschiedliche Werte in die entsprechenden Variablen speichern. Wir können uns sogar gegebenenfalls das Updaten einzelner Variablen sparen, wenn dort der richtige Wert bereits drinsteht.
Allerdings ist das jetzt keine wirkliche Vereinfachung, sondern speziell für den Prozessor ist eine Zusatzbelastung, da wir ja einen Schritt dazwischen gefügt haben (Trotzdem war es nicht vollends sinnlos; da das hier ein Tutorial ist, haben wir damit schon wieder ein wenig Praxiserfahrung sammeln können).
Was wir als nächstes machen, ist ein bisschen was verrücktes. Der Aufwand für den Prozessor wird zwar immer noch mehr sein als beim "direkten" DMA, den wir am Anfang hatten. Dafür ist das System ebenfalls flexibel und sogar noch bequemer zu bedienen als die anderen beiden Varianten.
Wir werden folgendes machen: Die Daten für die Register schreiben wir in einer Kette hintereinanderweg als Bytes in unser Programm. Der Subroutine übergeben wir nur die
Adresse dieser Kette, und die Subroutine geht dann zu dieser Adresse und verteilt dann selbstständig deren Inhalt auf die Register.
Machen wir das Einfache zuerst. Machen wir zunächst das, was jeweils außerhalb der Subroutine machen. Wir schreiben zunächst unsere Datenkette. Damit die nicht mit dem Code in die Quere kommt, schreiben wir sie unten in die Sektion "Bilddaten". Denn, wir erinnern uns, setzen wir das irgendwo ins Programm, nimmt der Prozessor an, das wären Befehle, interpretiert sie so, und verhält sich dann wie an Karneval im Rheinland. Und danach sieht alles aus wie Bierzeltgarnitur.
Dazu benutzen wir die Befehle ".DB" und ".DW". Die dienen dazu, dass die nachfolgenden Bytes (.DB) oder "Words", also Doppelbytes (.DW), so eins zu eins in das Programm geschrieben werden. Diese Befehle sind nun wieder, man kann es sich denken, keine Assembler-, sondern WLA-DX-Befehle. WLA-DX bietet auch einige andere Befehle, die genau dasselbe bewirken, aber ich benutze jetzt hier nur die und stelle die anderen Schreibweisen nicht dar. Wen es interessiert, der kann es in der
WLA-DX-Dokumentation nachschauen.
Wichtig dabei ist: Keine Raute davor. Wenn ".DB" oder ".DW" davor steht, weiß WLA DX sofort, dass das nachfolgende keine Adresse ist.
Also. Das Geschriebene sieht dann so aus:
Code: Alles auswählen
(...)
.SECTION "Bilddaten"
.DB DMAP_2_REG_WRITE_ONCE.b
.DB $18.b
.DW Tilemap.w
.DB :Tilemap.b
.DW $0800.w
.DB DMA_CHANNEL0_ENABLE.b
Tileset:
(...)
Falls Klärungsbedarf besteht:
Ich kann natürlich auch statt der festen Zahlen auch hier die definierten Konstanten aus d4s' Datei verwenden.
Beim dritten Eintrag sehen wir, dass wir ".DW" statt ".DB" verwenden
müssen, da es sich ja um eine 16-Bit-Adresse handelt.
Beim vierten Eintrag sehen wir, dass wir auch den Doppelpunkt-Befehl in diesem Zusammenhang nutzen können, damit die Bankadresse hier eingetragen wird.
Jetzt fügen wir noch unter dem ".SECTION" und über dem ".DB DMAP..." eine Sprungmarke für diesen Byte-Schlauch ein:
Code: Alles auswählen
.SECTION "Bilddaten"
Data_Tilemap:
.DB DMAP_2_REG_WRITE_ONCE.b
Jetzt gehen wir an die Stelle im Hauptprogramm, bevor wir zur Subroutine springen. Dort löschen wir den Kram, in dem wir die DMA-Daten in die Register $00 bis $07 speichern, und speichern die 24-Bit-Adresse in die Direct Page Register $00 bis $02.
Code: Alles auswählen
(...)
STX VMADDL.w
LDX #Data_Tilemap.w
STX $00.b
LDA #:Data_Tilemap.b
STA $02.b
JSR TilemapDMA
(...)
Es ist hier egal, welche Register man nimmt, es müssen allerdings (Merke 1!) Direct Page-Register sein und (Merke 2!) drei aufeinanderfolgende sein.
Jetzt kommt der knackige Teil!
Und dazu verwenden wir einen neuen ASM-Befehl...
...namens...
LDA!
Ja, in der Tat. Es gibt viele LDAs. Und wir kennen auch schon viele. Zum Beispiel wird ein "LDA #$0300.w" in die drei Bytes "$A9 $00 $03" zerlegt, das heißt, LDA ist $A9. In diesem Fall. Gucken wir uns "LDA $00.b" an. LDA in Verbindung mit einem Direct Page Register führt zu $A5. LDA in Verbindung mit einem
16-Bit-Register führt zu $AD. Das heißt, wir kennen schon mindestens drei Befehle, unter diesem Kürzel.
Bevor ich jetzt weitergehe, verweise ich wieder einmal auf das WLA-Handbuch (aber ich suche dieses Mal nicht nochmal den Link raus. Warum auch? Wenn du bis jetzt alle meine Ratschläge in den Wind geschlagen hast, dann kannst du jetzt auch ein bisschen arbeiten, um das Ding zu finden!

) oder aber auch gern
diese Seite, die ich des Öfteren nutze. Sie enthält dieselben Daten wie das Handbuch, ich meine auch, dass eine Aufstellung, die dieser sehr ähnlich ist, weiter hinten im Handbuch ist. Allerdings ist diese Aufstellung direkt digital und erspart mir das Blättern, wenn es mal ganz schnell gehen muss, um den Faden nicht zu verlieren.
Gut. Wir gebrauchen jetzt LDA im Sinne von $B7, das wird auf dieser Seite so schön "DP Indirect Long Indexed, Y" betitelt. Dieser Titel bedeutet: "DP" = Direct Page --- "Indirect" Indirekte Adressierung; wir geben dem Befehl nicht die Zieladresse, sondern eine
Adresse, in der die Zieladresse
steht --- "Long" = 24 Bit --- "Indexed" = Wir nehmen eins der Index-Register und zählen das auf die Adresse drauf. "Y" = Wir nehmen das Y-Register als Index (wir hätten ja auch X zur Auswahl).
Das Ganze sieht dann praktisch so aus:
MOI ERINNER MICH HIER MAL EIN BILDLI ZU MALEN
Wir speichern in Y "$0000.w" und in $10 bis $12 eine Adresse. Der Verständlichkeit halber nehme ich jetzt einen festen Wert, zum Beispiel $018980. Das heißt: $10 enthält #$80, $11 enthält #$89, $12 enthält #$01 (Low Byte zuerst, wie man sieht). Wenn ich jetzt Besagten "DP Indirect Long Indexed, Y"-LDA-Befehl nutze, der im Code so aussieht:
Geht der Prozessor zu $10 hin, guckt welche 24-Bit-Adresse in diesem Register und in dessen zwei Nachbarn ($11 und $12) liegt (du ahnst es schon: $018980), geht dann an diese Stelle (das heißt... $018980! Überrascht?) und zählt dann soviele Bytes weiter, wie in Y steht (in diesem Fall... null! Überrascht!?!) und lädt dann den nachfolgenden Byte (oder Doppelbyte, je nach Größe von A).
Wenn ich jetzt Y inkrementiere und denselben Befehl noch einmal aufrufe, lädt er logischerweise den Byte daneben, da zwar die indirekte Adresse (in $10 bis $12) dieselbe ist, aber der Index inkrementiert wurde. Verständlich?
Übrigens: Dieser Befehl funktioniert nur mit LDA und nur mit Y als Index. Andere Befehle gibt es in mehreren Ausführungen mit verschiedenen Lade-Registern und verschiedenen Indizes, z. B. das indizierte Direct-Page-Laden gibt es als "LDA $10.b, X", "LDX $10.b, Y" oder "LDY $10.b, X", diesen obenbeschriebenen Fall gibt es nur in
einer Variante, nämlich -oh-oh!- in der oben Beschriebenen.
Verwirrt? Ich hoffe nicht. Bringen wir das neu erworbene, duselig machende Wissen jetzt gleich in die Praxis. Diesen Befehl müssen wir jetzt immer wieder aufrufen, und zwischendrin müssen wir nur den Index inkrementieren. Und, ach ja, vorher sollten wir den Index auch auf null setzen.
Wichtig! Sorry, das hier ist nicht ganz die einfache Einsteiger-Übung, wie ich dachte; wir hatten in der alten Subroutine auch noch einige Stellen, wo wir 16 Bit auf einmal eingelesen und abgespeichert haben. Wir haben aber, wie ich oben beschrieben habe, nur einen einzigen dieser "DP Indirect Long Indexed"-Befehle, und der speichert in A, was wir zur Zeit auf 8 Bit Größe eingestellt haben. Da könnten wir jetzt mittendrin für die zwei Doppelbytes die Größe von A switchen, das finde ich aber doof, deswegen schreiben wir das, was wir vorhin auf einmal übertragen haben, jetzt hier in zwei Einzelbytes. Dann sieht das Ganze so aus:
Code: Alles auswählen
TilemapDMA:
LDY #$0000.w
LDA [$00.b], Y
STA DMAP0.w
INY
LDA [$00.b], Y
STA DMADEST0.w
INY
LDA [$00.b], Y
STA DMASRC0L.w
INY
LDA [$00.b], Y
STA DMASRC0H.w
INY
LDA [$00.b], Y
STA DMASRC0B.w
INY
LDA [$00.b], Y
STA DMALEN0L.w
INY
LDA [$00.b], Y
STA DMALEN0H.w
INY
LDA [$00.b], Y
STA MDMAEN.w
RTS
Chic, was? Als Übung kannst du ja jetzt mal für das Tileset und die Palette die Daten-Bytes in die Sektion "Bilddaten" schreiben und das ganze Programm soweit verschlanken, dass dann die drei DMAs über die Subroutine laufen. Ich schmeiß dir dann die Lösung hier als Anhang untendran. Da werde ich dann auch mal unsere Subroutine passender umbenennen.
Oh, kleiner Tipp: Den Byte für das DMA-Aktivieren musst du im Paletten-DMA mit dazuschreiben. Erinnerst du dich? Wir hatten die ersten beiden DMAs über zwei verschiedene Channels gemacht, mit unserer Subroutine können wir aber immer nur den Channel 0 benutzen.
Okay, nächstes Mal wirds wieder einfacher. Versprochen.
~~~~~~~~~~~
An alle, die das hier noch wirklich
lesen. Bei dem Ding hier brauche ich wirklich eure Hilfe. Gegen Ende empfinde ich, dass das Ding leider viel zu verwirrend geworden ist, einfach, weil mir aufgefallen war, wie viele dieser blöden Sonderregeln ich wieder beachten musste, deshalb konnte ich dem Leser nicht, wie ich wollte, einen einfachen Raum zum Üben geben, ohne ihn sicherheitshalber auf die ganzen Tücken hinzuweisen, die mir in letzter Sekunde auffielen.
Also, bitte: Fragt, fragt, fragt, und sagt mir, ab wo ihr mir nichtmehr folgen konntet. Nur durch Kritik kann das hier besser werden!
