www.mikrocontroller.net

AVR-GCC-Tutorial/Assembler und Inline-Assembler

Gelegentlich erweist es sich als nützlich, C- und Assembler-Code in einer Anwendung zu nutzen. Typischerweise wird das Hauptprogramm in C verfasst und wenige, zeitkritische oder hardwarenahe Operationen in Assembler.

Die GNU-Toolchain bietet dazu zwei Möglichkeiten:

Inline-Assembler
Die Assembleranweisungen werden direkt in den C-Code integriert. Eine Quellcode-Datei enthält somit C- und Assembleranweisungen
Assembler-Dateien
Der Assemblercode befindet sich in eigenen Quellcodedateien. Diese werden vom GNU-Assembler (avr-as) zu Object-Dateien assembliert und mit den aus dem C-Code erstellten Object-Dateien zusammengelinkt.

Inhaltsverzeichnis

[Bearbeiten] Inline-Assembler

Inline-Assembler bietet sich an, wenn nur wenig Assembleranweisungen benötigt werden. Typische Anwendung sind kurze Codesequenzen für zeitkritische Operationen in Interrupt-Routinen oder sehr präzise Warteschleifen (z. B. 1-Wire). Inline-Assembler wird mit asm volatile eingeleitet, die Assembler-Anweisungen werden in einer Zeichenkette zusammengefasst, die als "Parameter" übergeben wird. Durch Doppelpunkte getrennt werden die Ein- und Ausgaben sowie die "Clobber-Liste" angegeben.

Ein einfaches Beispiel für Inline-Assembler ist das Einfügen einer NOP-Anweisung (NOP steht für No Operation). Dieser Assembler-Befehl benötigt genau einen Taktzyklus, ansonsten "tut sich nichts". Sinnvolle Anwendungen für NOP sind genaue Delay(=Warte)-Funktionen.

   /* Verzögern der weiteren Programmausführung um
      genau 3 Taktzyklen */
    asm volatile ("nop");
    asm volatile ("nop");
    asm volatile ("nop");

Weiterhin kann mit einem NOP verhindert werden, dass leere Schleifen, die als Warteschleifen gedacht sind, wegoptimiert werden. Der Compiler erkennt ansonsten die vermeintlich nutzlose Schleife und erzeugt dafür keinen Code im ausführbaren Programm.

    uint16_t i;
 
    /* leere Schleife - wird bei eingeschalteter Compiler-Optimierung   wegoptimiert */
    for (i = 0; i < 1000; i++)
      ;
 
    ...
 
    /* Schleife erzwingen (keine Optimierung): "NOP-Methode" */
    for (i = 0; i < 1000; i++)
      asm volatile("NOP");

Als Beispiel für mehrzeiligen Inline-Assembler dient eine präzise Delay-Funktion. Die Funktion erhält ein 16-bit Wort als Parameter, prüft den Parameter auf 0 und beendet die Funktion in diesem Fall oder durchläuft die folgende Schleife sooft wie im Wert des Parameters angegeben. Inline-Assembler hat hier den Vorteil, dass die Laufzeit unabhängig von der Optimierungsstufe (Parameter -O, vgl. makefile) und der Compiler-Version ist.

static inline void delayloop16 (uint16_t count)
{
    asm volatile ("cp  %A0, __zero_reg__ \n\t"
                  "cpc %B0, __zero_reg__ \n\t"
                  "breq 2f               \n\t"
                  "1:                    \n\t"
                  "sbiw %0,1             \n\t"
                  "brne 1b               \n\t"
                  "2:                    "  
                  : "=w" (count)
	          : "0"  (count)
    );                            
}
  • Jede Anweisung wird mit \n\t abgeschlossen. Der Zeilenumbruch teilt dem Assembler mit, dass ein neuer Befehl beginnt.
  • Als Sprung-Marken (Labels) werden Ziffern verwendet. Diese speziellen Labels sind mehrfach im Code verwendbar. Gesprungen wird jeweils zurück (b) oder vorwärts (f) zum nächsten auffindbaren Label.

Das Resultat zeigt ein Blick in die Assembler-Datei, die der Compiler mit der option -save-temps nicht löscht:

	cp  r24, __zero_reg__ 	 ;  count
	cpc r25, __zero_reg__ 	 ;  count
	breq 2f               
	1:                    
	sbiw r24,1             	 ;  count
	brne 1b               
	2:

Detaillierte Ausführungen zum Thema Inline-Assembler finden sich in der Dokumentation der avr-libc im Abschnitt Related Pages/Inline Asm.

