Low-High-Berechnungen
Low-High-Berechnungen
Sprite-Animation

Ein 8-Bit-System wie der C64 besitzt - trotz seiner CPU-externen Zugriffsmöglichkeit auf 216 (65536) Speicherstellen - ausschließlich Register (CPU-interne Speicherstellen), die 28 verschiedene Werte annehmen können. Damit sind für jegliche Art von Berechnungen also nur Zahlen von 0 bis 255 möglich. Immer dann, wenn man den C64-Systemroutinen von Basic aus 16-bittige Werte vermitteln möchte, muss man sie daher auf zwei Bytes (8-Bit-Werte) aufteilen, die zusammengesehen den gewünschten 16-Bit-Wert ausmachen. Bei der Ansprache von Speicheradressen gibt das eine Byte (das sogenannte High-Byte) dann an, wie viele 256-Bytes-Blöcke (Pages) "übersprungen" werden müssen, um an die Stelle des anderen Bytes zu gelangen (der Stelle in der anvisierten Page, die das Low-Byte angibt).

Nehmen wir mal an, wir interessieren uns für die Startposition von Sprite 1, dessen Daten wir in Spriteblock 13 abgelegt haben, weil wir eine Sprite-Animation planen (siehe animiertes Bild oben). Dieser Block befindet sich an Adresse 13*64=832. Damit die Animation so flüssig abläuft wie im Bild hier, nehmen wir eine Systemroutine in Anspruch, die die Aufgabe hat, Daten schnell von einem Speicherbereich in einen anderen zu transferieren (Routine $B688, ist normalerweise für die Umlagerung von Strings zuständig).

Diese Routine braucht drei Informationen: 1. Woher soll ich die Daten holen, 2. wohin soll ich sie transportieren und 3. wie viele Daten sind es denn? Es geht also um zwei Adressen (16-Bit-Werte) und eine Mengenangabe (8-Bit-Wert). Systemroutinen ruft man von Basic aus mit dem Befehl SYS auf. Dabei kann man ihm die Werte für die drei Prozessor-Register (Akku, X und Y) über die Speicherstellen 780 bis 782 (in dieser Reihenfolge) gleich "mitgeben".

Für unsere Systemroutine muss im X- und Y-Register die Quelladresse der Daten angegeben werden (Antwort auf Frage 1 von eben), das soll die Adresse 49152 sein (das berühmte $C000). Und die Zieladresse ist die Adresse 832, der 13. Sprite-Block (Antwort auf Frage 2). Diese Angabe wird von der Systemroutine an der Adresse in 53/54 erwartet. Die gewünschte Datenmenge (64 Bytes für ein Sprite) soll im Akku übermittelt werden (Antwort auf Frage 3).

Wie finde ich jetzt heraus, wie viele Pages ich bei Adresse 49152 und bei Adresse 832 überspringen muss? Genau, ich teile die Zahl durch 256 (Ergebnis: Anzahl Pages) und schaue mir den Rest der Division an (Ergebnis: Offset in der anvisierten Page).

Dafür haben wir in TSB zwei Befehle, die genau diese Aufgabe erledigen (DIV und MOD):

Ergebnis: HI = 192 und LO = 0. Diese beiden Werte müssen in die Speicherstellen 782 und 781 gePOKEt werden. Das Ziel berechnen wir ganz genau so: Ergebnis: HI = 3 und LO = 64. Diese anderen Werte müssen für die Systemroutine in die Speicherstellen 53 und 54 gePOKEt werden. Die Mengenangabe (64 Bytes) POKEn wir in die Speicherstelle 780. Jetzt ist alles vorhanden und wir können den SYS durchführen. Hier die TSB-Routine zum Transfer von Daten:

...
155 loop: get x$: exit if x$="x"
160 for q=$c000 to $c000+15*l step l
165 transfer
185 next
190 end loop
...
215 proc transfer
220 zl=mod(sb,256): zh=div(sb,256)
225 poke 53,zl: poke 54,zh
230 lo=mod(q,256): hi=div(q,256)
240 poke 781,lo:poke 782,hi:poke 780,l
250 sys $b688
260 end proc

