RoboterNetz.de Foren-▄bersicht               
 RN-Wissen Home  -  Community Home  -  Alle Artikel -  Mitglieder -  Moderatoren  -  Bilderliste  -  Letzte ├änderungen
 Kategorien  -  Beliebte Seiten  -  Sackgassenartikel  -  Artikel ohne Kategorie  -  Neue Artikel  -  Anmelden

Inline-Assembler in avr-gcc

aus RN-Wissen, der freien Wissensdatenbank

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. Diese Beschreibung ist notwendig weil der Compiler keine Vorstellung davon hat, welche Effekte und Nebenwirkungen die Assembler-Kommandos auf Register, Variablen und Speicher haben, denn der Assembler-Schnippsel ist f├╝r den Compiler lediglich eine "Black Box".

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 den Inline Assembler von avr-gcc. Viele Erkl├Ąrungen aus diesem Artikel sind auch g├╝ltig f├╝r Inline-Assembler anderer C-Compiler der gcc-Familie. Instruktionen und Registerklassen werden sich aber unterscheiden, weil diese nat├╝rlich abh├Ąngig sind vom verwendeten Controller bzw. der Controller-Familie.

Inhaltsverzeichnis

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. Ein typischer Fall ist es, eine komplette ISR in Assembler zu schreiben.
Inline-Assembler
Mit Inline-Assembler kann man kleine Assembler-St├╝ckchen direkt in den C-Code einbetten. Es muss dann keine Assembler-Funktion aufgerufen werden. Dies kann von der Registerverwendung her deutlich g├╝nstiger sein, da gcc genau wei├č, welche Register gebraucht werden und welche nicht.
Eine (Assembler-)Funktion ist hingegen eine Black Box, bei der von der Standard-Registerverwendung ausgegangen werden muss, auch wenn weniger Register in der Funktion verwendet werden. Ein Funktionsaufruf bedeutet also meistens einen Laufzeit-Overhead im Vergleich zu einem Inline-Code.
Bei mehrfacher Verwendung einer l├Ąngeren Codesequenz 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.
Ein Vorteil von Inline-Assembler ist, da├č eine C-Funktion, die Inline-Assembler enth├Ąlt, vom C-Compiler geinlinet werden kann. Dies ist mit einer reinen Assembler-Funktion nicht m├Âglich.

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.

Ab Version 4.5 kennt GCC asm goto, mit dem ausgedr├╝ckt werden kann, dass der Codeflu├č des Assembler-Teils u.U. zu einem St├╝ck C-Code springt:

asm goto (asm-template : /* Leer */ : input-operand-list : clobber-list : C-labels);

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. Diese Ersetzung findet jedoch nur dann statt, wenn das asm nicht nur aus einem String besteht:

asm ("10% mehr");    /* "10% mehr" */
asm ("10%% mehr" :); /* "10% mehr" */

Ein Platzhalter kann zus├Ątzlich einen einbuchstabigen Modifier enthalten, um den Operanden in printf-├Ąhnlicher Manier in einem speziellen Format darzustellen. Wird z.B. ein Wert ab Register r28 (dem Y-Register) gehalten, dann w├Ąren folgende Ersetzungen denkbar (als erstes Argument):

%0     →  r28
%A0    →  r28
%B0    →  r29
%C0    →  r30
%D0    →  r31
%a0    →  y  

Im einfachsten Falle enth├Ąlt das Templater nur einen Befehl:

"nop"

oder sogar garkeinen Befehl oder lediglich einen Kommentar:

