Aus RN-Wissen.de
Wechseln zu: Navigation, Suche
Balkonkraftwerk Speicher und Wechselrichter Tests und Tutorials

Oft sieht man den Versuch, Warteschleifen zurch 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 asm 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 emfehlen ist, oder indem man von Hand die Optionen setzt, die diese Optimierungsstufe 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.

Näher am Ziel liegt ein Ausbau der asm-Variante, indem man dort erzwingt, daß i wirklich vom Compiler angelegt wird. Das kann nur dadurch sichergestellt werden, daß es verwendet wird. Dazu wird erzwungen, daß i in ein Register geladen wird. Wir steuern also die Reload-Phase (die Phase, die Variablen in Register verteilt) von gcc. Da diese Ausgabe möglicherweise öfter zu bewältigen ist, wurde der erzwungene Reload als Makro definiert, der noch eine asm-Ausgabe macht, was reloadet wird:

#define RELOAD(reg,var) \
   __asm__ __volatile (";RELOAD " reg " with " #var : "=" reg (var) : "0" (var) : "memory")

void wait ()
{
   int i;
   
   for (i=0; i<50; i++)
      RELOAD ("r", i);
}

Das Makro im Beispiel löst auf zu

__asm__ __volatile (";RELOAD r with i" : "=r" (i) : "0" (i) : "memory")

Das bedeutet, daß i in ein Register der Registerklasse r geladen wird. In allen GCC-Versionen steht dieses r für ein Standard-Register (GPR). Der erzeugte Code sieht nun besser aus, ist aber immer noch abhängig vom Optimierungsgrad etc.

wait:
   ldi r24,lo8(0)
   ldi r25,hi8(0)
.L5:
/* #APP */
   ;RELOAD r with i
/* #NOAPP */
   adiw r24,1
   cpi r24,50
   cpc r25,__zero_reg__
   brlt .L5
   ret

Eine weitere reine C-Variante könnte so aussehen:

static int volatile dummy;

void wait ()
{
   int i;
   
   for (i=0; i<50; i++)
      dummy = i;
}

Was zu diesem Code führt:

wait:
	ldi r24,lo8(0)
	ldi r25,hi8(0)
.L5:
	sts (dummy)+1,r25
	sts dummy,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.

Das kann dann so aussehen für eine feste Anzahl von Schleifendurchläufe von 1 bis 255:

void wait()
{
   unsigned char i = 0;

   __asm__ __volatile (
      "0:"                   "\n\t"
      "subi %0, 1"           "\n\t"
      "cpi  %0, lo8(%2)"     "\n\t"
      "brlo 0b"
      : "=d" (i)
      : "d" (i), "M" (50)
   );
}

Oder wenn ein 16-Bit-Wert übergeben wird sieht es so aus:

void wait (unsigned short num)
{
   unsigned short i = 0;

   __asm__ __volatile (
      "0:"                   "\n\t"
      "cp  %A0, %A2"         "\n\t"
      "cpc %B0, %B2"         "\n\t"
      "brsh 1f"              "\n\t"
      "subi %A0, lo8(-1)"    "\n\t"
      "sbci %B0, hi8(-1)"    "\n\t"
      "rjmp 0b"              "\n\t"
      "1:"
      : "=d" (i)
      : "0" (i), "r" (num)
   );
}

Siehe auch


LiFePO4 Speicher Test