© Eberhard Haug 2003-2024
Optimale Darstellung dieser Website
bei aktiviertem
"Active Scripting"
(LED-Menü-Buttons)
In dieser Rubrik sollen einige AVR-Stolperfallen beschrieben werden - in die insbesondere der Anfänger auch im Zusammenhang mit s’AVR gerne reintritt - und wie man diese vermeidet.
Außerdem werden an dieser Stelle s’AVR-Code-Schnipsel hinterlegt, die vielleicht hier oder dort brauchbar sind.
Der kurze Weg geht hier entlang:
Inkrement und Dekrement (9.12.2019)
Kleiner oder gleich, und größer (9.12.2019)
FOR ... DOWNTO (26.5.2021)
Inkrement und Dekrement (9.12.2019)
Die meisten CPUs beeinflussen bei INC und DEC wenig oder keine Flags, insbesondere nicht das Carry-Flag. Letzteres hat einen besonderen Grund.
Der 68k-Befehlssatz (CISC) z. B. unterstützt zwar kein INC und kein DEC, dafür ein DBcc, das gleich alle nötigen Branch-Bedingungen perfekt abgedeckt hat (sogar True und False), obwohl es eigentlich ein "DScc" = Decrement and Skip if cc" ist (wobei cc sogar vor dem DEC abgefragt und ggf. ausgeführt wird), was für den Anfänger vielleicht etwas verwirrend ist. Bei diesem BDcc werden überhaupt keine Flags verändert.
Bei Z80 (CISC) z. B. ist der vergleichbare Befehl DJNZ etwas einfacher ausgeführt (genau genommen ist es ein Branch = relativer Sprung bei <> 0, so wie man es sich vorstellt). Auch er beeinflusst keine Flags, auch nicht das Carry bei INC und DEC.
Manche CPUs haben ein separates Auxiliary-Flag AC, das bei einem INC- oder DEC-Überlauf gesetzt wird.
Bei den 8-bit-AVR (RISC) werden bei INC und DEC nur die Flags Z,N,V und S beeinflusst, das Carry bleibt auch hier bewusst außen vor.
Man darf deshalb nicht in Versuchung geraten, bei einem 8-bit-AVR-µC folgenden einfachen s’AVR-Code zu schreiben, wenn COUNT alle Werte von 0 bis 255 aufsteigend durchlaufen soll:
clr COUNT
REPEAT
.
.
.
.
inc COUNT ; INC does not generate a carry!
UNTIL C
denn diese REPEAT-Schleife wird entweder nur einmal durchlaufen (falls vorher oder zwischendurch das Carry anderweitig gesetzt wird) oder sie wird überhaupt kein Ende finden.
Einen solchen Programmierfehler zu entdecken, kann Zeit und Nerven kosten ...
Statt dem INC-Befehl jedesmal die Konstante +1 zu addieren, wäre dagegen eine denkbare Lösung.
Allerdings gibt es bei den 8-bit-AVR keinen "ADDI"-Befehl, sondern man muss sich in diesem Falle mit SUBI COUNT,-1 behelfen, jedoch mit der Einschränkung, dass man dann für COUNT nur die "oberen" AVR-Register R16 bis R31 verwenden kann.
Damit folgt auch gleich der nächste Fallstrick, nämlich dass das Carry beim Subtrahieren von -1 genau entgegengesetzt behandelt wird (im Vergleich zum Addieren von +1).
Mit anderen Worten:
Mit dieser Erkenntnis schaut der korrekte Code schließlich so aus:
clr COUNT
REPEAT
.
.
.
.
subi COUNT,-1 ; SUBI -1 must be used instead of INC
UNTIL NOT C ; however, the carry is handled the opposite way!
NOT C, NC und !C werden von s’AVR als gleichwertig behandelt.
Man kann beim Tippen sogar etwas länger nachdenken und zur besonderen Betonung schreiben ;-)
UNTIL NOT NOT NOT C
Dann aber unbedingt eine ungerade Zahl von NOT oder gleich !C nehmen ...
Die umgekehrte Reihenfolge, nämlich COUNT = 255 bis 0, wird mit diesem Wissen nun besonders einfach:
ldi COUNT,255
REPEAT
.
.
.
subi COUNT,1
UNTIL C ; = -1, here the carry works the regular way
Der Vollständigkeit halber soll erwähnt werden, dass man dennoch INC und DEC nehmen kann, dann aber für einen Überlauf nicht das Carry, sondern den Wert von COUNT abfragen muss, also bei aufsteigender Zählfolge per INC:
clr COUNT
REPEAT
.
.
.
inc COUNT
UNTIL COUNT == #0 ; = 256
Für diesen Fall kann man wieder alle AVR-Register R0 bis R31 verwenden, denn die Abfrage auf 0 erfolgt bei s’AVR per TST-Befehl und nicht per CPI-Befehl.
Spätestens jetzt stellt man fest, dass man das Ganze hätte einfach auch wie folgt schreiben können, denn der INC-Befehl beeinflusst zwar nicht das Carry, aber das Z-Flag:
clr COUNT
REPEAT
.
.
.
inc COUNT
UNTIL Z ; = 256
Deshalb kann hier der abschließende TST-Befehl durch s’AVR entfallen.
Bei absteigender Zählfolge per DEC schaut es für 256 Werte COUNT = 255 bis 0 mit der abschließenden Registerabfrage so aus:
ldi COUNT,255
REPEAT
.
.
.
dec COUNT
UNTIL COUNT == #255 ; = -1
Hier gilt wieder die Einschränkung für COUNT nur die "oberen" AVR-Register R16 bis R31.
Für die absteigende Zählfolge hätte man ganz ohne Abfrage und ohne Register-Einschränkung wie folgt programmieren können:
FOR COUNT := #256
.
.
.
ENDF
Allerdings durchläuft COUNT hier nacheinander die Werte 0, 255 ... 1 (dann Vorsicht beim Verwenden von COUNT innerhalb der FOR-Schleife), denn diese s’AVR-spezielle FOR-Schleife mit der Konstanten #256 ist nichts anderes als:
clr COUNT
FOR COUNT
.
.
.
ENDF
Bei den FOR-Schleifen wird übrigens durch s’AVR bei ENDF das FOR-Register (hier COUNT) per DEC dekrementiert, aber per BRNE an den Anfang der Schleife gesprungen (insgesamt ähnlich dem DJNZ bei Z80), denn das Z-Flag wird bei DEC bedient und das Carry eben nicht.
Für diese spezielle FOR-Schleife mit COUNT := #256 ist der von s’AVR erzeugte flache Assembler-Code sogar identisch mit REPEAT-UNTIL Z:
clr COUNT
REPEAT
.
.
.
dec COUNT
UNTIL Z ; = 256
Der Unterschied ist, dass bei der REPEAT-Schleife sowohl
clr COUNT
als auch
dec COUNT
im Programm getippt werden müssen, wogegen diese beiden Befehle bei dieser FOR-Schleife automatisch von s’AVR erzeugt werden.
Das fehlende Carry ist für etwas gut
Bei Betrachten der FOR-Schleife mit dem DEC-Befehl am Ende (bei ENDF) wird nun klar, warum INC und DEC das Carry gar nicht beeinflussen sollen, denn damit kann man innerhalb einer FOR-Schleife ungestraft Operationen verwenden, die das Carry beeinflussen und mit jedem Schleifendurchgang weiterverwenden, z. B. wiederholte Schiebe- und Rotationsbefehle, Arithmetik mit größereren Zahlen, die wiederholt durchgeführt wird, u. v. m.
Also:
Kleiner oder gleich, und größer (9.12.2019)
Dass die 8-bit-AVR nur einen ziemlich reduzierten Satz von Skip-Befehlen haben, wurde ja schon erwähnt.
Aber auch an anderen Stellen des Befehlssatzes musste man offensichtlich sparen, denn es gibt leider keine Assembler-Sprünge bei "<=" und ">" (sowohl mit als auch ohne Vorzeichen), sondern nur bei "<", ">=" und "=".
Deshalb müssen sich Compiler für AVR - und somit auch s’AVR[1] - in diesen Fällen etwas anderes einfallen lassen, besonders wenn mit Konstanten verglichen wird.
Kleiner oder gleich
Eine Abfrage auf "<=" kostet zunächst nur einen weiteren Branch-Befehl, indem man stattdessen nacheinander bezüglich "<" und "=" abfrägt (egal in welcher Reihenfolge):
BRLO
BREQ
Bei größer ist der Aufwand größer
Nun, wenn man zwei Register auf ">" vergleichen möchte, kann man für die Abfrage zunächst die beiden Register vertauschen und dann auf "<=" abfragen, wofür es die Ersatzlösung gibt.
Ein Register und eine Konstante für die Abfrage zu tauschen, geht natürlich nicht. Die mögliche Lösung findet man anhand eines Beispiels in C-Sprache, dessen Listing man analysiert.
Mangels eines Assembler-Sprungbefehls bei ">" (um die geschweifte Klammer zu überspringen) wird GCC z. B. diesen C-Code:
if (a <= 200)
{
.
.
.
}
wie folgt kompilieren:
if (a <= 200)
a4: 89 3c cpi r24, 0xC9 ; 201
a6: 18 f4 brcc .+6 ; 0xae <main+0x1e>
{
.
.
.
}
Wie man sieht, hat GCC mit der um 1 größeren Konstanten 201 verglichen, nämlich um BRCC statt einem nicht vorhandenen Assembler-Befehl "BRHI" verwenden zu können.
s’AVR wendet genau denselben Trick an, denn aus:
IF COUNT <= #200
.
.
.
ENDI
wird dieses flache AVR-Assembler-Programm:
;01// IF COUNT <= #200
CPI COUNT,200+1
BRSH _P29
.
.
.
;01// ENDI
_P31:
_P29:
Nur hat s’AVR statt dem BRCC des GCC den gleichwertigen Assembler-Befehl BRSH verwendet (der in diesem Zusammenhang sogar naheliegender ist).
Spannend wird dieser C-Code (mit uint8_t count):
while (count <= 255)
{
.
.
.
}
den ein C-Compiler normalerweise wegoptimiert (also ohne extra Assembler-Befehle für die Abfrage zu generieren [und sonst einen Assembler-Fehler zu provozieren], nur die Befehle in der geschweiften Klammer werden ausgeführt, und zwar in einer versteckten Endlosschleife), da diese Abfrage bei 8 Bit ohne Vorzeichen immer wahr ist.
Man könnte also genau so gut
while (1)
{
.
.
.
}
schreiben.
Bei s’AVR wird es mit diesem vergleichbaren Code:
WHILE COUNT <= #255
.
.
.
ENDW
durch den +1-Trick etwas schwieriger, denn s’AVR möchte (ohne Optimierung des Vergleichs) zunächst richtig rechnen, stellt dabei aber ein Problem[2] fest (das GCC durch die Optimierung vermieden hat):
;01// WHILE COUNT <= #255
_P32:
CPI COUNT,255+1
.ERROR "in s'AVR line 381: This case comparing with constant 255
will generate Assembler troubles! Use an AVR register instead."
BRSH _P33
.
.
.
;01// ENDW
RJMP _P32
_P33:
Dieser Code wird sich also nicht assemblieren lassen, denn der Programmentwickler muss das Problem nach der Fehlermeldung erst selbst erkennen (nämlich WHILE-Struktur ist immer wahr) und beseitigen.
Oder wollte er statt einer Endlosschleife (bei s’AVR normalerweise LOOP-ENDL) gar eine Abfrage auf Überlauf (siehe REPEAT-Schleife weiter oben)?
Dann war die Fehlermeldung von s’AVR sogar hilfreich.
Falls er aber (wie per Fehlermeldung empfohlen) ein Register zum Vergleichen verwendet, findet s’AVR prompt einen anderen trickreichen Weg, der allgemein gültig und immer assemblierbar ist, im Falle LIMIT=255 aber nicht wegoptimiert[3] wird:
;01// WHILE COUNT <= LIMIT
_P33:
SEC
CPC COUNT,LIMIT
BRSH _P35
.
.
.
;01// ENDW
RJMP _P33
_P35:
Und jetzt ist es wie bei GCC im Falle von LIMIT=255 eine versteckte Endlosschleife, allerdings mit drei extra Befehlen ausprogrammiert.
Also:
Nur gut, dass C-Compiler und s’AVR dem Programmentwickler Überlegungen bezüglich Flags meistens abnehmen (manchmal auch mit einer versteckten Falle) - wenn er nicht ausschließlich in AVR-Assembler-Sprache programmiert und dann immer wieder selbst über zulässige Abfragen und Sprünge nachdenken muss ...
Die in einigen höheren Programmiersprachen vorkommende FOR-DOWNTO-Struktur wird zwar von s’AVR nicht direkt unterstützt, aber man kann sie einfach per EXITIF emulieren, sprich statt:
FOR count := #100 DOWNTO #10 ; DOWNTO wird nicht von s’AVR unterstützt
.
.
.
ENDF
schreibt man einfach:
FOR count := #100 ; Anfangswert 100
EXITIF count < #10 ; Endwert 10 (DOWNTO)
.
.
.
ENDF ; Schrittweite -1
Natürlich kann man eine solche DOWNTO-Schleife nach der Initialisierung mit dem Anfangswert auch per WHILE realisieren, dann sogar mit anderer Schrittweite (auch aufsteigend).
Hier die WHILE-Version, ebenfalls mit Schrittweite -1 (absteigend):
ldi count, 100 ; Anfangswert 100
WHILE count >= #10 ; Endwert 10 (DOWNTO)
.
.
.
dec count ; Schrittweite -1
ENDW
Bei einer Zeile mehr Tipparbeit zeigt das compilierte s’AVR-Programm exakt dasselbe Verhalten, wenn auch bei geringfügig anderen AVR-Assembler-Befehlen, zunächst per abgebrochener FOR-Schleife:
;01// FOR count := #100 ; Anfangswert 100
LDI count,100
_L1:
;01// EXITIF count < #10 ; Endwert 10 (DOWNTO)
CPI count,10
BRLO _L4
.
.
.
;01// ENDF ; Schrittweite -1
DEC count
BRNE _L1
_L4:
und hier per WHILE-Schleife:
ldi count, 100 ; Anfangswert 100
;01// WHILE count >= #10 ; Endwert 10 (DOWNTO)
_L5:
CPI count,10
BRLO _L7
.
.
.
dec count ; Schrittweite -1
;01// ENDW
RJMP _L5
_L7:
Der einzige Unterschied ist der jeweils letzte Assembler-Befehl.
Speicherbedarf und Laufzeit der beiden Versionen sind identisch.
CONTINUE - vorzeitig weitermachen (26.5.2021/26.1.2022)
Am Rande sei bei dieser Gelegenheit verraten:
Für alle Schleifen (also FOR, LOOP, REPEAT und WHILE) wird in einer neueren s’AVR-Version (ab s’AVR 2.33) inzwischen auch CONTINUE unterstützt, ab dem vorzeitig die nächste Schleifen-Iteration durchgeführt wird, noch bevor das eigentliche Ende der jeweiligen Schleife erreicht ist.
s’AVR verwendet dafür die verkürzten Anweisungen CONT und CONTIF, wobei letztere ein bedingtes CONTINUE ist, welches sonst umständlich und weniger übersichtlich in einer zusätzlichen IF-Struktur untergebracht sein muss.
Bei s’AVR sind CONT und CONTIF also nur in den genannten Schleifen-Strukturen zugelassen, nicht aber in IF-Strukturen. Deshalb kommt bei s’AVR-Programmen im Normalfall bei Bedarf fast ausschließlich CONTIF zum Einsatz (und CONT nur sehr selten).
Hier eine einfache Anwendung für CONTIF bei einer schnellen Ganzzahldivision:
; Dividend64 / Divisor32 = Quotient64 + Rest32
; DD7:DD6:DD5:DD4:DD3:DD2:DD1:DD0 / DS3:DS2:DS1:DS0
; = Q7:Q6:Q5:Q4:Q3:Q2:Q1:Q0 + RE3:RE2:RE1:RE0
; Hierfür können beliebige AVR-Register verwendet werden:
; r15:r14:r13:r12:r11:r10:r9:r8 / r7:r6:r5:r4
; = r15:r14:r13:r12:r11:r10:r9:r8 Rest r3:r2:r1:r0
DIV64_32:
clr r0 ; Rest32 löschen und für Quotienten vorbereiten
clr r1
clr r2
clr r3
clr r17 ; Arbeitsregister (CY von Rest)
clr r18 ; Arbeitsregister (Wert 0)
FOR r16 := #64
lsl r8 ; Dividend << 1, LSB löschen
rol r9
rol r10
rol r11
rol r12
rol r13
rol r14
rol r15
rol r0 ; MSBit von Dividend zur Berechnung des Quotienten nach Rest32
rol r1
rol r2
rol r3
rol r17 ; ggf. Übertrag bei Rest
cp r0, r4 ; Dividend ? Divisor
cpc r1, r5
cpc r2, r6
cpc r3, r7
cpc r17, r18
CONTIF C ; Divisor ist > Dividend (in Rest), deshalb nächste Bitstelle
sub r0, r4 ; sonst Divisor von Dividend (in Rest) abziehen
sbc r1, r5
sbc r2, r6
sbc r3, r7
sbc r17, r18
inc r8 ; LSB setzen
ENDF ; Quotient r15:r14:r13:r12:r11:r10:r9:r8, Rest r3:r2:r1:r0
ret
Das ist sauber und übersichtlich strukturiert programmiert.
Mit einem Original-CONTINUE müsste man etwas aufwendiger z. B. so schreiben:
FOR r16 := #64
.
.
cp r0, r4 ; Dividend ? Divisor
cpc r1, r5
cpc r2, r6
cpc r3, r7
cpc r17, r18
IF C ; Divisor ist > Dividend (in Rest), deshalb nächste Bitstelle
CONTINUE ; von s’AVR nicht in dieser Form unterstützt
ENDI
sub r0, r4 ; sonst Divisor von Dividend (in Rest) abziehen
sbc r1, r5
sbc r2, r6
sbc r3, r7
sbc r17, r18
inc r8 ; LSB setzen
ENDF ; Quotient r15:r14:r13:r12:r11:r10:r9:r8, Rest r3:r2:r1:r0
Genau genommen macht dieses komplexer geschriebene CONTINUE in der nächst höheren Struktur-Ebene FOR ... ENDF weiter, was den Anfänger schon etwas irritieren kann.
Das einfachere CONTIF ist ähnlich wie EXITIF, nur dass es nicht die aktuelle Struktur in die nächst höhere Ebene verlässt, sondern die aktuelle Schleife (hier FOR) mit der nächsten Iteration fortsetzt und dabei einige restliche Befehle dieser Schleife (hier sub r0, r4 etc.) überspringt.
Alternativ könnte man bei der gezeigten FOR-Schleife statt dem äußerst kompakten CONTIF C auch ein IF NC ... ENDI über den Rest der Schleife verwenden. Das wäre zumindest für den Anfänger verständlicher als das Original-CONTINUE.
Falls das Ergebnis der Division noch korrekt gerundet werden soll, kann man die nachfolgenden DIV16_8-Programme sinngemäß anwenden.
Obige Routine DIV64_32 wird vielleicht nicht so oft benötigt, eher schon DIV16_8, also einen 16-bit-Dividenden durch einen 8-bit-Divisor teilen. Für den Quotienten muss man dann ebenfalls 16 Bit vorsehen. Für den Rest reichen 8 Bit.
Das obige Programm schaut dann auf 16/8 Bit reduziert wie folgt aus, wobei aufgrund der wenigen Register diese nun flexibler per .def festgelegt werden können:
; Dividend16 / Divisor8 = Quotient16 + Rest8
; DividendH:DividendL / Divisor = DividendH:DividendL + Rest
; optional DividendH:DividendL als gerundetes Ergebnis
; untere AVR-Register (wahlweise auch obere):
.def Null = r12
.def Divisor = r13
.def RestL = r14
.def RestH = r15
; obere AVR-Register (vorteilhaft, sonst weitere Register und Befehle nötig):
.def DividendL = r16 ; nachher QuotientL
.def DividendH = r17 ; nachher QuotientH
.def Count = r18 ; Schleifenzähler
DIV16_8:
clr Null
clr RestL ; Rest löschen und für Quotienten vorbereiten
clr RestH
FOR Count := #16
lsl DividendL ; Dividend << 1, LSB löschen
rol DividendH
rol RestL ; MSBit von Dividend zur Berechnung des Quotienten nach Rest schieben
rol RestH
cp RestL, Divisor ; Dividend (in Rest) ? Divisor, 9-bit-Vergleich
cpc RestH, Null
CONTIF C ; Divisor > Dividend (in Rest), deshalb nächste Bitstelle
sub RestL, Divisor ; Divisor von Dividend (in Rest) abziehen
sbc RestH, Null
inc DividendL ; LSB setzen
ENDF ; Quotient steht in DividendH:DividendL, Rest extra
; optional das Ergebnis aufrunden, falls Rest/Divisor >= 0,5 bzw. 2*Rest - Divisor >= 0
clr RestH ; RestH nötig zum Runden, da 2*Rest bis zu 508 sein kann
lsl RestL ; 2*Rest
rol RestH ; Übertrag
cp RestL, Divisor ; - Divisor
cpc RestH, Null
IF NC ; Ergebnis aufrunden
subi DividendL, low(-1)
sbci DividendH, high(-1)
ENDI
; clr RestL ; falls gewünscht
; clr RestH
ret
Der Rest muss immer auf 9 Bit abgefragt werden, da der Divisor volle acht Bit betragen kann. Deshalb ist für ein möglichst einfaches Programm das Register RestH nötig und ein weiteres Register mit dauerhaft dem Wert Null ganz praktisch.
Abschließend könnte man den Rest noch löschen, falls das optionale Aufrunden verwendet wird.
Register eingespart (26.1.2022)
Mit etwas komplexeren Abfragen kann man die beiden Register RestH und Null (s. o.) noch einsparen:
; Dividend16 / Divisor8 = Quotient16 + Rest8
; DividendH:DividendL / Divisor = DividendH:DividendL + Rest
; optional DividendH:DividendL als gerundetes Ergebnis
; untere AVR-Register (wahlweise auch obere):
.def Divisor = r13
.def Rest = r14
; obere AVR-Register (vorteilhaft, sonst weitere Register und Befehle nötig):
.def DividendL = r16 ; nachher QuotientL
.def DividendH = r17 ; nachher QuotientH
.def Count = r18 ; Schleifenzähler
DIV16_8:
clr Rest ; Rest löschen und für Quotienten vorbereiten
FOR Count := #16
lsl DividendL ; Dividend << 1, LSB löschen
rol DividendH
rol Rest ; MSBit von Dividend zur Berechnung des Quotienten nach Rest schieben
IF C ; bei Übertrag immer abziehen ...
sub Rest, Divisor ; Divisor von Dividend (in Rest) abziehen
inc DividendL ; LSB setzen
ELSEIF Rest >= Divisor ; ... sonst bei Rest >= Divisor ebenfalls
sub Rest, Divisor ; Divisor von Dividend (in Rest) abziehen
inc DividendL ; LSB setzen
ENDI
ENDF ; Quotient steht in DividendH:DividendL, Rest extra
; optional das Ergebnis aufrunden, falls Rest/Divisor >= 0,5 bzw. 2*Rest >= Divisor
lsl Rest ; Rest := 2*Rest, ggf. mit Übertrag
IF C ; bei Übertrag das Ergebnis immer aufrunden ...
subi DividendL, low(-1)
sbci DividendH, high(-1)
ELSEIF Rest >= Divisor ; ... sonst bei 2*Rest (in Rest) >= Divisor ebenfalls
subi DividendL, low(-1)
sbci DividendH, high(-1)
ENDI
; clr Rest ; falls gewünscht
ret
Diese Variante spart nicht nur zwei Register, sondern ist im Mittel auch noch ca. 4% schneller.
Größere FOR-Schleifen (30.6.2019)
Falls die 8 Bit eines FOR-Registers nicht ausreichen, lassen sich größere FOR-Schleifen ersatzweise sehr einfach mit einer REPEAT-UNTIL-Struktur und einem Doppelregister lösen.
Hier ein Beispiel, das die Endnullen aus dem Ergebnis einer sehr großen Fakultät bestimmt:
czeros: ; count # of zeros and print in line 2
clr yh ; count # of zeros in yh:yl
clr yl
ldi zh,HIGH(FArray) ; Z register is increasing pointer to FArray
ldi zl,LOW(FArray)
ldi CNTH,HIGH(Array) ; REPEAT loop counter (counting down)
ldi CNTL,LOW(Array)
REPEAT
ld AKKU, z
EXITIF AKKU <> #0 ; first digit <> 0 found
adiw YH:YL,1 ; trailing 0 detected
adiw ZH:ZL,5 ; point to next higher entry
sbiw CNTH:CNTL,1 ; loop counter
UNTIL Z
mov AKKU3,yh
mov AKKU2,yl
rcall TM1640_PRINT_DEC_2 ; print decimal, 4 digits, 2nd line, 2nd half
ret
Die nötigen Befehle sind rot markiert.
Als Doppelregister können paarweise die AVR-Register R24 bis R31 verwendet werden.
Mit dieser Methode sind im Unterschied zu FOR-ENDF auch Schrittweiten ungleich -1 möglich, nämlich Schrittweiten -0 ... -63.
Aufsteigend
Auch für große aufsteigende FOR-Schleifen lässt sich REPEAT-UNTIL verwenden.
Dann wird vorher der Anfangswert in das Doppelregister geladen, vor dem UNTIL Z per ADIW die gewünschte Schrittweite +0 ... +63 zum Doppelregister addiert und durch einen Vergleich mit dem Endwert beendet.
Eine 16-bit-Abfrage erfolgt vorteilhaft per CP gefolgt von CPC mit zwei zusätzlichen Registern, die den Vergleichswert beinhalten.
Per CPI mit dem LOW-Wert und nachfolgend per CPC mit einem Register, das den HIGH-Wert beinhaltet, kann man wenigstens ein Register einsparen, denn ein "CPIC" gibt es leider nicht.
Fazit: Wenn möglich, für 16-Bit-Schleifen die einfachere und Register sparende absteigende Methode verwenden.
[1] s’AVR führt nur 8-bit-Vergleiche und diese generell ohne Vorzeichen aus, siehe Handbuch.
[2] Fehlermeldung ab Version 2.26, allerdings nicht bei einer symbolischen Konstanten, sondern nur bei einer Konstanten 255.
[3] Der Wert von LIMIT ist s’AVR nicht bekannt, nur dem AVR-Assembler, der aber erst recht nicht optimiert.