GBA Assembler Tutorial 1 - Der Einstieg:

[Assembler was ist das?] [Zahlensysteme] [Laufzeitumgebung] [Stack] [Speicher] [Registermodell] [Datentypen] [Befehle] [Datentransfer] [Beispiele zum Datentransfer] [Unterprogramme] [Arithmetische Funktionen] [Logische Funktionen] [Moves] [Vergleichsfunktionen] [Multiplikationen]

Für wenn ist nun dieses Tutorial gedacht? Eigentlich für jeden, der etwas mit der GameBoyAdvance Programmierung zu tun hat. Für Programmierer von Hochsprachen könnte es in der Hinsicht Interessant sein, weil sie, falls sie noch keine Erfahrung mit Assembler haben, auch ihren Hochsprachencode so formulieren können, dass er näher an dem Prozessor ist und so auch effizienter abläuft. Für alle die Assembler lernen wollen sei gesagt, dass fast jede CPU einen anderen Befehlssatz bzw. es auch viele unterschiedliche Architekturen gibt und so nur eine gewisser Umgang mit Assembler gelernt werden kann. In diesem Tutorial werde ich versuchen die Grundlagen von Assembler auf einen ARM Prozessor zu vermitteln. Später gehe ich dann auch auf die Programmierung des GBA selber ein.

Assembler was ist das?

Viele Leute meinen Assembler sei eine Maschinensprache, haben aber unrecht damit. Maschinensprache besteht, wenn man es abstrakt betrachtet, nur aus 0 und 1, also Strom oder kein Strom. Assembler selber ist eine Definition für solche 0 und 1. So ist z.B. ein $E3A00000 eine andere Schreibweise für den Assemblerbefehl mov r0,#0 Wie man sehen kann, ist es leichter sich ein mov r0,#0 zum merken als ein $E3A00000. So, das ist Assembler :)

Zahlensysteme

Falls jetzt jemand gesagt hat $e3a0.... besteht wohl aus mehr als 0 und 1, hat er eigentlich fast recht. Es gibt in Programmiersprachen allgemein mehrere Zahlensysteme (das Dezimale kennt jeder). Das niedrigste ist das Binärsystem, es besteht nur aus 0 und 1. Nicht mehr so aktuell ist das Oktalsystem, welches von 0 bis 7 je Stelle geht. Dann kommt schon unsere Dezimalsystem von 0-9 je Stelle und jetzt das Hexadezimalsystem von 0-F. Es gibt natürlich theoretisch gesehen unendlich viele Zahlensysteme, aber Programmierer müssen nur 3 kennen. Ich gehe jetzt nur ganz kurz auf die Umrechnung von solchen Zahlensystemen ein, bzw. wie man es schnell im Kopf rechnen kann.. Umrechnung Binär in Dezimal: Binär: 0001 1010 Um diese Binärzahl umrechnen zu können, muss man wissen, welche Bedeutung die Position einer Stelle hat. Da es ein 2er System ist ergibt sich: x=a1*(2^0) + a2*(2^1) + ... + an*(2^(n-1)) Auf Deutsch: Erste Stelle mal 1 + zweite Stelle mal 2 + 3 Stelle mal 4 .... Somit ergibt sich für unser Beispiel 128*0+64*0+32*0+16*1 + 8*1+ 4*0+2*1+1*0 = 26 Dezimal Umgedreht müsste man also immer durch 2 Teilen und dann den Rest beachten. 26 : 2 = 13 Rest 0 13 : 2 = 6 Rest 1 6 : 2 = 3 Rest 0 3 : 2 = 1 Rest 1 1 : 2 = 0 Rest 1 ergibt: 11010 Selbiges geht mit Hexadezimal, wenn man durch 16 Teilt. Für alle die keinen Taschenrechner haben, gibt die Tabelle weiter unten eine Hilfestellung. Versuchen wir nun unsere 0001 1010 in eine Hexadezimalzahl umzuwandeln. In der Tabelle steht 0001 ist Hexadezimal 1 und 1010 ist A, also ergibt sich $1A. Um Zahlensysteme von einander unterscheiden zu können, schreibt man in Assembler meist ein $ vor eine Zahl, um sie als Hexadezimalzahl zu deklarieren. Bei Binärzahlen schreibt man ein %. Für alle C-Programmierer, der später genutzte Assembler versteht auch 0x. Ich weiß, dass es am Anfang nicht leicht ist sich die Zahlen zu merken, bzw. sie umzurechnen deshalb ist es für Anfänger immer gut, sich die obige Tabelle neben den Computer zu legen. Irgendwann kommt der Tag, wo die Tabelle nicht mehr gebraucht wird.

Laufzeitumgebung

