Arduino Assemlber Delay

Hallo Leute,

ich habe eine Frage zur Assembler Programmierung des Arduinos. Unzwar haben wir einen kleinen Code unseres Lehrers bekommen der dazu verwendet wird einen Delay von 1 Sekunde zu erzeugen in der Assembler Programmierung. Nun sollen wir es bis zur nächsten Stunde verstehen und selbst programmieren können.

Hier der Code:

"ldi r18, 82" "\n"
"ldi r19, 43" "\n"
"ldi r20, 0" "\n"
"1: dec r20" "\n"
"brne 1b" "\n"
"dec r19" "\n"
"brne 1b" "\n"
"dec r18" "\n"
"brne 1b" "\n"
"lpm" "\n"
"nop" "\n"

Nun habe ich schon etwas gegoogelt und weiß das:

ldi = ldi bedeutet das eine 8-Bit Konstante in ein register geladen wird.

dec = dec bedeutet, es wird eine 1 aus dem Inhalt des Registers subtrahiert.

brne = Weiß ich nicht was es bewirkt

lpm = Lädt ein Byte aud dem Z-Register in das Zielregister.

Ich bin leider etwas aufgeschmissen was die beiden Zahlen nach einem LDI z.B. bedeuten.
Vielleicht kann mir einer helfen und kurz erklären was hier genau passiert.

Wäre sehr dankbar.

Liebe Grüße
Marko

Ah jeee, Inline-Assembler...
Wie auch immer, hier ein Code-Beispiel mit wenigen Erklärung in den Kommentaren:

/* ------------------------------------------------------------------------- */
/* Das berühmte "Blink-Programm", in Assembler                               */
/* 06.10.2015, RudiDL5                                                       */
/* Controller: AT Mega 328                                                   */
/* Größe: 504 Bytes inkl. Bootloader                                         */
/* Netto: 32 Bytes                                                           */
/* ------------------------------------------------------------------------- */

void setup()   
{
  asm volatile            
  (    
    /* ------ I/O-Ports deklarieren ---------------------------------------- */
    "Setup:   SBI   %2, %3           \n\t"   /*   pinMode( 13, OUTPUT );     */    

    /* ------ Dauerschleife "Blink" ---------------------------------------- */    
    "Loop:    SBI   %[sfb], %[pb5]   \n\t"   /*   digitalWrite( 13, HIGH );  */
    "         RCALL Sekunde          \n\t"   /*   delay( 1000 );             */
    "         CBI   %[sfb], %[pb5]   \n\t"   /*   digitalWrite( 13, LOW  );  */
    "         RCALL Sekunde          \n\t"   /*   delay( 1000 );             */
    "         RJMP  Loop             \n\t"   /*                              */
    
    /* ------ delay(1000) für 16 MHz --------------------------------------- */
    "Sekunde: LDI   r17, 100         \n\t"   /*   r17 := 100; // ~100*10ms   */
    "Aussen:  LDI   r18, 254         \n\t"   /*   repeat: r18 := 254;        */
    "Mitte:   LDI   r19, 208         \n\t"   /*     repeat: r19 := 208;      */
    "Innen:   DEC   r19              \n\t"   /*       repeat: dec( r19 );    */
    "         BRNE  Innen            \n\t"   /*       until r19 = 0;         */
    "         DEC   r18              \n\t"   /*       dec( r18 );            */
    "         BRNE  Mitte            \n\t"   /*     until r18 = 0;           */
    "         DEC   r17              \n\t"   /*     dec( r17 );              */
    "         BRNE  Aussen           \n\t"   /*   until r17 = 0;             */    
    "         RET                    \n\t"   /*   return;                    */       
    
    /* ------ Compiler I/O ------------------------------------------------- */
    ::        [sfb] "I" (_SFR_IO_ADDR(PORTB)),
              [pb5] "I" (PORTB5),
                    "I" (_SFR_IO_ADDR(DDRB)), 
                    "I" (DDB5) 
 );    
}

void loop()
{  
}

