Path: Home => AVR-Überblick => Zeitschleifen => 24-Bit-SUBI    (This page in English: Flag EN) Logo
Zeitschleife

Zeitschleife mit 24 und mehr Bit für lange Zeiten in AVR Assembler

Für lange oder sehr lange Zeiten ist eine größere Anzahl an Bits erforderlich, um so lange zählen zu können. In der folgenden Tabelle sind die Bytes und die bei einem Takt von 1 MHz erreichbaren Zeiten zusammengestellt.

BytesBitsMax. ZählerSekundenMinuten StundenTageJahre
182560,000768
21665.5360,196608
32416.777.21650,330,84
4324,295E+09214,753,58
5401,100E+126.108101,814
6482,815E+1426.0621.0862,97
7567,206E+16761,1
8641,845E+19194.847


Ab Minuten muss man also noch längere Zeiten mit einer Zeitschleife absolvieren. Diese Methode hier benutzt 24 Bits, sie kann aber auch leicht auf 32, 40 oder 64 Bits erweitert werden. Hier wird auch noch die ultra-lange Version mit 64 Bits beschrieben.

Code einer 24-Bit-Schleife


.equ c = 12345 ; Die Konstante zum Abwaertszaehlen
;
.def rC1 = R16 ; Drei Register zum Zaehlen
.def rC2 = R17 ; Das Zweite
.def rC3 = R18 ; Das Dritte
;
Main: ; Der Haupt-Code beginnt hier
  sbi DDRB,PORTB0 ; PB0 als Ausgang
RestartCount: ; Die Zaehlschleife beginnt hier
  ldi rC1,Byte1(c-1) ; Laedt das LSB mit dem Zaehlerwert
  ldi rC2,Byte2(c-1) ; Laedt das mittlere Byte mit dem Zaehlerwert
  ldi rC3,Byte3(c-1) ; Laedt das MSB mit dem Zaehlerwert
CountDown:
  subi rC1,1 ; Abwaerts, setzt die Carry-Flagge, wenn vorher Null
  brcc CountDown ; Wenn noch nicht Carry: weiterzaehlen
  subi rC2,1 ; Abwaerts mit dem mittleren Byte
  brcc CountDown ; Wenn noch nicht Carry: weiterzaehlen
  subi rC3,1 ; Abwaerts mit dem MSB
  brcc CountDown ; Wenn noch nicht Carry: weiterzaehlen
  ; Ende der Zeitschleife, in den Registern steht 0xFFFFFF
  sbi PINB,PINB0 ; Toggle den PB0-Ausgang
  rjmp RestartCount ; Starte neuen Zaehlzyklus

Die SUBI-Instruktion setzt die Carry-Flagge wenn die Subtraktion von 1 von 0x00 auf 0xFF erfolgt. Das ist das Signal, dass jetzt das höhere Byte um Eins vermindert werden muss. Die Zählschleife endet, wenn alle drei Bytes 0xFF erreicht haben. Die Schleife wird also einmal mehr durchlaufen als wenn sie bei Null enden würde. Dieser zusätzliche Durchlauf wird durch das Abziehen von Eins bei dem Laden der Konstante mit c kompensiert.

Bitte beachten, dass nicht alle älteren AVR das Invertieren eines Portbits durch Schreiben einer Eins in den PIN-Port beherrschen. Der ATtiny13 und viele andere AVRs beherrschen das.

Berechnung der Verzögerung durch die Schleife

Eine sehr durchschaubare Methode, um die Anzahl der Takte einer solchen Schleife zu ermitteln, ist es, die Instruktionen aufzulisten und, nach Einer- und Zweier-Taktzyklen getrennt (wegen der Sprünge), die Anzahl Durchläufe durch die Schleife dranzuschreiben. Diese Tabelle zeigt das Vorgehen.

Berechnung der Taktzyklen Die Liste umfasst alle Instruktionen, aus denen die Schleife besteht. Die zweite Spalte gibt an, wie oft die Instruktion mit einem (kein Sprung) bzw. mit zwei Taktzyklen (Sprung) ausgeführt wird, abhängig von der Konstanten c.

