Ausblicke ("Programmieren" lernen)

  • Nachdem nun die hauptsächlichen Sachen eigentlich "durch" sind, kann man evtl. mal irgendwann noch ein zwei seltene ("unnütze") Themen ergänzen und evtl. auch noch die schonmal angesprochene "Codebesprechung" öffnen.


    Vor allem aber will ich ein kleines Kapitel über eine andere Prozessorarchitektur anfügen, die jeder 3te Erdenbewohner in Form eines kleinen Rechenzentrums (mittlere Datentechnik) in seiner Jackentasche spazierenträgt.

    Es soll kurz um ARM gehen.


    Warum ? Gar nicht unbedingt nur wegen der Verbreitung, sondern v.a. wegen der schicken Konstruktion der Architektur und dem ziemlich guten Bezug zum 6502 - der natürlich nicht wirklich da ist, aber irgendwie hat das gefühlt viel von einer direkten Fortsetzung. Und da der BBC Micro ja primär auch mit einem 6502 kam, ist diese Idee evtl. gar nicht so abwegig. Vor allem aber macht das auch Sinn, weil man dann noch eine andere Sache und evtl. die Gemeinsamkeiten und auch Unterschiede gut erkennt - mithin dann doch die "Gleichartigkeit" all dieser Maschinen auf dieser Ebene sieht.

    Die andere Sache ist: Es ist komplett möglich, sowas jetzt zu verstehen ! Inklusive komischer Begriffe der besonders informatischen Art.


    ARM stand für Acorn Risc Maschines. Es steht jetzt für Adavanced Risc Maschines. Und beides hat das RISC im Namen.

    Dabei handelt es sich um eine Art Prozessoren, die Mitte der 1980er Jahre nach dem Projekt 801 der IBM eingeführt worden waren und dann zunehmend den Workstation und HighEnd PC-Bereich angeführt haben. Und aktuell ist das Thema auch immer noch.


    Diese CPUs machen einiges ein klein wenig anders, als bisher (1970er). Insbesondere verzichten sie auf komplexe Befehle - quasi schonmal eine Gemeinsamkeit mit dem 6502. 8-) Dazu kommt, daß sie eine große Anzahl Register mitbringen - beim 6502 hat man ja schön gesehen, wie nervig es sein kann, wenn nur ein einziges echtes Rechenregister da ist. Und sie können - das ist eine Einschränkung - nicht direkt im RAM operieren, sondern nur auf den Registern. Das macht die Sache schnell, aber Kommandos wie AND $5F00 sind nicht möglich. Man muß stattdessen die Register mit Werten beladen, diese umrechnen und wieder abspeichern, was sich Load-Store-Architektur nennt.


    Der große Vorteil davon ist nun, daß bei RISC eine sogenannte Pipeline möglich wird, weil ja jeder Befehl nur auf den Registern läuft und das Laden davon abgetrennt ist. Dies bedeutet dann, daß die CPU quasi parallel einen Befehl abarbeitet und dazu den nächsten schonmal aus dem RAM holt. Sofern man diese Kette nicht unterbricht, ist das logischerweise "schneller".



    Und was gibt es nun für Befehle ?


    Natürlich laden und speichern. Logisch.

    Und wohin ? - in die Register. Davon gibt es 16 Stück ! Davon R0 bis R12 frei benutzbar. Und alle 32-Bit breit.

    Und dort kann man dann mit den Werten was machen.

    Zum Beispiel ADD oder ADC zum Addieren. ADC addiert mit Carry-Flag, ADD ohne.

    und analog SUB und SBC zum Subtrahieren.


    Dabei schreibt man Befehle immer in so einer Form:

    ADD Register-für-Ergebnis, Register-für-Operand1, Register-für-Operand2


    Will man Register R12 und R7 addieren, schreibt man also ADD R0, R12, R7.

    Will man 2 zum Register R7 addieren, schreibt man ADD R0, R7, #2.


    Will man 1 zum Register R7 addieren und in Register R7 das Ergebnis haben, schreibt man ADD R7, R7, #1

    Und das sollte bekannt sein ! Es ist ja i.P. der wichtige INX, INY Befehl des 6502 - nur daß man das Zählregister selbst wählen kann.

    Konsequenterweise gibt es dann keine Inkrement/Dekrement Befehle als "Extras".

    (Dekrement wäre SUB R7, R7, #1)


    Um eine Zahl in ein Register zu schreiben, benutzt man entweder den Load Befehl oder MOV.

    MOV R7, #123

    bringt den Startwert 123 nach R7.

    MOV R0, R1

    bringt den Inhalt von R1 nach R0 (das Erste ist immer das Ergebnisregister).

    Und auch das ist i.P. bekannt: Es entspricht ja den TAY, TAX, TXA, TYA Befehlen, nur wieder viel genereller. Man kann jeden Registerinhalt in jedes andere nach freier Wahl kopieren.

    Und: Man kann zusätzlich noch "austauschen" - Der Swap Befehl SWP tauscht die Inhalte zweier Register (SWP R0, R1).



    Und nun kommt eine Besonderheit, des ARMs - die so besonderes gar nicht ist, aber an einer interessanten Stelle sitzt. Man kann nämlich i.a. einen Operanden direkt IM GLEICHEN BEFEHL shiften. Meist ist das der letzte angegebene.

    MOV RO, R1 LSL#2

    macht einen LogicShiftLeft um zwei Stellen mit dem Register R1 - multipliziert den Wert aus R1 also mit 4 - und kopiert ihn nach R0.


    Das Gleiche geht mit anderen Befehlen wie

    ADD R3, R5, R6 LSR#4

    was R6 durch 16 teilt, dann diesen Wert zu dem in R5 addiert und das Ergebnis in R3 speichert.


    Und das geht dann auch z.B. bei den Multiplikationsbefehlen (MUL, MLA) und auch bei den Logik-Befehlen AND, ORR, EOR

    Ein Bit umschalten, wird so einfach zu einem

    EOR R7, R7, #1 LSL#7 (wobei man das so wohl eher nicht benutzt)

    und

    ORR R2, R9, R9 LSL#16 verschiebt bei einem 32Bit Wert in R9 das untere Nibble in den oberen Bereich und kopiert das Ergebnis nach R2.


    All diese SHIFTs kosten keine zusätzliche Rechenzeit.

    Und man hat verschiedene SHIFT Befehle, ASL, ASR, LSL, LSR, ROR, RRX.

    Also: arithmethisches Shiften li/re, logisches Shiften li/re, Rotieren und Rotieren mit CarryFlag.



    Es fehlen noch Vergleiche und Flags.

    Die gibt es auch - und wieder mit ein paar Besonderheiten.


    CMP vergleicht zwei Sachen und setzt die Flags entsprechend. Also z.B. CMP R0, R1.

    CMN vergleicht ebenfalls - aber der zweite Wert wird vorher mit NOT invertiert. CMN R0, R1 ist also eine Art CMP R0, NOT(R1).


    Auf die Flags reagiert man dann wie am 6502 mit den Sprungbefehlen, den relativen Sprüngen. Und die heißen eigentlich auch genauso, wie dort:

    Es wird immer ein B vorangestellt - wie Branch - und dann die Flagauswertung, z.B. NE - Not Equal - Zero Flag gesetzt. Es gibt z.B. neben

    BNE noch BMI, BPL (Minus/Plus) oder BCC, BCS (CarryClear/Set) oder BVC,BVS (OverflowFlag Clear/Set). Bekannt...

    Und es existieren andere Kombinationen von Flags bzw. sinnvolle Bezeichungen wie

    BGE (greater-than-or-equal), BGT (greater-than), BLE (lower-than-or-equal), BLT (lower-than) - also hier Kombis aus Carry und Zero-Flag.


    Das "Lustige" ist jetzt, der ARM kann nicht nur die Sprungbefehle in dieser Art, sondern quasi so ziemlich jeden Befehle durch Anfügen der Flagauswertungen in Abhängigkeit von den gerade gesetzten Flags ausführen oder eben nicht.

    Man muß sich also den Sprungbefehl BNE eigentlich als einen Sprung vorstellen, der durch Flags eingeschränkt wird.

    Genauso kann man aber auch einen Addierbefehl dadurch einschränken, indem man schreibt: ADDNE - addiere wenn Zero-Flag nicht gesetzt oder ADDEQ - addiere wenn Zeroflag gesetzt oder ADDPL - addiere wenn das Vorzeichenflag Plus anzeigt. Sowas nennt man Conditional Execution.

    Und das wird unterstützt dadurch, daß man Flags NICHT SETZEN MUSS ! Soll heißen, die normalen Befehle fassen die Flags überhaupt nicht an, wenn man es nicht erlaubt. So wird ein ADD R0,R1,#$FFFFFFFF zwar das Register R1 ziemlich sicher überlaufen lassen, aber das CarryFlag bleibt davon erstmal unberührt. Das Gleiche bei MOV R4,#0 - sollte ja definitv eine "0" ergeben, aber das Zero-Flag wird nicht automatisch gesetzt.

    Wenn man das haben möchte, hängt man noch was an den Befehl an, nämlich ein "S" - wie "Set Flags". D.h.

    ADDNES R0, R4, R5 LSL#4

    wird nur ausgeführt, wenn NE gilt (kein Zero-Flag). Es setzt dann aber selber wieder die Flags - wegen "S".

    Dabei shiftet es R5 4 Stellen nach links (also R5 mal 16) und addiert den Wert zu R4. Das Ergebnis kommt nach R0. Wenn dabei 0 entsteht, wird das Zero-Flag gesetzt, bei Überlauf das Carry-Flag usf.


    Dadurch ergeben sich Kombinationen, wie

    ADDS R0,R0,#8

    ADDNE R1,R1,#1

    ADDEQ R2,R2,#1

    BEQ irgendwohin

    weiter


    wobei nur das erste ADDS die Flags ändert und R0 um +8 hochzählt. Dann durch das zweite ADDNE das R1 erhöht wird, wenn keine "0" entstanden ist und später ein Sprung kommt, der aber das Zero-Flag aus dem ersten Kommando auswertet - das ADDNE hat ja keine Flags gesetzt.

    Was macht das dritte ADDEQ ? Was wird hier gezählt ?


    Nur das CMP ( CMN ) Kommando setzt immer Flags.



    Sprünge über den gesamten Adressraum macht man mit B $Adresse und wenn man eine Subroutine, dort allerdings nicht so weit weg (bis ca. +/- 32 MByte), anspringt mit BL $Adresse (Branch with Link). Dabei wird einfach die aktuelle Adresse im Register R14 gemerkt. Und da das Register R15 generell als Programmcounter fungiert, kann man durch einfaches Umkopieren wieder zurückspringen ins Hauptprogramm: MOV R15, R14 oder allgemeiner MOV PC, R14. Quasi ein RTS.

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

  • Das Register R13 wird meist als Stackpointer benutzt, zeigt also auf den Stack.

    Und damit der einfacher bedienbar wird, kann man nicht nur einen Kellerspeicher haben wie beim 6502, sondern beim Abspeichern eines Wertes angeben, ob die Adresse in R13 vor oder nach der Operation erhöht oder erniedrigt werden soll. IB - increment before, IA - increment after, DB - decrement before, DA - decrement after.

    Wieviel Bytes die Veränderung groß sein muß, errechnet der Befehl selbst. Wenn also 4 Register auf den Stack kommen, dann wird für 4 Register die Adresse erhöht. Da man die normale Speicheranweisung - STM - benutzt, kann man das auch mit jedem anderen Register als Pointer machen.


    STMIB R13, {R0,R1,R5-R8}

    speichert die Register R0 und R1 und R5, R6, R7, R8 in einem Rutsch auf die Adresse in R13, die aber vorher um sechs "Plätze" erhöht wird.

    Bemerkenswert ist, daß der Wert in R13 dabei unverändert bleibt. Nur durch Anfügen eines "!" wird der neue Stackpointer auch zurückgeschrieben nach R13.

    STMDA R13!, {R0,R1,R7}

    speichert also R0, R1 und R7 auf die Adresse ab der in R13 und erniedrigt den Wert jeweils danach (DA). Außerdem wird der neue Wert in R13 behalten.

    Das entspricht in der Version STMDA R13!, {R0} in etwa einem PHA auf dem 6502, nur hier könnten es eben auch 3 Register sein, oder mehr.

    Ein PHA:TXA:PHA:TYA:PHA ist also auf ARMs eine einzige StoreAnweisung.


    Laden geht genau andersherum, mit LDM für LoadMultiple.

    LDMIB R13!, {R0,R1,R7}

    holt die Register wieder ab.


    Und weil es bequeme Leute auf dieser Welt gibt, werden die Stackmodi auch noch extra benannt mit ED - empty descending - die Kellerspeichervariante (descending), die auf das nächste freie Feld (empty) zeigt; so wie beim 6502. Ein LDMED R13!,{R0,R1,R7} macht also das Gleiche wie eben, und gehört zu einem STMED R13!,{R0,R1,R7}, was einfach eine andere Bezeichung für das dafür zu benutzende STMDA ist.

    Es gibt dementsprechend auch Speicher, die aufs volle Feld zeigen (full) und welche die aufsteigend (ascending) sind.

    Und davon dann alle Kombinationen: FA, FD, EA, ED.


    Ach ja, die Speicherbefehle sind natürlich auch "conditional executes": eine STMCSED R13!,{R0-R3} ist ein Stack, der "empty descending" ist (wie 6502), R0 bis R3 abspeichert, aber nur, wenn das CarryFlag gesetzt ist.



    Für Einzelregister gibt es auch was: die LDR und STR Befehle.

    Denen gibt man einfach mit, welches Register man beladen möchte - und eben, wo die Information zu finden ist. Letzteres allerdings ist extrem (abartig) flexibel und erlaubt Adressierungen, die auf dem 6502 so gar nicht möglich sind. Dabei sind schon ganz einfache, wie

    LDR R0, [R5,R6]

    das Analog der komplexen auf dem 6502. Hier wird auf die Adresse in R5 der Wert aus R6 addiert und von der Ergebnisadresse wird dann etwas geladen, was am am ehesten so einem LDA ($D0),Y entspricht, mit R6 als Y.

    Man könne aber z.B. auch R6 abziehen - LDR R0, [R5,-R6] was dann schon schwieriger wird in der Umsetzung, insbesondere, wenn man R6 hochzählt und immer + und - davor wechselt.


    Man kann mit den Lade/Speicherbefeheln i.a. eine Struktur in der Größe der Register laden. Da es sich meist um 32-Bit ARMs handeln wird, sind das eben genau 32-Bit, also 4 Byte auf einmal. Es gibt allerdings auch die Möglichkeit Einzelbytes zu holen und zu speichern. Der normale RAM Zugriff geschieht zudem immer an einer Stelle, wo die Adresse so ist, daß man alle vorherigen Bytes in 32-Bit Grüppchen geladen haben könnte. Soll heißen, man kann sinnvoll nur aller 4 Bytes auf eine Speicherstelle zugreifen - und ab da immer in 4 Byte großen Häppchen.

    Das einzige was eben nicht so wirklich richtig gut geht, ist ein direktes Zugreifen auf die Adresse, so daß der Wert instantan in der CPU verrechnet wird - aber in der Adresse verbleibt. Es muß immer zuerst in ein Register geladen werden. Je nach Philosophie ist das eine ziemliche Beschränkung des Modells "Binärrechner".



    Und das war eigentlich auch schon alles.

    Es existieren noch kleine Befehle zum Bittesten oder -setzen (TEQ,TST,BIC) und zwei spezielle Subtraktionen (RSB,RSC).



    Und es gibt einen letzten SEHR wichtigen Befehl: SWI

    Dieser löst einen Softwareinterrupt aus und verzweigt nachfolgend über eine Tabelle in bestimmte definierte Routinen, die i.a. letztlich das Betriebssystem aufbauen. Also ein BRK, der aber eine Nummer mitbekommt und dann je nach Nummer unterschiedliches tut - nicht nur immer den Maschinesprachemonitor aufruft und die BREAK Meldung ausgibt, wie beim C64/C16/+4. Oder sowas wie eine Kernaltabelle - nur mit definierten Nummern statt Sprungadressen, die erst im nächsten Schritt auf echte Adressen "übersetzt" werden.



    Vielleicht ist - zumindest wenn's jemand bis hierher gelesen hat - erkennbar, daß die ARMaschine eigentlich nicht anderes als ein 6502 ist, nur mit einem besonderen Speicherzugriff (weil RISC) und mit ein paar netten Befehlen mehr (Multiplikation) v.a. aber mit einer ungeheuren Mächtigkeit durch geschicktes Ausnutzen von Dingen, die man auch beim 6502 schon hätte trennen, gezielt zuschalten, kombinieren können.

    Der Befehlsatz selbst hat quasi die gleichen Gruppen und es gibt auch eigentlich keine komplexen Befehle oder Spezialbefehle, die nur unter bestimmten Bedingungen funktionieren.


    Darum auch hier nochmal die Empfehlung: Wer Assembler machen und/oder insbesondere dies auf aktueller Maschine machen will - sollte sich das auf alle Fälle auch dort mal anschauen.

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

    Einmal editiert, zuletzt von ThoralfAsmussen ()

  • Find ich toll, dass Du jetzt auch mal sowas zeigt. Aber ehrlich gesagt, find ich effizientes ARM Assembler coden unfassbar komplex. Ich hab mal begonnen eine Arm CPU in Verilog zu implementieren, und ganz schnell erkannt dass sich das extrem auswächst, weil die Kombinationsmöglichkeiten so unglaublich vielfältig sind.


    Wenn Du allgemein mal so ganz andere CPU Architekturen ansprechen willst, dann wäre die ZPU vielleicht ein Kandidat. Gerade weil es dort gar keine allgemeinen Register gibt, sondern nur Programmcounter und Stackpointer. Ich denke, viele Anfänger können sich gar nicht vorstellen, dass man so komplexe und mächtige Programme schreiben kann.


    https://en.wikipedia.org/wiki/…rocessor)#Instruction_set