Pic32 Assembler: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung |
K (→Komisch) |
||
Zeile 110: | Zeile 110: | ||
} | } | ||
</nowiki> | </nowiki> | ||
so etwas macht: | mit -O2 so etwas macht: | ||
<nowiki> | <nowiki> | ||
00000000 <mul42>: | 00000000 <mul42>: |
Version vom 13. Oktober 2012, 12:48 Uhr
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
- MIPS32® Instruction Set Quick Reference (cheat sheet)
- MIPS32™ Architecture For Programmers Volume II: The MIPS32™ Instruction Set (ausführliche Referenz)
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
Register Konventionen
Die 32 Register können seitens der CPU eigentlich nahezu beliebig verwendet werden. Das kann man schon daran erkennen, dass sie $0, $1, ..., $31 heißen. 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. Die oben verlinkte Befehlskurzreferenz beschreibt dies auch kurz. Hier und da ist sicherlich noch mehr Information dazu zu finden. Wichtig ist: Die Register $at, $k0, $k1 fasst man am Besten gar nicht an. $at ist für ein temporäres, für den Assembler reserviertes Register, mit dem er Pseudo-Instruktionen übersetzen kann, die ein solches zusätzliches Register benötigen. $k0 und $k1 sind Register, die für den Kernel reserviert sind. Also, wenn man User-Land Code schreibt, hat man mit denen nix anzustellen. Die Register $s0 bis $s7 sind die, die man selbst zu sichern hat, weil die aufrufende Funktion Änderungen an diesen Registern nicht erwartet. Die Register $t0 bis $t9 sind die temporären Register, die eine aufgerufene Funktion einfach verändern darf. Das Register $ra speichert die Rücksprungadresse. Wenn man selbst Funktionen aufruft (oft per jal
-Befehl), speichert man typischerweise die alte Rücksprungadresse sowie die alten $s?-Werte vom Aufrufer auf dem Stack, damit man sich selbst in $s? etwas merken kann.
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:
#include "xc.h" .text /* Text-Segment */ .global dot32x16 /* Symbol dot32x16 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 vom "x-Vektor" * a2 = Adresse des ersten Elements vom "y-Vektor" * 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. */ .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 = *(uint16_t*)(a2+0); */ lw t1, 0(a1) /* t1 = *( int32_t*)(a1+0); */ seh t2, t2 /* t2 = sign_extend_half(t2); */ 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
(Diesen Code habe ich bisher nicht getestet, nur durch den Assembler gejagt. Er mag also falsch sein.)
Allerdings funktioniert die Verwendung der alternativen Namen auch ohne das Einbinden der Headerdatei. Nur dann muss den Namen das Dollarzeichen vorangestellt werden, also $v0
statt v0
beispielsweise.
Lokale Sprungmarken, die nicht als Symbole in der Objektdatei landen, kann man mit dem .L-Präfix erzeugen.
Das geübte Auge mag hier entdeckt haben, dass ich (sg) mich bei dem vorangegangenen Beispiel gar nicht um die Branch Delay Slots gekümmert habe, die das Nachvollziehen von MIPS32-Assembler etwas erschweren. So, wie ich das aktuell verstehe, ist das ein Feature des GNU Assemblers, der, wenn man ihm nicht explizit .set noreorder
sagt, die Jumps automatisch gegebenenfalls vorverlegt oder ein Nop dahinter einfügt. Das, was der Assembler hier aus diesem Code macht, wird weiter unten gezeigt.
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: 10800009 beqz a0,2c <dot32x16+0x2c> 8: 00000011 mthi zero c: 84ca0000 lh t2,0(a2) 10: 8ca90000 lw t1,0(a1) 14: 7c0a5620 seh t2,t2 18: 2484ffff addiu a0,a0,-1 1c: 24c60002 addiu a2,a2,2 20: 24a50004 addiu a1,a1,4 24: 1480fff9 bnez a0,c <dot32x16+0xc> 28: 712a0000 madd t1,t2 2c: 00001012 mflo v0 30: 03e00008 jr ra 34: 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. Ich (sg) denke, man sollte dieses Feature auch ausnutzen, weil der aufgeschriebene Code dadurch leichter nachvollziehbar ist. Um Nops zu sparen muss 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.
Komisch
Was ich komisch finde, ist, dass der GCC aus
int mul42(int x) { return x*42; }
mit -O2 so etwas macht:
00000000 <mul42>: /* x*6 --> x ... */ 0: 00041040 sll v0,a0,0x1 /* r = x << 1; */ 4: 000420c0 sll a0,a0,0x3 /* x <<= 3; */ 8: 00822023 subu a0,a0,v0 /* x -= r; */ /* x*7 --> r ... */ c: 000410c0 sll v0,a0,0x3 /* r = x << 3; */ 14: 00441023 subu v0,v0,a0 /* r -= x; */ 10: 03e00008 jr ra /* return r; */ nop
Sind drei Shifts und zwei Subtraktionen wirklich "optimaler" als eine einfache Multiplikation? Eigentlich nicht! Denn laut Doku kann die CPU eine solche Multiplikation in einem Taktzyklus erledigen. Was übersehe ich da nur? Weiß der GCC auch, wieviele Taktzyklen die CPU so für diverse Operationen benötigt? Gibt es da noch irgend etwas zur Pipeline, was ich hier nicht berücksichtige? Ist der Austausch zwischen Excecution Unit und der MDU (Multiply Divide Unit) irgendwie besonders teuer? Ich raff das nicht. Bitte erklärt es mir ...