|
|
AVR-GCC-Tutorial/Assembler und Inline-AssemblerGelegentlich 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:
[Bearbeiten] Inline-AssemblerInline-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.
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.
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.
Das Resultat zeigt ein Blick in die Assembler-Datei, die der Compiler mit der option -save-temps nicht löscht:
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-DateienAssembler-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:
Im Makefile ist der Name der Assembler-Quellcodedatei einzutragen: ASRC = useful.S Der Aufruf erfolgt dann im C-Code so:
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 DatenaustauschOftmals 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:
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).
[Bearbeiten] Globale Variablen im Assemblerfile anlegenAlternativ 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
In der Headerdatei wird dann auf die Variable nur noch verwiesen (Schlüsselwort extern): zaehl_asm.h
Im Gegensatz zu globalen Variablen in C werden so angelegte Variablen nicht automatisch mit dem Wert 0 initialisiert.
[Bearbeiten] Variablen größer als 1 ByteVariablen, 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:
Die dazugehörige Deklaration im Headerfile wäre dann:
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:
[Bearbeiten] Todo
[Bearbeiten] Weblinks[Bearbeiten] Englisch |