Oft sieht man den Versuch, Warteschleifen in C durch Zählschleifen zu realisieren:
void wait () { int i; for (i=0; i<50; i++); }
avr-gcc mit Optimierung erzeugt daraus
wait: ret
und das ist auch völlig in Ordnung, wenn man ein Blick in die C-Spezifikation wagt. Die Schleife hat keine Wirkung auf die Welt! Sie sagt:
- "Führe 50 mal ; aus"
Und 50 mal Nichtstun ist eben nichts Tun...
Falls man wirklich auf diese Art warten möchte, hilft folgendes: Man gaukelt dem Compiler vor, es gäbe etwas unheimlich wichtiges in der Schleife zu tun, von dem er nichts mitbekommt.
void wait () { int i; for (i=0; i<50; i++) __asm__ __volatile ("; nur ein asm-Kommentar"); }
daraus entsteht
wait: ldi r24,lo8(49) ldi r25,hi8(49) .L5: /* #APP */ ; nur ein asm-Kommentar /* #NOAPP */ sbiw r24,1 sbrs r25,7 rjmp .L5 ret
Die Schleife wird nun 50 mal durchlaufen.
Wir bemerken, daß das inline Assembler nicht in Code resultiert und daß die Schleifenvariable nicht hochzählt, sondern hinunter. Auch diese Optimierung ist ok, denn i wird nirgends verwendet.
Daß bei dem Beispiel der gewünschte Code erzeugt wird, ist mehr oder weniger Glückssache. Ein Compiler transformiert seine Eingabe in ein Assembler- oder Maschinenprogramm, das die gleiche Wirkung hat. Wie dies genau geschieht, ist in aller Regel nicht festgelegt, sondern nur, daß es passiert ansonsten ist der Compiler selbst fehlerhaft oder implementiert kein C.
In dem lezten Beispiel der Warteschleife ist die Bedeutung des C-Codes etwa
- "führe 50 mal das Inline-Assembler-Muster "; nur ein asm-Kommentar" in den Code ein"
Dies wäre auch möglich komplett ohne Schleife(nvariable), indem der Compiler diese wegoptimiert und nur das Inline Assembler 50 mal hintereinander ausgibt. Das Resultat wäre im Maschinencode dann wiederum absolut ohne Effekt. Bei gcc geschieht dies aber bestenfalls mit der Optimierungsstufe -O3, die für AVR absolut nicht zu empfehlen ist. (Oder indem man von Hand die Optionen setzt, die diese Optimierungsstrategie nach sich zieht.)
Eine Möglichkeit, dem auf C-Ebene zu begegnen, ist die Schleifenvariable i als volatile zu deklarieren, so daß sie angelegt werden und genau so wie codiert verwendet werden muss:
void wait () { int volatile i; for (i=0; i<50; i++) ; }
Das hat allerdings noch andere Konsequenzen, nämlich das Erzwingen des Framepointers (bei avr-gcc im Y-Register r28:r29), denn i lebt jetzt nicht mehr in einem Register, sondern im Frame und wird wirklich jedesmal von dort gelesen und geschrieben, was recht breiten Code ergibt:
wait: /* prologue: frame size=2 */ push r28 push r29 in r28,__SP_L__ in r29,__SP_H__ sbiw r28,2 in __tmp_reg__,__SREG__ cli out __SP_H__,r29 out __SREG__,__tmp_reg__ out __SP_L__,r28 /* prologue end (size=10) */ std Y+1,__zero_reg__ std Y+2,__zero_reg__ ldd r24,Y+1 ldd r25,Y+2 sbiw r24,50 brge .L7 .L5: ldd r24,Y+1 ldd r25,Y+2 adiw r24,1 std Y+1,r24 std Y+2,r25 ldd r24,Y+1 ldd r25,Y+2 sbiw r24,50 brlt .L5 .L7: /* epilogue: frame size=2 */ adiw r28,2 in __tmp_reg__,__SREG__ cli out __SP_H__,r29 out __SREG__,__tmp_reg__ out __SP_L__,r28 pop r29 pop r28 ret
Das ist der totale Overkill. Inakzeptabel breiter Code und ein einzelner Schleifendurchlauf dauert viele Instruktionen, so dass die verstrichene Zeit nur recht grobkörnig eingestellt werden kann, denn ein Durchlauf dauert 18 Zyklen.
Eine weitere reine C-Variante, die näher am Ziel liegt, könnte so aussehen. Das (void) vermeidet eine Warnung, weil dummy nicht verwendet wird:
void wait () { int i; for (i=0; i<50; i++) { static unsigned char volatile dummy; (void) (dummy = i); } }
Was zu diesem Code führt:
wait: ldi r24,lo8(0) ldi r25,hi8(0) .L5: sts dummy.0,r24 adiw r24,1 cpi r24,50 cpc r25,__zero_reg__ brlt .L5 ret
oder so was
void wait () { int i; for (i=0; i<50; i++) (void) (int * volatile) &i; }
was wieder den minimalen Code ergibt wie beim erzwungenen Reload:
wait: ldi r24,lo8(0) ldi r25,hi8(0) .L5: adiw r24,1 cpi r24,50 cpc r25,__zero_reg__ brlt .L5 ret
Fazit
Die einzig sichere Lösung, einen bestimmten Code generieren zu lassen, ist und bleibt das Gewünschte komplett in (Inline) Assembler auszudrücken.
delay.h
Durch den Header (Include-Datei) avr/delay.h werden zwei Funktionen (als static inline implementiert) zur Verfügung gestellt, die mit minimalem Overhead eine bestimmte Anzahl von Maschinenzyklen verstreichen lassen:
- void _delay_loop_1 (uint8_t count)
- Dauert 3·count-1 Zyklen
- void _delay_loop_2 (uint16_t count)
- Dauert 4·count-1 Zyklen
Dabei wird count=0 wie 256 ([math]2^8[/math]) bzw. 65536 ([math]2^{16}[/math]) genommen. Zyklen für den Funktionsaufruf müssen keine zugerechnet werden, da es Inline-Funktionen sind. Jedoch muss die Schleifenvariable vorgeladen werden und wenn Interrupts aktiv sind, kommen deren Zeiten hinzu, falls in der Schleife ne IRQ auftritt. Jede Inline-Funktion ist 4 Bytes lang.