Siehe auch:

[Bearbeiten] Assembler-Dateien

Assembler-Dateien erhalten die Endung .S (großes S) und werden im makefile nach WinAVR/mfile-Vorlage hinter ASRC= durch Leerzeichen getrennt aufgelistet.

Wenn man mit dem AVR Studio arbeitet werden ".S"-Dateien im "Source Files"-Projektordner automatisch mit übersetzt und gelinkt (ohne Umweg über externes Makefile).

Möchte man den Umweg über externes Makefile gehen, muss alternativ auch das standardmäßig erstellte Makefile bearbeitet und folgende Zeilen eingefügt werden:

## Objects that must be built in order to link
OBJECTS = (alte Dateien...) useful.o

## Compile
## Hier folgt eine Liste der gelinkten Dateien, darunter einfügen:
useful.o: ../useful.S
	$(CC) $(INCLUDES) $(ASMFLAGS) -c  $<

Das war es schon. Allerdings gilt es zu beachten, dass das makefile über "Project -> Configuration options" selbst einzubinden ist, sonst wird es natürlich wieder überschrieben.

Im Beispiel eine Funktion superFunc, die alle Pins des Ports D auf "Ausgang" schaltet, eine Funktion ultraFunc, die die Ausgänge entsprechend des übergebenen Parameters schaltet, eine Funktion gigaFunc, die den Status von Port A zurückgibt und eine Funktion addFunc, die zwei Bytes zu einem 16-bit-Wort addiert. Die Zuweisungen im C-Code (PORTx = ...) verhindern, dass der Compiler die Aufrufe wegoptimiert und dienen nur zur Veranschaulichung der Parameterübergaben.

Zuerst der Assembler-Code. Der Dateiname sei useful.S:

#include "avr/io.h"
 
//; Arbeitsregister (ohne "r") 
workreg  = 16
workreg2 = 17
 
//; Konstante:
ALLOUT = 0xff
 
//; ** Setze alle Pins von PortD auf Ausgang **
//; keine Parameter, keine Rückgabe
.global superFunc
.func superFunc
superFunc:
   push workreg
   ldi workreg, ALLOUT
   out  _SFR_IO_ADDR(DDRD), workreg  // beachte: _SFR_IO_ADDR()
   pop workreg
   ret
.endfunc
 
 
//; ** Setze PORTD auf übergebenen Wert **
//; Parameter in r24 (LSB immer bei "graden" Nummern)
.global ultraFunc
.func ultraFunc
ultraFunc:
   out  _SFR_IO_ADDR(PORTD), 24
   ret
.endfunc
 
 
//; ** Zustand von PINA zurückgeben **
//; Rückgabewerte in r24:r25 (LSB:MSB), hier nur LSB genutzt
.global gigaFunc
.func gigaFunc
gigaFunc:
   in 24, _SFR_IO_ADDR(PINA)
   ret
.endfunc
 
 
//; ** Zwei Bytes addieren und 16-bit-Wort zurückgeben **
//; Parameter in r24 (Summand1) und r22 (Summand2) -
//;  Parameter sind Word-"aligned" d.h. LSB immer auf "graden"
//;  Registernummern. Bei 8-Bit und 16-Bit Paramtern somit 
//;  beginnend bei r24 dann r22 dann r20 etc.
//; Rückgabewert in r24:r25
.global addFunc
.func addFunc
addFunc:
   push workreg
   push workreg2
   clr workreg2
   mov workreg, 22
   add workreg, 24
   adc workreg2, 1    // r1 - assumed to be always zero ...
   movw r24, workreg
   pop workreg2
   pop workreg
   ret
.endfunc
 
//; oh je - sorry - Mein AVR-Assembler ist eingerostet, hoffe das stimmt so...
 
.end

Im Makefile ist der Name der Assembler-Quellcodedatei einzutragen:

ASRC = useful.S

Der Aufruf erfolgt dann im C-Code so:

#include <stdint.h>
#include <avr/io.h>
 
extern void superFunc(void);
extern void ultraFunc(uint8_t setVal);
extern uint8_t gigaFunc(void);
extern uint16_t addFunc(uint8_t w1, uint8_t w2);
 
int main(void)
{
[...]
  superFunc();
  
  ultraFunc(0x55);
  
  PORTD = gigaFunc();
 
  PORTA = (addFunc(0xF0, 0x11) & 0xff);
  PORTB = (addFunc(0xF0, 0x11) >> 8);
[...]
}

Das Ergebnis wird wieder in der lss-Datei ersichtlich:

[...]
   superFunc();
 148:	0e 94 f6 00 	call	0x1ec
  
  ultraFunc(0x55);
 14c:	85 e5       	ldi	r24, 0x55	; 85
 14e:	0e 94 fb 00 	call	0x1f6
  
  PORTD = gigaFunc();
 152:	0e 94 fd 00 	call	0x1fa
 156:	82 bb       	out	0x12, r24	; 18
  
  PORTA = (addFunc(0xF0, 0x11) & 0xff);
 158:	61 e1       	ldi	r22, 0x11	; 17
 15a:	80 ef       	ldi	r24, 0xF0	; 240
 15c:	0e 94 ff 00 	call	0x1fe
 160:	8b bb       	out	0x1b, r24	; 27
  PORTB = (addFunc(0xF0, 0x11) >> 8);
 162:	61 e1       	ldi	r22, 0x11	; 17
 164:	80 ef       	ldi	r24, 0xF0	; 240
 166:	0e 94 fc 00 	call	0x1f8
 16a:	89 2f       	mov	r24, r25
 16c:	99 27       	eor	r25, r25
 16e:	88 bb       	out	0x18, r24	; 24

[...]
000001ec <superFunc>:
// setze alle Pins von PortD auf Ausgang
.global superFunc
.func superFunc
superFunc:
   push workreg
 1ec:	0f 93       	push	r16
   ldi workreg, ALLOUT
 1ee:	0f ef       	ldi	r16, 0xFF	; 255
   out  _SFR_IO_ADDR(DDRD), workreg
 1f0:	01 bb       	out	0x11, r16	; 17
   pop workreg
 1f2:	0f 91       	pop	r16
   ret
 1f4:	08 95       	ret

000001f6 <ultraFunc>:
.endfunc


// setze PORTD auf übergebenen Wert
.global ultraFunc
.func ultraFunc
ultraFunc:
   out  _SFR_IO_ADDR(PORTD), 24
 1f6:	82 bb       	out	0x12, r24	; 18
   ret
 1f8:	08 95       	ret

000001fa <gigaFunc>:
.endfunc


// Zustand von PINA zurückgeben
.global gigaFunc
.func gigaFunc
gigaFunc:
   in 24, _SFR_IO_ADDR(PINA)
 1fa:	89 b3       	in	r24, 0x19	; 25
   ret
 1fc:	08 95       	ret

000001fe <addFunc>:
.endfunc


// zwei Bytes addieren und 16-bit-Wort zurückgeben
.global addFunc
.func addFunc
addFunc:
   push workreg
 1fe:	0f 93       	push	r16
   push workreg2
 200:	1f 93       	push	r17
   clr workreg2
 202:	11 27       	eor	r17, r17
   mov workreg, 22
 204:	06 2f       	mov	r16, r22
   add workreg, 24
 206:	08 0f       	add	r16, r24
   adc workreg2, 1    // r1 - assumed to be always zero ...
 208:	11 1d       	adc	r17, r1
   movw r24, workreg
 20a:	c8 01       	movw	r24, r16
   pop workreg2
 20c:	1f 91       	pop	r17
   pop workreg
 20e:	0f 91       	pop	r16
   ret
 210:	08 95       	ret

[...]

Die Zuweisung von Registern zu Parameternummer und die Register für die Rückgabewerte sind in den "Register Usage Guidelines" der avr-libc-Dokumentation erläutert.

Siehe auch:

[Bearbeiten] Globale Variablen für Datenaustausch

Oftmals kommt man um globale Variablen nicht herum, z. B. um den Datenaustausch zwischen Hauptprogramm und Interrupt-Routinen zu realisieren. Hierzu muss man im Assembler wissen, wo genau die Variable vom C-Compiler abgespeichert wird.

Hierzu muss die Variable, hier "zaehler" genannt, zuerst im C-Code als Global definiert werden, z. B. so:

#include <avr/io.h>
 
volatile uint8_t zaehler;
 
int16_t main (void)
{
    // irgendein Code, in dem zaehler benutzt werden kann
    
}

Im folgenden Assembler-Beispiel wird der Externe Interrupt0 verwendet, um den Zähler hochzuzählen. Es fehlen die Initialisierungen des Interrupts und die Interrupt-Freigabe, so richtig sinnvoll ist der Code auch nicht, aber er zeigt (hoffentlich) wie es geht.

Im Umgang mit Interrupt-Vektoren gilt beim GCC-Assembler das Gleiche, wie bei C: Man muss die exakte Schreibweise beachten, ansonsten wird nicht der Interrupt-Vektor angelegt, sondern eine neue Funktion - und man wundert sich, dass nichts funktionert (vgl. das AVR-GCC-Handbuch).

