Pic32 Assembler: Unterschied zwischen den Versionen
Zeile 249: | Zeile 249: | ||
Der Compiler ist in dem Glauben, die Multiplikation mit 42 hier ganz toll optimiert zu haben; denn er schätzt eine Multiplikation sehr teuer ein. Unsere MIPS32 M4K CPU im PIC32 hat aber eine schnelle MDU (Multiply Divide Unit), die diese Multiplikation, sofern man den Faktor in ein Register geladen hat, in einem Taktzyklus hätte erledigen können. Wem das hier nicht passt, fragt einfach '''sg''' mal nach einem ''Binary Patch''. Ansonsten sollte man wahrscheinlich einfach -Os verwenden; denn wenn eh fast alle Operationen gleich "teuer" sind, ist die Optimierung auf Geschwindigkeit fast dasselbe wie die Optimierung der Code-Größe. | Der Compiler ist in dem Glauben, die Multiplikation mit 42 hier ganz toll optimiert zu haben; denn er schätzt eine Multiplikation sehr teuer ein. Unsere MIPS32 M4K CPU im PIC32 hat aber eine schnelle MDU (Multiply Divide Unit), die diese Multiplikation, sofern man den Faktor in ein Register geladen hat, in einem Taktzyklus hätte erledigen können. Wem das hier nicht passt, fragt einfach '''sg''' mal nach einem ''Binary Patch''. Ansonsten sollte man wahrscheinlich einfach -Os verwenden; denn wenn eh fast alle Operationen gleich "teuer" sind, ist die Optimierung auf Geschwindigkeit fast dasselbe wie die Optimierung der Code-Größe. | ||
Die GCC-Selbst-Kompilierer können | Die GCC-Selbst-Kompilierer können in gcc/gcc/config/mips.c die Stelle | ||
<nowiki> | <nowiki> | ||
{ /* M4k */ | { /* M4k */ |
Version vom 17. Oktober 2012, 21:58 Uhr
Hello World
Das obligatorische "Hello World!"-Programm für MIP32 in Assembler kann man sich hier angucken. Die im Programm verwendeten Syscalls gelten für einfache Simulatoren wie SPIM oder MARS.
Ressourcen
Interessiert einen die Assemblerprogrammierung für den PIC32 sind erst mal folgende Ressourcen von großem Wert:
- MPLAB® XC32 C/C++ Compiler User’s Guide.
- MPLAB® Assembler, Linker and Utilities for PIC32 MCUs User’s Guide (ist etwas älter und beschreibt u.a. die alten Tools, aber der Assembler-Kram lässt sich übertragen)
- MIPS32® Instruction Set Quick Reference (cheat sheet)
- MIPS32™ Architecture For Programmers Volume I: Introduction to the MIPS32™ Architecture (guter Überblick)
- MIPS32™ Architecture For Programmers Volume II: The MIPS32™ Instruction Set (ausführliche Referenz)
- Noch mehr zur Mips32 Architektur
Folgendes scheint auf den ersten Blick noch interessant zu sein. Es dürfte leichter lesbar als die Befehlssatzreferenz sein. Aber hier und da gibt es auch noch einige Lücken:
Bisher Gelerntes
To assemble or not to assemble
Für einige Dinge ist es gar nicht notwendig, sich die Hände dreckig zu machen. Beispielsweise bietet der Header <xc.h>
so einiges an Makros für schon fertige Inline-Assembler Schnipsel oder __builtin
-Funktionen (Intrinsics). Auch sollte man sich mal schlau machen, was Microchip so an Bibliotheken schon anbietet. Da gibt es zum Beispiel die DSP-Bibliothek, die praktische Funktionen für die digitale Signalverarbeitung anbietet. Es steht zwar nicht direkt dabei, aber ich würde mal davon ausgehen, dass die Funktionen handoptimiert wurden und, wo sinnvoll, Gebrauch von den madd
/msub
-Befehlen (multiply-accumulate mit einem 64-Bit Akkumulator) machen, die der MIPS32-Befehlssatz bietet. So ein 64-Bit Akkumulator ist echt super, wenn man auf Fließkommazahlen verzichten und dafür Festkommazahlen einsetzen will.
Andererseits entsteht mit der Nutzung dieser Bibliotheken natürlich auch eine PIC32-Abhängigkeit wohingegen selbst geschriebener MIPS32 Code relativ universellt ist; denn den versteht ja nicht nur der PIC32. Und ein bisschen Hände schmutzig machen kann ja auch Spaß machen. :D
Segmente
Programmcode und konstante Daten kann man im Text-Segment (.text) ablegen. Änderbare, vorinitialisierte, globale Variablen hält man im Daten-Segment (.data). Für änderbare Daten, die zum Programmstart 0 sind, ist das bss-Segment (.bss). Diese Nullen müssen ja nicht im Executable/Flash gespeichert werden. Der von der Toolchain automatisch generierte Startcode initialisiert dann zur Laufzeit die Datenbereiche durch das Kopieren aus dem Flash bzw durch das Füllen mit Nullen. Dann springt er die main-Funktion an.
Register/Aufruf Konventionen
Die 32 Register können seitens der CPU eigentlich nahezu beliebig verwendet werden. Das kann man schon daran erkennen, dass sie GPR (general purpose register) genannt und diese mit $0, $1, ..., $31 durchnummeriert werden. Es gibt aber ein paar Konventionen, was die Verwendung dieser Register angeht, an die man sich spätestens dann halten sollte, falls man mit C/C++ Code interagieren will. Die von der XC32-Toolchain verwendete Konvention heißt "O32". Sie legt alternative Registernamen fest und wie diese Register bzgl Funktionsparameterübergabe, Wertrückgabe, Stackbenutzung etc verwendet werden. Siehe
- Calling_convention#MIPS aus der englischen Wikipedia
- die oben verlinkte Befehlskurzreferenz
- XC32 C/C++ Compiler User Guide, Kapitel 9 und Kapitel 10
Kurzfassung:
- Eine Funktion muss dafür sorgen, dass der Stackpointer
$sp
sowie die "saved registers"$s[0-8]
, also alle, die mit s anfangen, bei der Rückkehr wieder genau dieselben Werte halten wie vorher.$s8
hat noch den alternativen Namen$fp
(frame pointer). - Eine Funktion bekommt die ersten vier (32-Bit) Parameter in den Registern
$a[0-3]
übergeben. - Weitere Parameter werden über den Stack übergeben.
- Der Stack wird bezüglich Funktionsparameter vom Aufrufer "aufgeräumt".
- Obwohl die ersten Parameter in Register übergeben werden, wird trotzdem auf dem Stack entsprechend Platz geschaffen. Die aufgerufene Funktion hat so die Möglichkeit, sich die Parameter in diesem Speicher zu merken, falls nötig.
- Die Rückgabe wird über
$v[0-1]
erledigt (nur$v0
für ein Rückgabewert von höchstens 32-Bit, die PIC32 Toolchain legt bei 64-Bit Rückgabewerten in$v0
die niederwertigen Bits ab) - Der Stack wachst von oben nach unten und
$sp
zeigt auf das zuletzt hinzugefügte Wort. - Die Parameter stehen in der erwarteten Reihenfolge im Stack. Für den Fall dass eine Funktion 6 32-Bit Parameter bekommt, sieht der Stack so aus:
0x0($sp)
reservierter Platz für ein mögliches Backup des 1. Parameters0x4($sp)
reservierter Platz für ein mögliches Backup des 2. Parameters0x8($sp)
reservierter Platz für ein mögliches Backup des 3. Parameters0xC($sp)
reservierter Platz für ein mögliches Backup des 4. Parameters0x10($sp)
speichert den 5. Parameter0x14($sp)
speichert den 6. Parameter
- "kleinere Parameter" (8 oder 16 Bit breit) werden zu 32-Bit breiten Werten "promoted" und in die Register abgelegt bzw auf dem Stack.
- 64-Bit Parameter werden auf zwei 32-Bit Werte aufgeteilt. Die PIC32 Toolchain speichert hier das niederwertige Wort zu erst (wie man es bei Little-Endian erwarten würde).
- Die im Stackpointer gespeicherte Adresse muss durch 4, sollte aber durch 8 teilbar sein, weil der C Compiler sich auch an dieses 64-Bit Alignment hält und das vielleicht auch so erwartet.
Funktionen, die keine anderen Funktionen aufrufen, nennt man Leafs. Diese sind relativ einfach zu bauen, da man sich kaum mit dem Stack auseinandersetzen muss. Alle anderen Funktionen nennt man Stems. Stems sind etwas komplizierter, weil sie vor dem Aufruf einer anderen Funktion für Parameter Platz auf dem Stack schaffen und ggf dort die restlichen Parameter tatsächlich speichern müssen, die nicht mehr in die Register $a[0-3]
passen. Ein Stem ruft eine weitere Funktion typischerweise über jal
(jump-and-link) auf. Diese Anweisung speichert automatisch die richtige Rücksprungadresse im Register $ra
und springt zur Funktion. Da dadurch natürlich die alte Rücksprungadresse überschrieben wird, muss ein Stem diese alte Rücksprungadresse auch auf dem Stack sichern.
Dateiendung, Assemblieren mit Präprozessor
Der oben verlinkten Dokumentation kann man entnehmen, dass man seine Assembler-Dateien eine Endung mit einem großen S spendieren solle. Das erlaubt, im Gegensatz zu Assembler-Dateien mit einem kleinen s als Endung die Verwendung des C-Präprozessors. Die von der Toolchain mitgelieferten Header-Dateien sind sogar teilweise dafür gedacht, in solche Assembler-Dateien eingefügt zu werden. So darf man z.B. den <xc.h>
-Header einbinden. Dieser Header enthält unter anderem Makro-Definitionen, die es erlauben, die alternativen Registernamen ohne das Dollarzeichen zu verwenden oder eben auch die SFRs (special function registers) zu nutzen. Hier mal ein Beispiel, wie so etwas aussehen könnte:
.text # Text-Segment .globl dot32x16 # Symbol exportieren # Funktion # int64_t dot32x16(int n, const int32_t x[], const int16_t y[]); # mit Eingabe # a0 = n, die Dimension # a1 = Adresse des ersten Elements des "x-Vektors" # a2 = Adresse des ersten Elements des "y-Vektors" # und erwarteter Ausgabe # v0 = niederwertiges Wort des Rueckgabewertes # v1 = hoeherwertiges Wort des Rueckgabewertes # nach O32 mit Little-Endian-Konvention bzgl Aufteilung von # 64-Bit-Werten auf zwei 32-Bit-Register. .set reorder # GAS soll wegen der Branch-Delay-Slots selbst # Spruenge umsortieren und/oder Nops einfuegen .ent dot32x16 dot32x16: mtlo $zero # LO = 0; mthi $zero # HI = 0; beqz $a0, .L_loopEnd # if (a0==0) goto .L_loopEnd; .L_loopStart: lh $t2, 0($a2) # t2 = *(int16_t*)(a2+0); lw $t1, 0($a1) # t1 = *(int32_t*)(a1+0); addiu $a0, $a0, -1 # a0--; addiu $a2, $a2, 2 # a2+=2; addiu $a1, $a1, 4 # a1+=4; madd $t1, $t2 # HI::LO += t1*t2; bnez $a0, .L_loopStart # if (a0!=0) goto .L_loopStart; .L_loopEnd: mflo $v0 # v0 = LO; mfhi $v1 # v1 = HI; jr $ra # goto ra (Ruecksprungadresse); .end dot32x16
Lokale Sprungmarken, die nicht als Symbole in der Objektdatei landen, kann man mit dem .L-Präfix erzeugen. Funktionscode muss noch mit .ent
und .end
umschlossen werden, um ihn auch als solchen zu markieren.
Das geübte Auge mag hier entdeckt haben, dass im vorangegangenen Beispiel gar nicht auf die Branch Delay Slots geachtet wurde, die das Nachvollziehen von MIPS32-Assembler etwas erschweren. Tatsächlich führt die MIPS32 CPU den Befehl, der hinter einem Sprung steht, noch aus (mit Ausnahmen), weil er noch der Pipeline hinzugefügt wird, bevor der Sprung fertig ausgewertet wird. Allerdings bietet der GNU Assembler die Möglichkeit an, automatisch die Befehle umzusortieren beziehungsweise NOPs einzufügen, damit man seinen Code "ganz normal" aufschreiben kann und auf die Branch-Delay-Slots nicht achten muss. Das, was der Assembler hier aus diesem Code macht, wird weiter unten gezeigt. Das Umsortieren kann man aber auch abschalten.
Kompilieren tut man das Ganze dann einfach mit dem C-Driver, also xc32-gcc und nicht xc32-as. Letzterer wird wahrscheinlich automatisch vom GCC aufgerufen, nachdem der Präprozessor seinen Dienst getan hat. Wenn man den Präprozessor nicht braucht, sollte es aber auch mit dem xc32-as direkt gehen. Allerdings sind die Optionen des xc32-as ganz anders. Ein "-c" zum Generieren der Objektdatei versteht das Ding nicht. Man muss ihm manuell sagen, wie die Objektdatei, die er erzeugen kann, heißen soll.
sebi@laptop ~/Documents/ccpp/pic32tests/audio-pwm-asmtest $ xc32-gcc -c dot32x16.S sebi@laptop ~/Documents/ccpp/pic32tests/audio-pwm-asmtest $ xc32-objdump -d dot32x16.o dot32x16.o: file format elf32-tradlittlemips Disassembly of section .text: 00000000 <dot32x16>: 0: 00000013 mtlo zero 4: 10800008 beqz a0,28 <dot32x16+0x28> 8: 00000011 mthi zero c: 84ca0000 lh t2,0(a2) 10: 8ca90000 lw t1,0(a1) 14: 2484ffff addiu a0,a0,-1 18: 24c60002 addiu a2,a2,2 1c: 24a50004 addiu a1,a1,4 20: 1480fffa bnez a0,c <dot32x16+0xc> 24: 712a0000 madd t1,t2 28: 00001012 mflo v0 2c: 03e00008 jr ra 30: 00001810 mfhi v1
Man kann hier auch schön sehen, wie der Assembler die Jumps alle vorgezogen hat. In diesem Fall ist er sogar ohne weitere Nops ausgekommen. Das kommt auch ein bisschen darauf an, wie man seine Befehle selbst sortiert. Man sollte vielleicht beim Schreiben darauf achten, dass der Compiler einen Jump auch vorziehen kann. Wenn es ein bedinger Sprung ist, und die Bedingung direkt von der vorhergehenden Anweisung beeinflusst wird, wird der Assembler den Sprung natürlich nicht vorziehen sondern stattdessen ein Nop einfügen. Die Umordnungsregeln scheinen also recht einfach zu sein. sg meint, man solle dieses Feature auch ausnutzen, damit der der aufgeschriebene Code leichter nachvollziehbar werde. Um NOPs zu sparen müsse man lediglich dafür sorgen, dass die Sprungbedingung nicht direkt vorher beeinflusst wird.
Alternativ kümmert man sich selbst um den Branch-Delay-Slot und teilt dies dem Assembler über ein .set noreorder
mit.
Vergleich zur GCC Ausgabe
Wenn man den Code
#include <stdint.h> int64_t dot32x16(int n, const int32_t x[], const int16_t y[]) { int64_t acc = 0; while (n>0) { --n; acc += (int64_t)x[0] * y[0]; ++x; ++y; } return acc; }
mit -Os durch den GCC jagt, kommt dann so etwas raus (von Hand kommentiert, umsortiert und ggf mit Nops erweitert, damit man nicht über die Branch-Delay-Slots stolpert):
00000000 <dot32x16>: Pseudo-C ------------------------------------------------------------------------------------- 0: 00004021 move t0,zero t0 = 0; 8: 00004821 move t1,zero t1 = 0; t0 und t1 scheint unserer "Akkumulator" zu sein 4: 08000016 j 58 <dot32x16+0x58> goto 58; nop c: 84c30000 lh v1,0(a2) v1 = *(int16_t*)(a2+0); v0 und v1 speichern x[k] und y[k] für k=0...n-1 10: 2484ffff addiu a0,a0,-1 a0--; 14: 00023fc3 sra a3,v0,0x1f a3 = s(v0) >> 31; 18: 00e30018 mult a3,v1 Hi::Lo = (signed)a3*(signed)v1; 1c: 000357c3 sra t2,v1,0x1f t2 = s(v1) >> 31; 20: 24a50004 addiu a1,a1,4 a1 += 4; 24: 24c60002 addiu a2,a2,2 a2 += 2; 28: 71420000 madd t2,v0 Hi::Lo += (signed)t2*(signed)v0; 2c: 00003812 mflo a3 a3 = Lo; 30: 00620019 multu v1,v0 Hi::Lo = (unsigned)v1*(unsigned)v0; 34: 00001012 mflo v0 v0 = Lo; 38: 00001810 mfhi v1 v1 = Hi; 3c: 00e31821 addu v1,a3,v1 v1 = a3+v1; 40: 01023821 addu a3,t0,v0 a3 = t0+v0; 44: 00e8502b sltu t2,a3,t0 t2 = (unsigned)a3<(unsigned)t0 ? 1 : 0; 48: 01234021 addu t0,t1,v1 t0 = t1+v1; 4c: 01481021 addu v0,t2,t0 v0 = t2+t0; 50: 00404821 move t1,v0 t1 = v0; 54: 00e04021 move t0,a3 t0 = a3; 58: 5c80ffec bgtzl a0,c <dot32x16+0xc> if (a0>0) { 5c: 8ca20000 lw v0,0(a1) v0 = *(int32_t*)(a1+0); /* lw wird nur "beim Sprung" ausgeführt! */ goto C; } 60: 01001021 move v0,t0 v0 = t0; 68: 01201821 move v1,t1 v1 = t1; 64: 03e00008 jr ra goto ra; nop
So richtig toll ist das jetzt nicht, was der GCC da generiert hat. Es ist mehr als doppelt so lang (trotz des -Os Schalters) und wahrscheinlich auch einiges langsamer als der von Hand geschriebene Code von oben.
Simulation
Mit dem Java-Programm MARS kann man kleine Assemblerprogramme in einer sehr einfachen, virtuellen Maschine laufen lassen. Schrittweise Ausführung vor- und rückwärts ist möglich. Man kann sich all die Registerinhalte und den Speicher dabei anschauen. MARS ist ein interaktiver Simulator mit Source Code Editor.
MARS unterstützt die Simulation von Branch-Delay-Slots. Dies muss man in den Einstellungen explizit einschalten.
Unser PIC32 mit dem MIPS32 M4K Kern versteht "MIPS32 revision 2" oder auch "MIPS32 Release 2" genannt. In diesem Update sind zum Beispiel die Anweisungen ROTR und ROTRV zum bitweisen Rotieren wie auch SEB und SEH für die "sign extension" bei Bytes und "Halfs" (halbe Worte, 16 Bit) enthalten. Diese Befehle versteht MARS leider nicht.
Branch-Delay-Instruction wird nicht immer ausgeführt!
Ich habe gerade gelernt, dass die Anweisung hinter einem Sprung, die Branch-Delay-Instruction, nicht immer ausgeführt wird, wie ich zunächst dachte. Es gibt einige bedingte Sprungbefehle, die gegebenenfalls die im Branch-Delay-Slot stehende Anweisung rausschmeißen. Das Beispiel, was mir vorhin untergekommen ist, sieht so aus
5c80ffec bgtzl a0, irgendwohin 8ca20000 lw v0, 0(a1)
Das ist ein Branch-on-Greater-Than-Zero-Likely. Die darauf folgende Anweisung liegt im "Branch-Delay-Slot". Diese wird aber nicht immer ausgeführt. Sie wird nur bei einem Sprung ausgeführt. Falls die Sprungbedingung nicht zutrifft, wird sie aus dem "Branch-Delay-Slot" rausgeschmissen und quasi ignoriert.
Was das alles soll kann man in MIPS32 for Programmers Vol I: Introduction, Abschnitt 4.1.3.2 nachlesen. Interessanterweise empfiehlt dieses Dokument, diese "Branch-Likely"-Befehle mit der Begründung zu vermeiden, weil sie in zukünftigen Revisionen der MIPS Architektur rausfliegen würden.
XC32-GCC weiß nicht, dass wir einen PIC32 mit schneller MDU haben
Übersetzt man folgenden Code
int mul42(int x) { return x*42; }
per xc32-gcc (Version 1.10) mit -O2 -mtune=m4k kommt so etwas dabei heraus:
00000000 <mul42>: /* x = x*6 ... */ 0: 00041040 sll v0,a0,0x1 /* v = x << 1; */ 4: 000420c0 sll a0,a0,0x3 /* x <<= 3; */ 8: 00822023 subu a0,a0,v0 /* x -= v; */ /* v = x*7 ... */ c: 000410c0 sll v0,a0,0x3 /* v = x << 3; */ 14: 00441023 subu v0,v0,a0 /* v -= x; */ /* return v; */ 10: 03e00008 jr ra nop
Der Compiler ist in dem Glauben, die Multiplikation mit 42 hier ganz toll optimiert zu haben; denn er schätzt eine Multiplikation sehr teuer ein. Unsere MIPS32 M4K CPU im PIC32 hat aber eine schnelle MDU (Multiply Divide Unit), die diese Multiplikation, sofern man den Faktor in ein Register geladen hat, in einem Taktzyklus hätte erledigen können. Wem das hier nicht passt, fragt einfach sg mal nach einem Binary Patch. Ansonsten sollte man wahrscheinlich einfach -Os verwenden; denn wenn eh fast alle Operationen gleich "teuer" sind, ist die Optimierung auf Geschwindigkeit fast dasselbe wie die Optimierung der Code-Größe.
Die GCC-Selbst-Kompilierer können in gcc/gcc/config/mips.c die Stelle
{ /* M4k */ DEFAULT_COSTS },
durch
{ /* M4k */ SOFT_FP_COSTS, COSTS_N_INSNS (2)+2, /* int_mult_si */ COSTS_N_INSNS (10), /* int_mult_di */ COSTS_N_INSNS (35), /* int_div_si */ COSTS_N_INSNS (69), /* int_div_di */ 2, /* branch_cost */ 4 /* memory_latency */ },
ersetzen. Allerdings ist das dann speziell für M4Ks mit schneller MDU. Es gibt nämlich auch welche mit langsamer MDU. Wenn man es also richtig machen will, müsste man aus dem M4K zwei Prozessoren machen, z.B. "m4k" und "m4k-slowmdu". Dann darf man aber nicht vergessen, den Rest auch anzupassen wo nötig, z.B. die enum-Definition im Header sowie das Array namens mips_cpu_info_table...