Hier heißen die Variablen etwas anders, für das Ziel haben wir SB gewählt (steht für Sprite-Block) und für die Quelle die Variable Q. Die Anzahl der zu übertragenden Bytes steht in L (für Länge). SYS $B688 erledigt dann den Transport der Daten von Q (49152) nach SB (832). Die Quelle Q ändert sich in diesem Fall fortlaufend, während das Ziel SB immer gleich bleibt (deshalb könnte man die Standard-Routine "transfer" in diesem Fall noch etwas auf Geschwindigkeit hin optimieren: Die Zeile 220 nach außerhalb der Prozedur, vor den LOOP in Zeile 155 verlegen).

Im zugehörigen Hauptprogramm sieht man diesen Ablauf. Das Sprite ist eingeschaltet, das Ziel SB ist bereits festgelegt und die Quelle Q ändert sich laut Zeile 160 in 64er-Schritten (L hat den Wert 64). Im Ergebnis sieht man das animierte Bild oben auf dieser Seite.

Auf der TSB-Demo-Disk ist dieses Programm unter dem Namen "spriteanim.dmo" enthalten.


Etwas modifiziert kann man mit DIV und MOD auch die Werte der beiden Halbbytes (Nibbles) eines Byte-Wertes herausbekommen:

HI ist das High-Nibble, LO das Low-Nibble, beim Wert 100 ergibt das HI = 6 und LO = 4.


Eine andere Anwendung des Aufteilens einer 16-Bit-Zahl auf ihre zwei 8-Bit-Komponenten zeigt das folgende kleine Programm in PROC split. Das Programm dient dazu, die Startadressen der einzelnen Zeilen des im Speicher befindlichen Basic-Programms anzuzeigen (und ihre Anzahl zu zählen, eine Information, die ein Programmierer sicher auch gern mal wissen möchte). Es wird zu einem vorhandenen Programm dazu geMERGEt und dann mit RUN 55555 gestartet.

Für seine Aufgabe sucht es vom Programmende her (steht in $2D/$2E, Z: 55555) die Verlinkungsadresse auf die jeweils zuletzt gefundene Basic-Zeile. Die zugehörige Basic-Zeilennummer ist (natürlich) 16-bittig, muss also zunächst in eine für Basic brauchbare Form umgerechnet werden (Z. 55558). Umgekehrt muss beim Suchen die aktuelle Verlinkungsadresse (Variable S) im Low-High-Format vorliegen (als SL und SH), weil sie eben im Speicher so abgelegt ist (Z. 55564).

Das Programm kennt also immer die Speicheradresse der jeweils aktuellen Zeile und sucht von da aus nach vorne (zum Anfang des Basic-Speichers hin) die beiden Werte SL und SH. Sind sie gefunden, testet es vorsichtshalber, ob sie wirklich die Verlinkungsadresse darstellen (vor der Verlinkung steht im Speicher der Wert $00, der das Ende der vorherigen Zeile markiert (Z. 55567). Trifft das zu, liefert PROC parse in P die gefundene Startadresse der Basic-Zeile zurück. ZZ zählt einfach die Anzahl der gefundenen Zeilen, wobei die Routine die Zeilen, die sie selbst belegt, nicht mitzählt und ausgegraut anzeigt (Z. 55559).

55555 cls: s=d!peek($2d)-2:
      zz=0: split: p1=s: colour,0
55556 print" PRG end at: $" $$p1: print

55557 repeat: s=p1: split: parse: p1=p:
      print" $" $$p1;
55558 zl=d!peek(s+2)
55559 if zl<55570 and zl>55554 then
      zz=zz-1: colour,15
55560 print": Zeile ";: use"#####",zl;:
      use" (# ####)",s,zz: colour,0
55561 until s<=$0801

55562 end

55563 proc split
55564 sl=mod(s,256): sh=div(s,256)
55565 end proc

55566 proc parse
55567 p=0: repeat: if peek(s)=sh then
        if peek(s-1)=sl then
        if peek(s-2)=0 then p=s-1
55568 s=s-1: until p: zz=zz+1
55569 end proc






Ist der Basic-Start erreicht ($0801) ist der Suchlauf beendet und das Programm endet ebenfalls.