In der nächsten Spalte stehen die Einer-Instruktionen, in der vierten Spalte die Zweier-Instruktionen, multipliziert mit zwei. Die Spalte danach summiert diese beiden Formeln und ordnet die Einzelteile nach ihrem Typ: Konstanten, Vielfachen von c und c/256 sowie c/65536.

Bitte unbedingt beachten: die Division von c durch 256 bzw. 65536 ist in Ganzzahlen-Manier durchzuführen: nur die Ganzzahl zählt, die Kommastellen werden abgeschnitten und auch nicht zum Aufrunden verwendet.

Die komplette Formel für die Anzahl Taktzyklen wird durch Aufsummieren der vier Einzelbestandteile gebildet und ist sehr einfach.

Berechnung von c für eine gewünschte Anzahl Taktzyklen

Die Formel, um die Konstante c aus der Anzahl Taktzyklen CC zu berechnen, ist ziemlich einfach:
c = (CC - 7) / (3 + 2 / 256 + 2 / 65536) = (CC - 7) / 3.00784301757812
oder kann gerundet durch Teilen mit 3 berechnet werden.

Wenn das allerdings in Assembler ganz genau gerechnet werden soll, wo es ja keine Nachkommastellen gibt, dann muss man dafür sorgen, dass bei der Division durch 256 und 65536 nicht Null herauskommt. Dazu multipliziert man den Zähler und alle Bestandteile des Nenners mit einer großen Zahl, z. B. mit 0x100000000, bevor man die 2 durch 256 und durch 65536 teilt. Die Formel lautet dann in Assembler-Manier:

.equ cM=0x100000000
.equ cCalc =(cM*(cc-7))/(3*cM+2*cM/256+2*cM/65536)

Das sorgt dafür, dass c ganz genau erhalten wird. Natürlich kann die genaue Anzahl Taktzyklen nicht immer ganz genau getroffen werden, da dazu die Schleifenkonstruktion zu grob ist (+/- 3 Taktzyklen). Wer es ganz genau braucht, fügt noch das eine oder andere NOP auszlig;erhalb der Schleifen hinzu. Und wer noch löngere Zeiten braucht, kann in den innersten Loop noch ein paar NOP einstreuen. Und wer 16 mal längere Zeiten und mehr braucht, fügt ein weiteres Zählregister oder auch zwei oder drei hinzu. Das dürfte auch einen Tag, einen Monat oder ein Jahr nach dem Einschalten der Betriebsspannung ausreichen, um die LED danach umzuschalten.

Die ultra-lange Version mit 64 Bits

Das Ganze funktioniert auch mit noch viel mehr Bits ganz genauso. Mit 64 Bits lassen sich so Verzögerungen von 730.000 Jahren erzielen.

Die eigentliche Schleife geht dann schlicht so:

Restart:
  ldi rCnt0,Byte1(cCnt) ; +1 = 1
  ldi rCnt1,Byte2(cCnt) ; +1 = 2
  ldi rCnt2,Byte3(cCnt) ; +1 = 3
  ldi rCnt3,Byte4(cCnt) ; +1 = 4
  ldi rCnt4,Byte1(cCnt/65536/65536) ; +1 = 5
  ldi rCnt5,Byte2(cCnt/65536/65536) ; +1 = 6
  ldi rCnt6,Byte3(cCnt/65536/65536) ; +1 = 7
  ldi rCnt7,Byte4(cCnt/65536/65536) ; +1 = 8