Stellt sich nun die Frage, was brauche ich für Software? Erst einmal einen Editor, hier kann ich UltraEdit empfehlen, im Notfall geht auch Wordpad. Dann brauchen wir noch einen Assembler, hier empfehle ich Goldroad (www.goldroad.co.uk). Auf diesen baue ich auch in meinen nachfolgenden Programmen auf. Jetzt brauchen wir noch ein Shell, oder eine DOS-Console. Das war es, unsere Entwicklungsumgebung ist fertig. Ich würde jetzt vorschlagen, dass wir uns ein Verzeichnis mit dem Namen goldroad auf der Festplatte erstellen. In dieses kopieren wir unseren Assembler, dann erstellen wir uns ein neues Verzeichnis in dem goldroad-Verzeichnis. Nennen wir es ueb1. Dort legen wir eine Datei mit den Namen prg1.s an. Es sollte dann so aussehen: goldroad/goldroad.exe goldroad/ueb1/prg1.s Gehen wir nun mit der DOS-Console in das Verzeichnis ueb1 und geben ein ../goldroad.exe prg1.s dann erscheint „assembled successfull“. Im ueb1 Verzeichnis sollte nun ein GBA-Rom mit den Name prg1 sein.

Stack

Bevor wir richtig loslegen, noch etwas zum Stack bzw. Stapelspeicher. Für alle die wissen wie ein Stack funktioniert, können schon zum nächsten Kapitel springen. Der Stack ist eigentlich nur ein Stück Speicher, der nach der LIFO (Last in, First out) Methode benutzt wird. Praktisch gesehen heißt das: Ich habe drei Bücher A, B und C, lege nun Buch A auf den Stack dann Buch B und Buch C. Will ich nun Buch A haben, muss ich Buch C und Buch B wieder vom Stapel nehmen. Natürlich kann man dies nicht einfach so im Computer ausdrücken, hierfür nimmt man einen StackPointer (sp) oder auch Stapelzeiger genannt, zu Hilfe. Wichtig ist auch, dass der Stack im Speicher „rückwärts“ gesehen wird, d.h. das 1. Element steht ganz unten in diesem Speicherbereich. Wie lege ich nun etwas auf den Stack. Am Anfang muss natürlich der SP auf das oberste Element im Stack zeigen. Also so:
    [     ]
    [     ]
    [     ]
    [     ] <- SP
Nun legen wir unser Buch A in den Stack mit der Regel 1.erniedrige Stack 2.kopiere Element in Stack (auf den der Stackpointer zeigt) Somit ergibt sich:
    [     ]
    [     ]
    [  A  ] <- SP
    [     ]
Jetzt noch Buch B
    [     ]
    [  B  ] <- SP
    [  A  ]
    [     ]
und Buch C
    [  C  ] <- SP
    [  B  ]
    [  A  ]
    [     ]
Jetzt haben wir unsere Bücher auf dem Stack, jetzt wollen wir sie wieder zurück. Die Regel lautet: 1. hole (kopiere) Daten vom Stack (auf den der Stackpointer zeigt) 2. erhöhe Stackpointer Also los geht's: Buch C holen
    [  C  ]
    [  B  ] <- SP
    [  A  ]
    [     ]
usw. Es ist noch zu bemerken, dass Daten die vom Stack geholt werden, nicht automatisch gelöscht werden, sondern sie werden nur kopiert.

Speicher

Ich glaube hier muss hier nicht viel erklären. Nur soviel, der GBA ist eine von Neuman-Architektur, d.h. Daten und Code sind im selben Speicher. Wichtiger ist, dass wir beim GBA noch zwischen RAM und ROM unterscheiden müssen, RAM ist der Speicher den wir verändern dürfen, z.B. Grafikspeicher, WRAM... dann gibt's noch den ROM dieser Speicher kann nicht verändert werden, z.B. BIOS, Cartridge Da unser Programm und seine Daten am Anfang im ROM liegen, kann man sie somit auch nicht verändern, also muss man hier RAM benutzen. Hier mal schnell ein Überblick über die GBA-Speicherverteilung: (auf diese gehe ich dann später noch genauer ein)

Registermodell des ARM7tdmi

Der ARM7 ist eine RISC-CPU, solche Prozessoren haben viele Register. In den meisten Fällen mehr als 16. Der ARM7 hat nur anfänglich gesehen 16, dies ist nicht besonders viel. Aber der ARM hat noch andere Stärken (auf diese gehe ich dann wieder später ein.) Eigentlich interessiert uns im Moment nur das ganz linke Viereck, in diesen kann man die Register des ARM erkennen. Was sind jetzt schon wieder Register? Register sind auch nur Speicher, nur mit dem Unterschied, dass dieser Direkt in der CPU ist und nicht über Adressen angesprochen wird sondern direkt über seinen Namen. Solch ein Register ist genau 32Bit breit, also besteht er aus 4Bytes (1Byte hat 8Bit). Die Register R0-R14 sind frei (generell Purpose), d.h. man kann sie jederzeit benutzen. Hier sollte man beachten, dass das Register R13 auch den Namen SP (Stackpointer) trägt (wie oben besprochen). Der Register R14 wird auch als LR (Linkregister) angesprochen, dazu aber später mehr. Deshalb sollte man im normalen Programmgebrauch auch diese beiden Register nicht einfach so belegen. Der Register R15 ist der PC (Program-Counter, Programmzähler), seine Aufgabe ist es, die Position des Befehls im Programm zu kennzeichnen. z.B.
    Position         Befehl
    0       mov     r0,#0
    4       add     r1,r0,#4
    8       str     r1,[r0]