"; ein Kommentar"
Tabelle: asm-Platzhalter und ihre Modifier, Sonderzeichen
Platzhalter wird ersetzt durch AVR-spezifisch
%n Wird ersezt durch Argument n mit n = 0...9
%An das erste (untere) Register des Arguments n (Bits 0...7) X
%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
X
%in Ab 4.7. Ausgabe einer RAM-Adresse als I/0-Adresse. Beispiel: 0x30 wird auf Xmega-Controllern als 0x30 ausgegeben und auf nicht-Xmega als 0x10. X
%rn Ab 4.8. Ausgabe eines Registers ohne die Register-Pr├Ąfix "r". Damit kann per Inline-Assembler auf 64-Bit Variablen zugegriffen werden, z.B. kann mit "clr %r0+7" das High-Byte gel├Âscht werden. X
%Tn%Tm Ab 4.7. Gibt einen Doppel-Operanden aus wie er zum Beispiel in BLD oder BST ben├Âtigt wird, d.h. durch ein Komma getrennte Register- und Bitnummer. Das erste %T erh├Ąlt ein Register und speichert es zwischen bis zum n├Ąchsten %T, das die konstante Bitnummer m enth├Ąlt. Die Ausgabe erfolgt erst mit dem zweiten %T: Beispielsweite kann durch folgende Zeile das High-Bit von var gel├Âscht werden:
asm ("clt $ bld %T[v]%T[b]" : [v] "+r" (var) : [b] "n" (8*(sizeof var)-1));
X
%Tn%tm Ab 4.7. Funktioniert wie %Tn%Tm, es wird aber nur das zum Bit m geh├Ârende Register ausgegeben, d.h. weder Komma noch Bitnummer. X
%xn Ab 4.5. Gibt ein Label ohne Operand-Modifier gs() aus. X
%~ wird auf AVR mit Flash bis max. 8kiByte durch ein r ersetzt, ansonsten bleibt es leer.
Zum Aufbau von Sprungbefehlen, etwa "%~call foo"
X
%! Ab 4.4. Wird auf AVR mit Flash ab 128kiByte durch ein e ersetzt, ansonsten bleibt es leer.
Zum Aufbau von indirekten Sprungbefehlen, etwa "%!icall"
X
%= eine f├╝r dieses asm-Template und die ├ťbersetzungseinheit eindeutige Zahl.
Zum Aufbau lokaler Sprungmarken.
Sequenz wird ersetzt durch Sonderzeichen AVR-spezifisch
%% das %-Zeichen selbst
\n ein Zeilenumbruch zum Trennen mehrerer asm-Befehle/Zeilen
$ trennt mehrere Befehle in der gleichen Zeile X
\t ein TAB, zur ├ťbersichtlichkeit im erzeugten asm
\" ein " wird eingef├╝gt
\\ das \-Zeichen selbst
Kommentar Beschreibung AVR-spezifisch
; Text einzeiliger Kommentar bis zum Ende des Templates bzw. n├Ąchsten Zeilenumbruch X
/* Text */ mehrzeiliger Kommentar wie in C

Operanden und Constraints

