​Werte im RAM laden und speichern , 3 einfache Adressierungsmodi ("Programmieren" lernen)

  • Bisher war bekannt, daß Register mit Werten versehen werden können, daß man die auch von da ins RAM speichern kann und die Werte im Register ändern kann.


    (In Beispielen gezeigt haben wir bisher nur das Laden ( in den Akku ) und das Hochzählen von zwei Registern (XR,YR). Außerdem haben wir - ohne ein Register zu benutzen - eine Adresse direkt im Speicher hochgezählt.)


    Beim Zugriff aufs RAM taucht das Problem auf, daß man wissen muß, worauf man gerade zugreifen will. Um das zu ermöglichen hat, zumindest bei den "normalen" "kleinen" Homecomputern, um die es hier vornehmlich geht, jede Speicherstelle im RAM einen eigenen Nummerncode. Das RAM wird dazu i.P. einfach bei Null beginnend durchnummeriert. Fortlaufend.

    Diese "Hausnummern" der Speicherstellen sind ihre jeweilige ADRESSE


    So eine Adresse ist in dem einfachen Fall der Homecomputer der 8 Bit Zeit i.a. einfach eine einzige Zahl.


    Und natürlich ist das wieder eine hexadezimale Zahl. Zumindest im Assembler, meistens.

    Man kann die aber natürlich in eine dezimale Zahl umrechnen.

    So ist z.B. die schon oft benutzte Adresse $5000 das Gleiche wie die dezimale Zahl 20480. Es handelt sich um den identischen Wert nur in einem anderen Zahlensystem.


    Um eine solche Adresse anzusprechen, kann man jeden Befehl benutzen, der Adressierung erlaubt.


    Einer der häufigsten benutzten ist dabei einer der Art

    LADE_den_Wert : der_in_Adresse_NR_x_steht : in_das_Register_mit_Bezeichnung_soundso


    entsprechend gibt es dann auch das Gegenteil, nämlich

    SPEICHERE_den_Wert : aus_dem_Register_mit_Bezeichnung_soundso : in_die_Speicherstelle_mit_der_Adresse_NR_x


    Wenn es sich dabei um die gleiche Adresse handelt, benutzt man in beiden Fällen auch die gleiche Zahl dafür (z.B. $5000).

    Möchte man seine Zahl aber in die nachfolgende Adresse speichern, würde man ADRESSE+1 benutzen (z.B. $5001).



    Wie groß die größte Adresse sein kann, hängt einfach vom Rechner ab.


    Wichtig ist dabei, daß die Adresse und ihr möglicher größter Wert völlig unabhängig davon ist, wie groß die Zahl sein darf, die man in dieser Adresse speichern darf.

    Es macht also einen Unterschied, ob man von der Adresse ODER vom Inhalt der Adresse spricht !


    Bei den 8Bit Rechnern, ist das noch gut unterscheidbar, weil da i.a. der Wert, der unter einer Adresse gespeichert werden kann, ein 8 Bit Wert sein kann. Der Inhalt einer bestimmten Adresse kann also - genau wie ein 8 Bit Register - nur Zahlen von 0 bis 255 enthalten.

    Dagegen ist der Adresswert selber oft deutlich größer. So ein Adressbereich hat ja oft 16 Kilobyte, oder 64 Kilobyte abzudecken. Und schon so ein Uralt-Teil wie ein ZX81 mit großzügigen 1 Kilobyte RAM kann schon nicht alle Adressen mit 256 Zahlen durchnummerieren - 1 KB sind ja bereits 1024 Adressen !



    Um dem Befehl eine Adresse mitzugeben, gibt es dann zusätzlich noch unterschiedliche Varianten. Oft ist es sogar so, daß man einen Befehl im Assembler mit verschiedenen Arten der Adressübermittlung benutzen kann. Der Befehl liest sich dann fast gleich, oder ähnlich, es passiert aber möglicherweise etwas leicht anderes.

    (auf Ebene der Maschinencodes handelt es sich dabei dann auch tatsächlich um unterschiedliche Befehle, die man aber im Assembler wie einen wahrnimmt; vielleicht kann man sagen, daß sie unter Assembler ein Befehlsfamilie bilden)



    Typ A - die einfache Adressierung (oder: direkte Adressierung)


    die ist "einfach", weil man dem Befehl die komplette Adresse mitgibt. Es steht also im Befehl exakt die gewünschte Adresse drin.

    Man schreibt also: Befehl $Adresse



    Typ B - die Zeropage Adressierung


    das ist möglicherweise ein besonderes Feature der 6502 CPU, prinzipiell gibt es aber eine Zeropage auch auf anderen Rechnern.

    Gemeint ist mit "Zeropage" die allererste Abteilung der Hausnummern, d.h. der Adressen. So eine Adresse wäre z.B. die $0042, oder die $0023, die $0074, die $00EA, die $0011, die $00C0 oder, die letzte Adresse auf der Zeropage, die $00FF.


    Wenn man die Zahlen ansieht, haben die alle was gemeinsam !

    Die ersten beiden "Ziffern" sind immer "00".

    Es sind daher Adressen, die man auch einfach exakt wiederfindet, wenn man nur die beiden letzten "Ziffern" kennt - also die $42, $23, $74, $EA usf.

    Und genau das macht die CPU bei der Zeropage Adressierung auch - sie benutzt nur die hintere Hälfte der "echten" Adresse, um dort was hineinschreiben zu können oder daraus zu lesen.


    Es ist damit jetzt auch schon klar, wieviel Adressen die Zeropage hat. Denn: $FF war so ein Merkwert, den man einfach weiß !

    Er war genau 255 (wenn man bei 0 beginnt zu zählen). Es gibt also die Adressen 0 bis 255 in der Zeropage, was 256 Adressen insgesamt sind.


    Auch hier schreibt man einfach: Befehl $Adresse , ABER die Adresse wird nur mit der abgekürzten Form angegeben, als z.B. nicht $0023, sondern $23


    Vorteil : Es ist kürzer. Und es ist schneller, bei der Ausführung.

    Nachteil: Viele dieser Adressen sind schon belegt und nicht frei nutzbar (, aber einige sind schon noch da).


    Typ C - indexierte Adressierung


    Das klingt schon kompliziert.

    Ist es aber eigentlich nicht.


    Es bedeutet einfach, daß man eine Adresse, wie bei der normalen, einfachen Adressierung angibt. Zusätzlich gibt man aber noch einen zweiten Wert mit. Bevor die CPU irgendwas macht, berechnet sie aus beiden eine Summe, also

    Adresse + Indexwert

    und diese Summe ist dann die Stelle im RAM, wo gesucht wird, es ist also

    Adresse = angegebene Adresse + angegebener Indexwert


    Geschrieben wird sowas oft in der Art:

    Befehl $Adresse , Indexwert


    Das ist ein mächtiges Tool !



    Es gibt noch viele weitere Adressierungsarten, aber die kommen mal extra.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

    Einmal editiert, zuletzt von ThoralfAsmussen ()

  • =6502=


    Diesmal passiert wieder bißchen was auf dem Bildschirm (hoffentlich).


    Der Hintergrund ist folgender: Es gibt auf den Commodore Rechnern, und auch sonst (CPC usf.), einen Speicherbereich, wo der Text, den man auf dem Bildschirm sieht, gespeichert wird. Dafür ist dort im RAM für jede Cursorposition auf dem Bildschirm eine Speicherstelle vorhanden.

    Steht nun darin ein Wert, der einem bestimmten Zeichen entspricht, erscheint dieses automatisch an der zugeordneten Position auf dem Bildschirm. Es hat also jedes Zeichen auf dem Text-Bildschirm eine korrespondierende Adresse (!).


    Der Unterschied der Geräte liegt nun oft nur darin, daß dieser Speicherbereich beim PET woanders anfängt als beim CPC oder beim C64 oder beim ... usf. (Für diese Geräte müßte diese Anfangsadresse des Bildschirmspeichers in der Doku gesucht werden.)


    Beim C16 startet dieser Speicher an der Adresse $0C00.


    Ob das stimmt kann man testen, indem man sich den Anfang als Speicherauszug anzeigen läßt.


    Das geht mit

    M 0C00 0C20



    Nun ist das schöne, daß man da jetzt auch direkt Werte eintragen kann. Dazu bewegt man den Cursor auf einen der vielen $20 Werte und überschreibt ihn mit einem anderen Wert und drückt dann Return.


    Hier mal ein Beispiel



    Hier wurde also an Adresse $0C00 die Wertereihe $21,$08,$01,$0c,$0c,$0f eingetragen - und es erscheint in der ersten Bildschirmzeile !HALLO


    Strenggenommen wurde somit mittels "einfacher Adressierung" je ein "Wert" in je eine bestimmte "Adresse" geschrieben.


    Da sonst nichts in der ersten Zeile steht, darf man wohl auch vermuten, daß die $20 für ein Leerzeichen steht.

    (Kann man sich mal "vormerken".)



    Nun könnte man ja z.B. ein Programm schreiben, was irgendeinen Wert an irgendeine Stelle im Bildschirmspeicher schreibt.

    Etwa könnte man analog zu dem Erhöhen des Farbregisters $FF19 einfach eine Adresse aus dem Bildschirmspeicher nehmen und diese hochzählen - damit hätten dann auch VC20 Leute eine Beobachtungsmöglichkeit für das Hochzählen.


    .5000 INC $0c00

    .5003 BRK


    (Das klappt auch schön, aber man muß aufpassen, daß nicht nach dem Ausführen der Bildschirm hoch scrollt, dann sieht man das nämlich nicht mehr.)


    Schöner ist aber, gleich mal eine Art "Scroller" zu bauen; na ja, nur so in etwa.


    Dazu muß man ja einfach nur einen Wert aus dem Bildschirmspeicher lesen und gleich in die davorliegende Adresse speichern. Und dann macht man das für die darauffolgende usf.

    Da bisher noch Kontrollstrukturen fehlen (bedingte Sprünge), benutzt man einen "schmutzigen" Trick. Man - und das geht schon - läßt einfach die Stelle im Code ständig inkrementieren, wo die Adresse steht, und zwar nur die hintere Hälfte der Adresse.


    .5000 LDA $0C01

    .5003 STA $0C00

    .5006 INC $5001

    .5009 INC $5004

    .500C JMP $5000


    Cooler Effekt. Schmutzig. Effektiv. Und vor allem eine schöne Überlegung zum Nachvollziehen, warum das klappt !

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

    Einmal editiert, zuletzt von ThoralfAsmussen ()

  • =6502=


    Das Beispiel obendrüber ist zwar "schmutzig", weil es seinen eigenen Code verändert während das Programm läuft, aber es hat dafür alle bisherigen Befehle und alle benutzen direkte, einfache Adressierung.


    Vielleicht sollte man noch ergänzen (sorry), daß der Befehl

    STA $Adresse

    den Wert aus dem Akkumulator in der angegebenen Adresse speichert.

    Er ist somit die Umkehrung des Ladebefehles LDA.



    Hier nun ein Beispiel für die Adressierung mit einem Index, der zur Adresse addiert wird.


    Dabei wird der Wert der Adresse mit dem Wert aus dem X-Register addiert. Das Ergebnis der Addition ist die Adresse auf der nachgeschaut wird.

    Die Basisadresse ist der Anfang vom Bildschirmspeicher, also die Position Ecke oben-links.

    Dazu wird nun immer der aktuelle Wert vom X-Register addiert und aus dieser automatisch errechneten Adresse der Inhalt gelesen ( LDA $0C00,X ). Der Wert steht nun im Akku und könnte dort auch noch verändert werden. Im Beispiel wird er aber gleich wieder zurückgeschrieben, an eine andere Adresse, die aber genauso, wie vorher die Ladeadresse, aus einer Basisadresse und dem X-Register als Index zusammengesetzt wird ( STA $0DE0,X ).

    Dann wird XR inkrementiert und wieder an den Anfang gesprungen.


    Hat also z.B.

    XR den Wert $00 dann ist die Ladeadresse die $0c00 + $00 = $0c00

    hat

    XR den Wert $01 dann ist die Ladeadresse die $0c00 + $01 = $0c01

    hat

    XR den Wert $32 dann ist die Ladeadresse die $0c00 + $32 = $0c32

    usf.


    So werden insgesamt 256 Werte aus den Adressen $0c00 bis $0cff an die Adressen bei $0de0 bis $0edf geschrieben - und zwar in Endlosschleife. Es wird dadurch eine Kopie von 256 Buchstaben in 12 Zeilen Abstand vom Original angelegt.





    Das INC Kommando ist quasi nur ein zusätzliches Rätsel !

    Die Frage ist dabei, warum läuft der Wert oben-links weiter, was er ja auch machen müßte, da er bei jedem Aufruf um eins erhöht wird, aber der Wert in der Kopie an der dazugehörigen Stelle läuft nicht weiter ?



    Man sieht aber sicher, welche "Macht" so eine indexierte Adresse hat - mit einem einzigen Kommando lassen sich 256 unterschiedliche Adressen ansprechen !

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

    Einmal editiert, zuletzt von ThoralfAsmussen ()

    • Offizieller Beitrag

    Die Frage ist dabei, warum läuft der Wert oben-links weiter, was er ja auch machen müßte, da er bei jedem Aufruf um eins erhöht wird, aber der Wert in der Kopie an der dazugehörigen Stelle läuft nicht weiter ?

    Janz einfach: Sowohl X, als auch der Inhalt der Adresse $0C00 werden bei jedem Durchlauf um 1 erhöht, also erhöhen sich beide synchron.

    Somit ist der Inhalt von $0C00 jedesmal, wenn X nach 256 Durchläufen wieder 0 ist, und somit der Inhalt von $0C00 in den Akku geladen wird, wieder der gleiche wie beim ersten Durchlauf.


    Da mußte ich allerdings auch ein bißchen drüber Nachdenken, bis mir das klar war. :)

  • kleine Korrektur !


    die "indexierte Adressierung" (Typ C) ist natürlich offiziell eine "indizierte Adressierung" !

    Da war leider der Bergriff falsch.



    Das Prinzip bleibt aber das Gleiche.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Oben wurde nur das Laden und Speichern mit dem Akku "indiziert".


    Es ist aber auch möglich das bei anderen Befehlen zu benutzen. Darum liest sich der letzte Absatz vor den drei simplen Adresstypen bei #1 evtl. auch ein wenig schwammig, da allgemeingültig.


    Es ist nämlich durchaus möglich das Laden des X-Registers mit dem Wert des Y-Registers "zu indizieren".

    Oder anders: der Ladebefehl für das X-Register unterstützt die indizierte Adressierungsart.

    Oder: Es ist erlaubt zu schreiben LDX $Adresse, Y


    Ebenso erlaubt der Ladebefehl für das Y-Register die Adressirungsform namens indizierte Adressierung.

    Oder: Es ist erlaubt zu schreiben LDY $Adresse, X


    Dagegen ist es nicht möglich LDX mit XR so zu verknüpfen. Das ist auch logisch, weil sich ja dann beim ersten geladen Wert plötzlich das Element mit dem indiziert wird in seinem Wert unvorhersehbar verändert. Das gleiche gilt für die Variante LDY mit YR, das ist auch nicht erlaubt.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • Welche Adressierungsformen erlaubt sind, das schaut man immer am Besten in Tabellen nach. Diese sind zwar anfangs unglaublich kryptisch, aber sind doch sehr hilfreich.


    Eine deratige findet man normalerweise in jedem Assemblerbuch. Manchmal steht es auch nur in den Befehlsbeschreibungen.


    Für 6502 findet sich sowas hier https://en.wikibooks.org/wiki/6502_Assembly#Instructions


    Für andere CPUs wäre es nett, wenn da mal jemand noch nachtägt, in ähnlich kurzer Tabellenform.



    Man liest dort zu jeder Instruktion die Adressierungsart (Addressing Mode) - die Information steckt nun darin, daß zu jedem Befehl nur die zulässigen Adressierungsarten aufgeführt sind.


    Steht das also z.B. bei LDX in der ersten Spalte : a und a,y und # und zp und zp,y

    dann ist gemeint:

    a - man kann die "einfache Adressierung" (Typ A) verwenden (also z.B. $5000, $7F00, $2100)

    a,y - man kann die "indizierte Adressierung" (Typ C) verwenden, aber nur mit dem y Y-Register (also z.B. LDX $5000,Y)

    zp - man kann die "Zero Page Adressierung" (Tyb B) verwenden (also $23, $44, $0f, $fe)

    zp,y - man kann Typ B und Typ C kombiniert benutzen, als Zeropage indiziert, aber nur mit dem y Y-Register, oder hochwissenschaftlich: "Y-Register indizierte Zeropage Adressierung" (also $23,Y oder $D2,Y)


    und dann ist da noch "#"


    # - das ist was, was schon ganz als erstes Beispielprogramm mal auftauchte. Nämlich als LDA #$03. Damit wird ein fixer Wert geladen. Befehle, die das können, sind gut geeignet um Zahlen direkt aus dem Programm entgegenzunehmen.



    Bei LDA kann man übrigens schonmal schauen, was es noch so gibt für 6502, der LDA kann nämlich eigentlich "alles".



    Warum steht das jetzt als allgemeiner Text hier ?


    Die anderen CPUs haben alle auch genau solche Tabellen. Manche können noch andere Modi, oder bessere Verknüpfungen oder machen bestimmte Zugriffe einfacher oder können evtl. keine zp (ZeroPage) Zugriffe als gesonderte Adrssierung dafür laden sie aber mit einem Befehl gleich mehrere Registerinhalte gleichzeitig aus dem RAM in die Rechenregister. Die Art des Aufschreibens (die Notation) unterscheidet sich oft auch. Trotzdem


    Es ist auch deshalb interessant, weil die Adressierungen ein ganz wesentlicher Unterschied zwischen CPUs sein können. Insbesondere die CISC CPUs definieren sich oft auch darüber, daß sie sehr viele verschieden Möglichkeiten haben, auf den RAM zuzugreifen. Vielleicht haben sie ja nur ein einziges Rechenregister, aber dieses kann überall im RAM und mit allen möglichen Zusatzoptionen auf Daten zugreifen. Die RISC CPUs dagegen haben oft VIEL mehr Register, können aber bei weitem nicht so elegant an einzelne RAM Adressen gelangen, weil sie vielleicht nur ganz einfaches Adressieren beherrschen.



    Und nochmal zur "#" Adressierung. Eigentlich kann man sowas ja gar nicht Adressierung nennen. "Wertübergabe" würde es evtl. besser treffen. Das ist aber nun tatsächlich etwas, was vermutlich alle heute üblichen CPUs eint, daß sie fix definierte Werte direkt aus dem Programm in Register übernehmen können.


    Wenn man sich aber anschaut, was da eigentlich geschieht, ist das ja der maximale Ausdruck des Aufhebens der Trennung von Programm und Datenbereich. Man könnte also sagen LDA#$03 ist der elementare Ausdruck für den Sieg der von Neumann Architektur.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Noch zwei kurze Sachen zu den Adressierungen.


    Es wird quasi zweimal das gleiche Programm, nur eben unterschiedlich ...


    Variante A:


    .5000 LDA $0c00,X

    .5003 STA $5100

    .5006 LDY $5100

    .5009 INY

    .500A STY $5100

    .500D LDA $5100

    .5010 STA $0c00,X

    .5013 INX

    .5014 JMP $5000





    Es wird dabei wieder im Bildschirmspeicher was gemacht (beim C16 $0c00).

    Es wird außerdem in einem Register "gerechnet", nämlich im Y-Register +1 addiert. Da das bisher noch nicht mit dem Akku geht und man nochmal schön die Adressierung sieht, wird dazu in Adresse $5100 zwischengespeichert.


    Fragen, die man sich überlegen kann, sind: Warum wird nicht das X-Register zum Addieren von +1 verwendet ? und wie könnte man das Programm auf zp Adressierung umbauen ? und ist das überhaupt ein sinnvoll gebautes Programm ?



    Variante B:


    .5000 INC $0c00,X

    .5003 INX

    .5004 JMP $5000





    Anscheinend geht es auch VIEL einfacher !

    Das klappt so schön aber nur deshalb, weil der Befehl INC beim 6502 ebenfalls die indizierte Adressierung unterstützt.


    Frage dazu: Könnte man dieses Programm in dieser Kürze auch anders schreiben, etwa mit dem Y-Register ?

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

    Einmal editiert, zuletzt von ThoralfAsmussen ()

  • Die Fragen sind anscheinend zu einfach. ;)


    Stimmt !

    Das sind dann so die Ärgernisse, weil man wohl normalerweise irgendwie annehmen würde, daß wenn X klappt auch Y funktionieren sollte.


    Anschlußfrage : Kann man Variante A noch anders vereinfachen ?

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • Genau. Darauf wollte ich hinaus.


    Es gibt noch die Variante, daß man das Abspeichern ganz wegläßt und sogenannte Transferbefehle benutzt, aber das Kapitel hat ja noch niemand schreiben wollen ... deshalb ist das noch unbekannt :) - damit könnte man den Akku in ein anderes Register bringen, dort erhöhen, und auf die gleiche Art zurück in den Akku schubsen ( TAX, TXA, TAY, TYA )

    Ach ja, vernünftigerweise könnte man die 1 natürlich auch im Akku addieren, aber das Akkurechnen, ist ja auch noch "offen".

    Die letzte Übung sollte auch primär dazu gut sein, daß man sieht, daß es noch viel mehr Versionen gibt, diesen einen Effekt zu erzielen.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

    Einmal editiert, zuletzt von ThoralfAsmussen ()