#include "avr/io.h"
 
temp = 16
 
.extern zaehler
 
.global INT0_vect
INT0_vect:
 
     push temp                      //; wichtig: Benutzte Register und das
     in temp,_SFR_IO_ADDR(SREG)     //; Status-Register (SREG) sichern!
     push temp
 
     lds temp,zaehler               //; Wert aus dem Speicher lesen
     inc temp                       //; bearbeiten
     sts zaehler,temp               //; und wieder zurückschreiben
 
     pop temp                       //; die benutzten Register wiederherstellen
     out _SFR_IO_ADDR(SREG),temp
     pop temp
     reti
 
.end

[Bearbeiten] Globale Variablen im Assemblerfile anlegen

Alternativ können Variablen aber auch im Assemblerfile angelegt werden. Dadurch kann auf eine .c-Datei verzichtet werden. Für das obige Beispiel könnte der Quelltext dann die Dateien zaehl_asm.S und zaehl_asm.h abgelegt werden, so dass nur noch zaehl_asm.S mit kompiliert werden müsste.

Anstatt im Assemblerfile über das Schlüsselwort .extern auf eine vorhandene Variable zu verweisen, wird dazu mit dem Schlüsselwort .comm die benötigte Anzahl von Bytes für eine Variable reserviert.

zaehl_asm.S

#include "avr/io.h"
 
temp = 16
 
//; 1 Byte im RAM für den Zähler reservieren
.comm zaehler, 1
 
.global INT0_vect
INT0_vect:
 
...

In der Headerdatei wird dann auf die Variable nur noch verwiesen (Schlüsselwort extern):

zaehl_asm.h

#ifndef ZAEHL_ASM_H
#define ZAEHL_ASM_H
 
extern volatile uint8_t zaehler;
 
#endif 

Im Gegensatz zu globalen Variablen in C werden so angelegte Variablen nicht automatisch mit dem Wert 0 initialisiert.


[Bearbeiten] Variablen größer als 1 Byte

Variablen, die größer als ein Byte sind, können in Assembler auf ähnliche Art angesprochen werden. Hierzu müssen nur genug Bytes angefordert werden, um die Variable aufzunehmen. Soll z. B. für den Zähler eine Variable vom Typ unsigned long, also uint32_t verwendet werden, so müssen 4 Bytes reserviert werden:

...
// 4 Byte im RAM für den Zähler reservieren
.comm zaehler, 4
...

Die dazugehörige Deklaration im Headerfile wäre dann:

...
extern volatile uint32_t zaehler;
...

Bei Variablen, die größer als ein Byte sind, werden die Werte beginnend mit dem niederwertigsten Byte im RAM abgelegt. Das folgende Codeschnippsel zeigt, wie unter Assembler auf die einzelnen Bytes zugegriffen werden kann. Dazu wird im Interrupt nun ein 32-Bit Zähler erhöht:

#include "avr/io.h"
 
temp = 16
 
// 4 Byte im RAM für den Zähler reservieren
.comm zaehler, 4
 
.global INT0_vect
INT0_vect:
 
     push temp                      // wichtig: Benutzte Register und das
     in temp,_SFR_IO_ADDR(SREG)     // Status-Register (SREG) sichern !
     push temp
 
     // 32-Bit-Zähler incrementieren
     lds temp, (zaehler + 0)        // 0. Byte (niederwertigstes Byte)
     inc temp
     sts (zaehler + 0), temp
     brne RAUS
	
     lds temp, (zaehler + 1)        // 1. Byte
     inc temp
     sts (zaehler + 1), temp
     brne RAUS
 
     lds temp, (zaehler + 2)        // 2. Byte
     inc temp
     sts (zaehler + 2), temp
     brne RAUS
 
     lds temp, (zaehler + 3)        // 3. Byte (höchstwertigstes Byte)
     inc temp
     sts (zaehler + 3), temp
     brne RAUS
	
RAUS:
     pop temp                       // die benutzten Register wiederherstellen
     out _SFR_IO_ADDR(SREG),temp
     pop temp
     reti
 
.end

[Bearbeiten] Todo

  • 16-Bit / 32-Bit Variablen, Zugriff auf Arrays (Strings)

[Bearbeiten] Weblinks

[Bearbeiten] Englisch

avr-libc-Manual
GCC-Manual
webmaster@mikrocontroller.netImpressumNutzungsbedingungenWerbung auf Mikrocontroller.net