AVR-cadabra: AVR Assemblersprache mit Arduino

Das Wort „Assemblersprache“ löst bei den meisten Menschen neben Kopfschmerzen vor allem zwei Assoziationen aus: „kompliziert“ und „unnötig“. Assembly-Programmierung ist eine geheimnisvolle Kunst, deren Kniffe nur Eingeweihte durchschauen können. Und Assembly-Code wird vermutlich in düsteren Katakomben von lang-bärtigen Programmier-Zauberern zusammengebraut. Höchstwahrscheinlich sind auch Drachen oder Kobolde involviert. So (oder so ähnlich) das Klischee. Aber trotz der mysteriösen Aura, die Assemblersprachen umgibt, ist maschinennahes Programmieren kein Hexenwerk. Und für kleine Experimente braucht der Assembler-Zauberlehrling weder teure Hardware noch komplizierte Software oder einen Bart: mit einem Arduino, der Arduino IDE und Spaß am Tüfteln schreiben sich die ersten Zeilen Code wie von Zauberhand. In diesem Tutorial zeigen wir dir, wie du einen Arduino mit Assemblersprache programmierst.

Das brauchst du für das Projekt

Am Anfang war das Bit

Dieses kleine Tutorial kann (und will) natürlich kein Lehrbuch ersetzen. Trotzdem müssen ein paar Begriffe geklärt werden. Denn auch wenn Assembler nicht unbedingt schwieriger ist als andere Sprachen, es ist auf jeden Fall anders. (Ungeduldige können die nächsten Abschnitte auch überspringen und direkt mit dem Code weitermachen).

Assemblersprachen sind low-level Programmiersprachen, d.h. wir arbeiten direkt an der Hardware eines Computers. Trotz mitunter großer Unterschiede haben Computer etwas gemeinsam: ein Prozessor mit Registern und Speicher. Der Prozessor arbeitet Befehle ab, Register sind Prozessorkomponenten, in denen Informationen abgelegt werden können.

Was diese Informationen anbelangt, ist das Weltbild eines Computers simpel, es gibt genau zwei Zustände: wahr oder falsch. Spannung oder keine Spannung. 1 oder 0 – auch bekannt als ein Bit, der kleinsten Einheit eines Computers. In grauer Vorzeit, lange vor Neuland und Digitalisierung, bestand genau darin die einzige Möglichkeit, mit Maschinen zu kommunizieren. Nämlich als Abfolge von Nullen und Einsen, also Maschinensprache.

Im Gegensatz zu Computern haben Menschen allerdings gewisse Schwierigkeiten mit dem Lesen und Schreiben von Binärcode. Um Programmierer-Zeit und – nerven zu sparen, können die Maschinenbefehle auch abgekürzt in Textform geschrieben werden: einer Assembler-Sprache.

Der Aufbau von Prozessoren – also die Prozessorarchitektur – ist von Prozessor zu Prozessor verschieden. Deswegen hat jede Architektur ihr eigenes Set Assemblerbefehle. Beispielsweise gehört unser Arduino mit einem ATmega328-Chip zur megaAVR Familie und hat 32 Register mit jeweils 8 Bit.

Hex Hex: Assembly-Zaubertheorie

Wie spätestens seit Harry Potter bekannt ist, braucht man zum Zaubern Zaubersprüche. In unserem Fall sind das die einzelnen Assembler-Befehle. Glücklicherweise gehen die meisten Assembler-Befehle etwas leichter über die Lippen als Harry Potter’s Beschwörungen: statt Zungenbrechern wie „Wingardium Leviosa“ genügen für Assembly-Magie meist wenige Buchstaben. Der Aufbau von Assembler-Befehlen ist dabei häufig gleich: ein Kürzel, das dem Prozessor sagt, was passieren soll gefolgt von weiteren Angaben wie Zahlen oder Registern. Möchte man beispielsweise eine Zahl in ein Register names EAX bewegen, könnte der Quellcode so aussehen:

MOV EAX, 16

Also übersetzt: „move (bewege) wohin, was“.

Mit diesem Code füttert man dann zwei Programme, den Assembler und Linker, die ein Hex-File zurückgeben. (Aber darüber müssen wir uns in diesem Tutorial keine Gedanken machen, das erledigt die Arduino-IDE.)

