MezData-Logo Creative Commons License 400 :AVR: Warteschleifen

Schlüsselwörter: Atmel, AVR, Assembler, Warteschleife, Zählschleife

Zählschleifen zum Zeit vertrödeln

PAPEine Möglichkeit Zeitverzögerungen zu erreichen ist den Prozessor mit Zählen zu beschäftigen:

Der Systemtakt sei 1 MHz, ein Taktzyklus benötigt 1 us.

  ldi tmp,0  ;Register mit 0 initialisieren
loop:
  dec tmp    ;temp um 1 erniedrigen
  brne loop  ;solange ungleich 0 springen
 weiter...
Code
Die vertrödelte Zeit zwischen 1 und 2 lässt sich leicht ermitteln, wenn man zwei Fälle unterscheidet:

Lösung, betrachte die Werte von tmp vor brne loop:

Von 255 bis 1 wird nach loop gesprungen: 255*3 Takte. Im Fall tmp = 0 wird die Schleife beendet: 2 Takte; den Takt der Initialisierung dazu sind es dann 3 Takte.

Summe: 256*3 = 768 Takte = 768 us zwischen 1 und 2

Länger warten mit nop

  ldi temp,0 ;Register mit 0 initialisieren
loop:
  dec temp   ;temp um 1 erniedrigen
  nop        ;tue 1 Takt nichts
  brne loop  ;solange ungleich 0 springen
 weiter...

Es gibt einen Befehl nop "No Operation" der nix tut ausser 1 Takt Zeit kosten und 1 Adresse (2 Byte) Programm-Speicher verbrauchen:

Der nop-Befehl benötigt einen Takt und verlängert die Durchlaufzeit.

Die Summe der Zeit ist nun 256*4 = 1024 somit warten wir 1024 us = 1,024 ms.

Länger warten mit zwei Schleifen

PAPDurch Verschachteln von zwei Schleifen kann erheblich länger gewartet werden, der Systemtakt betrage 1MHz, die Taktzeit ist 1us. Wir ermitteln die maximal mögliche Wartezeit:

 

  ldi aussen,0 ;Register auf 0 initialisieren
  ldi innen,0  ;Register auf 0 initialisieren
loop:
  dec innen  ;innen um 1 erniedrigen
  brne loop  ;solange ungleich 0 springen
  dec aussen ;aussen um 1 erniedrigen
  brne loop  ;solange ungleich 0 springen 
 weiter...
 

Ermitteln der Takte (Blockdenken)

Die innere Schleife benötigt 255 * 3 + 2 (letzter Lauf) Takte = 767 Takte, betrachte dies als langes nop.

Die äussere Schleife braucht 255*(767+1+2) für den Rundlauf und 767+1+1 für den letzten Durchgang = 256*(767+3)-1 = 197119 Takte.

Fehlen noch die 2 Takte für die ldi am Anfang: 2 + 197119 = 197121Takte ca. 0,2 s

Alternative Rechnung (Flussdenken)

Bei der alternativen Rechnung wird gezählt, wie oft ein Befehl im Programmablauf ausgeführt wird: Programmflussdenken.

Die innere Schleife benötigt 256 + 510 + 1 = 767 Zyklen und wird 256 mal durchlaufen macht 196352 Zyklen.
Dazu kommen noch Initialisierung und dec aussen 256 mal und brne loop somit ergibt sich: 2 + 196352 + 256 + 510 + 1 = 197121 Zyklen also ca. 0,2 s warten.

 

Leichter verständlich ist für viele der Ansatz sich Blöcke vor zu stellen.

Genau 1 ms warten

Wie bekommt man bei 1 MHz genau 1 ms Wartezeit? Sieht nach einem Fall mit einer Schleife und nop aus: 1000 / (1+1+2) = 250 somit folgendes Programm:

wait1ms:
  ldi tmp,250	;initialisieren
loop:
  dec tmp
  nop
  brne loop
 weiter..
Analyse: 1 (ldi tmp) + 249 * 4 (Schleifendurchläufe) + 3 (letzter Durchgang) = 1000 Takte.

Alternative Rechnung:
1 (ldi) + 250 *(1 (dec) + 1 (nop)) + 249 * 2 (brne Sprung) + 1 (brne kein Sprung)= 1000 Takte!
Dieses Code-Beispiel lässt sich also mühelos einstellen von 1 * 4 µs bis 256 * 4 µs.

