Path:
Home =>
AVR-Überblick =>
Zeitschleifen => 24-Bit-SUBI
(This page in English:
)
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.
Bytes | Bits | Max. Zähler | Sekunden | Minuten |
Stunden | Tage | Jahre |
1 | 8 | 256 | 0,000768 | | | | |
2 | 16 | 65.536 | 0,196608 | | | | |
3 | 24 | 16.777.216 | 50,33 | 0,84 | | | |
4 | 32 | 4,295E+09 | | 214,75 | 3,58 | | |
5 | 40 | 1,100E+12 | | 6.108 | 101,81 | 4 | |
6 | 48 | 2,815E+14 | | | 26.062 | 1.086 | 2,97 |
7 | 56 | 7,206E+16 | | | | | 761,1 |
8 | 64 | 1,845E+19 | | | | | 194.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.
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
- "cCnt - cCnt / 256 + 1" mal mit zwei Takten, plus
- "cCnt / 256 + 1" mal mit einem Takt
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:
Schleife | Durchläufe | Kürzel |
1 | cCnt + 1 | c + 1 |
2 | cCnt / 256 + 1 | c8 |
3 | cCnt / 65.536 + 1 | c16 |
4 | cCnt / 16.777.216 + 1 | c24 |
5 | cCnt / 4.294.967.296 + 1 | c32 |
6 | cCnt / 1.099.511.627.776 + 1 | c40 |
7 | cCnt / 281.474.976.710.656 + 1 | c48 |
8 | cCnt / 72.057.594.037.927.936 + 1 | c56 |
Letzte | cCnt / 18.446.744.072.719.551.616 + 1 | c64 |
Man beachte, dass die Divisionen hier wie immer jeweils ohne Dezimalrest
erfolgen (abgerundet).
Das ergibt die nachfolgende Reihe an Taktzyklen:
Codezeile | Anzahl Durchläufe mit | Gesamtanzahl Takte |
einem Taktzyklus | zwei Taktzyklen |
; Loading | - | - | 8 |
subi rCnt0,1 | c+1 | - | c + 1 |
brcc Count | c8 | c-c8 | c8 + 2*c - 2*c8 |
subi rCnt1,1 | c8 | - | c8 |
brcc Count | c16 | c8-c16 | c16 + 2*c8 - 2*c16 |
subi rCnt2,1 | c16 | - | c16 |
brcc Count | c24 | c16-c24 | c24 + 2*c16 - 2*c24 |
subi rCnt3,1 | c24 | - | c24 |
brcc Count | c32 | c24-c32 | c32 + 2*c24 - 2*c32 |
subi rCnt4,1 | c32 | - | c32 |
brcc Count | c40 | c32-c40 | c40+ 2*c32 - 2*c40 |
subi rCnt5,1 | c40 | - | c40 |
brcc Count | c48 | c40-c48 | c48 + 2*c40 - 2*c48 |
subi rCnt6,1 | c48 | - | c48 |
brcc Count | c56 | c48-c56 | c56 + 2*c48 - 2*c56 |
subi rCnt7,1 | c56 | - | c56 |
brcc Count | c64 | c56-c64 | c64 + 2*c56 - 2*c64 |
sbi pIn,bIn | - | 1 | 2 |
rjmp Restart | - | 1 | 2 |
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