Dazu zwei Links, die du intensiv durcharbeiten solltes - dann weisst du vieles, was du über Assembler auf dem Arduino wissen musst. Nicht alles - aber so einiges brauchbares schon. Mich hat es auf jeden Fall auf einen (erfolgreichen) Weg gebracht:

http://www.nongnu.org/avr-libc/user-manual/inline_asm.html

und

OK, versuchen wirs mal, ist schon lange her mit Assembler ^^

"ldi r18, 82" "\n" //Register r18 mit dem Wert 82 laden
"ldi r19, 43" "\n" //Register r19 mit dem Wert 43 laden
"ldi r20, 0" "\n" //Register r20 mit dem Wert 0 (bzw. 256 da als nächstes ein Underflowauf 255 kommt) laden
"1b: dec r20""\n" //r20 um 1 veringern (subtrahieren)
"brne 1b" "\n" //Ist r20 nicht 0, zwar eigentlich heisst es not equal aber auch gut zur 0-Prüfung, springe zur Marke "1b" zurück
"dec r19" "\n" //r19 um 1 veringern (subtrahieren)
"brne 1b" "\n" //Ist r19 nicht 0 springe zur Marke 1b
"dec r18" "\n" //r18 um 1 veringern (subtrahieren)
"brne 1b" "\n" //Ist r18 nicht 0 springe zur Marke 1b
"lpm" "\n" //Lade 1 Byte aus dem Flash in r0(?), warum auch immer
"nop" "\n" //No OPeration, für einen Takt nichts tun

Im Prinzip ist es eine verschachtelte 3-Fach Schleife zum Zeit schinden ^^ Delay halt welche 82 x 43 x 256 mal durch gegangen wird.
Der Kern der Schleife ist "1b: dec r20" und "brne 1b", dieser wird auf jeden Fall 82x43x256 durchlaufen.
Wenn du wissen willst wieviele Takte das ganze "verbräht" kannst es dir ausrechnen.
Jeder Befehl benötigt 1 Takt. brne benötigt 2 Takte wenn dieser eine Verzweigung (Branch) bzw. einen Sprung auslöst, z. B. wenn r20, r19 oder r18 nicht 0 sind.
Übrigens: In deinem Codeschnipsel war ein Fehler. Überall steht "brne 1b" aber die Marke 1b hat gefehlt. Habe "1:" mal durch "1b:" ersetzt.

MfG
Andi

EDIT: Oh man, voll übersehen. r19 wird nur 1 mal auf 43 gesetzt somit gilt für r19 81 mal auf 256 (0) + 43, böse Falle oder Faulheit vom Lehrer?^^

3DGamer:
Ich bin leider etwas aufgeschmissen was die beiden Zahlen nach einem LDI z.B. bedeuten.
Vielleicht kann mir einer helfen und kurz erklären was hier genau passiert.

Wäre sehr dankbar.

Liebe Grüße
Marko

Ich bin kein Assembler-Freak und kann überhaupt kein AVR-Assembler, aber es sieht ganz danach aus, als wenn der Code drei ineinander verschachtelte Schleifen mit drei Registern als Zählvariablen ausführt, die zu Anfang so geladen werden:

"ldi r18, 82" "\n"
"ldi r19, 43" "\n"
"ldi r20, 0" "\n"

Also Register r18 wird mit Startwert 82 geladen

Register r19 mit Startwert 43

Register r20 mit Startwert 0

und jetzt wird fleißig heruntergezählt und solange die Register nicht 0 sind, wird jeweils auf eine Sprungmarke im Code zurückgesprungen:

"brne 1b" "\n"

also branch-if-not-equal: Solange das Register ungleich null ist, Rücksprung zur Sprungmarke 1:

Eine Besonderheit ist beim Register r20 zu beachten:

Dieses wird mit 0 vorgeladen, und wenn Du von einem 8-Bit Register, das eine 0 enthält, eins abziehst, beträgt der Wert 255.

D.h. die Schleife, bei der die Zählvariable mit 0 vorgeladen wird, läuft insgesamt 256 mal, bis der Wert wieder auf 0 heruntergezählt ist und das Programm sich beendet.

