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
- Arduino Uno
- Widerstände
- LEDs
- alternativ ein Arduino Starterkit (perfekt, wenn man auch noch andere Projekte basteln will)
- Arduino IDE
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:
ldi | mit „load immediate“ wird eine 8 Bit Zahl in eines der Register zwischen 16 und 31 geladen |
call, ret | mit „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.
Dieser Code bewirkt bei mir kein leuchten der LED. Wenn ich es direkt in C Code schreibe hingegen schon:
int led=10;
void setup() {
pinMode(led,OUTPUT);
}
void loop() {
digitalWrite(led,HIGH);
delay(2000);
digitalWrite(led, LOW);
delay(2000);
}
Der Assembler Code hat bei mir nichts bewirkt, obwohl ich mich genau an die Anleitung gehalten habe.
Hallo ich bin Ihren Tutorial gefolgt. Leider hat der Assembler Code bei mir nicht die LED zum Leuchten gebracht, obwohl ich jeden einzelnen Schritt befolgt habe. Ist das richtig das: “ sbi DDRB, 2 “ für den Pin mit der dezimalen Nummer 10 steht? Bitte antworten Sie mir!
Bei mir funktioniert der Code, ich habe auch einen Elegoo Uno R3.
Haben Sie vielleicht den ersten Assembler-Code verwendet? Da passiert nämlich tatsächlich nichts.
Vielen Dank für die Anleitung!
Dieses Tutorial ist super!
Ich habe nun auch die Lösung meines Problems gefunden. Der Mikrokontroller, den ich zuerst mit der AVR Assemblersprache aus dem Tutorial hier programmiert habe war ein Elegoo Arduino und kein originaler Arduino. Daher konnte er mit der Programmiersprache C für Arduino und der Arduino IDE programmiert werden aber nicht mit der Assemblersprache. Ich habe hier die Vermutung, dass die Assemblerbefehle für Elegoo ähnlich wie für den gewöhnlichen Arduino ist, aber dennoch weitere Befehle hinzukommen müssen. Fazit: Für originale Arduino Mikrokcontroller kann man wie im Beispiel genannt programmieren, aber nicht für Arduinos von anderen Marken. Da sollte man dann besser C benutzen.