Count:
  subi rCnt0,1 ; Downcount rCnt0
  brcc Count ; First inner loop
  subi rCnt1,1 ; Downcount rCnt1
  brcc Count ; First outer loop
  subi rCnt2,1 ; Downcount rCnt2
  brcc Count ; Second outer loop
  subi rCnt3,1 ; Downcount rCnt3
  brcc Count ; Third outer loop
  subi rCnt4,1 ; Downcount rCnt4
  brcc Count ; Fourth outer loop
  subi rCnt5,1 ; Downcount rCnt5
  brcc Count ; Fifth outer loop
  subi rCnt6,1 ; Downcount rCnt6
  brcc Count ; Sixth outer loop
  subi rCnt7,1 ; Downcount rCnt7
  brcc Count ; Seventh outer loop
  sbi pIn,bIn ; Ignite, +2 = 10
  rjmp Restart ; Restart, +2 = 12

Die Berechnung ist auch ziemlich leicht. Die innerste Schleife zu Beginn wird cCnt mal plus einmal durchlaufen. Dabei verursacht jeder Durchlauf drei Taktzyklen (einer für SUBI, zwei für den Rücksprung (solange Carry nicht gesetzt ist). Bis auf den jeweils letzten Durchlauf: der braucht nur zwei Taktzyklen, weil er nicht verzweigt. Die innerste Schleife wird also durchlaufen. Die beiden + 1 kommen daher, dass alle Schleifen einmal mehr durchlaufen werden als nötig, denn der letzte Durchgang, auch durch alle anderen nachfolgenden Schleifen, führt zu 0xFFFF.FFFF.FFFF.FFFF und nicht zu Null.

Die folgende Schleife wird "(c / 256) + 1" durchlaufen, die nachfolgenden Schleifen jeweils 256 mal weniger oft. Das ergibt folgende Reihe:

SchleifeDurchläufeKürzel
1cCnt + 1c + 1
2cCnt / 256 + 1c8
3cCnt / 65.536 + 1c16
4cCnt / 16.777.216 + 1c24
5cCnt / 4.294.967.296 + 1c32
6cCnt / 1.099.511.627.776 + 1c40
7cCnt / 281.474.976.710.656 + 1c48
8cCnt / 72.057.594.037.927.936 + 1c56
LetztecCnt / 18.446.744.072.719.551.616 + 1c64


Man beachte, dass die Divisionen hier wie immer jeweils ohne Dezimalrest erfolgen (abgerundet).

Das ergibt die nachfolgende Reihe an Taktzyklen:

CodezeileAnzahl Durchläufe mitGesamtanzahl Takte
einem Taktzykluszwei Taktzyklen
; Loading--8
subi rCnt0,1c+1-c + 1
brcc Countc8c-c8c8 + 2*c - 2*c8
subi rCnt1,1c8-c8
brcc Countc16c8-c16c16 + 2*c8 - 2*c16
subi rCnt2,1c16-c16
brcc Countc24c16-c24c24 + 2*c16 - 2*c24
subi rCnt3,1c24-c24
brcc Countc32c24-c32c32 + 2*c24 - 2*c32
subi rCnt4,1c32-c32
brcc Countc40c32-c40c40+ 2*c32 - 2*c40
subi rCnt5,1c40-c40
brcc Countc48c40-c48c48 + 2*c40 - 2*c48
subi rCnt6,1c48-c48
brcc Countc56c48-c56c56 + 2*c48 - 2*c56
subi rCnt7,1c56-c56
brcc Countc64c56-c64c64 + 2*c56 - 2*c64
sbi pIn,bIn-12
rjmp Restart-12


Zählt man alle Instruktionszyklen in der Gesamtspalte zusammen erhält man folgende Formel für die Gesamtzyklen:
CC = 3*c + 2*c8 + 2*c16 + 2*c24 + 2*c32 + 2*c40 + 2*c48 + 2*c56 - c64 + 13
Annähernd ergibt das ein c von CC / 3, man kann es aber auch mit etwas Rechenaufwand ganz genau rechnen.

Da die Umrechnung z. B. von Jahren in Taktzyklen nicht so ganz einfach ist, habe ich noch folgende Zeilen im Quellcode hinzugefügt:

; **********************************
;   A D J U S T A B L E   C O N S T
; **********************************
;
; Compose the duration of counting
.equ cCntYears = 0
.equ cCntMonthes = 0
.equ cCntDays = 0
.equ cCntHours = 0
.equ cCntMinutes = 0
.equ cCntSeconds = 0
.equ cCntMilliseconds = 100
.equ cCntMicroseconds = 0
;
; The clock frequency
.equ Clock = 1200000 ; of the ATtiny13
;
; **********************************
;  F I X  &  D E R I V.  C O N S T
; **********************************
;
.equ cCntSec = cCntSeconds+60*cCntMinutes+3600*cCntHours+86400*cCntDays+2629800*cCntMonthes+31557600*cCntYears
.equ cCntUSec = 1000*cCntMilliseconds+cCntMicroSeconds
.equ cCnt = (cCntSec * Clock + Clock * cCntUSec / 1000000 - 70) / 3

Die Eingabe der Zeiten erfolgt also sehr bequem, die Umrechnerei in Takte wird vom Assembler erledigt. Wer also den ersten Impuls an PB0 nach einer Stunde haben will, trägt einfach bei cCntHours eine Eins ein.

Den Code der 64-Bit-Schleiferei gibt es hier.

Man sieht an diesem Beispiel sehr eindrucksvoll, dass Optimierung in Assembler schon durch die Auswahl der verwendeten Instruktionen (hier SUBI statt DEC und als Flagge Carry statt Zero) den Code kürzer, einfacher verständlich und eleganter machen kann. Die gezeigte Berechnungsmethode wird auch mit ganz komplexen Code-Formulierungen fertig und liefert immer korrekte Ergebnisse.

Man beachte allerdings, dass bei der Berechnung von sehr langen Zeiten Assembler, die nur mit Integerzahlen von 32 Bit rechnen, leicht in die Knie gehen können und mit einem Überlauf enden. gavrasm und avr_sim arbeiten mit 64 Bits und können daher auch einige tausend Jahre noch gut rechnen.

Modifizierte 64-Bit-Berechnung

Die etwas komplizierte Berechnung der Verzögerungszeit kann man vereinfachen, indem man alle 256 Durchläufe einer Schleife gleich lang macht, sodass das Problem mit dem letzten Durchgang, der nur einen statt zwei Taktzyklen benötigt, los wird. Das geht, indem man nach dem Schleifenausgang noch einen NOP hinzufügt, also so:

  ldi rCnt,Schleifenanzahl
Schleifenloop:
  subi rCnt,1
  brcc Schleifenloop
  nop

Jetzt braucht jeder Durchgang durch die Schleife genau 3 Taktzyklen. Kombiniert man nun acht solcher Schleifen, dann braucht jede weitere durchlaufene Schleife jeweils genau 3 zusätzliche Taktzyklen und alles wird gleich lang. Die Anzahl Taktzyklen ist dann
CC = 3 * cCnt / 2560 + 3 * cCnt / 2561 + 3 * cCnt / 2562 + 3 * cCnt / 2563 + ... + 3 * cCnt / 2567 + 36
Die Termini cCnt/256N stellen dabei die Anzahl Durchläufe durch jede weitere Byte-Schleife N dar. Die Konstante 36 enthält dabei die Ladezeiten, den Durchgang durch die alle letzten Schleifen sowie das Schalten und den Rücksprung.

Das lässt sich in Assembler leichter berechnen als im vorherigen Fall. Das ist in dem Quellcode hier realisiert und demonstriert. Zusätzlich ist in diesem Quellcode auch die Berechnung von sehr langen Zeiten (>10 Jahre) optimiert, um mit der 64-Bit-Integer-Verarbeitung des Assemblers noch kompatibel zu sein. Allerdings gelingt die Berechnung von 2567 auch in 64-Bit-Assembler nicht, weshalb der letzte Term entfallen muss und bei Zeiten >100 Jahren daher geringfügige Fehlzeiten produziert. Immerhin lassen sich damit noch Zeiten von 1.000 Jahren verarbeiten, was aber die Batterie-Haltbarkeit vor große Herausforderungen stellen würde.

An den Seitenanfang

©2020 by http://www.avr-asm-tutorial.net