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

Bei absteigender Zählfolge per DEC schaut es entsprechend 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.

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-Assemblersprache programmiert und dann immer wieder selbst über zulässige Abfragen und Sprünge nachdenken muss ...

 

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 (vorteilhaft per CP und CPC mit zwei Registern) abgeschlossen.

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.