Und Dein Lehrer scheint mir ziemlich faul zu sein, und fremden Code nicht als fremden Code zu kennzeichnen.

Offensichtlich hat er sich den Assembler-Code für das Delay hier generiert:

http://www.bretmulvey.com/avrdelay.html

Aber wenn man sich den Code dort erzeugen läßt, stehen immer solche Kommentarzeilen im Code:

// Generated by delay loop calculator
// at http://www.bretmulvey.com/avrdelay.html

Für mich sieht das so aus, als wenn Dein Lehrer diesen Codegenerator verwendet hat, und aus dem erzeugten Code die Kommentarzeilen anschließend entfernt hat, die auf die Quelle des Codes verweisen.
Nicht so ganz die feine englische Art.

Aber Du weißt jetzt wenigstens die Adresse, wo Du Dir AVR-Assembler-Code für ein Delay " selbst programmieren" kannst.

Genau so " selbst programmieren" wie es offenbar Dein Lehrer gemacht hat. :wink:

jurs:
Genau so " selbst programmieren" wie es offenbar Dein Lehrer gemacht hat. :wink:

Naja, bei einem dermaßen einfachen code, der wahrscheinlich schon Fantastilliarden mal zu Übungszwecken herangezogen wurde, zu denken, er sei aus einem Generator kopiert, ist IMO ein bisschen weit hergeholt. Derartige Sachen standen auch schon in einem Schmöker zum Z80, der von Anfang '80 stammt. Wahrscheinlich war auch das schon eine Kopie von einer Kopie von einer Kopie ...

Gruß

Gregor

Ertappt Herr Lehrer.
Das erzeugt der AVR Delay Loop Calculator:

; Generated by delay loop calculator
; at http://www.bretmulvey.com/avrdelay.html
;
; Delay 16 000 000 cycles
; 1s at 16 MHz

ldi r18, 82
ldi r19, 43
ldi r20, 0
L1: dec r20
brne L1
dec r19
brne L1
dec r18
brne L1
lpm
nop

Bis auf die falsche Änderung der Sprungmarke "L1" zu "1" und bei den brne´s dann "b1" sieht es gleich aus ^^
Und das sinnlose "lpm" am Ende wodurch r0 verändert wird - könnte ja sein, das man r0 noch benötigt - ist auch da.
Lieber 2 rjmp +0 verwenden statt LPM und NOP.

MfG
Andi

Die Berechnung der Takte ist wirklich vertrackt, eigentlich kaum zu schaffen :frowning:

Die Schleife mit r20 wird immer 256 mal durchlaufen.
Die Schleife mit r19 zuerst 43 mal, danach auch wieder 256 mal.
Die Schleife mit r18 wird 82 mal durchlaufen.
Das macht rund 5M Durchläufe, mit unterschiedlicher Laufzeit. Mit etwa 4 Takten pro Durchlauf wären das 20M, damit könnte bei 16Mhz tatsächlich 1s rauskommen. Gehört die genaue Berechnung der Takte auch zur Aufgabenstellung?

Hint: Schleifen durchlaufen lassen und die Durchläufe mitzählen, dann bekommt man die Laufzeit direkt ausgerechnet. Für die innerste Schleife wären das 255*3 (branch taken) + 2 (branch not taken), usw. In C etwa so:

unsigned long z = ???;  //call, init and return?
byte r18 = 82;
byte r19 = 43;
byte r20 = 0;
do {
 do {
   do {
     z += 3; //decrement and branch
   } while (--r20);  z--; //last branch not taken
   z += 3;
 } while (--r19); z--;
 z += 3;
} while (--r18); z--;

Was mich noch interessieren würde, da ich den AVR Assembler nicht näher kenne: von MIXAL(?) kenne ich 10 Labels (0-9), die beliebig oft eingefügt und dann jeweils vorwärts (n=next - oder f=forward?) oder rückwärts (b=back) angesprungen werden können. Ist das beim AVR Assembler auch so gedacht? Zumindest würde mir das erklären, warum der Label "1" heißt, aber mit "1b" (zur vorhergehenden "1") angesprungen werden soll.

