​Werte im RAM laden und speichern - 3 komplizierte(re) Adressierungsmodi ("Programmieren" lernen)

  • Nachdem es im Logik-Thread schonmal angeklungen ist, folgen hier nun die noch verbleibenden Adressierungsvarianten.

    Bisher kam man ja ganz gut damit zurecht und eigentlich auch überall hin im Speicher - in niedrige Bereiche (Zeropage), in normales RAM (absolute Adressierung), in Adressen mit einem zusätzlich aufaddierten X- oder Y-Register (absolut X-indiziert bzw. absolut Y-indiziert) oder in gar keine Bereiche, sondern direkt an die Zahlen (#$FF) (unmittelbare Adressierung).


    also allgemein: (#$FF) unmittelbar ; (einfach, Typ A) absolut ; (einfach mit Index, Typ C) absolut-indiziert


    Allen gemeinsam ist, daß man den gewünschten Wert genau an dem Platz vorfindet, der angegeben worden ist.

    Das ist zwar sehr schön übersichtlich und verständlich, aber leider auch ein wenig unflexibel. Wenn man etwa für eine absolut-indizierte Adressierung den Startwert ändern will, bleibt einem nur, den Befehl nochmal mit der zweiten, neuen Adresse zu schreiben, oder - was man wohl generell besser vermeiden sollte (obwohl es sehr COOL! sein kann) - den Programmcode selbst durch das Programm abändern zu lassen und so die Adresse direkt dort zu ändern.



    Es gibt aber noch ein andere Variante: die indirekte Adressierung.


    Der Name sagt es schon - man landet nicht direkt dort, wo man hinwill, sondern nur indirekt. Soll heißen, man wird im Speicher auf eine andere Stelle verwiesen, wo dann der gesuchte Wert (das Datum) zu finden ist.


    Das funktioniert also so:

    Anfrage an $Adresse1 ---> Adresse1 "zeigt" auf Adresse2 ---> Adresse2 enthält den Wert


    Eigentlich ist es schon ein bißchen wie bei der indizierten Adressierung ($aaaa,X): die eigentliche Adresse wird erst nach einem Zwischenschritt gefunden - mit dem wichtigen Unterschied, daß die Adresse, die bei Indirektion gefunden wird, i.a. den ganzen Speicherbereich abdecken kann. (Und man kann dann sogar noch eine Indizierung anschließen.)


    Wer mit dem Pointerkonzept von C/Pascal vertraut ist, wird die Idee dahinter wiedererkennen - Stichwort Doppelpointer.

    Für alle BASIC-"Jünger" ist das so ähnlich wie ein Feld/Array, in das man die Positionen eines (anderen) Datenarrays einträgt, wodurch es sich z.B. vermeiden läßt, das komplette (zweite) Datenfeld umzusortieren, wenn dort ein neuer Eintrag erfolgt. Es reicht, das Feld mit den "zeigenden" Positionen im ersten Array an die neue Sortierung anzupassen.



    Soetwas nennt man eine Indirektion.


    Nicht jede CPU kann soetwas. Und nicht jede, die es kann, kann es für alle Kombinationen mit den anderen Adressierungsmöglichkeiten.


    Vorteil der Sache ist z.B., daß man relativ einfach auf große Datenbestände in immer gleicher Form zugreifen kann. Es ist prinzipiell auch möglich, die Indirektion selbst noch ein weiteres Mal indirekt erfolgen zu lassen. Das kann dann kaum eine CPU (zumindest nicht von den MikrocomputerCPUs). Damit lassen sich Listen führen, die auf Listen zeigen, die auf Daten zeigen - wodurch eine sehr schöne "Entkopplung" von Programm und "Werten" erreichbar wird. Man kann soetwas, in gewissen Grenzen, auch durch Kombinieren zweier Indirektionsvarianten nachbilden, auch auf den Mikros.

  • =6502=


    Es ist leider nicht mit allen Befehlen möglich, eine Indirektion zu benutzen. Strenggenommen können es nur die Befehle die in irgendeiner Form mit dem Akku assoziiert sind - also Addieren (ADC), Subtrahieren (SBC), Akku vergleichen (CMP) sowie die Logikbefehle (AND,ORA,EOR). Zudem ist hierbei der indirekte Zugriff ausschließlich über Adressen in der Zeropage möglich, die dann auf "große" (16-Bit) Adressen weiterverweisen.


    Der einzige Befehl, der 16-Bit Adressen zur Indirektion benutzen kann ist der Sprungbefehl (JMP).


    Mit dem fangen wir mal an, da man sich das da am einfachsten vorstellen kann.

    Geschrieben wird die Indirektion im Assembler mit einer geklammerten Adresse, also

    JMP ($Adresse)


    Um zu springen, schaut die CPU jetzt in $Adresse nach, was da steht. Sie liest dort 2 Bytes - zunächst das Low-Byte der Zieladresse (aus $Adresse) und dann das High-Byte (aus $Adresse+1).

    Steht also in $7000 : $24 $40 , ist das Low-Byte die $24 und das High-Byte die $40 - und zusammengesetzt gibt das $4024.

    Die eigentliche Zieladresse ist nunmehr die $4024 - und dort springt der Befehl JMP ($7000) dann hin.



    Komplizierter wird das bei den Akku gebundenen Befehlen mit Indirektion. Zum Einen können die nicht indirekt mit normalen 16-Bit Adressen benutzt werden - Indirektion ist nur über Zeropage-Adressen möglich. Zum Zweiten ist Indirektion allein nicht möglich, sondern nur in Kombination mit einer Indizierung.

    Aber langsam...


    Das nur Zeropage-Adressen erlaubt sind, ist nicht so schlimm - man muß sich nur trauen, die ganzen fürs BASIC reservierten Adressen zu nutzen und nicht nur die wenigen offiziell "frei" verfügbaren. Daneben gibt es auch noch Platz im Kassettenpuffer oder im reservierten Bereich für die Spracherweiterung, die sowieso niemand (kaum jemand) hat.

    Zudem würde man das evtl. sowieso dort machen, da Zeropage gerade bei Adressierungen immer schneller ist.


    Man schreibt wieder einfach : Befehl ($Adresse)

    da aber nur Zeropage erlaubt ist, kann da nicht CMP ($7000) oder LDA ($1232) stehen. Aber CMP ($D0) bzw. LDA ($D8) würden passen, wenn da nicht die zweite Einschränkung wäre, die mit der Indizierung.


    Man MUSS nämlich einen Index dazusetzen.

    Und der kann nun - damit es noch bißchen unübersichtlicher wird - mit in der Klammer stehen ODER außerhalb.

    Also Befehl ($aa,index) bzw. Befehl ($aa),index


    Dabei ist nun das X-Register nur für die erste Variante zugelassen und das Y-Register nur für die zweite.


    Also Befehl ($aa,X) bzw. Befehl ($aa),Y


    Und nun kann man die beiden Sachen auch sehr schön mit Namen versehen:

    Befehl ($aa,X) heißt - indiziert indirekt , oder exakter X-indiziert indirekt

    Befehl ($aa),Y heißt - indirekt indiziert , bzw. exakter indirekt Y-indiziert


    Die machen dann auch genau das, was da so geschrieben steht.

    Steht der Index mit in der Klammer, wird die Adresse $aa zuallererst mit dem X-Register "indiziert", also $aa um den Wert aus dem X-Register erhöht. Anschließend wird in der sich ergebenden Adresse nachgeschaut, welche Adresse dort hinterlegt ist und diese ist erst die eigentliche Zieladresse, aus der die Daten geladen werden.


    Bei der zweiten Variante wird die Indirektion erstmal komplett ausgeführt - ($aa) hat bei $aa das Low-Byte der Zieladresse und bei $aa+1 das High-Byte. Diese Zieladresse wird genommen und anschließend noch (wie bei einer normalen absolut indizierten Adressierung, $aaaa,Y ) mit dem Y-Register aufaddiert. Erst von dort werden nun Daten geladen.


    Mit Variante1 - X-indiziert indirekt - lassen sich so in der Zeropage mehrere indirekte Adressen hintereinander ablegen und dann per X-Register anwählen.


    Variante2 - indirekt Y-indiziert - erlaubt mehrere Listen an unterschiedlichen Orten im Speicher liegen zu haben, die man mit dem Y-Register durchlaufen will. Welche Liste genommen wird, bestimmt man durch die Änderung der Adresse in $aa/$aa+1 oder durch Ablegen verschiedener Adressen hintereinander und Anpassen von $aa.


    Beispiele:


    Hat die Adresse $D8/$D9 den Adresswert $7050 gespeichert, liegt also das Low-Byte ($50) in $D8 und das High-Byte ($70) in $D9.


    Dass diese Reihenfolge stimmt, darauf hat der Programmierer zu achten; der CPU ist das egal, die liest das eben dann einfach als $5070, wenn es verkehrt (bzw. nach menschlichem Ermessen "richtigherum") da abgelegt wurde. Sowas gibt sehr schöne Fehlersuchen ! Kann man stundenlang Spaß dran haben - darum immer 2mal schauen, ob's paßt.


    (Das kann übrigens z.B. bei anderen CPUs genau andersherum sein, je nachdem, ob die CPU, das MSB zuerst im RAM ablegt oder das LSB. Der Begriff dafür ist Endianness. Die 6502 ist "by Design" eine little-endian Maschine, d.h. das Erste, was man von der abgelegten Adresse im Speicher zu sehen bekommt, ist das "kleine" Ende. So lange mit reinen 8-Bit Daten gearbeitet wird, hat das hier auch nur bei Adressen (16-Bit) eine Bedeutung.)


    Also, in $D8/$D9 liegt die Adresse $7050, abgelegt als $50/$70.

    Benutzt man nun die Variante mit dem Y-Register - ($aa),Y - wird einfach die gefundene $7050 als Adresse benutzt. Zu dieser kommt jetzt noch der Wert vom Y-Register hinzu, fertig. Wenn also YR z.B. $40 ist, wird letztlich in $7090 nachgeschaut.

    D.h. ganz einfache Indirektion (nur eben lediglich über die Zeropage möglich), und dann im Anschluß mit YR nachindiziert.


    Die X-Variante ist ein bißchen "hakeliger". ($aa,X) bedeutet, daß das X-Register dazukommt, bevor, die Adresse "fertig" ist. Nun wird aber NICHT der Wert in $D8/$D9 um das XR erhöht, sondern lediglich das $aa. Ist also das X-Register mit dem Wert $02 geladen, wird aus $aa eine $aa+$02 - für das Beispiel würde $D8/$D9 zu einer $DA/$DB. Und erst dort wird nun die Adresse ausgelesen. Und erst in dieser wird dann, indirekt, nachgeschaut, was drinsteht.

    Man indiziert also die Zeropage-Adresse und nicht deren Inhalt.



    Nun ist sicher auch klar, was man macht, wenn man eine simple, einfache Indirektion haben will.

    Man verknüpft mit einem Register, welches einen Wert $00 hat. Welche Version man dabei wählt - indirekt indiziert oder doch indiziert indirekt - ist dabei theoretisch egal; rein formal landet man ja bei beiden auf der Adresse, die per Indirektion erreicht wird, also in der angegebenen Zeropage-Adresse abgelegt ist. Praktisch ist es dann aber so, daß die Variante mit dem Y-Register (indirekt indiziert) in fast allen Befehlen, wo sie überhaupt zulässig ist, genau um einen Taktzyklus Rechenzeit schneller abgearbeitet wird - was sie zum klaren Favoriten macht.


    [Doppelte Indirektion bastelt man sich dadurch, daß man eine Liste anlegt, in die man die Werte für das zweite zu benutzende Register schreibt. Die Adressen dieser Liste kommen dann in die Zeropage. Dort kann man sie nun per ($aa,X) durchlaufen - den passenden Wert aus der Liste holen - und dann für eine zweite Indirektion mittels ($aa),Y benutzen.

    Es geht natürlich auch andersherum. Oder zweimal die gleiche Art des indirekten Adressholens hintereinander.]

  • =6502=


    JMP Indirektion

    das Programm springt einfach nur hin und her, das aber indirekt und jedesmal in sich selbst zurück.



    Viel sinnvoller als hier gezeigt, ist soetwas für Sprünge auf Routinen, die man evtl. gern auch mal austauschen oder an andere Stellen im Speicher verschieben will.


    Dazu würde man einfach definieren, welche Routine den ersten Platz in der Adressliste bekommt, welche den zweiten usf. - dann schriebe man die Startadressen der Routinen hintereinander ins RAM und benutzte den indirekten Sprung. Wechselt die Routine ihren Platz im RAM oder wird durch eine neue, die aber eine gleiche Aufgabe erfüllt, ersetzt, muß man nicht mehr alle Sprünge einzeln an jeder Stelle im Programm anpassen. Es genügt dann, die Adresse auf diese Routine in der angelegten Adressliste durch die neue/aktuelle zu ersetzen und alle Sprünge erfolgen "automatisch" nun dorthin.


    Oft sind basale Betriebsystemroutinen in so einer Form angeordnet - wodurch man ihren Platz beliebig verschieben kann und die Aufrufe trotzdem immer "über" die gleiche Stelle laufen, d.h. frühere Software kompatibel bleibt. (Bei den Commodores gibt es auch so eine Tabelle, allerdings benutzt diese auch direkte JMPs, nicht nur indirekte).

  • =6502=


    Hier wird nun mal "alles" zusammen benutzt. Tabellen, Logik-Verknüpfung, Indirektion, Stack, Additon, bedingte Sprünge, Flags, Transferbefehle usw.


    Zunächst taucht die Routine aus dem Logik-Thread nochmal auf, diesmal als Subroutine SUBZEILEX2D0D1 bei $6050.

    Bitte mit S"SUBZEILEX2D0D1",8,6050,6069 abspeichern.



    Man sieht, daß Akku und YR auf den Stack gerettet werden.

    Anschließend werden sie mit einer Adresse beladen ($0BD8). Auf diese wird nachfolgend immer wieder eine 40 addiert ($28), d.h. jedesmal eine Bildschirmzeile. Das Ganze geschieht so oft, bis das X-Register auf $00 heruntergezählt ist. Wenn das erreicht ist, wird die Adresse nach $D0/$D1 geschrieben, die Register vom Stack geholt und mit RTS zurückgesprungen.


    Man übergibt also per X-Register die gewünschte Zeilennummer an die Funktion und erhält in Adresse $D0/$D1 den Adresswert des ersten Zeichens der Zeile. Dabei ist die oberste Zeile auf dem Bildschirm die mit den Nummer 1, es gibt also die Zeilen 1 bis 25.


    Frage: Was ließe sich verbessern ? (Zeile1)


    Frage: Ist es besser eine allgemeingültige Subroutine zu haben oder eine, die nur für den Bildschirm gilt, mit fixer Adresse am Beginn ? Legt man die Schrittweite (40) besser fix fest, oder läßt man sie besser auch als Parameter übergeben ?




    Der zweite Teil,legt per BASIC zwei Tabellen an. Jede mit 256 Werten. Eine Sinustabelle bei $5100 für die Auslenkung in X-Richtung und 40 möglichen Positionen. Eine Cosinustabelle mit 25 Positionen für die Vertikale.

    Abspeichern ist evtl. auch hilfreich ( DSAVE"NAME"), insbesondere, wenn man an die Hauptroutine ein JMP $5000 anfügen will.


    Die interessanten Werte sind die Faktoren zum W in 130, 140. Die möge man gerne mal abändern und die Tabellen dann neu rechnen.





    SUBWAIT bei $6000 wird benötigt.


    Die Hauptroutine nutzt die beiden Tabellen. Dabei werden diese einmal komplett durchlaufen. Zu jedem Wert für die Vertikale wird mittels SUBZEILEX2D0D1 die Startadresse dieser Zeilennummer ermittelt und nach $D0/$D1 geschrieben. Anschließend wird aus der Tabelle bei $5100 für die Horizontale ein Wert gelesen und ins Y-Register gebracht. Dieser gibt direkt die Position in X-Richtung an und verknüpft in einem indirekten adressierenden EOR-Befehl den Wert aus dem Bildschirmaspeicher an genau dieser X-Position mit einem fixen Wert, der im Akku steht. Anschließend wird kurz gewartet und die gleiche Verknüpfung gleich noch einmal ausgeführt.


    Frage: Was wird man auf dem Bildschirm sehen können ?


    Frage: Warum wird eine $38 EOR verknüpft ? (und nicht eine $18)

  • =6502=


    Hier noch eine kleine X-indizierte Indirektion.



    SUBWAIT bei $6000 wird benötigt.


    Die Tabelle bei $D0 muß vorher angelegt werden. Sie enthält die Adressen, die X-indiziert abgelaufen werden. $D0/$D1 z.B. die $0C00. Es empfiehlt sich auch diese noch woanders abzulegen, da sie dort in der Zeropage bei einem Reset gelöscht wird.

    ( T 00D0 00DF 52D0 ) ( mit T 52D0 52DF 00D0 zurückschreiben )


    Die wichtige Kommandokombination ist in $5006,$5009 - es wird ein Zeichen aus dem Bildschirm geholt und mit ($D0,X) abgespeichert. Das X (X-Register) läuft dabei von $0E bis $00 - in Zweierschritten(!), die Adresswerte, werden also von oben nach unten durchlaufen. Im ersten Durchgang also ($D0,#$0E) , d.h. das X-Register und die Basisadresse werden addiert - wodurch ($DE) die zusammengesetzte Adresse wird. Darauf folgt die Indirektion: Es wird in der Zeropage-Adresse $DE (das ist das Gleiche wie $00DE) geschaut, welcher 16-Bit Adresswert dort abgelegt wurde. Laut Tabelle liegt dort: $18/$0D in Low-Byte/High-Byte Anordnung - das ist daher Adresse $0D18 in "normaler" Schreibweise. In $0D18 wird das Byte jetzt abgespeichert.


    Außenherum passiert: XR läuft immer 8 Stellen durch die Tabelle; parallel dazu wird mit dem Y-Register die erste Bildschirmzeile abgefahren (die Position ganz links wird dabei ausgelassen) - irgendeine Datenquelle muß man ja haben. Das Y-Register wird immer wieder korrigiert, so daß es nur zeichenweise versetzt wird, obwohl es zwischendurch immer schon sieben Positionen "weiter" war.


    Man stelle sich die Warteschleife "schön" ein und probiere dann mal die Werte der Indirektionsadressen zu ändern.

    Etwa alle $0C auf $0D ändern, oder eine Reihe in der dritten Bildschirmzeile bilden, oder jeweils eine Stelle Platz lassen, oder die Adressen nicht aufsteigend, sondern absteigend ab $0D17 (beginnend mit diesem in $D0/$D1). Damit man ein Gefühl für das flexible Anordnen bekommt, was so möglich wird.

  • =6502=


    Um die Subroutinen indirekt in einer Tabelle anzuordnen, ließe sich folgende Sprungtabelle mit indirekten Sprüngen einrichten.

    (rein theoretisch)



    Um soetwas zu benutzen, würde man die Sprünge bei $7000 "veröffentlichen" (die Einsprungadressen) zusammen mit einer Funktionsbeschreibung. Je nachdem, ob es eine Subroutine oder eine einfache Routine (ohne Rücksprung) ist, die sich dahinter verbirgt, würde man nun mit z.B. JSR $7000 auf die Warteschleife springen können. Diese erste Tabelle müßte man dann allerdings "konstant" halten in ihrer Zuordnung von Adresse und Funktion.


    Das schöne ist nun, daß die Sprünge indirekt weiterspringen. Sie holen also die eigentlichen Adressen aus der Adressliste bei $7080. Immer als Low-Byte/High-Byte.

    Die Einträge in der Tabelle könnten jetzt beliebig geändert werden, da der Nutzer der Funktion sie ja immer nur über die definierte Funktionstabelle bei $7000 "anspringt".

    Dadurch ist die Subroutine nicht mehr an einen festen Speicherplatz gebunden, sondern kann etwa ihren Platz bei $6000 verlassen und z.B. nach $2300 verschoben werden. Das Einizige was man anpassen müßte, sind die Einträge bei $7080 - also 2 Bytes je verschobener Funktion.


    Solche Bytes, die als Adressen für indirekte Sprünge dienen, nennt man auch Vektoren oder Sprungvektoren.


    Bei Commodore Gerätschaft gibt es davon sogar relativ viele. Viele davon liegen in der sogenannten erweiterten Zeropage (Adressen ab $0100). Beim C16 beginnt der Bereich der Sprungvektoren bei $02F2. Da auch das Betriebsystem über diese dort abgelegten Adressen indirekt springt, ist es möglich die jeweils dazugehörige Funktion durch eine eigene zu ersetzen oder sie zu ergänzen, indem man den Vektor "verbiegt", d.h. in die Stelle eine andere Adresse in Low-Byte/High-Byte Darstellung einträgt, welche dann auf eine selbstgeschriebene Routine zeigt.

  • =6502=


    Zeropage indiziert


    Eine Adressierung ist noch zu erwähnen. Die Zeropage kann nämlich für viele Befehle auch mit einem Index adressiert werden. Dabei ist das X-Register das i.a. zu verwendende. Bei Befehlen, die das X-Register selbst betreffen, kommt dann aber das YR als Index zum Einsatz.


    Befehl $aa,index - meist als Befehl $aa,X


    So werden etwa Logik-Befehle oder Akku Ladebefehle oder die Akku Rechenbefehle und auch die Schiebebefehle in der Zeropage mit dem XR verbunden benutzt.


    Die wesentlichen Ausnahmen sind STX und LDX, die man nur als STX $aa,Y bzw. LDX $aa,Y verwenden kann.


    Im Schnitt spart man hiermit einen Taktzyklus im Vergleich zur indizierten Adressierung einer "vollen" Adresse.