AVR-Tipps & Tricks

 

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)

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:

  • Bei allen aufsteigenden Werten COUNT = 0 bis 254 entsteht bei jeder Subtraktion von -1 = 0xff immer ein Übertrag.
     
  • Erst bei der letzten Subtraktion bei Zählerstand COUNT = 255 = 0xff ensteht beim Ergebnis von 0xff-0xff = 0 kein Übertrag.

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:

  • Aufpassen bei Status-Abfragen nach INC und DEC.
    Lediglich die Flags Z, N , V und S werden beeinflusst und können nach INC und DEC per BRcc abgefragt werden, also nicht per BRCS bzw. BRLO und BCC bzw. BRSH.
     
  • Beim Subtrahieren mit negativen Zahlen (z. B. als Ersatz für eine bei AVR nicht verfügbare Addition einer Konstanten) wird das Carry umgekehrt behandelt.
     
  • FOR-Schleifen erlauben normalerweise, dass man das Carry innerhalb der FOR-Schleife für andere Zwecke verwenden kann, ohne dass das Carry (und nur das Carry!) durch den Schleifenzähler in Mitleidenschaft gezogen wird.
     
  • Ohne Beispiele sei noch erwähnt, dass die AVR-Befehle CPC, SBC, SBCI (also Vergleich und Subtraktion mit Carry) für das Z-Bit auch den Zustand des Z-Bits vor dem Vergleichen bzw. vor dem Subtrahieren berücksichtigen, womit man beim Vergleichen und Subtrahieren mehrerer Bytes nacheinander ohne Zusatzaufwand feststellen kann, ob aus mehreren Bytes bestehende Zahlen gleich sind bzw. ob das aus mehreren Bytes bestehende Ergebnis der Subtraktion 0 ist.

    Warum das bei ADC nicht so ist, erschließt sich mir momentan nicht, denn aus der Tatsache, dass das Ergebnis der letzten Teiladdition (per ADC) 0 ist, kann man nicht schließen, dass das Ergebnis der gesamten Kettenaddition 0 ist (was nur der Fall sein kann, wenn alle Summanden 0 sind).

nach oben

 


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:

  • Keine ungewollten Endlosschleifen programmieren, die nicht so schnell entdeckt werden.

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 ...

 

nach oben

 


FOR ... DOWNTO (26.5.2021)

 

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.

 

nach oben

 


 

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.

 

nach oben

 


 

DIV16_8 (16.01.2022)

 

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.

 

nach oben

 


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.

 

 

nach oben

 


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.

nach oben

 


[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.