Aus RN-Wissen.de
Wechseln zu: Navigation, Suche


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.


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.

Siehe auch