DrDiettrich:
Die Berechnung der Takte ist wirklich vertrackt, eigentlich kaum zu schaffen :frowning:

Beim rechnen bin ich ohne dem Ausgangszustand von r19 auf über 16,16 Mil. Takte gekommen. Also nicht gerade genau dieses Delay. Da kann man das LPM und NOP am Ende auch weglassen, das macht auch nichts mehr aus.
Total Sinnlos sowas als Lehrer zum lernen zu geben.

DrDiettrich:
Was mich noch interessieren würde, da ich den AVR Assembler nicht näher kenne: von MIXAL(?) kenne ich 10 Labels (0-9), die beliebig oft eingefügt und dann jeweils vorwärts (n=next - oder f=forward?) oder rückwärts (b=back) angesprungen werden können. Ist das beim AVR Assembler auch so gedacht? Zumindest würde mir das erklären, warum der Label "1" heißt, aber mit "1b" (zur vorhergehenden "1") angesprungen werden soll.

Nein, beim AVR geht man mit Branches (brxx und rjmp) auf relative Adressen, also Zieladresse minus aktuelle Adresse im Bereich von -128 bis +127 aber Word-Orientiert welches durch eine Marke im Quellcode bestimmt wird und bei Jumps auf absolute Adressen.

MfG
Andi

Haribo68:
Ertappt Herr Lehrer.
Das erzeugt der AVR Delay Loop Calculator:

Und wenn Du die Codeerzeugung im Loop-Calculator jetzt noch von "assembler" auf "avr-gcc" umstellst, dann bekommst Du direkt verwendbaren Inline-Assembler Code erzeugt, der direkt in der Arduino-IDE verwendbar ist, so wie ihn
3DGamer von seinem Lehrer bekommen hat.

Das ist ja witzig ^^
// Generated by delay loop calculator
// at http://www.bretmulvey.com/avrdelay.html
//
// Delay 16 000 000 cycles
// 1s at 16 MHz

asm volatile (
" ldi r18, 82" "\n"
" ldi r19, 43" "\n"
" ldi r20, 0" "\n"
"1: dec r20" "\n"
" brne 1b" "\n"
" dec r19" "\n"
" brne 1b" "\n"
" dec r18" "\n"
" brne 1b" "\n"
" lpm" "\n"
" nop" "\n"
);

Ist das jetzt normal in Zeile 4 mit der 1: statt 1b: oder ist das speziell für AVR-GCC?
Hatte zwar viel mit AVR-ASM gemacht aber mit dem Atmel Studio wo man die Labels immer komplett angibt.

MfG
Andi

Haribo68:
...Total Sinnlos sowas als Lehrer zum lernen zu geben.

Meiner Erfahrung nach ist der „Fehler“ von gerade mal 1% poplig. Meiner Erfahrung nach ist die Gang„genauigkeit“ eines Arduinos dermaßen „variabel“, dass ein solcher Fehler wirklich wurscht ist. Deshalb zu meinen, es sei sinnlos, jemandem anhand dieses Beispiels Schleifenkonstruktionen oder ähnliches beibringen zu wollen, ist IMO arg übertrieben.

Gruß

Gregor

Das Kopieren aus dem Internet ist ziemlich beliebt bei Lehrern.

Sagen wir mal ich geh in die erste Schleife mit r20. Da wird jetzt von 0 eins abzogen also bleiben noch 256 durchläufe bis wir in die r19 Schleife gehen. Heißt das jetzt wenn bei der r19 Schleife eins abziehe bleiben noch 42 und dann springen wir wieder nach oben zur r20 Schleife. Dort wird dann nochmals das ganze r20 Register runtergezählt?

OK, hast ja recht gregorss.
Für das Verständnis von Schleifen ist es natürlich Sinnvoll und man muss natürlich auch seinen Grips ein bißchen anstrengen um bei Bedarf auf die genauen Takte zu kommen.

MfG
Andi

