Assembler Tutorial 2 - Die GBA CPU:

[ARM und THUMB] [Speicher] [Thumb Code]

Es ist wieder einmal Zeit für ein neues Tutorial und ihr habt entschieden, um welches Thema es gehen soll. Die Umfrage auf meiner Homepage ergibt bei 36 abgegeben Stimmen einen 27 Prozent Sieg für „mehr zur CPU“. Erstmal vornweg gesagt, es gibt nicht mehr so viel über die CPU zu berichten, aber viel über die Verknüpfung mit ihrer Umgebung im GBA. Weiterhin gehen wir dann noch auf die Eigenarten der Emulatoren ein, denn hier ergeben sich teilweise große Unterschiede zum Original.

ARM und THUMB

Im letzten Tutorial hatte ich es schon einmal angesprochen, die ARM-CPU besitzt zwei unterschiedlich Kodearten. Wenn man es etwas abstrakt betrachtet, hat sie eigentlich zwei Verschieden Assemblerdialekte. Jetzt stellt sich die Frage, warum eigentlich? Um diese Frage zu beantworten, gehen wir einmal die Vor bzw. Nachteile der beiden Betriebsmodies durch. Da ich im letzten Tutorial nur über den ARM-Modus geredet habe und ihr ihn sicherlich auch etwas fest im Programmieren mit ihm seid, fangen wir auch mit ihm an.
ARM Pro: |
- | Befehle mit meist 3 Operanten
- | Shift/Rollfunktionen in vielen Befehlen integriert
- | Bedingungen für die Ausführung der Befehle setzbar
- | Schnelle Aufführung auf 32Bit Bussystemen
ARM Contra: |
- | langsam bei 16Bit Speicher
- | mehr Speicherverbrauch da Befehle 4Byte lang
Kommen wir nun zum Thumb-Modus, über dessen Programmierung ich dann auch noch eingehe.
THUMB Pro: |
- | schnelle Ausführung auf 16Bit Bussystemen (ca. 160% schneller als ARM)
- | weniger Speicher da nur 2Byte pro Befehl
THUMB Contra: |
- | da die Befehle kleiner sind, haben sie eine niedrige Komplexität, deshalb fallen Funktionen wie integrierter Shift und Bedingungen pro Befehl etc. weg
- | es sind bei den meisten Befehlen nur die Register r0-r7 benutzbar

Speicher