Ein Operand besteht aus der Angabe des Constraint-Strings (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. Die Operanden werden mit 0 beginnend von links nach rechts durchnumeriert und ├╝ber diese Nummer angesprochen, um sie ins Assembler-Schnippsel einzuf├╝gen.

Mehrere Input- bzw. Output-Operanden werden durch Komma getrennt.

Tabelle: Constraints und ihre Bedeutung
Constraint Register Wertebereich   Constraint Konstante Wertebereich
aeinfache obere Registerr16...r23 GFloatingpoint-Konstante 0.0
bPointer-Register y, z iKonstante, entspricht "sn"  
dobere Register r16...r31 ssymbolischer Wert  
ePointer-Register x, y, z nWert bekannt zur Compilezeit  
luntere Register r0...r15 Ipositive 6-Bit-Konstante 0...63
qStack-Pointer SPH:SPL Jnegative 6-Bit Konstante -63...0
rein Register r0...r31 M8-Bit Konstante 0...255
tScratch-Register r0  
wobere Register-Paare r24, r26, r28, r30 Constraint Memory Wertebereich
xPointer-Register X x (r27:r26) m Memory  
yPointer-Register Y y (r29:r28)  
zPointer-Register Z z (r31:r30)
0...9Identisch 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
+ dieser Operanden ist Input- und 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 (Constraint "d"):

"d" (foo)

In den Klammern kann ein beliebiger, g├╝ltiger C-Ausdruck stehen, der beim folgenden Beispiel in irgendeinem Register landet (Constraint "r"), ohne weitere Einschr├Ąnkung an das Register:

"r" ((foo >= 0) ? foo : -foo)

Um einen Operanden als Output-Operanden zu kennzeichnen, wird dem Constraint ein "=" vorangestellt. Soll foo ein Output-Operand sein, der in den Registern r0...r15 landen soll (Constraint "l", sieht es so aus. Dabei muss foo ein sogenannter Lvalue sein, also ein Wert, dem etwas zugewiesen werden kann:

"=l" (foo)

Ist foo sowohl Input als auch Output, schreibt man foo als Output- und als Input-Operand hin. In der Input-Constraint bezieht man sich dann auf die Operanden-Nummer von foo. 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 ("swap %0" : "=r" (foo) : "0" (foo));

foo als Input-Operand soll im gleichen Register liegen wie foo als Output-Operand. Daher wird als Constraint "0" angegeben, d.h. es wird ins gleiche Register geladen wie der Operand Numero 0 "r" (foo).

Benannte Constraints

Um das Assembler-Schnippsel besser lesbar zu halten, kann man Constraints einen Namen geben:

asm ("swap %[bar]" : [bar] "+r" (foo));

Damit ist der Assembler auch unabh├Ąngig von der Reihenfolge der Operanden. Das macht die Anpassung, wenn ein neuer Operand hinzukommt, wesentlich einfacher und den Schnippsel zudem besser lesbar.

Hier wird bar als Alias f├╝r die C-Variable foo benutzt; es k├Ânnte aber auch jeder andere g├╝ltige C-Bezeichner sein, also insbesondere auch foo.

Instruktionen und Constraints

Die folgende Tabelle enh├Ąlt eine Auflistung von AVR-Instruktionen und dazu passende Argumente bzw. Constraints. Nicht alle 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 nur vier Instruktionen abbilden. Instruktionen wie nop, die keine Argumente brauchen, sind ebenfalls nicht in der Tabelle enthalten. Gleiches gilt f├╝r den Krypto-Befehl des, f├╝r die keine Constraint verf├╝gbar ist.

"load" ist ein "load from SRAM"
"store" ist "store to SRAM"

Tabelle: ├ťbersicht AVR-Instruktionen und passende Constraints
Mnemonic Constraint Bedeutung   Mnemonic Constraint Bedeutung
adcr,r add with carry addr,r add
adiww,I add immediate to word andr,r and
andid,M and with immediate asrr arithmetic shift right
bclrI bit clear in SREG bldr,I bit load from T
brbcI,label branch if bit in SREG clear brbsI,label branch if bit in SREG set
bsetI bit set in SREG bstr,I bit store from T
cbiI,I clear bit in I/O comr complement
cpr,r compare cpcr,r compare with carry
cpid,M compare against immediate cpser,r compare, skip if equal
decr decrement [e]lpmr,z load from program memory1
eorr,r exclusive-or fmul*a,a fractional multiply
inr,I input from I/O incr increment
lacz,r load and clear latz,r load and toggle
lasz,r load and set  
ldr,e load indirect ldr,e+ load indirect, post-increment
ldr,-e load indirect, pre-decrement lddr,b+I load indirect with displacement
ldid,M load immediate ldsr,label load direct from SRAM
lpmt,z load from program memory1 lsrr logical shift right
movr,r move movwr,r move word
mulr,r multiply unsigned mulsd,d multiply signed
mulsua,a multiply signed/unsigned  
negr negate orr,r or
orid,M or with immediate outI,r output to I/O
popr pop from Stack pushr push to Stack
rorr rotate right  
sbcr,r subtract with carry sbcid,M subtract with carry immediate
sbicI,I skip if bit in I/O clear sbisI,I skip if bit in I/O set
sbiI,I set Bit in I/O sbiww,I subtract immediate from word
sbrcr,I skip if bit in register clear sbrsr,I skip if bit in register set
ste,r store indirect ste+,r store indirect, post-increment
st-e,r store indirect, pre-decrement stdb,r store indirect with displacement
stslabel,r store direct subr,r subtract
subid,M subtract immediate swapr swap nibbles
xchz,r exchange  

1 Je nach Derivat werden die folgenden lpm-Auspr├Ągungen unterst├╝tzt oder nicht:

  • lpm
  • lpm r,z bzw. lpm r,z+
  • elpm
  • elpm r,z bzw. elpm r,z+

Hier noch ein paar der gebr├Ąuchlichsten Shorthands und deren Abbildung auf "Basis"-Instruktionen:

Tabelle: Shorthands
Shorthand gleichbedeutend mit Bedeutung   Shorthand gleichbedeutend mit Bedeutung
rol r adc r,r rotate left cbr r,M andi r, ~M clear bits in reg
clr r eor r,r clear reg to zero lsl r add r,r logical shift left
sbr d,M ori d, M set bits in reg ser d ldi d, 0xff set all bits in reg
tst r and r,r test against zero  

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 avr-gcc und sichert es, falls n├Âtig

       	ld	r0, Z+
       	ld	r24, Z
       	st	Z, r0
       	st	-Z, r24

Falls das Register r1 ver├Ąndert wird – was z.B. geschieht, wenn man Multiplikationsbefehle verwendet – dann muss am Ende des Templates das Register wieder auf 0 gesetzt werden, denn bei avr-gcc enth├Ąlt dieses Register immer den Wert 0. Das Register in die Clobber-Liste aufzunehen bleibt wirkungslos. Hat man es zerst├Ârt, dann schreibt man ans Ende des Templates ein

clr __zero_reg__

und stellt es dadurch wieder her.

asm goto mit C-Labels

Ab Version 4.5 kennt GCC asm goto, mit dem ausgedr├╝ckt werden kann, dass der Codeflu├č des Assembler-Teils u.U. zu einem St├╝ck C-Code springt:

asm goto (asm-template : /* Leer */ : input-operand-list : clobber-list : C-labels);

Die Codeflu├č-Analyse des Compilers muss wissen, da├č die angegebenen C-Code Sequenzen verwendet werden, damit sie nicht wegoptimiert werden. Solch ein Inline-Assembler mit goto darf keine Output-Operanden haben, denn der Compiler hat keine M├Âglichkeit, Code f├╝r die entsprechenden Output-Reloads zu erzeugen.

Beispiel
extern void __attribute__((noreturn))
panic (void);

char panic_if_x (char x)
{
    asm goto ("cpse %0, __zero_reg__" "\n\t"
              "%~jmp %x1"             "\n\t"
              "%~jmp %x2"
              :: "r" (x) :: proceed, do_panic);
    __builtin_unreachable();
    
    if (0)
    {
    do_panic:
        panic();
    }
    
    if (0)
    {
    proceed:
        x++;
    }
    
    return x;
}

Im Beispiel wird der Inline-Assembler nur ├╝ber die Marken do_panic oder proceed verlassen, weshalb der Code direkt danach unerreichbar ist. Dies wird der Codeflu├č-Analyse per __builtin_unreachable() mitgeteilt. Es wird folgender Code erzeugt:

panic_if_x:
/* #APP */
	cpse r24,__zero_reg__
	rjmp .L2
	rjmp .L3
/* #NOAPP */
.L3:
	rcall panic
.L2:
	subi r24,lo8(-(1))
	ret

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

Vordefinierte Makros und
Relocatable Expression Modifiers

 
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) Bits 0...7 der Konstanten const
hi8(const) Bits 8...15 der Konstanten const
hlo8(const)
hh8(const)
Bits 16...23 der Konstanten const
hhi8(const) Bits 24...31 der Konstanten const
pm(const) Konstante const durch 2 geteilt,
zur Berechnung von Word-Adressen f├╝r icall und ijmp.
pm_lo8(const)
pm_hi8(const)
pm_hh8(const)
Abk├╝rzung f├╝r lo8(pm(const))
Abk├╝rzung f├╝r hi8(pm(const))
Abk├╝rzung f├╝r hh8(pm(const))
gs(label) Word-Adresse von label. F├╝r Devices mit mehr als 128k Flash wird die Adresse eines Jump-Pads eingetragen. Der Linker erzeugt ein Jump-Pad, das einen direkten Sprung zu label erh├Ąlt. gs(label) wird durch diese Adresse ersetzt.

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 ("swap %0" : "+r" (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 ("swap %0" : "+r" (_x_));                          \
    _x_;                                                   \
  })

alternativ k├Ânnten wir eine inline-Funktion definieren:

static inline unsigned char swap (unsigned char x)
{
    asm ("swap %0" : "+r" (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. Liegt der 16-Bit-Wert in x_in und soll der konvertierte Wert nach x_out gespeichert werden, dann k├Ânnte man auf die Idee kommen, so etwas zu schreiben:

   asm (
      "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?

avr-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 daher kennzeichnen, da├č sich x_out in einem Register befindet, das nur als Output dient, was durch das & in der Output-Constraint erreicht wird:

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

Damit erfolgt eine korrekte Registerzuordnung. x_out steht jetzt in r19:r18 und ├╝berschneidet nich nicht mehr mit x_in:

       	mov	r18, r25
       	mov	r19, r24

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


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

Ab avr-gcc 4.7 kann auch der Operand-Modofier %i verwendet werden, um eine RAM-Adresse, wie sie von avr/io.h definiert werden, als I/O-Adresse auszugeben:

   uint16_t tcnt1;

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

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

uint16_t tcnt1 = TCNT1;

Zugriff aufs SRAM

Auf bekannte globale Symbole kann man direkt von Assembler aus zugreifen:

   extern int einInt;

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

ergibt

	lds r24, einInt
	lds r25, einInt+1

alternativ ginge

   asm volatile (
      "lds %A0, %A1"   "\n\t"
      "lds %B0, %B1"
         : "=r" (...)
         : "m" (einInt)
   );

Falls die Adresse eines Objekts zur Compilezeit bekannt ist, kann man auch die Adresse von C aus ├╝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

Auch hier muss der Output-Operand mit einem & gekennzeichnet werden, damit er nicht ins gleiche Register geladen wird wie die Adresse.

Labels und Schleifen

F├╝r Labels in Spr├╝ngen und Schleifen k├Ânnen keine festen Bezeichner verwendet werden, weil ein Label, 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 und den Code-Schnippsel eindeutige Zahl ersetzt. Die obigen Sequenzen k├Ânnten also z.B. umgesetzt werden als L_a14 und L_b14, wenn sie im gleichen Schnippsel stehen.

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 ( 
      "clr %0"                "\n"
      "0:"                    "\n\t"
      "lsr %1"                "\n\t"
      "adc %0, __zero_reg__"  "\n\t"
      "tst %1"                "\n\t"
      "brne 0b"
         : "=&r" (count), "+r" (eingabe)
   );

   return count;
}

Damit kann man sich ein Parity bauen:

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

und h├Ątte eine schlankere (aber etwas 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
0:
	lsr r24
	adc r25, __zero_reg__
	tst r24
	brne 0b
/* #NOAPP */
	sbrs r25,0
	rjmp .L1
        ...

Fallstricke

  • Um die Sonderzeichen "%~" und "%=" benutzen zu k├Ânnen, muss bei parameterlosen Inline Assembler dem Template ein Doppelpunkt folgen:
asm volatile ("%~call some_function" :);

Quellen

Dokumentation


GCC-Quellen

Siehe auch

'Pers├Ânliche Werkzeuge

RN-AVR Universal
Controllerboard
und universelle
Experimentierplatine
f├╝r nur 29,80 Euro
robotikhardware.de


Lichtprofi.de
LED Shop
www.lichtprofi.de