3DGamer:
Sagen wir mal ich geh in die erste Schleife mit r20. Da wird jetzt von 0 eins abzogen also bleiben noch 256 durchläufe bis wir in die r19 Schleife gehen. Heißt das jetzt wenn bei der r19 Schleife eins abziehe bleiben noch 42 und dann springen wir wieder nach oben zur r20 Schleife. Dort wird dann nochmals das ganze r20 Register runtergezählt?

In die ersten Schleife, die Kern-Schleife, wird erst von 0 ausgehend 256 mal der Inhalt von r20 mit "dec r20" und "brne 1b" heruntergezählt. Erst wenn r20 nach einem "dec r20" auf 0 gekommen ist wird weiter runter gegangen zu "dec r19" und mit dem folgenden "brne b1" dann wieder hoch zu "dec r20" und wieder 256 mal r20 heruntergezählt. Und das solange, bis r19 endlich auch auf 0 ist wodurch dann r18 decrementiert wird (dec r18) und da ja noch nicht auf 0 dann wieder zurück zur Marke 1 (oder b1) durch brne b1 usw. usf.
Raus aus allen Schleifen kommt der Prozessor erst wenn in einem Durchmarsch von "dec r20" bis zu "dec r18" alle 3 Register auf 0 decrementiert wurden.

MfG
Andi

Noch mal wegen dem Verständnis für 8-Bit Register.
Ohne Vorzeichen sind Werte möglich von 0 bis 255 (Binär B00000000 bis B11111111).
Die 0 ist ja auch ein Wert, also 0 ist nicht nichts sondern 0.
Somit gehört die 0 als Zahl wie die anderen auch dazu.
Hat ein 8-Bit Register z. B. den wert 255 und man addiert eins dazu kann es das nicht darstellen und es gibt einen Überlauf (Overflow) und es geht auf 0.
Wenn du von 0 mit dec(rcement) ein subtrahierst gibt es auch einen Überlauf und das Register geht dann auf 255, seinen höchsten darstellbaren Wert.
Da in den Schleifen nach einem dec und Prüfung auf 0 mittels brne die Register auf 0 belassen werden statt neu gesetzt passiert das immer wieder in der Kern-Schleife mit r20 und in der mittleren das die, zusammen mit der 0, 256 mal decrementiert werden.
r20 wird, bis r18 um eins decrementiert wird, sogar 65536 mal decrementiert während r19 256 mal decrementiert wird.

Ich hoffe, du verstehst was ich meine und ich denke ich höre lieber auf damit bevor ich mich total verhedder ^^

MfG
Andi

Haribo68:
Ist das jetzt normal in Zeile 4 mit der 1: statt 1b: oder ist das speziell für AVR-GCC?
Hatte zwar viel mit AVR-ASM gemacht aber mit dem Atmel Studio wo man die Labels immer komplett angibt.

Ich hab's jetzt endlich gefunden :slight_smile:

Lokale Labels sind nur Ziffern (Zahlen?), auf die mit dem Suffix 'b' (back) rückwärts und mit 'f' (forward) vorwärts gesprungen wird. Entsprechend müßte alles, was mit einem Buchstaben anfängt, ein globaler Label (Funktionsname) sein. Zumindest scheint gdb das so anzunehmen.

Ja so ein....., wärs mag ^^
Da will doch nur jemand noch mehr Verwirrung stiften :slight_smile:
Es ist ja schon gut das man in C++ Labelnamen wie Start und End und 1, 2, 3 in verschiedenen Funktionen öfter benutzen kann, muss ja nicht sein, das man ne 1 mehr als ein mal in einer Funktion benutzt. Wo bleibt da die Kreativität? ^^
Aber jeder wie ers mag.
Das ist aber reine Interpretationssache von GDB, die AVRs haben im Endeffekt auch nur relative oder absolute Sprungadressen wie es sich gehört.

MfG
Andi

DrDiettrich:
... Ziffern (Zahlen?), ...

Ziffern sind die Zeichen, die zusammen eine Zahl bilden.

Gruß

Gregor