Aus RN-Wissen.de
Wechseln zu: Navigation, Suche
Balkonkraftwerk Speicher und Wechselrichter Tests und Tutorials

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.

Assembler oder Inline-Assembler?

Assembler
Längere und komplexere Code-Stücke sind komfortabler direkt in Assembler auszudrücken. Dazu schreibt man Assembler-FUnktionen, und ruft diese von C aus auf. Natürlich können auch C-Funktionen von Assembler aus aufgerufen werden. Zudem kann man im Assembler den C-Präprozessor nutzen, was bei Inline-Assembler nur auf C-Ebene geht. Der Build-Prozess wird allerdings komplizierter, da extra asm-Dateien übersetzt werden müssen.
Inline-Assembler
Mit Inline-Assembler kann man kleine Assembler-Stückchen direkt in den C-Code einfügen. Es muss dann keine Assembler-Funktion aufgerufen werden. Dies kann von der Registerverwendung her deutlich günstiger sein, da avr-gcc genau weiß, welche Register gebraucht werden und welche nicht. Eine Funktion hingegen ist eine Black Box, bei der von der Standard-Registerverwendung ausgegangen werden muss, auch wenn weniger Register in einer Funktion verwendet werden. Ein Funktionsaufruf bedeutet also meistens einen Laufzeit-Overhead im Vergleich zu einem Inline-Code, bei mehrfacher Verwendung ist eine Funktion jedoch sparsamer im Flash-Verbrauch. Legt man eine Funktion als C-Funktion an und ihren Body als Inline-Assembler (eine s.g. Stub-Funktion, von engl. Stub = Stumpf), dann übernimmt GCC das Verwalten von Funktions-Argumenten, return-Wert, etc. und man brauch sich nicht selber um die Aufruf-Konvention zu kümmern. Auch innerhalb einer Funktion kann C mit Assembler gemischt werden.

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"
Tabelle: asm-Platzhalter und ihre Modifier, Sonderzeichen
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.

Tabelle: Constraints und ihre Bedeutung
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
Tabelle: Constraint Modifier
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

Die folgende Tabelle enhält eine Auflistung von AVR-Instruktionen dazu passende Argumente bzw. Constraints. Nicht alls Shorthands sind in der Tabelle enthalten, so ist "clr Rn" nur eine Abkürzung für "eor Rn, Rn", ähnliches gilt für den Zoo von Instruktionen rund um das SREG wie branch, bit set, bit clear, etc., die im Endeffekt auf vier Instruktionen abbilden.

Tabelle: Übersicht AVR-Instruktionen und passende 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

Je nach Assembler, für den avr-gcc Code erzeugt, gibt es unterschiedliche vordefinierte Funktionen/Makros, die von Inline-Assembler aus verwendbar sind.

GNU-Assembler

Tabelle: vordefinierte Bezeichner/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 Nibbles

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__ = (unsigned char) 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;
}

swap Bytes

Werden Zahlen zwischen verschiedenen Plattformen übertragen, kann es sein, daß diese unterschiedlich dargestellt werden: Das low-Byte kann in sich im unteren Byte befinden (AVR), es kann aber auch im oberen Byte sein. Ist das so, dann müssen die Werte beim Senden/Empfang umgewandelt werden, indem die Bytes getauscht werden. Liegtn der 16-Bit-Wert in x_in und soll der konvertierte Wert nach x_out dann könnte man auf die Idee kommen, so etwas zu schreiben:

   asm volatile (
      "mov %A0, %B1"   "\n\t"
      "mov %B0, %A1"
         : "=r" (x_out)
         : "r"  (x_in)
   );

Daraus könnte folgender Code entstehen:

      	mov	r24, r25
       	mov	r25, r24

Das ist offenbar Käse! Was ist passiert?

gcc hat sich dazu entschieden, x_in in r25:r24 anzulegen. Auch x_out wird in diesen Registern angelegt. Das ist erst mal in Ordnung, wenn x_in nach dem Inline nicht mehr gebraucht wird. Allerdings wird das Inline nicht en bloc – also nicht zeitparallel – ausgeführt, sondern sequenziell. Bei gleichzeitiger Ausführung der beiden mov-Instruktionen wäre auch nichts dagegen zu sagen. Ein swap-Kommando z.B. tauscht die Nibbles gleichzeitig, und der Input-Operand kann im gleichen Register leben wie der Output-Operand, wenn der Input nicht weiter verwendet wird. Mit den beiden Bytes geht es aber nicht. Wir müssen kennzeichnen, daß sich x_out in einem Register befindet, daß nur als Output dient, was durch das & erreicht wird:

   asm volatile (
      "mov %A0, %B1"   "\n\t"
      "mov %B0, %A1"
         : "=&r" (x_out)
         : "r"   (x_in)
   );

Damit erfolgt eine korrekte Registerzuordnung:

       	mov	r18, r25
       	mov	r19, r24

Alternativ können wir wie bei swap Nibbles eine lokale Variable verwenden, und alles als (inline-)Funktion machen:

static inline unsigned short swap_16 (unsigned short x_in)
{
   asm volatile (
      "eor %A0, %B0"   "\n\t"
      "eor %B0, %A0"   "\n\t"
      "eor %A0, %B0"
         : "=r" (x_in)
         : "0"  (x_in)
   );
	
   return x_in;
}

