An diesem Artikel arbeitet gerade Mitglied SprinterSB.
Am besten momentan noch keine gravierenden Ergänzungen / Änderungen vornehmen. Dieser Hinweis verschwindet wenn der Autor soweit ist. Sollte dieser Hinweis länger als drei Tage auf einer Seite sein, bitte beim Autor SprinterSB per PM / Mail oder Forum nachfragen ob er vergessen wurde. |
In C versteht man unter Inline Assembler die Möglichkeit, direkt Assembler-Befehle in den Code einzufügen bzw. die eingefügten Assembler-Befehle selbst.
Neben den einzufügenden Befehlen muss beschrieben werden, welche Nebeneffekte die Befehle auf die Maschine haben und wo/wie Parameter übergeben werden, bzw. wie die Zuordnung von Variablen zu den Registern ist.
Obgleich das dazu verwendete Schlüsselwort __asm zum ANSI-C-Standard gehört, ist dies in jedem C-Compiler anders implementiert. Das gilt insbesondere für die Schnittstellenbeschreibung Variablen/Register. Dieser Artikel bezieht sich auf Inline Assembler von avr-gcc.
Inhaltsverzeichnis
Begriffe
- Assembler-Template
- Das Template (Schablone) ist ein statischer, konstanter String im Sinne von C. Es enthält die Assembler-Befehle sowie Platzhalter, in deren Stelle später die Operanden treten
- Constraint
- Die Constraints (Nebenbedingungen) beschreiben Einschränkungen an die zu verwendeten Register. Dies ist notwendig, da nicht alle Maschinenbefehle auf alle Register anwendbar sind
- Clobber-List
- Das ist eine Liste von Registern, deren Inhalt durch den Inline-Assembler zerstört wird
Syntax und Semantik
Das Schlüsselwort, um eine Inline-Assembler Sequenz einzuleiten, ist __asm (ANSI). Oft ist auch asm oder __asm__ verwendbar. Um zu kennzeichnen, daß die Sequenz keinesfalls wegoptimiert werden darf – etwa dann, wenn der Assembler keine Wirkung auf C-Variablen hat – wird dem asm ein volatile bzw. __volatile nachgestellt. Danach folgen in runden Klammern die durch : getrennten Abschnitte des Inline-Assemblers:
asm volatile (asm-template : output-operand-list : input-operand-list : clobber-list);
Abschnitte, die leer sind, können auch weggelassen werden, wenn dahinter kein weiterer Abschnitt folgt:
asm volatile (asm-template);
Oder, wenn weder Input- noch Output-Operanden gebraucht werden, aber Register oder Speicher verändert werden:
asm volatile (asm-template ::: clobber-list);
Aus Compiler-Sicht werden die Assembler-Befehle im Template parallel, also gleichzeitig ausgeführt! Dies ist zu bedenken, wenn Register sowohl als Input als auch als Output verwendet werden.
Assembler-Template
Im Template stehen die durch Zeilenumbrüche getrennten Assembler-Befehle. Das Template kann zudem %-Ausdrücke als Platzhalter enthalten, welche durch die Operanden ersetzt werden. Dabei bezieht sich %0 auf den ersten Operanden, %1 auf den zweiten Operanden, etc. Die Operanden selbst werden im zweiten und dritten Abschnitt des Templates als Komma-getrennte Liste angegeben.
Ein Platzhalter kann zusätzlich einen einbuchstabigen Modifier enthalten, um ein Register in einem speziellen Format darzustellen. Wird z.B. ein 16-Bit-Wert in den Registern r31:r30 gehalten, dann wären folgende Ersetzungen denkbar (als erstes Argument):
%0 → r30
%A0 → r30
%B0 → r31
%a0 → y
Im einfachsten Falle enthält das Templater nur einen Befehl:
"nop"
oder sogar garkeinen Befehl und lediglich einen Kommentar:
"; ein Kommentar"
Platzhalter | wird ersetzt durch |
---|---|
%n | Wird ersezt durch Argument n mit n = 0...9 |
%An | das erste (untere) Register des Arguments n (Bits 0...7) |
%Bn | das zweite Register des Arguments n (Bits 8...15) |
%Cn | das dritte Register des Arguments n (Bits 16...23) |
%Dn | das vierte Register des Arguments n (Bits 24...31) |
%an | Ausgabe des Arguments als Adress-Register, also als x, y bzw. z. Erlaubt zusammen mit Constraint b, e, x, y, z |
%~ | wird auf AVR mit Flash bis max. 8kByte durch ein r ersetzt, ansonsten bleibt es leer. Zum Aufbau von Sprungbefehlen, etwa "%~call foo" |
%= | eine für dieses asm-Template und die Übersetzungseinheit eindeutige Zahl. Zum Aufbau lokaler Sprungmarken. |
Sequenz | wird ersetzt durch Sonderzeichen |
%% | das %-Zeichen selbst |
\n | ein Zeilenumbruch zum Trennen mehrerer asm-Befehle/Zeilen |
\t | ein TAB, zur Übersichtlichkeit im erzeugten asm |
\" | ein " wird eingefügt |
\\ | das \-Zeichen selbst |
Kommentar | Beschreibung |
; Text | einzeiliger Kommentar bis zum Ende des Templates bzw. nächsten Zeilenumbruch |
/* Text */ | mehrzeiliger Kommentar wie in C |
Operanden und Constraints
Ein Operand besteht aus der Angabe des Constraints (also der Registerklasse und Kennzeichnung, ob es sich um einen Output-Operanden handelt) und dahinter in runden Klammern der C-Ausdruck, der in Register der angegebenen Klasse geladen werden soll.
Mehrere Input- bzw. Output-Operanden werden durch Komma getrennt.
Constraint | Register | Wertebereich | Constraint | Konstante | Wertebereich | |
---|---|---|---|---|---|---|
a | einfache obere Register | r16...r23 | G | Floatingpoint-Konstante | 0.0 | |
b | Pointer-Register | y, z | i | Konstante | ||
d | obere Register | r16...r31 | I | positive 6-Bit-Konstante | 0...63 | |
e | Pointer-Register | x, y, z | J | negative 6-Bit Konstante | -63...0 | |
l | untere Register | r0...r15 | M | 8-Bit Konstante | 0...255 | |
q | Stack-Pointer | SPH:SPL | ||||
r | ein Register | r0...r31 | ||||
t | Scratch-Register | r0 | ||||
w | obere Register-Paare | r24, r26, r28, r30 | ||||
x | Pointer-Register X | x (r27:r26) | ||||
y | Pointer-Register Y | y (r29:r28) | ||||
z | Pointer-Register Z | z (r31:r30) | ||||
0...9 | Identisch mit dem angegebenen Operanden Wird verwendet, wenn ein Operand sowohl als Input als auch als Output dient, um sich auf diesen Operanden zu beziehen |
Modifier | Bedeutung |
---|---|
= | der Operand ist Output-Operand |
& | diesen Operanden nicht als Input-Operanden verwenden, sondern nur als Output-Operand |
Ein Input-Operand könnte also so aussehen, wobei foo eine C-Variable ist. Als Register dient ein (je nach Typ von foo auch mehrere) obere Register, irgendwo von r16 bis r31:
"d" (foo)
Soll foo ein Output-Operand sein, der in den Registern r0...r15 landet, sieht es so aus:
"=l" (foo)
Ist foo sowohl Input als auch Output, sieht es so aus. Hier ein komplettes Beispiel, das die Nibbles von foo tauscht. Weil swap auf alle Register anwendbar ist, kann als Registerklasse r genommen werden:
unsigned char foo; ... asm volatile ("swap %0" : "=r" (foo) : "0" (foo));
Instruktionen und Constraints
Mnemonic | Constraint | Mnemonic | Constraint | Mnemonic | Constraint | Mnemonic | Constraint | |||
---|---|---|---|---|---|---|---|---|---|---|
adc | r,r | add | r,r | adiw | w,I | and | r,r | |||
andi | d,M | asr | r | bclr | I | bld | r,I | |||
brbc | I,label | brbs | I,label | bset | I | bst | r,I | |||
cbi | I,I | cbr | d,I | com | r | cp | r,r | |||
cpc | r,r | cpi | d,M | cpse | r,r | dec | r | |||
elpm | t,z | eor | r,r | in | r,I | inc | r | |||
ld | r,e | ldd | r,b | ldi | d,M | lds | r,label | |||
lpm | t,z | lsl | r | lsr | r | mov | r,r | |||
movw | r,r | mul | r,r | neg | r | or | r,r | |||
ori | d,M | out | I,r | pop | r | push | r | |||
rol | r | ror | r | sbc | r,r | sbci | d,M | |||
sbi | I,I | sbic | I,I | sbiw | w,I | sbr | d,M | |||
sbrc | r,I | sbrs | r,I | ser | d | st | e,r | |||
std | b,r | sts | label,r | sub | r,r | subi | d,M | |||
swap | r |
Clobbers
In der Komma-getrennten Clobber-Liste kann man angeben, welche Register durch den Inline-Assembler ihren Wert ändern. Ändern z.B. r2 und r3 ihre Werte, dann ist die Clobber-Liste
"r2", "r3"
Wird schreibend auf das RAM zugegriffen, dann muss man das auch mitteilen, damit RAM-Inhalte, die sich evtl. in Registern befinden, nach dem Inline-Assembler neu gelesen werden. Der Clobber dafür ist:
"memory"
Beispiel:
Es soll ein Inline-Assembler geschrieben werden, das den Inhalt zweier aufeinanderfolgender Speicherstellen austauscht. Die Adresse soll in addr stehen. Sie ist Input-Operand und muss in Register X, Y oder Z stehen, um den ld bzw. st-Befehl anwenden zu können. Die passende Constraint ist also "e". Nach der Sequenz liegt addr unverändert vor.
asm volatile ( "ld r2, %a0+" "\n\t" "ld r3, %a0" "\n\t" "st %a0, r2" "\n\t" "st -%a0, r3" : /* keine Output-Operanden */ : "e" (addr) : "r2", "r3", "memory" );
avr-gcc entscheidet sich dazu, das Z-Register für addr zu verwenden:
ld r2, Z+ ld r3, Z st Z, r2 st -Z, r3
Günstiger ist es jedoch, dem Compiler auch die Entscheidung zu überlassen, welche(s) Register als Hilfsregister verwendet werden sollen. Ein Register kann __tmp_reg__ sein, für das zweite legen wir eine lokale 8-Bit-Variable hilf an:
{ char hilf; asm volatile ( "ld __tmp_reg__, %a1+" "\n\t" "ld %0, %a1" "\n\t" "st %a1, __tmp_reg__" "\n\t" "st -%a1, %0" : "=r" (hilf) : "e" (addr) : "memory" ); }
__tmp_reg__ (also r0) brauch nicht in die Clobber-Liste aufgenommen zu werden. Um das zweite benötigte Register (hier r24, in dem hilf lebt) kümmert sich gcc und sichert es, falls nötig
ld r0, Z+ ld r24, Z st Z, r0 st -Z, r24
Vordefinierte Bezeichner und Makros
Bezeichner | Bedeutung |
---|---|
__SP_L__ | unteres Byte des Stack-Pointers, für in bzw. out |
__SP_H__ | oberes Byte des Stack-Pointers, für in bzw. out |
__SREG__ | Status-Register, für in bzw. out |
__tmp_reg__ | ein Register zur temporären Verwendung (r0) |
__zero_reg__ | ein Register, das 0 enthält (r1) |
lo8(const) | die unteren 8 Bit der Konstanten const |
hi8(const) | Bits 8...15 der Konstanten const |
hlo8(const) | Bits 16...23 der Konstanten const |
hhi8(const) | Bits 24...31 der Konstanten const |
Beispiele
nop
Mit den bisherigen Vorkenntnissen ist zu nop nicht viel zu sagen:
asm volatile ("nop");
Oder als C-Makro:
#define nop() \ asm volatile ("nop")
nop hat weder Input- noch Output-Operanden, und Register/RAM ändern sich natürlich nicht. Bei der Makro-Definition ist lediglich darauf zu achten, daß das Makro nicht mit einem ; endet, damit man den C-Code wie gewohnt mit ; schreiben kann:
if (x) nop(); else ...
swap
Ein einfaches Beispiel für swap haben wir bereits oben kennen gelernt. Der Inline-Assembler dreht die Nibbles von foo um:
unsigned char foo; ... asm volatile ("swap %0" : "=r" (foo) : "0" (foo));
Wünschenswert wäre eher, swap wie eine normale C-Funktion verwenden zu können und hinzuschreiben:
a = swap(b);
ohne daß b seinen Wert ändert. Soll b geswappt werden, dann via
b = swap(b);
zudem soll das Argument ein Ausdruck sein können:
if (b == swap (b+1)) ...
Dazu verwenden wir eine lokale Variable __x__, in die der ursprüngliche Wert x gesichert wird:
#define swap(x) \ ({ \ unsigned char __x__ = x; \ asm volatile ("swap %0" : "=r" (__x__) : "0" (__x__)); \ __x__; \ })
alternativ könnten wie eine inline-Funktion definieren:
static inline unsigned char swap (unsigned char x) { asm volatile ("swap %0" : "=r" (x) : "0" (x)); return x; }
Quellen
- Doku zur avr-libc
- Doku zu avr-gcc
- Quellen von gcc