Wir fangen am Anfang des Programms an, also steht der PC auf 0, nach dem ersten Befehl (mov r0,#0) steht er auf 4 usw.

Datentypen

Assemblerprogrammierer habe es leicht, sie kennen nicht soviel Datentypen wie Hochsprachenprogrammierer. Wir brauchen im Allgemeinen nur diese vier:
Name | Maximalgröße
Bit | 1
Byte | 255
Wort | 65535
Langwort | 4294967299
Bei ARM sehen wir diese Bezeichnung etwas anders:
Name | Maximalgröße | Bitbreite
Byte | 255 | 8
HalfWord | 65535 | 16
Word | 4294967299 32 | 32
Da unsere Register alle 32Bit breit sind, passt in ein Register also ein ganzes „Word“ oder 2 „HalfWord“ oder 4 Bytes.

Befehle

Ein Befehl ist eine Anweisung was die CPU zu machen hat. Viele Befehle haben noch Operanden, diese enthalten die Daten, die der Befehl bearbeiten soll. Hier einige Beispiele:
            mov     r1,r0
Der Befehl ist mov, er bewirkt, dass Daten kopiert werden. Eigentlich müsste er copy heißen, da die Quelle bei einem kopieren erhalten bleibt. Wie man sehen kann sind auch zwei Operanten mit im Spiel, r1 und r0. Wichtig ist, dass beim ARM die Quelle rechts steht, also r0 und das Ziel links, also r1. Somit bewirkt der Befehl, dass der Inhalt von r0 in r1 kopiert wird. Man könnte es auch in Pseudocode so hinschreiben: r1=r0;

Datentransfer (Speicherzugriffe)

Jetzt wird es kompliziert, wenn die nachfolgenden Erklärungen nicht genau verstanden werden, ist dies nicht so schlimm, da ich dann in den Beispielen noch näher darauf eingehen werde. Da die ARMCPU auf einer "load and store" Architektur basiert, beziehen sich alle Speicherzugriffe auf load (lesen) und store (schreiben) Befehle. D.h. es ist (fast) nicht möglich, direkt oder indirekt auf den Speicher zuzugreifen ohne diese Befehle zu benutzen. Speicherinhalt laden:
0 | ldr r0,[r1] ;r0 = speicher[r1]
Es wird also der Inhalt der Speicherzelle, deren Adresse in r1 steht in r0 geladen (ldr). Also muss in r1 eine Adresse einer Speicherzelle liegen, in anderen Worten die Adresse einer Variablen. z.B.:
    Speicheradresse  Wert    Name
    0                0
    1                0
    2                0
    3                0
    4                8       VariableX
    5                0
    6                0
Ein Variablenname ist eigentlich nur ein Name für eine Speicheradresse. Also hat die Variable X die Adresse 4 und den Inhalt 8. Wenn ich also schreibe:
0 | mov r1,VariableX ;VariableX ist anderes Wort für 4
1 | ldrb r0,[r1]
ich lese den Inhalt der Variable X in r0 Eine Größenangabe ist auch möglich, d.h. man kann 8, 16 und 32 Bit Daten schreiben.
0 | ldrb r0,[r1] ;Byte
1 | ldrh r0,[r1] ;Halbwort (16Bit)
2 | ldr r0,[r1] ;Wort(32Bit)
Speicherinhalt schreiben:
0 | str r0,[r1] ;speicher[r1] = r0
Der Wert von R0 wird in die Speicherzelle geschrieben, welche auf der Adresse liegt die in R1 steht. Zu bemerken ist in diesem Fall, dass das Ziel rechts steht und nicht wie beim ARM üblich links . Darauf sollte man immer achten! Die Typengrößen sind wie beim load, also strb (store byte), strh (store half word) und str (store word). Die load und store Befehle können aber noch einige Sachen mehr. Ein konstantes Offset ist zusätzlich möglich.
0 | Ldr r0,[r1,#4] ;r0 = speicher[r1 + 4]
Logischerweise sind Offset Befehle immer "pre-indexed", Diese Konstante ist max. 12Bit breit. Es gibt auch eine extra Shiftmethode (der Assembler wird schon meckern). Es gibt noch eine Erweiterung zum "pre-indexed", nämlich ein "auto-indexing"
0 | ldr r0,[r1,#4]! ;r0 = speicher[r1 + 4]
1 | ;r1 = r1 + 4
Ein "post-indexed" ist auch vorhanden.
0 | ldr r0,[r1],#4 ;r0 = speicher[r1]
1 | ;r1 = r1 + 4
D.h. nach dem Speicherzugriff wird die Quelladresse um den gegebenen Wert erhöht. Es ist auch möglich den Index in einem Register zu übergeben, dieser Register kann auch mittels mit den Schiebeoprationen verknüpft werden. Beispiele:
0 | ldr r0,[r1,r2 lsl 2] ;r0 = speicher[r1+(r2*4)]
1 |
2 | str r0,[r1,r3] ;speicher[r1+r3] = r0
Eine schöne Sache ist auch ein Laden/Schreiben von mehreren Registern. Weiterhin gibt es ein Post-/Pre-increment und auch ein Post-/Pre-decrement. Hier nun einige Beispiele:
0 | ldmia r1,{r2,r3,r4} ;r2 = mem[r1]
1 | ;r3 = mem[r1+4]
2 | ;r4 = mem[r1+8]
0 | ldmia r1!,{r2,r3,r4} ;r2 = mem[r1]
1 | ;r3 = mem[r1+4]
2 | ;r4 = mem[r1+8]
3 | ;r1 = r1 + 4 + 4 + 4
Das ia beim ldmia bedeutend increment after (bezieht sich also auf r1). Da es nun aber 4 Möglichkeiten gibt, hier die Beschreibung: ia = increment after ib = increment before da = decrement after db = decrement before Zu beachten ist noch, dass diese Funktionen bei Stackoperationen anders heißen. (Der Stackpointer ist bei ARM als R13 definiert)
    (Stack) (Normal)  (Funktion)
    ldmed   ldmib     pre increment
    ldmfd   ldmia     post increment
    ldmea   ldmdb     pre decrement
    ldmfa   ldmda     post decrement
    stmea   stmib     pre increment
    stmfa   stmia     post increment
    stmed   stmdb     pre decrement
    stmfd   stmda     post decrement
Normalerweise wird man also die ldm-/stmed-Funktionen benutzen (store post decrement bzw. load pre increment). Beispiel:
0 | stmfd sp!,{r1,r2} ;push r1 & r2
1 | ldmfd sp!,{r1,r2} ;pop r1 & r2

2.0 Beispiele zum Datentransfer

Um diese Beispiele jetzt durchspielen zu können, sollte man die im Verzeichnis ueb2/ueb3 ansehen. In diesem Kapitel gehe ich nicht auf alle wie oben beschrieben Funktionen ein, sondern nur auf die Grundlegendsten.
Beispiel 1 (theoretisch)
Versuchen wir nun eine Zahl in eine Variable zu schreiben. Als erstes brauchen wir eine Variable, die Definition einer Variablen in Assembler (goldroad) erfolgt so:
0 | meineVariable @dcd 0
Der Varibalenname wird in der ersten Spalte angegeben, hier 'meineVariable'. Als nächstes kommt der Type der Variable. Grundlegend unterscheidet man zwischen:
    @dcb    ;8Bit
    @dcw    ;16Bit
    @dcd    ;32Bit
Also hat unsere Variable eine Größe von 4 Byte. Dann folgt noch die 0, das ist die Vorbelegung der Variable. In Pseudcode könnte man es so formulieren: meineVariable=0 Jetzt wollen wir denn Inhalt von meineVariable ändern. Also schreiben wir eine Konstante (unsere gewünschter Wert) in ein Register und dann schreiben wir diesen Wert in die Variable.
0 | mov r0,#22 ;r0 = 22
1 | str r0,[meineVariable] ;meineVariable = 22
Hätten wir eine 1 Byte große Variable, also @dcb dann hätten wir mit strb schreiben müssen. Jetzt lesen wir diesen Wert wieder zurück in einen Register:
0 | ldr r3,[meineVariable] ;r3 = meineVariable
Beispiel2
In der Praxis ergibt sich bei dem GBA das Problem, dass der Programmcode und seine Variablen im ROM stehen. Deswegen ist es auch unmöglich Variablen im ROM zu ändern. Folglich müssen sie in den RAM. Am Ende vom ueb2.s Programm stehen diese Zeilen:
0 | ;********************* WRAM DATA *********************************
1 | startofewramcode @textarea $02000000
2 | @endarea
3 | endofewramcode
Dies zeigt den Assembler wo er den WRAM zu deklarieren hat. Also schreiben wir jetzt mal eine Variable dazwischen:
0 | meineVaribale @dcd 0
Wichtig ist zu bemerken, dass eine Vorbelegung nicht ohne weiteres möglich ist, d.h. der RAM ist am Anfang leer. Jetzt kommt noch ein Problem hinzu. Die Variable ist soweit vom Code entfernt, dass die Lese-/Schreibe-Befehle nicht auf sie zugreifen können. Deshalb müssen wir etwas tricksen. Hier das Beispiel:
0 | meineVaribaleADR @dcd meineVaribale
1 | .
2 | .
3 | .
4 | meineVaribale @dcd 0
Wir erzeugen eine Hilfsvariable meineVaribaleADR, in der die Adresse von meineVariable enthalten ist. Jetzt versuchen wir eine Zahl in meineVaribale zu schreiben und diese dann auch wieder zu lesen.
0 | ldr r1,[meineVaribaleADR] ;hole Adresse in r1
1 | mov r3,#$22 ;r3=$22
2 |
3 |
4 | str r3,[r1] ;[r1]=r3 schreibe
5 | ldr r4,[r1] ;r4=[r1] lese
6 |
7 |
8 | ;**** gib Hexzahl auf Bildschirm aus
9 |
10 | mov r0,#0 ;xpos
11 | mov r1,#0 ;ypos
12 | mov r2,r4 ;wert
13 |
14 | bl drawint
Jetzt assemblieren wir dieses Programm und starten es im Emulator. Jetzt sollte in der oberen Ecke 22 stehen. Wenn jetzt noch etwas unklar ist, dann sollte etwas mit dem Programm gespielt werden, z.B. neue Variablen anlegen, ...
Beispiel 3
Ein „Hello World“ ist für einen C-Programmierer immer wieder eine Belustigung, dass können wir auch. Als erstes brauchen wir den Text:
0 | text @dcb "Hello World",0
Wie man sehen kann, ist es einfach eine String zu erzeugen. Man schreibt einfach @dcb für Bytes und dann den Text in Hochkommas, gefolgt von der 0, um die String zu terminieren. Für die Textausgabe nehmen wir die Funktion printtext die ich später noch erkläre.
0 | ldr r0,[textADR] ;hole Adresse in r1
1 | mov r1,#0
2 | mov r2,#0
3 | bl printtext
Den Text legen wir in den ROM
0 | ;*********************** ROM DATA ********************************
1 | startofromdata
2 |
3 | text @dcb "Hello World",0
4 | pic @incbin font_gba.ord
5 |
6 | endofromdata
Deswegen können wir uns auch sicher sein, dass er beim Start auch vorhanden ist. So sollte dann unserer Bildschirmausgabe aussehen: Jetzt haben wir ein Beispiel in dem man schön einen Indexzugriff zeigen kann. Wir wollen beim „Hello World“ das 'e' zum 'o' machen. Leider liegt der text im ROM also muss er in den RAM. Wir könnten hier einfach kopieren, aber wir verschieben einfach den Text in die IRAMCode (Siehe prg4.s). Um unser Ziel zu erreichen, müssen wir im Text das 2. Zeichen überschreiben (Byte 1). Beispiel:
0 | ldr r0,[textADR] ;hole Adresse in r1
1 |
2 | mov r3,'o' ;r3 = 'o'
3 | strb r3,[r0,1] ;setze r3 an r0+1
4 |
5 |
6 | mov r1,#0
7 | mov r2,#0
8 | bl printtext
Dieses Beispiel zeigt u.a. auch, dass ein Register mit einem ASCII-Zeichen vorbelegt werden kann, wir machen dies hier mit r3. Dann speichern wir r3 in die Adresse von r0 + 1 und zwar als Bytewert (strb). Dieser Index (bei uns 1) kann einen 8 Bit Wert annehmen und ist vorzeichenbehaftet. Nun kommen wir zum „post-index“. Beispiel:
0 | ldr r0,[textADR] ;hole Adresse in r1
1 |
2 | mov r5,r0 ;r5=r0
3 |
4 | mov r3,'W' ;r3 = 'W'
5 | mov r4,'a' ;r4 = 'a'
6 |
7 | strb r3,[r5],1 ;schreibe r3, r5++
8 | strb r4,[r5] ;schreibe r4
9 |
10 |
11 | mov r1,#0
12 | mov r2,#1
13 | bl printtext
Wir schreiben in Register r3 ein W und in Register r4 a. Weiterhin sichern wir uns die Adresse r0 in r5. Jetzt wollen wir das „Ho“ von unserer String durch „Wa“ ersetzen. Also speichern wir an der Stelle 0, unser r3 mit
0 | strb r3,[r5],1
Ganz hinten am Befehl steht nun eine 1, diese bedeutet, dass r5 um genau diese 1 hochgezählt wird. Deswegen sind wir dann bei dem nächsten Byte und können dort weitermachen mit:
0 | strb r4,[r5] ;schreibe r4
Bildschirmausgabe: Diese Technik macht es auch möglich auf einfache Art Speicherbereiche mit der CPU zu kopieren. Um jetzt aber mehrer Daten zu kopieren müssen wir diesen Vorgang wiederholen, deswegen benutzen wir Schleifen. Da die ARM-CPU keine Speziellen Prozessorschleifen unterstütz, bauen wir uns selber eine.
0 | mov r8,#10
1 | loop
2 |
3 | subs r8,r8,#1
4 | bne loop
Gehen wir dieses Programm einmal durch. Wir belegen ganz am Anfang das Register r8 mit 10. Dann kommt in der nächsten Zeile das Wort loop. Dies ist eine Sprungmarke, d.h. alle Wörter die in der ersten Spalte stehen sind Sprungmarken. Jetzt kommen wir zum subs, wie man sich denken kann, bedeutet es genau das r8=r8-1. Falls sich jetzt jemand wundert, dass 3 Operanden vorhanden sind und nicht wie von anderen Assemblern gewohnt 2 oder gar einer, dem sei gesagt bei den meisten RISC-Prozessoren 3-Operanden Befehle-benutzt werden. Wichtig ist auch, dass man subs schreibt und nicht sub. Das ‚s’ gibt den Befehl die Anweisung, auch die Flags zu setzen. Mit dem bne (Branch Not Equal) Fragen wir ob das Ergebnis nicht Null ist und wenn nicht gehen wir wieder zu loop. Zu den Conditions bzw. Flags noch einige Anmerkungen. Die Flags können von fast jeden Befehl gesetzt werden, man muss meist nur (im ARMModus) ein s hinter den Befehlsnamen schreiben. Die Flags selber stehen in einem Extra-Register CPSR. So etwas ist bei RISC üblich. Eines der größten Features bei der ARM-CPU ist aber, dass jeder Befehl (im ARMModus) auch eine Condition haben kann. Beispiel:
0 | cmp r8,#0
1 | subne r8,r8,#1
2 | addeq r8,r8,#1
Wir vergleichen r8 mit 0, und erhöhen r8 um 1 wenn r8 = 0 (addeq), wenn es R8 ungleich 0 ist, dann erniedrigen wir es um 1 (subne). Das eq bedeutet equal = gleich Das ne bedeutet not equal = ungleich Hier schnell alle vorhandenen Conditions:
    Name    Condition

    Eq      equal
    Ne      not equal
    cs/hs   carry set
    cc/lo   carry cleat
    mi      minus
    pl      plus
    vs      overflow set
    vc      overflow clear
    hi      higher unsigned
    ls      lower or same unsigned
    ge      greater than or equal
    lt      less than
    gt      great than
    le      less than or equal
Jetzt wissen wir wie Schleifen und Sprünge funktionieren, also wenden wir sie auch gleich an. Unsere Aufgabe ist es, ein Bild auf dem Bildschirm des GBAs darzustellen. Wir benutzen dazu den Mode3 des Gameboys, dieser Modus ist ein simpler „HiColor“ Modus in PC-Sprache, was soviel heißen soll wie, dass jedes Pixel einen eigenen RGB-Wert hat. Ein Pixel ist in diesen Modus genau 16 Bit breit und ist so aufgeteilt: xBBB BBGG GGGR RRRR R=Rot B=Blau G=Grün Wir haben somit 5 Bit pro Farbkanal, also 32 Möglichkeiten, insgesamt sind so 32768 verschieden Farben möglich. (Das oberste Bit (‚x’) ist unbestimmt) Der Gameboy hat in diesen Modus eine Auflösung von 240 x 160 Pixel, also verbrauchen wir genau 75kByte (240*160*2Byte) Grafikspeicher. Öffnet am Besten, die Datei mode3_bild.s unter den Verzeichnis „Schleifen“ Am Ende des Programms steht,
0 | pic @incbin Bild.gfx
Das bedeutet, dass eine Datei mit dem Namen Bild.gfx Binär geladen wird. Sie wird also nur eingefügt, aber nicht Assembliert. In der Mitte des Programms können wir unsere Schleife schon sehen.
0 | ldr r0,[picadr]
1 | mov r1,#$6000000
2 |
3 | mov r2,#76800/2
4 | loop
5 | ldrh r3,[r0],2
6 | strh r3,[r1],2
7 |
8 | subs r2,r2,#1
9 | bne loop
Wir holen uns also erst die Bildquelle in r0, dann unser Ziel der Bildschirmspeicher (fängt ab ,#$6000000 an) Uns reichen 76800/2 Schleifendurchläufe. Wir holen uns dann je 2 Byte von der Quelle und speichern sie in unser Ziel. Das sollten wir jetzt sehen: Kopieren wir es einmal Rückwärts
0 | ldr r0,[picadr]
1 | add r0,r0,#76800 ;+Bildgröße
2 | sub r0,r0,#2 ;-1 Pixel
3 |
4 | mov r1,#$6000000
5 |
6 | mov r2,#76800/2
7 | loop
8 | ldrh r3,[r0],-2 ;nach kopieren Adresse -2Byte
9 | strh r3,[r1],2
10 |
11 | subs r2,r2,#1
12 | bne loop
Ergebnis: Jetzt versuchen wir nur den Blauanteil des Bildes zu kopieren, hiefür nehmen wir die and-Funktion. And-Wertetabellen
    0 0 | 0
    0 1 | 0
    1 0 | 0
    1 1 | 1
And ist also nichts anderes wie ein Filter, wo wir eine 1 setzen kommt auch etwas „durch“. Also schreiben wir zwischen unser ldrh und strh einfach mal diese Zeile:
0 | and r3,r3,#$7c00 ;0111 1100 0000 0000
Ach so, alles hinter einem Strichpunkt ist ein Kommentar! Da ein Pixel wie gesagt so (xBBB BBGG GGGR RRRR)-Bitweise aufgebaut ist, lassen wir nur die B´s, also die Blauwerte durch. Nun ergibt sich unsere Maske:
            xBBB BBGG GGGR RRRR
    and     0111 1100 0000 0000
            ----------------------------------
            0BBB BB00 0000 0000
Ziemlich einfach oder? Unser Bild sieht jetzt so aus:

Unterprogramme

Kommen wir jetzt zu einem wichtigen Teil, den Unterprogramm. In vielen Hochsprachen wird immer davon ausgegangen, dass ein Unterprogramm Seine Variablen über den Stack bekommt und auch der Rücksprungpointer auf dem Stack steht. In Assembler muss man dieses Prinzip aber nicht benutzen, da es nicht immer optimal ist, bzw. bei einigen Architekturen Besonderheiten gibt. Kommen wir jetzt zu eigentlichen Unterprogrammaufruf, wie üblich muss Eine Sprungmarke vorhanden sein, an welcher sich das Unterprogramm befindet, weiterhin muss die Rücksprungadresse gesichert werden und genau hier kommen wir zu einer Besonderheit der ARM-CPU. Bei vielen Prozessoren wird die Rücksprungadresse auf dem Stack gesichert, die ARM-CPU macht dies nicht, sondern speichert diese in dem LR-Register (Link-Register). Beispiel:
0 | .
1 | .
2 | bl Unterprogramm ;berechne LR und springe
3 | .
4 | .
5 |
6 |
7 | Unterprogramm
8 |
9 |
10 | mov pc,lr ;setze PC mit Adresse aus LR
Wie man sehen kann, wird mit der bl Funktion ein Unterprogramm „aufgerufen“. Die bl Funktion berechnet dafür den Rücksprungwert und kopiert ihn in den LR, dann springt sie zum Unterprogramm (setzt den PC auf Unterprogramm). Am Ende des Unterprogramms wird dann einfach der PC auf den Wert in LR gesetzt und schon sind wir wieder zurück. Natürlich ergibt sich hier ein Problem, der LR darf im Unterprogramm nicht verändert werden, also darf auch nicht in ein Neues Unterprogramm verzweigt werden. Um dieses Problem zu beseitigen muss dann auch der LR auf dem Stack gesichert werden (in einigen Fällen kann man auch alternative einen anderen Register nehmen). Beispiel:
0 | bl unter1
1 | .
2 | .
3 | .
4 |
5 | unter1
6 |
7 | stmfd sp!,{lr}
8 |
9 | .
10 | .
11 | bl unter2
12 | .
13 | .
14 | ldmfd sp!,{pc}
15 |
16 | unter2 .
17 | .
18 | .
19 | mov pc,lr
In „unter1“ kann man sehen, dass man den lr aus dem Stack nimmt und gleich in den pc schreibt. Kommen wir nun zu den Parameterübergaben, es ist in den meisten Fällen unklug Parameter über den Stack zu übergeben. Da dies Speicher und Zeit verbraucht. Eine Übergabe über Register ist schneller. Beispiel:
0 | mov r0,#2
1 | bl multiplaktion_mit_2
2 | .
3 | .
4 |
5 | multiplaktion_mit_2
6 |
7 | add r0,r0,r0
8 | mov pc,lr
Natürlich ergeben sich bei Registerübergaben einige Probleme, man muss beachten welche Register man benutzt und sie evtl. auf den Stack sichern. Nicht in jeden Fall ist dies aber nötig, hier sollte man aber aufpassen bzw. gut dokumentieren Welche Register von welcher Funktion verändert werden, sonst kann man schnell in arge Probleme kommen. Beispiel mit Sicherung von Registern auf Stack:
0 | mov r0,#$6000000
1 | mov r1,#65536
2 | bl clearMemory
3 | .
4 | .
5 |
6 | clearMemory
7 |
8 | stmfd sp!,{r1,r2}
9 |
10 | mov r2,#0
11 | mov r1,r1 lsl 2 ;r1 = r1 / 4
12 |
13 | cloop str r2,[r0],4
14 | subs r1,r1,#1
15 | bne cloop
16 |
17 | ldmfd sp!,{r1,r2}
18 | mov pc,lr

Arithmetische Funktionen

0 | Add r3,r2,r1 ;r3 = r2 + r1
1 | Adc r3,r2,r1 ;r3 = r2 + r1 + C
2 | Sub r3,r2,r1 ;r3 = r2 - r1
3 | Sbc r3,r2,r1 ;r3 = r2 - r1 + C - 1
4 | rsb r3,r2,r1 ;r3 = r1 - r2
5 | rsc r3,r2,r1 ;r3 = r1 - r2 + C - 1
Das C steht in diesem Fall für das Carry Flag. Rsb bedeutet Reverse Subtract und *verdreht* nur die beiden Quellenoperanten.

Logische Funktionen

and,orr,eor,bic And ist ein logisches UND, orr ein logisches ODER, eor ein logisches exklusiv ODER. Bic ist eigentlich ein AND NOT, welches als Filter gesehen wie ein Bit Clear (bic) wirkt. z.B.
0 | mov r1,#40
1 | mov r2,#44
2 | bic r3,r2,r1
3 |
4 | ;1000 1000
5 | ;0000 1000
6 | ;
7 | ;=1000 0000
Natürlich arbeiten diese Funktionen bitweise.

Moves

0 | mov r2,r1;r2 = r1
1 | mvn r2,r1;r2 = ~r1
Über die move Funktionen gibt es nicht *viel* zu sagen, bis auf dass mvn move negative bedeutet. D.h. der Quelloperant wird negiert (not) und dann erst kopiert. Somit kann man ein not simulieren. Der Quelloperant kann auch eine Konstante sein, bei der es aber einige Dinge zu beachten gibt. Da die Konstante in den Befehl steckt (wie bei moveq beim 68k) kann sie nicht beliebig lang sein (Da der ARM eine feste Befehlslänge hat). In diesem Fall sind Konstanten 8Bit groß. Dies macht natürlich einige Probleme. Da aber eine Rotation/Schieben erlaubt ist, kann man einen 8Bit Wert auch nach links schieben ohne es explizit zu schreiben.
0 | mov r2,#$ab00 ;r2 = $ab << 8
Will man nun $abba in r2 schreiben:
0 | mov r2,#$ab00
1 | orr r2,r2,#$ba
Dies ist natürlich nicht immer optimal, besonders bei einigen 32Bit Zahlen wird es etwas unangenehm. In diesen Fall sollte man Werte aus dem Speicher laden.

Vergleichsfunktionen

0 | cmp r2,r1 ; r2 - r1
1 | cmn r2,r1 ; r2 + r1
2 | tst r2,r1 ; r2 and r1
3 | teq r2,r1 ; r2 xor r1
Die Flags die gesetzt werden können, sind: Z(Zero) N(Negative) V(Overflow) C(Carry)

Multiplikationen

Die ARMCPU stellt 6 Multiplikationsfunktionen zur Verfügung. mul,mla,umull,umlal,smull,smlal Wobei zu beachten ist, dass mul,mla nur ein 32Bit Ergebnis zurück liefern. Beispiel:
0 | mul r3,r2,r1 ;r3 = (r2 * r1) & $ffffffff
1 | mla r4,r3,r2,r1 ;r4 = (r3 * r2 + r1) & $ffffffff
Die restlichen Funktionen (umull,umlal,smull,smlal) speichern das 64Bit große Ergebnis in 2 Registern ab.
0 | umull r4,r3,r2,r1 ;r4:r3 = r2 * r1
1 | umlal r4,r3,r2,r1 ;r4:r3 = r2 * r1 + r4:r3
2 | smull r4,r3,r2,r1 ;r4:r3 = r2 * r1
3 | smlal r4,r3,r2,r1 ;r4:r3 = r2 * r1 + r4:r3
Das u bzw. s am Anfang stehen für unsigned und signed. Wer jetzt weiter nach Divisionsfunktionen sucht, hat leider Pech, der ARM7 bietet solche Funktionen nicht an. Doch hier hilft uns das Bios, es bietet unter vielen anderen Funktionen auch eine Divisionsfunktion an. Um diese Funktion zu benutzen muss man nur den Softwareinterrupt 6 (über den Befehl swi) starten, welcher die interne Funktion zum Dividieren aufruft. Als Quellen erwartet diese Funktion r0 und r1, wobei gilt r0/r1, zurück gibt es r0 als Ergebnis und r1 als Rest, zusätzlich wird noch in r3 der Absolutbetrag des Ergebnisses gespeichert. Beispiel:
0 |
1 | mov r0,#10
2 | mov r1,#5
3 | swi $6000 ;r0=r0/r1 r1=r0%r1 r3=abs(r0/r1)
4 |
Wie euch sicherlich aufgefallen ist muss man an den gewünschten Softwareinterrupt noch 3 Nullen anhängen. Also ist Softwareinterupt 10 gleich swi $a000.

DevGBA
www.robsite.de