Und wie es sich für ordentliche Zaubersprüche gehört, findet man Assembler-Befehle in einer Art Zauberbuch: dem Instruction Set Manual oder Programming Manual. Natürlich sind PDF-Dokumente weniger stilvoll als ein in Leder gebundener Almanach, aber mindestens genauso nützlich.

Arduino mit Assemblersprache: Hello World

Nach der trockenen Theorie ist es endlich Zeit für etwas Praxis. Das „Hello World“ Programm der Hardware-Welt ist die blinkende LED und selbstverständlich wollen wir diese Tradition nicht brechen.

Arduino IDE

Zuerst wird die Arduino IDE vorbereitet. Damit du die Entwicklungsumgebung auch für Assembler-Programme verwenden kannst, ist ein kleiner Trick nötig. Die IDE ist misstrauisch gegenüber Dateiendungen, die sie nicht kennt. Deswegen verpacken wir den Assembly-Code: in einem C-Programm rufen wir mit dem Schlüsselwort „extern“ die Funktionen aus der Assembler-Datei auf.

Die Ordnerstruktur für das Blink-Beispiel sieht also folgendermaßen aus:

In die “.ino”-Datei kommt das C Programm, der Assmebler-Code kommt in die “.S”. Achtung: beim Öffnen die “.ino”-Datei verwenden, sonst meckert die IDE.

Das Grundgerüst in der C-Datei (.ino) sieht so aus:

extern "C" {
  // die Funktionen aus dem Assemblercode
  void start();
  void blink();
}

void setup() {
  start();
}

void loop() {
    blink();
}

Schaltbild

Als Nächstes baust du die Schaltung auf:

Code

Dann sind wir endlich bereit für die ersten Zeilen Assemblerzauberspruch. Damit die LED blinkt, reichen an sich drei Befehle:

sbi“set bit” setzt ein Bit in einem IO-Register. Am Beispiel der LED heißt das, der Pin, an dem die LED ist, wird auf 1 (also “an”) gesetzt
cbi“clear bit” macht genau das Gegenteil: ein Bit wird auf 0 gesetzt, unsere LED ist damit also aus
rjmp“relative jump” ist ein Sprungbefehl, um an einem Punkt im Programm zu springen. Da unsere LED ununterbrochen blinken soll, basteln wir uns damit eine Endlosschleife. Wir springen immer wieder an den Anfang des Programms, schalten die LED an, schalten sie aus, springen wieder zum Anfang usw.

Und hier der Code:

#define __SFR_OFFSET 0
  
#include "avr/io.h"

.global start
.global blink

start:
  sbi   DDRB,2  
  ret

blink:
  sbi PORTB, 2
  cbi PORTB, 2
  rjmp blink

Wenn man diesen Code auf den Arduino lädt (natürlich zusammen mit dem C-Code in der “.ino”-Datei) geschieht – scheinbar gar nichts. Tatsächlich passiert aber genau das, was passieren soll, die LED blinkt. Nur leider kann unser Auge nicht mit der Geschwindigkeit des Arduinos mithalten.

Das kleine hex-Einmaleins

Um die menschliche Trägheit zu kompensieren, müssen wir also noch eine kleine Verzögerung einbauen. Hier kommt ein wichtiger Assembly-Zaubertrick ins Spiel, sogenannte Zählschleifen. Die Idee ist recht simpel, der Prozessor bekommt für eine bestimmte Dauer eine einfache Aufgabe, die ihn beschäftigt hält. Und eine Methode, die sich schon beim Versteckspiel im Kindergarten bewährt hat, eignet sich auch hervorragend für Prozessoren: zählen.

Ein Arduino UNO läuft mit einer Taktfrequenz von 16MHz, das entspricht 16 Millionen Taktzyklen pro Sekunde. Für eine Verzögerung von ca. einer Viertelsekunde, müssen 4 Millionen Taktzyklen vergehen.

Wie bereits erwähnt bieten die Register des Arduinos Platz für 8 Bit. Das heißt die größte Zahl, die darin abgelegt werden kann, ist also 1111 1111, 0xff in hexadezimal oder 255 im vertrauten Dezimalsystem. Um von 255 auf 4 Millionen zu kommen, verschachteln wir die Schleifen.