Überprüfen der Zeiten durch Messung mit Frequenzzähler oder Speicheroszilloskop

init: ;initalisieren nach Reset
  ldi tmp, low(RAMEND) ;Stackpointer initialisieren 
  out SPL,tmp  sbi DDRB,PB0 ;PB0 als Ausgang main_loop: ; Warteschleife z.B. rcall wait10ms sbi PINB,PB0 ;invertiere Bit0 in PORTB 2 Takte rjmp main_loop ; 2 Takte
Das Beispiel bezieht sich auf einen ATtiny2313 mit 1 MHz Systemtakt und PB0 wird als Ausgang für das Messgerät verwendet. Um genaue Messungen erhalten zu können sollte statt des internen RC-Oszillators ein Quarz-Oszillator zum Einsatz kommen. Das Messgerät wird auf die Messung der Periodendauer eingestellt. Eine Periode ist die Zeit bis sich ein Signalverlauf wiederholt, hier z..B. zwischen zwei positiven Taktflanken.

Bei der 1 ms Warteschleife sollte 2008 us angezeigt werden: Nach jedem Durchgang wird der Pegel umgeschaltet, die Zeit ist: 2 * (Warteschleifenzeit + 2T (sbi) + 2T (rjmp)).

Genau 10 ms warten als Unterprogramm

wait10ms: ; rcall 3 Takte
  ldi aussen,10
loop_aussen:	 
  ldi innen,250	;initialisieren
loop_innen:
  dec innen
  nop
  brne loop_innen
  dec aussen
  brne loop_aussen
  ret ;zurueckspringen 4 Takte

Die 1 ms Verzögerung einfach 10 mal ausführen lassen?

Flussrechnung: 3 (rcall) + 1 (ldi aussen) + 10*1000 + 10 (dec aussen) + 9*2 (brne ausgeführt) + 1 (brne nicht ausgeführt) + 4 (ret) = 10037 us

Die 37 us zu viel können eingespart werden: Die innere Schleife nicht mit 250 laden, sondern mit 249. Die Zeit verkürzt sich um 10*4 Takte. Jetzt fehlen 3 Takte, mit 3 nops am Ende ausgleichen!

Überprüfen Sie die Lösung mit einem Messgerät.

Erstellen Sie eine Lösung, die ohne nop in der inneren Schleife auskommt und messen Sie die Zeit.

wait10ms:
  ldi aussen,10
loop_aussen:	 
  ldi innen,249  ;initialisieren
loop_innen:
  dec innen
  nop
  brne loop_innen
  dec aussen
  brne loop_aussen
  nop
  nop
  nop
  ret

Unterprogramm ca. 100 ms warten

wait100ms:
  ldi aussen,130
loop_aussen:	 
  ldi innen,0	;Register auf 0
loop_innen:
  dec innen
  brne loop_innen
  dec aussen
  brne loop_aussen
  ret  ; Rücksprung

Es muss nicht immer so genau sein – etwa 100 ms bei 1 Mhz Takt warten:

Die innere Schleife benötigt 1+255*(1+2)+1+1 = 256 * 3 = 768 Zyklen = 0,768 ms Mit der äusseren Schleife ergibt sich 3 (rcall) + 1 (ldi) + 130 * 768 + 130 * 1 (dec) + 129 * 2 (brne) + 1 (brne) + 4 (ret) =
4 + 130 * 769 + 129 *2 + 5 = 100237 Zyklen = 100,237 ms.

Erstellen Sie einen PAP für das Unterprogramm. Lösung...

Entwickeln Sie eine Lösung, die genau 100 ms wartet.

Nix genaues mehr mit Interrupts

Sobald man Timer und Interrupts verwendet kann es mit der Genauigkeit vorbei sein. Der Ablauf wird unterbrochen und zur Interrupt-Routine verzweigt..

Aufgaben

Der µC sei mit 4 MHz getaktet, erstellen Sie eine Lösung für eine 1 ms Warteschleife und messen Sie die Genauigkeit.

Der µC sei mit 6 MHz getaktet, erstellen Sie Quellcode für eine 5 ms Warteschleife und messen Sie die Genauigkeit.

Doppelblinker

Ein ATtiny 2313 wird mit 4MHz betrieben an PB0 soll zunächst ein 1kHz-Signal ohne Verwendung von Interrupts ausgegeben werden.

