Aus RN-Wissen.de
Wechseln zu: Navigation, Suche
Laderegler Test Tueftler Seite

(delay.h)
 
(14 dazwischenliegende Versionen von 3 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
Oft sieht man den Versuch, Warteschleifen zurch Zählschleifen zu realisieren:
+
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>
  
<tt>avr-gcc</tt> mit Optimierung erzeugt daraus
+
[[avr-gcc]] mit Optimierung erzeugt daraus
 
<pre>
 
<pre>
 
wait:
 
wait:
Zeile 15: Zeile 15:
 
</pre>
 
</pre>
 
und das ist auch völlig in Ordnung, wenn man ein Blick in die C-Spezifikation wagt.
 
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 <tt>;</tt> aus". Und 50 mal Nichtstun ist eben nichts Tun...
+
Die Schleife hat ''keine'' Wirkung auf die Welt! Sie sagt:  
 +
:''"Führe 50 mal ''<tt>;</tt>'' 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.
 
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.
Zeile 47: 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 53: Zeile 55:
  
 
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 asm 50 mal hintereinander ausgibt.  
+
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 emfehlen ist, oder indem man von Hand die Optionen setzt, die diese
+
AVR absolut nicht zu empfehlen ist. (Oder indem man von Hand die Optionen setzt, die diese
Optimierungsstufe nach sich zieht.
+
Optimierungsstrategie nach sich zieht.)
  
 
Eine Möglichkeit, dem auf C-Ebene zu begegnen, ist die Schleifenvariable&nbsp;<tt>i</tt>
 
Eine Möglichkeit, dem auf C-Ebene zu begegnen, ist die Schleifenvariable&nbsp;<tt>i</tt>
Zeile 120: 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 158: Zeile 161:
 
   ret
 
   ret
 
</pre>
 
</pre>
 +
-->
  
Eine weitere reine C-Variante könnte so aussehen:
+
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>
static int volatile dummy;
 
 
 
void wait ()
 
void wait ()
 
{
 
{
Zeile 168: Zeile 170:
 
    
 
    
 
   for (i=0; i<50; i++)
 
   for (i=0; i<50; i++)
       dummy = i;
+
  {
 +
       static unsigned char volatile dummy;
 +
      (void) (dummy = i);
 +
  }
 
}
 
}
 
</pre>
 
</pre>
Zeile 177: Zeile 182:
 
ldi r25,hi8(0)
 
ldi r25,hi8(0)
 
.L5:
 
.L5:
sts (dummy)+1,r25
+
sts dummy.0,r24
sts dummy,r24
+
 
adiw r24,1
 
adiw r24,1
 
cpi r24,50
 
cpi r24,50
 
cpc r25,__zero_reg__
 
cpc r25,__zero_reg__
 
brlt .L5
 
brlt .L5
 
 
ret
 
ret
 
</pre>
 
</pre>
Zeile 214: Zeile 217:
 
Die einzig sichere Lösung, einen bestimmten Code generieren zu lassen, ist und bleibt das Gewünschte komplett in (Inline) Assembler auszudrücken.
 
Die einzig sichere Lösung, einen bestimmten Code generieren zu lassen, ist und bleibt das Gewünschte komplett in (Inline) Assembler auszudrücken.
 
}}
 
}}
Das kann dann so aussehen für eine feste Anzahl von Schleifendurchläufe von 1 bis 255:
+
<!--
 +
Das kann dann so aussehen für eine feste Anzahl von Schleifendurchläufen von 1 bis 255, hier für 50 Durchläufe:
 
<pre>
 
<pre>
 
void wait()
 
void wait()
Zeile 221: Zeile 225:
  
 
   __asm__ __volatile (
 
   __asm__ __volatile (
       "0:"                   "\n\t"
+
       "0:               "   "\n\t"
       "subi %0, 1"           "\n\t"
+
       "subi %0, 1       "   "\n\t"
       "cpi  %0, lo8(%2)"     "\n\t"
+
       "cpi  %0, lo8(%2) "   "\n\t"
       "brlo 0b"
+
       "brlo 0b         "
 
       : "=d" (i)
 
       : "=d" (i)
       : "d" (i), "M" (50)
+
       : "0" (i), "M" (50)
 
   );
 
   );
 
}
 
}
 
</pre>
 
</pre>
Oder wenn ein 16-Bit-Wert übergeben wird sieht es so aus:
+
Und wenn ein 16-Bit-Wert übergeben möchte, dann könnte es so aussehen:
 
<pre>
 
<pre>
 
void wait (unsigned short num)
 
void wait (unsigned short num)
Zeile 237: Zeile 241:
  
 
   __asm__ __volatile (
 
   __asm__ __volatile (
       "0:"                   "\n\t"
+
       "0:               "   "\n\t"
       "cp %A0, %A2"         "\n\t"
+
       "cp   %A0, %A2     "   "\n\t"
       "cpc %B0, %B2"         "\n\t"
+
       "cpc %B0, %B2     "   "\n\t"
       "brsh 1f"             "\n\t"
+
       "brsh 1f           "   "\n\t"
       "subi %A0, lo8(-1)"   "\n\t"
+
       "subi %A0, lo8(-1) "   "\n\t"
       "sbci %B0, hi8(-1)"   "\n\t"
+
       "sbci %B0, hi8(-1) "   "\n\t"
       "rjmp 0b"             "\n\t"
+
       "rjmp 0b           "   "\n\t"
       "1:"
+
       "1:               "
 
       : "=d" (i)
 
       : "=d" (i)
 
       : "0" (i), "r" (num)
 
       : "0" (i), "r" (num)
Zeile 250: Zeile 254:
 
}
 
}
 
</pre>
 
</pre>
 +
-->
 +
 +
=delay.h=
 +
Durch den Header (Include-Datei) <tt>avr/delay.h</tt> werden zwei Funktionen (als <tt>static inline</tt> implementiert) zur Verfügung gestellt, die mit minimalem Overhead eine bestimmte Anzahl von Maschinenzyklen verstreichen lassen:
 +
;<tt>void _delay_loop_1 (uint8_t count)</tt>: Dauert <tt>3&middot;count-1</tt> Zyklen
 +
;<tt>void _delay_loop_2 (uint16_t count)</tt>: Dauert <tt>4&middot;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 [[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:AVR]]
+
[[Kategorie:Grundlagen]]
 
[[Kategorie:Quellcode C]]
 
[[Kategorie:Quellcode C]]
 +
[[Kategorie:Quellcode Assembler AVR]]
 +
[[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

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


LiFePO4 Speicher Test