Dafür kommen ein paar neue Assembler-Befehle dazu:

ldimit “load immediate” wird eine 8 Bit Zahl in eines der Register zwischen 16 und 31 geladen
call, retmit “call” wird eine Subroutine (quasi eine Unterfunktion) aufgerufen, mit “return” kehrt man zum Punkt, an dem man die Subroutine aufgerufen hat, zurück
sbiw“subtract immediate from word” subtrahiert eine Zahl von einem Registerpaar
subi“subtract immediate” subtrahiert eine Zahl vom angegebenen Register
brne“branch if not equal” ist eine Art bedingter Sprung, wenn eine Bedingung erfüllt ist, springen wir an eine bestimmte Stelle im Programm

Und das vollständige Programm sieht dann so aus:

#define __SFR_OFFSET 0
  
#include "avr/io.h"

.global start
.global blink

start:
  sbi   DDRB,2  ; PB2 = Pin 10 als Output
  ret

blink: 
  ldi   r20, 250  ; Verzoegerung in ms, max. 255 (8 Bit)
  call  delay_n_ms ; delay_n_ms aufrufen, dort zaehlen -> warten
  sbi   PORTB,2  ; Pin 10 an
  ldi   r20,250 ; wieder 250 ins Zaehlregister
  call  delay_n_ms ; wieder warten
  cbi   PORTB,2 ; Pin 10 aus
  ret
  
delay_n_ms:
  ; warten fuer ca r20*1ms, zaehlt r20 und r30 runter 
  ; 1ms = 16000 cycles bei 16MHz
  ; ca 5 Cycles pro Schleife, also ca 3000 Schleifendurchläufe
  ; da 3000 in binaer laenger als 8-bit ist, laden wir einen Teil der Zahl in 31
  ; und den anderen in 30
  ldi   31, hi8(3000) ; high(3000)
  ldi   30, lo8(3000)  ; low(3000)
delaylp:
  sbiw    r30, 1 ; zaehlen vom Registerpaar 30/31 eins runter
  brne    delaylp ; solange r30/31 nicht 0 sind, springen wir auf delayp
  subi    r20, 1 ; zaehlen von Register r20 eins runter
  brne    delay_n_ms ; solange r20 nicht 0 ist, springen wir auf delay_n_ms
  ret       ; ansonsten zurück

Mit diesem Code (und dem C-Wrapper) blinkt die LED endlich.

Jetzt bist du dran

Natürlich war das erst der Anfang. Um ein bisschen Übung zu bekommen, kannst du die Schaltung aus dem Titelbild nachbauen:

Am besten versuchst du dich erst einmal selbst am Code. Und falls etwas nicht klappt, kannst du dich an diesem Beispielprogramm orientieren:

#define __SFR_OFFSET 0
  
#include "avr/io.h"

.global start
.global fourButtons

start:
fourButtons:
  ldi r16, 0xff ; 1 = Output
  ldi r18, 0x00 ; 0 = Input
  out DDRB, r16 ; Output
  out DDRD, r18 ; Input
  out PORTB, r16 ; Output
loop:
  in r16, PIND ; Input
  out PORTB, r16 ; Output
  rjmp 

Beachte, dass du die Funktionen im C-Wrapper anpassen musst. Statt der Funktion “blink” steht hier “fourButtons”.

10 Punkte für Gryffindor!

Damit ist dein Ausflug in die Assemblerwelt erst mal vorbei – hoffentlich ganz ohne Migräne. Und vielleicht hast du ja Lust auf mehr bekommen: die Beispiele aus diesem Mini-Tutorial sollen gut verständlich sein, sind aber nicht unbedingt die eleganteste Umsetzung. Ein beliebter Weg, neu erworbene Coding-Fähigkeiten zu festigen, ist Codegolf. Dabei wird versucht, einen Algorithmus oder ein Programm so kurz wie möglich zu schreiben. Mit wie vielen (oder wenigen) Befehlen schaffst du es, die LED zum Blinken zu bringen?

Natürlich eignet sich dein Arduino nicht nur für Assembly-Spielereien. Hier findest du weitere, spannende Projektideen.

Und wie immer, falls du Fragen, Anregungen oder Verbesserungen hast: schreib’ einfach einen Kommentar.

Die mobile Version verlassen