[:en]Arduino Assembler Tutorial : Our First Programm[:de]Arduino Assembler Tutorial : Das erste Programm[:]

Thumbnail image for an Arduino and Assembler tutorial, featuring a central Arduino board with subtle Matrix-style green code in the background, highlighting the blend of modern technology and classic Assembler programming

[:en]In this series I want to show you, how to use the Assembler (ASM) language on your Arduino and AVR. Instead of using plain assembly however, I will show you how to merge it with your C++ Code. Keep in mind, that ASM code is not very portable. This tutorial is only for AVR processors (like the Atmega168 or Atmega328 for example). Your code will not work on an Arduino Due (except for trivial cases), since it uses an ARM processor with vastly different architecture. It is also assumed, that you use the AVR-GCC compiler, which is the standard compiler used by the Arduino IDE. Anyway, a good place to start is to modify the “Blink” example sketch which comes with the Arduino.

First of all, you should know that there are (at least) 2 ways to merge C/C++ Code with ASM. The first one ist to use Inline Assembler and the second one is to write function calls in assembly. I will show you the second option, since Inline Assembler uses (in my opinion) a horrible syntax which makes the already hard to read ASM even harder to debug.

The workflow is similar to writing a library. We will need to create a header file in which we will declare our functions and a second .S file which will contain our implementation. You can find all files on Github: https://gist.github.com/madgyver/92540e0f8621bc8b0566

Lets get started:

  1. Create a new folder called “Assembler” in your library folder
  2. In this folder, create the files “ASM_Blink.H” and “ASM_Blink.S”

ASM_Blink.H contains our declarations and looks like this:

void ASM_digitalWrite(char pin);
void ASM_delay();

This part:

extern "C" { }

is a small detail, that makes AVR-GCC compile the declarations as C Code instead of C++. It smoothes out some issues with the function calls that I don’t understand completely but has never been a problem so far that Yamitenshi at reddit explained to me as follows:

Because C++ allows overloading (defining multiple functions with the same name but different arguments), function names are mangled, meaning (as a simple example) int foo(int a, int b) might end up being named foo_riaiai when the compiler is done with it, as opposed to int foo(int a) might be called foo_riai so the two can be told apart.

C does not allow this – therefore both int foo(int a, int b) and void foo(int a, char *b, float c) will be named foo when the compiler is done with it.

extern "C" {} makes sure that the compiler knows that whatever library has defined foo was compiled in such a way that it’s still called foo, and not some variation thereof, which has the added benefit of allowing you to define foo in assembly, because you know the linker will be looking for foo, and not foo_riaiai.

Besides that, this is a pretty standard declaration of 2 functions.

The file ASM_Blink.S contains the implementation:

.global ASM_digitalWrite 

ASM_digitalWrite: 
    out 0x05,r24 ;write pin_value to port 
    reti ;



    delay for 1 sec 
.global ASM_delay 
ASM_delay: 
    ldi R17, 0x53 
delay_loop: 
    ldi R18, 0xFB 
delay_loop1: 
    ldi R19, 0xFF 
delay_loop2: 
    dec R19 brne 
    delay_loop2 

    dec R18 
    brne delay_loop1 

    dec R17 
    brne delay_loop 

    ret

 

The directive

.global

makes the following function names globally visible so that the linker can find it. The function ASM_digitalWrite uses a 1 Byte argument, which will be placed into register R24 by AVR-GCC. The function itself will then output that byte to PortB. You may be wondering, why AVR-GCC places our argument at R24 of all places. Turns out, there is an algorithm it uses to determine the placement of arguments, which you can read up on in the official wiki..

It is a little bit complicated, when you read it for the first time.
For our example it boils down to: Our function ASM_digitalWrite only uses one argument with the size of 1 Byte. Since 1 is an odd number, we round it to 2 and subtract that from R26, which gives us R24 where our argument will reside.

Now we just need to put everything inside a sketch to make it work on our Arduino:

#include "ASM_Blink.h" 

// change LEDPIN based on your schematic 
#define LEDPIN PINB1 

void setup(){ 
pinMode(13, OUTPUT); 
} 

void loop(){ 
ASM_digitalWrite(0x20); 
ASM_delay(); 
ASM_digitalWrite(0x00); 
ASM_delay(); 
}

 

As you can see, it is not different from including and using a “normal” C++ library.[:de]Die meisten fangen beim Arduino mit dem „Blink“ Programm an, welches man bei den Beispielen unter 01.Basics–>Blink findet. Dieses Programm setzt den Pin 13 als Ausgang und schaltet diesen dann in der Loop-Schleife abwechselnd auf HIGH oder LOW, mit jeweils einer Sekunde Wartezeit zwischen dem Umschalten. Dieses einfache Einsteigerprogramm ist gut geeignet um zu zeigen wie man dieses Programm auch in Assembler (ASM) lösen kann.

Zuallererst sollte man wissen, dass es 2 Möglichkeiten gibt um C/C++ code mit ASM zu mischen. Die erste besteht in der Verwendung von sog. Inline Assembler und die zweite ist das einbinden von Funktionsaufrufen die in ASM geschrieben sind. Wir beschäftigen uns hier mit der letzteren Möglichkeit, weil diese mit der Arduino IDE deutlich angenehmer ist.

Die Prozedur ist ähnlich wie beim Schreiben einer Library. Wir brauchen auch hier eine Headerdatei in dem wir unsere Funktionen zuerst deklarieren und eine zweite Datei in der wir die Funktionen dann implemtieren. Fangen wir also an:

Alle Dateien sind auch auf Github zu finden: https://gist.github.com/92540e0f8621bc8b0566.git

  1. Einen Ordner mit Namen“Assembler“ im Library Ordner anlegen
  2. In diesem Ordner die Dateien „ASM_Blink.H“ und „ASM_Blink.S“ erstellen

ASM_Blink.H beinhaltet lediglich unsere Funktionsdeklarationen und sieht so aus:

void ASM_digitalWrite(char pin);
void ASM_delay();

Die Anweisung
extern "C" { }
Ist ein kleines Detail, welches dazu führt das AVR-GCC die Funktionsdeklarationen in C statt C++ kompiliert. Dies gewährleistet, dass die Funktionsaufrufe korrekt aufgelöst werden. Ansonsten passiert hier nicht mehr, als das  man 2 Funktionen deklariert.

In der Datei ASM_Blink.S steht nun die Implementierung

.global ASM_digitalWrite 

ASM_digitalWrite: 
    out 0x05,r24 ;write pin_value to port 
    reti ;



    delay for 1 sec 
.global ASM_delay 
ASM_delay: 
    ldi R17, 0x53 
delay_loop: 
    ldi R18, 0xFB 
delay_loop1: 
    ldi R19, 0xFF 
delay_loop2: 
    dec R19 brne 
    delay_loop2 

    dec R18 
    brne delay_loop1 

    dec R17 
    brne delay_loop 

    ret

 

Die Direktive

.global
macht den dahinter liegenden Funktionsnamen global sichtbar und sorgt für Fehlerfreies verlinken („einbauen“ unseres codes). Die Funktion ASM_digitalWrite erwartet ein einziges 1 Byte Argument welches beim Funktionsaufruf ins Register R24 geladen wird. Die Funktions schreibt den Inhalt nur an den PortB. Natürlich kommt

Bei der Funktion ASM_digitalWrite fragt man sich vielleicht, wie die Argumentenübergabe funktioniert. Nun, AVR-GCC folgt einem Algorithmus der festlegt wo etwaige Argument gespeichert werden, den man hier in English nachlesen kann. Meine Übersetzung ins Deutsche:

Aufrufkonvention

  • Ein Argument wird entweder vollständig in Registern übergeben oder vollständig über den Speicher.
  • Um heraus zu finden, welche Register bei der Übergabe der Argumente verwendet werden denken wir uns eine Registernummer Rn die wir als R26 initieren:

    1. Falls das Argument eine ungerade Anzahl an Bytes groß ist, rundet man die Größe zur nächsten geraden Zahl auf.
    2. Subtrahiere die gerundete Größe von der Registernummer Rn.

    3. Falls das neue Rn mindestens  R8 ist und die Größe ungleich null, dann wird das Low-Byte des Arguments in Rn gespeichert. Evtl. Weiter folgende Bytes des Argumentes werden der Reihe nach in den nächsten, aufsteigenden Registern gespeichert.

    4. Sollte das neue Register kleiner als R8 sein oder die größe des Arguments null sein, dann wird das Argument in vollständig über den Speicher.

    5. Sollte das momentane Argument über den Speicher übergeben werden, kann man aufhören: Alle weiteren Argumente werden ebenfalls über den Speicher übergeben.
    6. Falls noch Argumente übrig sein, dann fange wieder bei Punkt 1 und mache mit dem nächsten Argument weiter.
  • Rückgabewerte mit einer Größe von 1 bis einschließlich 8 Byte werden über die Register zurück gegeben. Rückgabewerte die diese Grenze überschreiten werden über den Speicher zurückgeben.
  • Falls ein Rückgabewert nicht über die Register übergeben werden kann, dann wird auf dem Stack Speicherplatz reserviert und die dazugehörige Adresse beim Funktionsaufruf übergeben. Die Funktion übergibt den Rückgabewert über den Speicher an diese Adresse.
  • Sollten die Rückgabewerte über die Register zurück gegeben werden, dann werden die selben Register verwendet als wäre der Wert der erste Paramter einer nicht-varargs function. Zum Beispiel: Ein 8-Bit Wert wird in R24 zurückgegeben und ein 32-Bit Wert wird über R22…R25 zurückgegeben.
  • Argumente einer varargs Funktion werden über den Speicher übergeben. Dies gilt auch für diejenigen Argumente mit festen Bezeichnern.

Anm.: Die Übergabe über den Speicher meint konkret, dass die Argumente auf den Stack geschoben werden. Dazu aber später mehr.

Schon ein bischen kompliziert, oder? Für unser konkretes beispiel bedeutet dies folgendes: Wie man in der Funktionsdeklaration sehen kann, hat die Funktion ASM_digitalWrite nur einen einzigen Parameter namens pin und dieser ist genau ein Byte groß. Da 1 ungerade ist, runden wir die Größe auf 2 und ziehen dies von R26 ab. Das bedeutet, dass unser Argument sich in R24 befindet.

Um unseren ASM Code zu nutzen brauchen wir nur noch einen Sketch in der Arduino IDE anlegen, mit folgendem Inhalt:

 

#include "ASM_Blink.h" 

// change LEDPIN based on your schematic 
#define LEDPIN PINB1 

void setup(){ 
pinMode(13, OUTPUT); 
} 

void loop(){ 
ASM_digitalWrite(0x20); 
ASM_delay(); 
ASM_digitalWrite(0x00); 
ASM_delay(); 
}

 

Wie man sieht unterscheidet sich das Einbinden und Aufrufen der Funktionen nicht anders, als hätte man es mit einer gewöhnlichen C/C++ Bibliothek zu tun.[:]


Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.