Daraus ergibt sich natürlich die Frage, welchen Modus soll benutzt werden, der ARM-Modus lässt sich schöner Programmieren aber er ist bei 16Bit Speicher langsamer. Dadurch ergibt sich wieder die Frage, welche Speichertypen gibt es. Im letzten Tutorial habe ich schon von den Unterschiedlichen Arten des Speichers gesprochen. Für unseren Code sind aber nur drei davon wichtig. Nämlich der ROM der WRAM und der IRAM. Wie in Tutorial eins schon gezeigt, hier noch einmal die Speicheradressierung des GBA´s. Der ROM ist auf dem Cartridge und kann Maximal 32MByte groß sein, im Normalfall läuft der Code auch auf ihn ab. Dann gibt es noch den WRAM (Work-RAM), das ist ein 256KByte großer Speicher, wenn ihr den GBA schon einmal offen gesehen habt, dann könnt ihr den kleinen Speicherbaustein rechts von der AGBCPU sehen. Zu guter letzt noch der IRAM (interner Work-RAM), er ist in die AGBCPU eingebaut und ist 32KByte groß. Gehen wir etwas detaillierter auf die Speicherarten ein:
Type | Busbreite | Lesen | Schreiben | Zyklen
Cartridge ROM | 16 | 8/16/32 |  | 5/5/8 (Default)
Externer Work RAM (WRAM) | 16 | 8/16/32 | 8/16/32 | 3/3/6 (Default)
Interner Work RAM (IRAM) | 32 | 8/16/32 | 8/16/32 | 1/1/1
BIOS ROM | 32 | 8/16/32 |  | 1/1/1
I/O | 32 | 8/16/32 | 8/16/32 | 1/1/1
OAM (Sprites) | 32 | 8/16/32 | 16/32 | 1/1/1 + 1 wenn Zugriff von GBA
Palette RAM | 16 | 8/16/32 | 16/32 | 1/1/1 + 1 wenn Zugriff von GBA
VRAM (Videoram) | 16 | 8/16/32 | 16/32 | 1/1/1 + 1 wenn Zugriff von GBA
FLASH (Cartridge) | 16 | 8/16/32 | 16/32 | 5/5/8 (Default)
SRAM (für Spielestände) | 8 | 8 | 8 | 5 (Default)
Eigentlich interessieren uns im Moment nur die ersten drei, über die anderen werden wir später noch sprechen. Sehen wir uns nun einmal die Busbreite unsere Speicher an, wie wir sehen können hat der Cartridge ROM und der WRAM nur einen 16Bit breiten Bus, hier bietet sich eigentlich Thumbcode an. Die Speicherzyklen sind beim ROM schlechter als beim WRAM, aber hier kommt es auch darauf an, welchen Typ von ROM man benutzt. Die Zyklenzeit des WRAM kann sogar über die Software geändert werden, aber das sollte man lassen, da es nicht garantiert werden kann, dass auf jedem GBA schnellere Timings auch funktionieren. Der IRAM hat einen 32Bit breiten Bus und eine 1/1/1 Zyklenzeit, somit ist er der schnellste RAM im GBA. In ihm läuft also ARM-Code gleich schnell wie Thumb-Code ab, also benutzen wir immer im IRAM unsere ARM-Code, da er mächtiger als Thumb ist. Stellt sich nun die Frage soll man in ROM und WRAM Thumb-Code benutzen. Ich persönlich hallte schon einmal gar nichts davon, im WRAM überhaupt Code zu platzieren Da er mit nur 256KByte schon recht klein ist und hier auch unsere Variablen bzw. Datenarrays hinein müssen. Der Geschwindigkeitsunterschied zwischen ROM und WRAM ist so gering, dass wir auf dem ROM ausweichen können. Nun zum ROM, optimaler ist hier in Thumb zu schreiben, aber ist das nun auch wirklich besser. Ich persönlich schreibe ungern Thumb, denn meistens schreibt man in mehr Code um eine Sache zu formulieren die in Armcode viel kürzer zu schreiben wäre. Weiterhin stellte sich die Frage, sind einige Programmteile zeitkritisch oder nicht. Ich selber bin zu dem Schluss gekommen, dass ich sehr Rechenintensive Programmteile in den IRAM auslagere. Der restliche Code läuft im ROM als Armcode ab und wenn es nötig ist schreibe ich hier Teile auch in Thumb. Da Nintendo wohl auch Probleme mit den ROM Geschwindigkeiten hatte, habe sie einen Speziellen Modus in den GBA integriert. Den so genannten Prefetch Buffer, gehen wir näher auf seine Vor und Nachteile ein. Der Vorteil ist, dass die CPU schon Daten aus dem ROM liest, wenn der Bus frei ist und dann wenn diese Daten benutzt werden sie schon in einen internen schnelle Puffer befinden. Somit steigt die Geschwindigkeit, dadurch ist der ROM durchschnittlich schneller als der WRAM. Der Nachteil ist, dass dieses Verfahren mehr Strom benötigt, angeblich sollen 10% mehr Verbraucht werden. Man kann aber auch an anderen Stellen Strom sparen indem man Busywaits (in einer Schleife solange bleiben bis ein Ereignis eintritt ) vermeidet. Dazu aber später mehr. Um diesen Modus (Prefetch Buffer) zu aktivieren, muss man im Register $204 das Bit 15 aktivieren. Register $204 ist relative zu der allgemeinen Registeradresse $4000000 zu sehen. Er trägt den Name WSCNT, welches soviel heißt wie Waitstat Control. Hier die Übersicht des Registers. Register $204 (WSCNT) F E D C B A 9 8 7 6 5 4 3 2 1 0 (BIT) G P X C C N K K M J J L I I S S S = SRAM WaitControl 00 = 4 Zyklen 01 = 3 Zyklen 10 = 2 Zyklen 11 = 8 Zyklen I/J/K = Wait Stat 0/1/2 First Access 00 = 4 Zyklen 01 = 3 Zyklen 10 = 2 Zyklen 11 = 8 Zyklen L/M/N = Wait Stat 0/1/2 Second Access 0 = 2 Zyklen 1 = 1 Zyklus C = PHI Terminal Output 00 = Disable 01 = 4.19 MHz 10 = 8.38 MHz 11 = 16.76 MHz P = Prefetch Buffer 0 = off 1 = on G = Game Pak Type Flag 0 = GBA 1 = CGB In diesem Register kann man auch gleich die Zyklen für seinen Rom einstellen. Ich nehme Standardmäßig den wert $4317, damit habe ich optimale Ergebnisse. Also würde ich z.B. diesen Wert so setzen:
0 |
1 | mov r6,#$4000000 ;basis
2 | orr r6,r6,#$200 ;+$200
3 | mov r0,#$4300 ;
4 | orr r0,r0,#$17 ;r0 = $4317
5 |
6 | strh r0,[r6,4] ;[r6+4]=r0
7 |
Bevor wir uns mit den Thumbcode befassen, noch einige Worte zu Emulatoren. Diese haben natürlich Probleme mit dem Speichertimings, besonders aber ist zum Beispiel beim VisualBoyAdvance Code im WRAM und IRAM immer gleich schnell und zwar auf IRAM Geschwindigkeit. Den Emulator ist es egal ob der Code im Prefetch Modus läuft oder nicht er ist immer gleich schnell und das Schlimme ist, er ist immer schneller als auf dem Original GBA! Hier muss immer auf der Original Hardware getestet werden. Ich hatte selber schon Programme die brauchten auf diesen Emulator die halbe Rechenzeit als auf dem GBA. Der Emulator BatGBA hat eine bessere Zyklenemulation, er reicht meist zum genaueren Testen aus. Sehen wir uns nun folgendes Bild an, es soll eigentlich nur den Zeitverbauch eines Programms in den verschiedenen Arten des Speichers anzeigen. Der rote Balken ist immer der Zeitverbrauch des Armcodes, der grüne der Zeitverbrauch des Thumbcodes. Der Thumbcode und der Armcode haben in beiden Fällen die gleiche Anzahl der Befehle und machen auch exakt das gleiche. Anzumerken ist, dass der ROM mit normalen Timing und „Prefetch Buffer“ läuft. Schön zu sehen ist, dass der Thumbcode im ROM erwartungsgemäß schneller als der Armcode ausgeführt wird. Da die Befehlsanzahl gleich ist, ist die Geschwindigkeit doppelt so hoch. Im IRAM sieht es ganz anders aus, hier kann die CPU mit vollen 32Bit ihre Befehle holen. Der Vorteil des Thumbcodes verschwindet und da er auch nicht so mächtig ist, ist bei steigender Komplexität der Thumbcode im Nachteil. Es sei denn den Programmierer geht es um die Größe des Codes. Über den WRAM braucht man nicht viel zu sagen, er verhält sich ähnlich wie der ROM. Bevor es mit den Programmieren los geht noch schnell etwas sehr wichtiges, ihr müsst natürlich euren Code den ihr im IRAM ausführen wollt, dort auch hinkopieren. Also aus dem ROM in IRAM, somit ergibt sich aber ein Problem, die Adressen der Variablen stimmen nicht. Deshalb gibt es bei den Goldroad Assembler die Direktiven @textarea und @endarea. Hinter dem Wort @textarea gebt ihr die Adresse an, die ab jetzt für alle Variablen als Basis gilt. Mit @endarea wird eine solche Grenze wieder geschlossen. Hier ein Beispiel, wie man Variablen IRAM-Tauglich bekommt.
0 |
1 | @textarea $03000000
2 |
3 | .
4 | .
5 | .
6 |
7 |
8 | @endarea
9 |
10 |
Der Adressbereich des IRAM ist von $3000000 an (siehe Tutorial 1) Zwischen diese Grenzen kann man dann seinen Code schreiben. Allerdings muss man am Anfang diesen noch in den IRAM kopieren z.B. so:
0 |
1 |
2 | ;**** kopiere iramcode
3 |
4 | ldr r0,[startofiwramcodeadr]
5 | mov r1,#$3000000
6 | ldr r3,[endofiwramcodeadr]
7 | sub r2,r3,r0
8 | bl dmacpy
9 |
10 | .
11 | .
12 |
13 | startofiwramcodeadr @dcd startofiwramcode
14 | endofiwramcodeadr @dcd endofiwramcode
15 |
16 | .
17 | .
18 |
19 | startofiwramcode
20 | @textarea $03000000
21 |
22 | .
23 | .
24 | .
25 |
26 | @endarea
27 | endofiwramcode
28 |
Natürlich kann man so auch mehre Codes für den IRAM schreiben und je nach Bedarf nachkopieren.