Als Erweiterung soll nun an PB1 ein 1Hz Signal ausgegeben werden, dass mit einem Timerinterrupt erzeugt wird.

Nachschlag: Lange warten mit nur einer Schleife

Lutz Lißeck schrieb mir eine sehr interessante Mail:

..ich bin auf Ihrer Webseite über die Warteschleifenprogrammierung beim AVR gestolpert. Hier wird eine Verzögerung nach klassischem Beispiel realisiert, in dem in verschachtelten Schleifen Rechenzeit verbraten wird. Die Verschachtelung der Schleifen macht aber die Berechnung von Wartezeiten (unnötig) kompliziert.
Der AVR kann auch mit Zahlen größer als ein Byte rechnen, dazu wurdem dem Controller spezielle Flags spendiert (dazu gibt es auch ein gutes App-Note von Atmel). Wird damit einen Zähler mit 16, 24, 32 oder mehr Bits programmiert, so läßt sich die Verzögerungszeit viel einfacher berechnen. Wie das gemacht wird, hatte ich vor einiger Zeit bereits in einem Forum auf Mikrocontroller.net gepostet [mikrocontroller.net/forum/read-4-11426.html#1142].

Auf den Mailtext folgend ist diese Routine bereits als Makro verpackt, das lnop-Makro muss allerdings evtl. direkt in das Verzögerungsmakro geschrieben werden (alternativ kann auch 2x nop verwendet werden), da AVRASM Makros in Makros (noch?) nicht unterstützt.

;***************************************************************************
;* lnop
;***************************************************************************
;* Typ  		: Macro, Öffentlich ;* Kurzbeschreibung	: Quasi ein NOP-Befehl, der aber 2 Zyklen benötigt
;* Eingabe		: keine
;* Ausgabe		: keine
;* Benutzte Register	: keine
;* Zyklen		: 2
;* PrgMem-Verbrauch	: 1 Words
;***************************************************************************
; Langer NOP
; Braucht 2 Zyklen zur Verarbeitung, aber nur ein Word Code.
.macro 	lnop
   ; rjmp	$ + 1	; Für AvrTerse  (Muss im listfile zu c000 kompiliert werden)
   rjmp	PC + 1	; Für AVRASM (Muss im listfile zu c000 kompiliert werden)
.endmacro

;***************************************************************************
;* delay_flex3Kern
;***************************************************************************
;* Typ  		: Makro, Öffentlich
;* Kurzbeschreibung	: FlexiWait mit 3 freiwählbaren Registern,
;*			  die mit der Verzögerungszeit initialisiert sein
;*			  müssen. Verzögerungsbereich: 10 - 167,7M Zyklen	
;* Eingabe		: @0: Lowbyte, @1: Midbyte, @2: Highbyte
;*			  @0-@2: Alles initialisierte Register ab R16
;*                        Zugelassener 24-Bit-Wertebereich:
;*                           $1 - $ffffff ($0 entspr. $1000000) ;* Ausgabe		: keine
;* Benutzte Register	: Status, @0-@2
;* Zyklen		: 24-Bit-Wert * 10
;***************************************************************************

; Flexible Waitroutine zum abwarten von bel. Zyklenanzahlen
; Eingabe: 24-Bit in drei Registern aufgeteilt.
; Verzögerung: (24-Bit-Wert * 10) = Zyklen
; Variablen: @0 Low-Byte, @1 Mid-Byte, @2 High-Byte ; (alles Reg. oder Reg.Def's von R16-R31)

; Beispiel:
;		; Register mit Verzögerung initialisieren
;
;		.set delayzyklen = 1000000
;		
;		ldi	r16, low(delayzyklen)	; 1
;		ldi	r17, byte2(delayzyklen)	; 1
;		ldi	r18, byte3(delayzyklen)	; 1
;		
;		delay_flex3Kern r16, r17, 18
;
;		Gesammtverzögerung bis hier: 3 + 10*delayzyklen


.macro			delay_flex3Kern
		
		  	; Schleifenkern:
		  	; Großen Zähler decrementieren 		  	
delay_f3K_W1:  		subi	@0, 1		; 1
		  	sbci	@1, 0		; 1
		  	sbci	@2, 0		; 1
		  	lnop			; 2 (LNOP)
		  	nop			; 1
		  	lnop			; 2 (LNOP)
		  	brne	delay_f3K_W1	; 2 (1)
			
			nop

.endmacro