Home / Arduino / Arduino Assembler: Stackpointer und Argumente

Arduino Assembler: Stackpointer und Argumente

Dieser Beitrag ist Teil 3 von 3 der Serie: Arduino Assembler

Im ersten Teil haben wir gelernt wie man Funktionen in Assembler implementieren und diese dann wie gewöhnliche C-Funktionen aufrufen kann. Nur die Übergabe von Argumenten, vor allen Dingen über den Arbeitsspeicher, haben wir noch nicht sehr ausführlich behandelt. Das wollen wir in diesem Artikel nachholen.
Alle Dateien kann man sich auch dieses Mal herunterladen:https://gist.github.com/madgyver/926a6156abed6757c202

extern{
int ASM_addsomenumbers(
 char small_number,
 int normal_number,
 long big_number);
}

Abarbeiten der Argumentenliste

Wie im Post zu unserem ersten Program angedeutet werden beim Funktionsaufruf die Argumente der Reihe nach abgearbeitet. Dies geschieht nach einem festen Algorithmus, denn man dort nachlesen kann.
ASM_addsomenumbers verlangt 3 Argumente die jeweils unterschiedlich viel Speicher benötigen und gibt einen Wert vom Typ int zurück.

  1. Das Argument small_number ist vom typ char und genau 1 Byte groß. 1 Byte ist ungerade, daher runden wir gemäß Vorschrift auf 2 auf. R26 minus 2 ergibt R24. „small_number“ kann nun im ASM code direkt in Register 24 abgerufen werden. R25 wurde auch reserviert, wird aber nicht benutzt.
  2. Das nächste Argument normal_number ist 2 Byte groß, jedenfalls auf dem dem Arduino. 2 ist gerade, daher können wir diese Zahl direkt von R24 abziehen und erhalten R22. Das Low-Byte von small_number steckt im Register 22 und das High-Byte in R23.
  3. Longs sind auf dem Arduino 4 Byte groß und wieder gerade, daher können wir auch hier direkt von R22 abziehen und erhalten R18. Das Argument wird also über R18, R19, R20 und R21 übergeben. In R18 steckt das Byte mit der niedrigsten Wertigkeit und in R19, R20 und R21 jeweils die nächst höherwertigen Bytes von big_number.

Der Rückgabewert

Wenn wir nun innerhalb unserer Funktion mit unseren Argumenten getan haben, was immer wir auch vor hatten, kommt der Zeitpunkt an dem wir das Endergebnis zurück an den Funktionsaufruf geben müssen. Hier gehen wir wieder nach dem Rezept im ersten Posting vor:

Unser Rückgabewert ist ein Standartinteger und 2 Bytes groß. Wir ziehen von R26 also 2 ab, womit wir bei R24 landen. Es wird vom Funktionsaufruf erwartet, dass das Ergebnis in R24 und R25 gespeichert wird, bevor wir aus unserer ASM Funktion zurückspringen. Das wars schon. Auf der C++ Seite müssen wir nichts weiteres machen. Alles wird so gut abgekapselt, als hätten wir eine völlig normale C++ Funktion vor uns.

Die Funktion ASM_addsomenumbers könnte jetzt natürlich alles mögliche mit den Zahlen machen, in diesem konkreten Fall addiert sie aber einfach small_number mit normal_number und dem niedrigsten Byte von big_number:


.global ASM_addsomenumbers
ASM_addsomenumbers: 
    ;add small_number to normal_number
    add r24, r22
    adc r25, r23
 
    ;add Low-Byte of big_number to previous result
    add r24, r21
    reti

Stackpointer, oder das ominöse „ablegen im Speicher“

Was ist aber, wenn wir sehr viel mehr Argumente haben oder diese sehr viel größer sind? Für den Fall, dass uns die Register ausgehen werden die Argumente auf dem Stack abgelegt. Wer sich schon einmal mit komplexeren Zugriffen auf den Stack auseinander gesetzt hat, wird jetzt erst einmal leicht mit den Zähnen knirschen, aber das ganze klingt schlimmer als es ist.
Schauen wir erst einmal, ab wann wir den Stack eigentlich benötigen.
Die Funktion ASM_add4longs ist schon nahe dran:


.global ASM_addsomenumbers
long ASM_add4longs(long long_a,long long_b,long long_c,long long_d);

Jedes long benötigt ganze 4 Bytes an Speicherplatz! Das Argument long_a steckt z.B. in den Registern R25,R24,R23 und R22. Zur Verdeutlichung ist hier eine kleine Tabelle:

Argument Register
long_a R25 | R24 | R23 | R22
long_b R21 | R20 | R19 | R18
long_c R17 | R16 | R15 | R14
long_d R13 | R12 | R11 | R10

Soweit so gut. So lange wir nicht unterhalb von R8 „landen“, werden alle Argumente über die Register übergeben. Was ist aber, wenn wir jetzt noch ein longübergeben wollen, wie hier?

 

long ASM_addstack  (long long_a,long long_b,long long_c,long long_d,long long_e);

Bei dieser Funktion verhalten sich die Argumente long_a bis long_d genauso wie bei unserem vorherigen Beispiel. Das Argument long_e passt allerdings nicht mehr in die Register, den rein theoretisch müsste man long_e in R9,R8,R7 und R6 stecken. Da wir jetzt aber unterhalb von R8 gelandet sind, wird das Verfahren abgebrochen und das Argument long_e komplett über den Speicher übergeben, mit Hilfe des Stacks. Komplett heißt hierbei auch wirklich komplett, es wird nicht etwa die eine Hälfte in R9 und R8 gespeichert und die andere Hälfte im Arbeitsspeicher sondern alle 4 Bytes wandern vorübergehend auf den Stack. An dieser Stelle gehe ich davon aus, dass man zu mindestens schon einmal vom Stackpointer gehört hat.

Beim Funktionsaufruf werden die Bytes von long_e automatisch auf den Stack geschoben, Byte für Byte. Der Stackpointer zeigt dabei immer auf den letzten Wert der abgelegt wurde. Wenn wir aus unserer Funktion wieder zurückkehren, sorgt avr-gcc dafür, dass die Werte aus dem Stack wieder runter geschoben werden (alle nacheinander ins Register R0, falls es jemanden interessiert). Alles das geschieht automatisch, wir brauchen keine pop oder push Befehle „von Hand“ benutzen, was die Fehleranfälligkeit stark reduziert.

Um jetzt an die Werte zu gelangen, kopieren wir die Adresse des Stackpointers in ein Pointerregister (in diesem Fall das Register Z) und greifen über den Befehl LDD auf das Argument zu.

ASM_addstack:
 ; copy stackpointer into Z-Pointer
 in r30, 0x3d
 in r31, 0x3e 
 
 ;add b to a, a is already in the return registers
 add r22,r18
 adc r23,r19
 adc r24,r20
 adc r25,r21
 
 ;add c to previous result
 add r22,r14
 adc r23,r15
 adc r24,r16
 adc r25,r17 
 
 ;add d to previous result
 add r22,r10
 adc r23,r11
 adc r24,r12
 adc r25,r13 
 
 ;load e form memory to registers
 ldd r18, Z+3 ;Call Instruction increments Stack pointer +2
 ldd r19, Z+4
 ldd r20, Z+5
 ldd r21, Z+6
 
 ;add e to previous result
 add r22,r18
 adc r23,r19
 adc r24,r20
 adc r25,r21
 reti

LDD unterstützt auch einen sogenannten Offset, das heiß wir können nicht nur den Wert von der Stelle laden, auf den das Z-Register zeigt, sondern auch 1,2,3…64 Stellen danach. Man beachte allerdings: Auf dem Stackpointer wird beim Ausführen unserer Funktion auch die Rücksprungadresse in die Hauptschleife gespeichert. Diese nimmt selbst 2 Byte in anspruch, weshalb sich unser Argument nicht bei Z+1 sondern bei Z+3 befindet.
Hier ein kleiner Arduino Sketch zum testen:

 

#include <ASM_Memory.h>
 
// the setup routine runs once when you press reset:
void setup() {
  static char str1[] = "Lorem Ipsum";
  static int int_array[] = {1,2,3};
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);
 
  Serial.println("Adding some numbers, 17,18,19");
  Serial.println(ASM_addsomenumbers(17,18,19));
 
  Serial.println("Adding 4 longs, 12,23,34,4");
  Serial.println(ASM_add4longs(12,23,34,45));
 
  Serial.println("Adding 5 longs, 12,23,34,45,56");
  Serial.println(ASM_addstack(12,23,34,45,56));
}
 
// the loop routine runs over and over again forever:
void loop() {
 
}

Im nächste Artikel wollen wir uns dann mit Strings und Arrays beschäftigen.

Andere Teile der Serie<< Arduino Assembler: Das erste Programm

Leave a Reply

Your email address will not be published. Required fields are marked *

*