Thumb-Code

Bevor wir etwas Thumb schreiben hier noch die Befehlsübersicht.
Befehl | Arm | Thumb | Name
ADC | • | • | Add with Carry
ADD | • | • | Add
AND | • | • | Logical And
B | • | • | Branch
BIC | • | • | Bit Clear
BL | • | • | Branch and Link
BX | • | • | Branch and Exchange
CDP | • |  | Coprozessor Data Processing
CMN | • | • | Compare Negative
CMP | • | • | Compare
EOR | • | • | Logical Exclusive Or
LDC | • |  | Load Coprocessor
LDM | • | • | Load Multiple
LDR | • | • | Load Register
LDRB | • | • | Load Register (unsigned Byte)
LDRBT | • |  | Load Register Translate (unsigned Byte)
LDRH | • | • | Load Register (unsigned half Word)
LDRSB | • | • | Load Register (signed Byte)
LDRSH | • | • | Load Register (signed half Word)
LDRT | • |  | Load Register Translate
LSL |  | • | Logical Shift Right
LSR |  | • | Logical Shift Left
MCR | • |  | Move to Coprocessor from ARM reg
MLA | • |  | Multiply Accumulate
MOV | • | • | Move
MRC | • |  | Move to ARM reg from Coprozessor
MRS | • |  | Move from Status Register
MSR | • |  | Move to Status Register
MUL | • | • | Multiply
MVN | • | • | Move Negative
NEG |  | • | Negate
ORR | • | • | Logical Or
POP |  | • | Pop Multiple Register
PUSH |  | • | Push Multiple Register
ROR |  | • | Rotate Right
RSB | • |  | Reverse Sub
RSC | • |  | Reverse Sub with Carry
SBC | • | • | Sub with Carry
SMLAL | • |  | Signed Long Multiply Accumulate
SMULL | • |  | Signed Long Multiply
STC | • |  | Store Coprocessor
STM | • | • | Store Multiple
STR | • | • | Store Register
STRB | • | • | Store Register Byte
STRBT | • |  | Store Register Byte Translate
STRH | • | • | Store Register half Word Translate
STRT | • |  | Store Register Translate
SUB | • | • | Sub
SWI | • | • | Software Interrupt
SWP | • |  | Swap
SWPB | • |  | Swap Byte
TEQ | • |  | Test Equivalence
TST | • | • | Test
UMLAL | • |  | Unsigned Long Multiply Accumulate
UMULL | • |  | Unsigned Long Multiply
Da sich bei vielen Befehlen aber auch die Operanten geändert haben, empfehle ich euch dringend die Befehlsreferenzen von reEject herunter zuladen. Diese sind wirklich übersichtlich und enthalten alle wichtigen Informationen, ich benutze sie natürlich selber auch. Um den Thumbcode benutzen zu können muss man der CPU auch sagen in welchen Modus sie arbeiten soll. Dies geschieht mit den befehl bx (branch and exchange). Mit ihm kann man zwischen den Modus wechseln. Bx erwartet als Operand einen Register, in diesen Register steht die Adresse zu welcher gesprungen werden soll. Und im untersten Bit eine 1 wenn in den Thumb Modus gewechselt werden soll oder eine 0 wenn es zum Arm Modus gewechselt werden soll. Noch eins ist anzumerken, ihr müsst den Assembler sagen welchen Code er erzeugen soll, mit den Direktiven @thumb und @arm macht ihr das. Beispiel:
0 |
1 | addr r0,thumb_code|1 ;wechsel in Thumb
2 | bx r0 ;und los
3 | .
4 | .
5 | @thumb
6 | thumb_code
7 | . ; mach was
8 | .
9 |
10 | addr r0,arm_code ;wechsel zu arm
11 | bx r0
12 |
13 | .
14 | .
15 | @arm
16 | arm_code
17 |
18 |
Den addr Befehl habe ich noch nicht besprochen, eigentlich gibt es ihn auch nicht, er ist eine Pseudodirektive die den Assembler sagt er soll hier ein Adresse Relativ zum Programmcounter laden soll. Wenn ihr einfach nur den Modus wechseln wollt ohne zu einer Sprungmarke zu springen könnt ihr aus schreiben:
0 |
1 | .
2 | .
3 | orr r0,pc,#1
4 | bx r0 ;in thumb modus wechseln
5 |
6 | @thumb
7 | .
8 | .
9 | mov r0,pc ;in arm modus wechseln
10 | bx r0
11 |
12 | @arm
13 | .
14 | .
15 |
Es gibt noch die Möglichkeit durch setzen des Statusregisters den Modus zu wechseln, darauf gehe ich aber nicht weiter ein. Ich werde jetzt nicht alle Befehle des Thumbcodes durchgehen, sondern nur einige die sich vom Armcode unterscheiden. Da beim Thumb die Befehle nicht von einer Bedingung abhängig ausgeführt werden können, muss man den einzigen Befehl der von einer Bedingung abhängig ist benutzen. Dieser Befehl heißt b, was für Branch steht. Alle die Assembler von anderen Maschinen kennen, können diesen Teil des Tutorial überspringen. Die Bedingungen sind die gleichen wie im Armmodus. z.B.:
0 |
1 |
2 | cmp r0,#3
3 | beq registerIstDrei
4 | .
5 | .
6 |
7 | registerIstDrei:
8 |
9 | .
10 | .
11 | .
12 |
13 |
Als kleine Ergänzung, hier noch einmal die möglichen Bedingungen: 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 Wichtig ist auch das man nicht wie beim Armcode sagen kann, dass ein Befehl Flags setzen soll oder nicht. Hier muss man genau nachsehen welcher Befehl welche Flags setzt (siehe reEject). Da es keine Schiebe bzw. Rollerweiterungen in den Befehlen gibt, wurden dafür einzelne Befehle geschaffen: asr (Arithmetisch rechst schieben) lsl (Logisch links schieben) lsr (Logisch rechst schieben) ror (Rollen rechts) zum Beispiel:
0 |
1 |
2 | mov r0,#1
3 | mov r1,#8
4 |
5 | lsl r0,r1 ;r0=r0<<r1
6 | ror r0,#2 ;r0=r0 rolle rechts um 2
7 |
8 |
Bei Konstanten ist es besonders auffällig, da man nicht wie gewohnt schreiben kann z.B. wird aus Armcode
0 | mov r0,#$1200
dieser Thumcode
0 | mov r0,#$12
1 | lsl r0,r0,#8
Ansonsten sind noch die Befehle neg, push und pop dazu gekommen. Push und Pop ersetzen somit die stmfd und ldmfd Befehle. Hier ein Beispiel für einen Unterprogrammaufruf mit Parametersicherung auf dem Stack mittels push und pop.
0 |
1 |
2 | mov r0,#6
3 | mov r1,#7
4 | bl tuwas
5 | .
6 | .
7 | tuwas
8 | push {r0,r1}
9 | .
10 | .
11 | pop {r0,r1}
12 | mov pc,lr
13 |
Mehr will ich jetzt nicht über den Thumbcode sagen, ich werde ihn in kommenden Beispielen noch einige male erwähnen. Aber eher weniger da ich ihn ungern zum Programmieren benutze. Ich glaube das Reicht erstmal zur CPU und dem Speicher, wenn ihr noch einige Sachen genauer wissen wollt dann schreibt einfach eine Email. Diese werde ich dann vielleicht in kommenden Tutorials bzw. Erweiterungen zu Tutorials besprechen.

DevGBA
www.robsite.de