Die eor-Sequenz tauscht die Inhalte von r24 und r25. Sie ist genauso schnell und knapp wie ein "Dreiecks-Tausch", kommt aber ohne Hilfsregister aus.

       	eor	r24, r25
       	eor	r25, r24
       	eor	r24, r25

Zugriff auf SFRs

Um auf SFRs zuzugreifen, können im asm-Template keine Defines aus avr/io.h verwendet werden, weil der Präprozessor nicht mehr über den erzeugten Assembler-Code läuft (er läuft vor dem Compilieren). Um nicht die hex-Codes des SFRs angeben zu müssen, kann man dem Inline die Adressen als Konstanten übergeben. Weil die Adressen RAM-Adressen sind, müssen sie in das _SFR_IO_ADDR Makro verpackt werden, um den Offset für den I/O-Bereich abzuziehen. tcnt1 wird auf den Inhalt von TCNT1 gesetzt:

#include <avr/io.h>
   ... 
   uint16_t tcnt1;

   asm volatile (
      "in %A0, %1"    "\n\t"
      "in %B0, %1+1"
         : "=r" (tcnt1)
         : "M" (_SFR_IO_ADDR (TCNT1))
   );

wird umgesetzt zu

	in r24, 44
	in r25, 44+1

Das nur als Beispiel. Von C aus geht das natürlich auch mit

uint16_t tcnt1 = TCNT1;

Zugriff aufs SRAM

Auf bekannte Symbole kann man direkt zugreifen:

   extern int einInt;

   asm volatile (
      "lds %A0, einInt"   "\n\t"
      "lds %B0, einInt+1"
         : "=r" (...)
   );

ergibt

	lds r24, einInt
	lds r25, einInt+1

Falls die Adresse eines Objekts zur Compilezeit bekannt ist, kann man auch die Adresse übergeben:

struct foo_t {
   int a[2], b[2];
} foo;

...
{
   asm volatile (
      "lds %A0, %1"    "\n\t"
      "lds %B0, %1+1"
         : "=r" (...)
         : "i" (& foo.b[1])
   );
}

ergibt

	lds r24, einStruct+6
	lds r25, einStruct+6+1

Falls die Adresse zur Compilezeit nicht bekannt ist, muss sie natürlich in einem Adress- oder Basisregister übergeben werden:

void blah (struct foo_t * pfoo)
{
   asm volatile (
      "ld  %A0, %a1"    "\n\t"
      "ldd %B0, %a1+1"
         : "=&r" (...)
         : "b" (& pfoo->b[1])
   );
}

ergibt

	ld  r24, Z
	ldd r25, Z+1

Labels und Schleifen

Für Schleifen und Labels können keine festen Bezeichner verwendet werden, weil ein Assembler-Schnippsel, der via Makro oder inline-Funktion in den Code eingefügt wird, nicht mehrfach vorkommen darf. Dazu kann man sich durch Einfügen von "%=" Labels zusammenbauen, etwa L_a%=, L_b%=, etc. Das "%=" wird durch eine für die Übersetzungseinheit eindeutige Zahl ersetzt, die obigen Sequenzen könnten also z.B. umgesetzt werden als L_a14 und L_b14.

Etwas bequemer ist die Verwendung einer Ziffer als Label. Beim Sprung gibt man direkt hinter der Ziffer an, in welche Richtung das Label gesucht wird. Ist das Label n, dann sucht und springt

  • nb zurück (backward)
  • nf nach vorne (forward)

Es wird zum nächsten auffindbaren Label in der angegebenen Richtung gesprungen.

Bits zählen

Dieses Assembler-Schnippsel zählt die Anzahl der gesetzten Bits in einem Byte eingabe. Die Eingabe wird nach rechts ins Carry geschoben, und das Carry zum Ergebnis dazu addiert.

static inline unsigned char count_bits (unsigned char eingabe)
{                                              
   unsigned char count;

   asm volatile ( 
      "clr %0"                "\n\t"
      "clc"                   "\n"
      "0:"                    "\n\t"
      "adc %0, __zero_reg__"  "\n\t"
      "lsr %1"                "\n\t"
      "brne 0b"               "\n\t"
      "adc %0, __zero_reg__"
         : "=&r" (count), "=r" (eingabe)
         : "1"  (eingabe)
   );

   return count;
}

Damit könnte man sich ein Parity bauen:

#define parity(x) (count_bits(x) & 1)

und hätte eine etwas schlankere (aber langsamere) Parity-Implementierung als in avr/parity.h. Ein

if (parity(foo))
   ...

wird in Assembler dann zu (r24 = eingabe, r25 = count):

	lds r24,foo
/* #APP */
	clr r25
	clc
0:
	adc r25, __zero_reg__
	lsr r24
	brne 0b
	adc r25, __zero_reg__
/* #NOAPP */
	sbrs r25,0
	rjmp .L1
        ...

Quellen

  • Doku zur avr-libc
  • Doku zu avr-gcc
  • Quellen von gcc

Siehe auch


LiFePO4 Speicher Test