(→Fazit) |
(→delay.h) |
||
(5 dazwischenliegende Versionen von 3 Benutzern werden nicht angezeigt) | |||
Zeile 1: | Zeile 1: | ||
− | Oft sieht man den Versuch, Warteschleifen | + | Oft sieht man den Versuch, Warteschleifen in C durch Zählschleifen zu realisieren: |
<pre> | <pre> | ||
void wait () | void wait () | ||
Zeile 9: | Zeile 9: | ||
</pre> | </pre> | ||
− | + | [[avr-gcc]] mit Optimierung erzeugt daraus | |
<pre> | <pre> | ||
wait: | wait: | ||
Zeile 49: | Zeile 49: | ||
Auch diese Optimierung ist ok, denn <tt>i</tt> wird nirgends verwendet. | Auch diese Optimierung ist ok, denn <tt>i</tt> wird nirgends verwendet. | ||
− | Daß bei dem Beispiel der gewünschte Code erzeugt wird ist mehr oder weniger Glückssache. | + | 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, | 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 | sondern nur, ''daß'' es passiert | ||
Zeile 56: | Zeile 56: | ||
In dem lezten Beispiel der Warteschleife ist die Bedeutung des C-Codes etwa | In dem lezten Beispiel der Warteschleife ist die Bedeutung des C-Codes etwa | ||
:''"führe 50 mal das Inline-Assembler-Muster ''<tt>"; nur ein asm-Kommentar"</tt>'' in den Code ein"'' | :''"führe 50 mal das Inline-Assembler-Muster ''<tt>"; nur ein asm-Kommentar"</tt>'' in den Code ein"'' | ||
− | Dies wäre auch möglich komplett ohne Schleife(nvariable), indem der Compiler diese wegoptimiert und nur das | + | 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. | Das Resultat wäre im Maschinencode dann wiederum absolut ohne Effekt. | ||
Bei gcc geschieht dies aber bestenfalls mit der Optimierungsstufe <tt>-O3</tt>, die für | Bei gcc geschieht dies aber bestenfalls mit der Optimierungsstufe <tt>-O3</tt>, die für | ||
− | AVR absolut nicht zu empfehlen ist | + | 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 <tt>i</tt> | Eine Möglichkeit, dem auf C-Ebene zu begegnen, ist die Schleifenvariable <tt>i</tt> | ||
Zeile 122: | Zeile 122: | ||
so dass die verstrichene Zeit nur recht grobkörnig eingestellt werden kann, denn ein Durchlauf dauert 18 Zyklen. | so dass die verstrichene Zeit nur recht grobkörnig eingestellt werden kann, denn ein Durchlauf dauert 18 Zyklen. | ||
+ | <!-- | ||
Näher am Ziel liegt ein Ausbau der asm-Variante, indem man dort erzwingt, daß <tt>i</tt> | Näher am Ziel liegt ein Ausbau der asm-Variante, indem man dort erzwingt, daß <tt>i</tt> | ||
wirklich vom Compiler angelegt wird. Das kann nur dadurch sichergestellt werden, | wirklich vom Compiler angelegt wird. Das kann nur dadurch sichergestellt werden, | ||
Zeile 160: | Zeile 161: | ||
ret | ret | ||
</pre> | </pre> | ||
+ | --> | ||
− | Eine weitere reine C-Variante könnte so aussehen. Das <tt>(void)</tt> vermeidet eine Warnung, weil <tt>dummy</tt> nicht verwendet wird: | + | Eine weitere reine C-Variante, die näher am Ziel liegt, könnte so aussehen. Das <tt>(void)</tt> vermeidet eine Warnung, weil <tt>dummy</tt> nicht verwendet wird: |
<pre> | <pre> | ||
void wait () | void wait () | ||
Zeile 258: | Zeile 260: | ||
;<tt>void _delay_loop_1 (uint8_t count)</tt>: Dauert <tt>3·count-1</tt> Zyklen | ;<tt>void _delay_loop_1 (uint8_t count)</tt>: Dauert <tt>3·count-1</tt> Zyklen | ||
;<tt>void _delay_loop_2 (uint16_t count)</tt>: Dauert <tt>4·count-1</tt> Zyklen | ;<tt>void _delay_loop_2 (uint16_t count)</tt>: Dauert <tt>4·count-1</tt> Zyklen | ||
− | Dabei wird <tt>count=0</tt> 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 | + | Dabei wird <tt>count=0</tt> 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 [[Interrupt]]s aktiv sind, kommen deren Zeiten hinzu, falls in der Schleife ne [[IRQ]] auftritt. Jede Inline-Funktion ist 4 Bytes lang. |
+ | |||
+ | |||
+ | Bei neueren Versionen von <tt>avr/delay.h</tt> werden zusätzlich 2 Funktionen definiert, um feste Zeiten in µs oder ms zu warten: | ||
+ | ;<tt>void _delay_us(double us)</tt>: Zeit in µs | ||
+ | ;<tt>void _delay_ms(double ms)</tt>: Zeit in ms | ||
+ | Die Zeiten werden schon beim compilieren in Zyklen umgerechent und dann eine passende Warteschleife erzeugt. So muß der µC hier nicht mit Fließkommazahlen rechen und es entsteht kein unnötig langer Code. Diese beiden Funktionen funktionieren daher nur mit einer konstanten Zeit und mit eingeschalteter Optimierung. Ohne Optimierung oder variabler Zeit werden die Verzögerungen viel zu lang. | ||
+ | |||
+ | =Tip: Nützliches Tool= | ||
+ | Ein sehr praktisches Tool, um Schleifen von beliebiger Dauer zu erzeugen, kann man hier herunterladen: http://www.home.unix-ag.org/tjabo/avr/AVRdelayloop.html. | ||
+ | Man gibt einfach die Taktfrequenz, die gewünschte Zeit und drei Register ein und das Programm erzeugt einen passenden Assembler Code. | ||
=Siehe auch= | =Siehe auch= | ||
* [[avr-gcc]] | * [[avr-gcc]] | ||
+ | * [[Inline-Assembler in avr-gcc|Inline-Assembler]] | ||
[[Kategorie:Grundlagen]] | [[Kategorie:Grundlagen]] | ||
[[Kategorie:Quellcode C]] | [[Kategorie:Quellcode C]] | ||
− | [[Kategorie:Quellcode Assembler]] | + | [[Kategorie:Quellcode Assembler AVR]] |
[[Kategorie:Software]] | [[Kategorie:Software]] |
Aktuelle Version vom 14. Dezember 2009, 22:23 Uhr
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
Inhaltsverzeichnis
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.
Bei neueren Versionen von avr/delay.h werden zusätzlich 2 Funktionen definiert, um feste Zeiten in µs oder ms zu warten:
- void _delay_us(double us)
- Zeit in µs
- void _delay_ms(double ms)
- Zeit in ms
Die Zeiten werden schon beim compilieren in Zyklen umgerechent und dann eine passende Warteschleife erzeugt. So muß der µC hier nicht mit Fließkommazahlen rechen und es entsteht kein unnötig langer Code. Diese beiden Funktionen funktionieren daher nur mit einer konstanten Zeit und mit eingeschalteter Optimierung. Ohne Optimierung oder variabler Zeit werden die Verzögerungen viel zu lang.
Tip: Nützliches Tool
Ein sehr praktisches Tool, um Schleifen von beliebiger Dauer zu erzeugen, kann man hier herunterladen: http://www.home.unix-ag.org/tjabo/avr/AVRdelayloop.html. Man gibt einfach die Taktfrequenz, die gewünschte Zeit und drei Register ein und das Programm erzeugt einen passenden Assembler Code.