1 Vorwort
Diese Einführung entstand im Rahmen eines Semester Praktikums in China. Bei einem Projekt das über die Programmierung und den Aufbau eines Quadrocopters ging wurde mit einem Arduino gearbeitet. Um alle Projektmitglieder auf einen gemeinsamen Wissensstand zu bringen brauchte ich ein speziell für Arduino und C ausgelegte Einführung. Alle die ich im Internet dazu gefunden habe waren nicht vollständig und deckten nur kleine Teilgebiete ab. Gleichzeitig war das schreiben dieser Einführung eine gute Wiederholung für mich. Es soll für all-diejenigen sein, die etwas eingerostet sind in der Sprache C oder den Umgang mit einem Arduino lernen möchten. Wobei der Arduino schon sehr benutzerfreundlich ist und für Einsteiger im Mikrocontroller Bereich gilt, finden dennoch viele die ersten Schritte schwierig. Weil es sehr knapp gehalten wurde kann es auch als Nachschlagewerk für die Syntax genutzt werden.
2 Einführung in C
Es gibt immer mehr als eine Möglichkeit, eine Aufgabe in einem Computerprogramm abzubilden. Finde deinen eigenen Programmierstil und verbessere ihn. Ablauf eines Programms:
- Präprozessing: Headerfiles(*.h) werden zum Quellfile hinzugefügt
- übersetzen in Assemblercode: Hier wird ein Quelltextfile in der prozessorspezifischen Programmiersprache Assembler erzeugt.
- Objektcode erzeugen: Hier wird aus dem Assemblerfile ein Hex-File generiert, welches die direkten Steuerbefehle für den Prozessor beinhaltet.
- Linken: Alle Objektfiles und notwendigen Bibliotheken werden miteinander zum ausführbaren Programm verbunden.
2.1 Datentypen
Die Größe der Datentypen ist System abhängig und besonders bei Mikrocontroller sehr wichtig, da der Speicher begrenzt ist. Ein Bool ist eigentlich nur ein Bit groß dennoch wird dafür ein ganzes Byte reserviert, weil kleinere Datentypen nicht verarbeitet werden können.
Type |
Vorzeichen |
Anzahl Bit |
Wertebereich |
char |
mit |
8 |
Character-Zeichen 'A' |
bool |
ohne |
8 |
false, true |
byte |
ohne |
8 |
0-255 |
short |
mit |
16 |
-32 768 bis 32 767 |
word |
ohne |
16 |
0 bis 65 535 |
int |
mit |
16 |
-32 768 bis 32 767 |
unsigned int |
ohne |
16 |
0 bis 65 535 |
long |
mit |
32 |
-2 147 483 648 bis 2 147 483 647 |
unsigned long |
ohne |
32 |
0 bis 4 294 967 295 |
float |
mit |
32 |
6-7 Stellen insgesamt |
double |
mit |
32 |
wie float |
2.1.1 Arrays
In einem Array werden Daten gleichen Datentyps zusammengefasst.
int x[3]; // Deklaration
x[0] = 1; // 1. Element
x[1] = 2; // 2. Element
x[2] = 3; // 3. Element
int y[3] = {1, 2, 3}; // Kann auch bei der Deklaration festgelegt werden
for(int i = 0; i < 3; i++) // array durchlaufen
x[i] = y[i]; // vorsicht: Element muss existieren!
2.1.2 Strings
Ein String ist nichts anderes als ein char-Array.
char str1[5] = {’T’, ’e’, ’s’, ’t’, ’\0’}; // \0 beendet den String
char str2[] = "Test"; // nach der Deklaration ist das Array 5 lang
char str3[5] = "Test";
char str4[15] = "Test"; // platz halten für einen größeren String
2.1.3 Strukturen
Die Struktur definiert einen neuen Datentyp, welcher Komponenten unterschiedlicher Datentypen beinhaltet
struct Auto // neue Struktur
{
int gewicht;
int maxGeschwindigkeit;
};
Auto bmw;
bmw.gewicht = 1600;
bmw.maxGeschwindigkeit = 290;
2.1.4 Aufzählungstyp
Der Aufzählungstyp wird verwendet, wenn man Werte gruppieren möchte, aber ein Array zu unübersichtlich ist.
enum Tag
{
montag, dienstag, mittwoch, donnerstag, freitag
};
Tag arbeitstag; // neue Variable Deklarieren
arbeitstag = montag; // Variable zuweisen
if (arbeitstag == montag)
{
// schlechte Laune
}
2.1.5 Konstanten
Es gibt Konstanten mit Variablennamen. Dabei wird eine Variable zusätzlich mit dem Schlüsselwort const gekennzeichnet, so kann sie später im Programm nicht mehr verändert werden. Es gibt auch symbolische Konstanten. Hier wird zu Beginn ein Name festgelegt, der später beim Kompilieren durch den dazugehörigen Wert ersetzt wird.
// Symbolische Konstanten
#define NL ’\n’
#define Number 5
#define Hello "Hello World\n"
//Konstanten mir Variablennamen
const int N = 5;
2.2 Operatoren
2.2.1 Arithemtische Operatoren
Zu jeder arithmetischen Operation gibt es auch eine Kurzschreibweise, die aber mit bedacht verwendet werden sollte (führt aber zu schlecht lesbarem Code).
a = a + b; // Addition
a += b;
a = a - b; // Subtraktion
a -=b;
a = a * b; // Multiplikation
a *=b;
a = a / b; // Division (Vorsicht bei Integer)
a /=b;
a = a % b; // Modulo (Rest bei Division)
a %= b;
a = b++; // Post Inkrement
a = ++b // Pre Inkrement
a = b--; // Post Dekrement
a = --b; // Pre Dekrement
2.2.2 Vergleichsoperatoren
Alle Operationen geben true oder false zurück.
int a = 3, b = 6;
a < b; // a kleiner b
a <= b; // a kleiner gleich b
a > b; // a größer b
a >= b; // a größer gleich b
a == b; // a gleich b (Vorsicht bei Gleitkommazahlen)
a != b; // a ungleich b (Vorsicht bei Gleitkommazahlen)
if(a == b) // wenn erfüllt
{
//..dann tue das
}
2.2.3 Logische Operatoren
Alle Operationen geben true oder false zurück.
bool a = true;
a = !a; // jetzt hat a den wert false
a = a && true // logisches UND
a = a || true // logisches ODER
2.2.4 Bitorientierte Operatoren
Alle Operatoren werden bitweise angewandt.
short int n = 6 // 0..000110 entspricht 6
n = ~n; // Complement, d.h. jede 0 wird eine 1 und jede 1 eine 0
n = n & 1 // bit-AND
n = n | 1 // bit-OR
n = n ^ 1 // bit-XOR
n = n << 2 // shift left by 2
n = n >> 2 // shift right by 2
if ((n | 1) == n) // 10101 OR 00001 = 10101
{
// zahl n ist ungerade
}
2.3 Kontrollstrukturen
2.3.1 Verzweigung
Es wird überprüft, ob eine Bedingung erfüllt ist. Je nach dem wird ein anderer Code ausgeführt.
bool a = true;
if(a == true)
{
// wenn a == true
}
else
{
// wenn a == false
}
int c = (a > b) ? a : b // Kurzschreibweise ist aber offiziell ein Ternärer Operator
2.3.2 for-Schleife
Der Code im Block wird eine bestimmte Anzahl durchlaufen. Auf die Zählvariable kann innerhalb des Blocks zugegriffen werden.
for(int i=0; i<10; i++)
{
// dieser Code wird 10 mal ausgeführt
}
2.3.3 while-Schleife
Der Code innerhalb des Blocks wird solange ausgeführt, wie die Bedingung im Kopf erfüllt ist.
while(a == true)
{
// dieser Code wird solange ausgeführt wie a gleich true ist
}
2.3.4 do-while-Schleife
Dies ist die fußgesteuerte while-Schleife. Sie funktioniert wie die normale while-Schleife nur, dass die Bedingung am Schluss kommt. Der Vorteil ist, dass der Code unabhängig von der Bedingung mindestens einmal ausgeführt wird.
do {
// dieser Code wird mindestens einmal ausgeführt
} while(a == true); // nicht das Semikolon vergessen!
2.3.5 break and continue
Diese beiden Anweisungen werden im Zusammenhang mit Schleifen verwendet. Vorsicht bei der Anwendung. break: der Aufruf dieser Anweisung beendet die gesamte Schleife. continue: Der Aufruf beendet nur den aktuellen Durchlauf an dieser Stelle und setzt die Codeausführung an dem Schleifenkopf fort.
int i = 0;
for(;;)
{
i++; // i um 1 hochzählen
if(i % 2 == 0) // alle geraden Zahlen überspringen
continue; // den Rest vom Programmteil überspringen und beim Schleifen Kopf starten
if(i>1000) // nach 1000 durchläufen Schleife beenden
break;
}
2.3.6 switch-Anweisung
Falls man einen Wert einer Variablen auf verschiedene Werte überprüfen möchte, ist die switch-Anweisung die beste Wahl.
int n = 7;
switch(n)
{
case 3:
// falls n gleich 3 ist
break; // die Anweisung break beendet die switch-Anweisung
case 7:
// falls n gleich 7 ist
break;
default:
// Falls es auf kein Fall zu trifft
break;
}
2.4 Zeiger (Pointer)
Zeiger zeigen auf ein Objekt eines jeden beliebigen Datentyps. Mit einem Sternchen vor dem Namen wird ein Zeiger beim Deklarieren gekennzeichnet. Er muss vom gleichen Datentyp sein, wie der, auf den er späte zeigen soll.
int n = 5 // Deklaration einer normalen Variablen
int *ip; // Deklaration eines Zeigers der auf Integer zeigen kann
ip = &n; // Pointer wird initialisiert mit der Adresse der Variablen
*ip = *ip + 2; // n wird mithilfe des Zeigers um 2 erhöht
struct Auto // Struktur erstellen
{
int maxGeschwindigkeit;
};
Auto bmw, *pBmw; // Anlegen einer Variablen und einem Pointer
pBmw = &bmw; // Pointer bekommt die Adresse von bmw
(*pBmw).maxGeschwindigkeit = 290; // zugriff auf die Variable
pBmw->maxGeschwindigkeit = 290; // bessere alternative
2.5 Funktionen
Wenn Programmabschnitte meist öfter als einmal benötigt werden, oder einfach nur der übersichtlichkeit halber, bietet sich eine Funktion an. Sie besteht aus dem Datentyp, welchen sie zurückgibt, dem Namen und der übergabeparameter. Doch zuerst muss man sie, wie auch bei den Variablen, bekannt geben.
int add(int a, int b); // Deklaration der Funktion meist außerhalb in einem Header File
int add(int a, int b) // Definition der Funktion
{
return (a + b);
}
int c = add(1, 2); // Aufruf der Funktion
2.5.1 call-by-value
void calcY(int y) // Definition der Funktion. Void bedeutet das diese Funktion nichts zurückgibt
{
y = y + 2;
}
int y1 = 2;
calcY(y1); // nach dem Aufruf hat die lokale Variable y1 immer noch den Wert 2
2.5.2 call-by-pointer
Falls die Übergabeparameter durch den Aufruf der Funktion verändert werden sollen, gibt es die Möglichkeit sie per Zeiger zu übergeben.
void swap (int *a, int *b)
{
int *tmp = a; // Variable wird temporär zwischen gespeichert
a = b;
b = tmp;
*tmp = (*a) + (*b); // Falls man auf die Werte zugreifen will
}
int x = 1, y = 2;
swap(&x, &y); // nachdem Aufruf besitzt x den Wert 2, y den Wert 1
2.5.3 call-by-reference
ähnlich wie call-by-pointer, funktioniert call-by-reference. Man gibt nur die Speicheradresse der Variablen an die Funktion weiter.
void swap (int &a, int &b)
{
int tmp = a; // Variable wird temporär zwischen gespeichert
a = b;
b = tmp;
}
int x = 1, y = 2;
swap(x, y); // nachdem Aufruf hat x den Wert 2 und y den Wert 1
2.5.4 Default-Parameter
Um bei einem Funktionsaufruf nicht alle Parameter angeben zu müssen, kann man die Variablen vor belegen.
int add(int a, int b, int c=0, int d=0); // muss bei der Deklaration geschehen
add(1, 2, 3, 4); // man muss nicht alle Parameter angeben, für die fehlenden wird der Defaultwert 0 übergeben
add(1, 2, 3);
add(1, 2);
2.5.5 Funktionen überladen
Man kann den gleichen Namen einer Funktion verwenden, solange sie sich in der Parameterliste unterscheiden. Der Compiler entscheidet dann welche Funktion er verwenden muss.
int add(int a, int b); // Deklaration der add Funktion mit zwei Integer Parameter
int add(int a, int b, int c); // Deklaration mit drei Parameter
2.6 Formatierung
Ein wichtiges Thema wird meist vergessen, die Formatierung.
- Bei der Benennung der Variablen werden wir dem CamelCase Style folgen. D.h. der Anfangsbuchstabe ist jeweils klein und jedes weitere Wort wird groß geschrieben.
- Wichtig ist es auch sein Code zu kommentieren. Zu Beginn eine kurze Beschreibung, welche Funktion diese Datei hat, den Autor und das Datum, sowie Kommentare im Code. Ein gut kommentierter Code ist zu 50% Grün.
- Außerdem ist es wichtig den Code übersichtlich zu gestalten und bei zu langen Ausdrücken in einer Zeile in mehrere Zeilen aufzuteilen.
- Bitte auch darauf achten, dass der Code einer Funktion keinen größeren Umfang als eine Seite hat.
- Jede neue Klasse sollte in einer neuen Datei geschrieben werden.
int meineZahl = 5;
Auto einObjectDerKlasseAuto;
x = 1/2(b^2 * sin(angle/PI)) // dies ist ein Kommentar
/* ein Kommentar..
.. über mehrere Zeilen */
if(a == true){b*=(a-b)%2} // kein Spaghetticode erzeugen
3 Einführung in Arduino
Arduino ist eine Open-Source-Plattform, basierend auf einem Mikrocontroller-Board und einer Entwicklungsumgebung. Warum Arduino? Arduino ist viel günstiger als so manches Entwickler Board. Sie sind leichter zu Programmieren als zum Beispiel die Atmel AVR Controller und es gibt eine große aktive Community.
3.1 Software und Installation
Arduino liefert eine eigene kleine Software die Ihr hier herunterladen könnt. Es existieren auch Plugins für Microsoft Visual Studio und Eclipse. Nach der Installation könt ihr auch schon gleich loslegen. Geht auf neuen Sketch erstellen (Programme in Arduino heißen Sketch). Dann wählt ihr noch den richtigen USB Port aus, unter Tools -> Serial Port. Nun muss noch das richtige Arduino Modell ausgewählt werden, unter Tools -> Board. Zum Schluss könnt ihr den Sketch hochladen.
// Beispiel Sketch - Blinkende LED
void setup()
{
pinMode(13, OUTPUT);
}
void loop()
{
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
}
3.2 Programmieren
Man kann den Arduino in C und teilweise in C++ programmieren, wobei man bei C++ auf den begrenzten Speicher acht geben muss. Zusätzlich gibt es noch spezielle mikrocontroller-typische Befehle, wie zum Beispiel das Einlesen und Ausgeben eines Pins. Für eine detaillierte Erklärung bitte hier nachlesen.
3.2.1 Sketch
Als Sketch wird ein Programm bezeichnet. Es beinhaltet immer zwei Funktionen, nämlich setup und loop. Setup wird genau einmal ausgeführt und dient zur Deklarationen von Variablen. Loop ist wie eine endlos-Schleife. Sie wird nach jedem Durchlauf wieder von neuem aufgerufen.
void setup()
{
// wird einmalig ausgeführt
}
void loop()
{
// Endlos-Schleife
}
3.2.2 Digital I/O
Folgendes dient zur Manipulierung der Digitalen Pins. Zuerst muss man im setup bekanntgeben, ob der Pin als Eingang oder als Ausgang fungiert.
int val = 0;
void setup()
{
pinMode(13, OUTPUT); // setzt den Pin 13 als Ausgang
pinMode(12, INPUT); // setzt den Pin 12 als Eingang
}
void loop()
{
val = digitalRead(12); // einlesen des Digitalen Wert
digitalWrite(13, val); // macht den Ausgang HIGH
delay(1000); // 1s warten
digitalWrite(13, LOW); // schaltet den Ausgang wieder auf LOW
}
3.2.3 Analog I/O
Für die Analogen Pins gilt fast das gleiche, wie bei den Digitalen Pins. Bei analogWrite wird der Wert in ein Rechtecksignal (PWM ca. 490Hz) umgewandelt, dass im Mittelwert dem gewünschten Analogen Wert entspricht.
void setup()
{
pinMode(9, OUTPUT); // Pin 9 unterstützt analogWrite()
}
void loop()
{
int val = analogRead(3); // Potentiometer einlesen
analogWrite(9, val >> 2); // val muss von 0-1023 auf 0-255 angepasst werden
}
3.2.4 Zeit
Für die Zeitgebung und die Verzögerung gibt es einige fertige Funktionen.
// 1 s = 1 000 ms = 1 000 000 us
t = millis(); // gibt die Millisekunden zurück seit der Mikrocontroller läuft. (Datentyp läuft in 50 Tagen über)
t = micros(); // gibt die zeit in Mikrosekunden zurück. Die Auflösung beträgt 4 us bei 16MHz (Datentyp läuft in 70 Minuten über)
delay(t); // verzögert das Programm um ms
delayMicroseconds(t); // verzögert um us
3.2.5 Kommunikation
Die Kommunikation mit dem Arduino erfolgt meist über einen Seriellen Port. Zum einen wird der Arduino darüber programmiert, zum anderen kann man auch mit ihm kommunizieren. Zum Beispiel Sensorwerte auslesen und direkt anzeigen. Um dies zu machen muss man zuerst den Serial Monitor aktivieren und die richtige Baudrate auswählen. Für weitere Funktionen bitte hier entnehmen.
void setup()
{
Serial.begin(9600); // Neue Kommunikation einrichten mit einer Baudrate von 9600
}
void loop()
{
Serial.println(analogRead(0)); // den Analogen Pin0 einlesen und auf dem Serial Monitor ausgeben
delay(100); // verzögerung von 100 ms
}
3.2.6 Mathe
In der Standard Bibliothek gibt es schon vorhandene Funktionen für die Berechnung. Bei der Verwendung immer darauf achten, dass der Mikrocontroller nicht viel Leistung hat.
minX = min(x1, x2); // bestimmt die kleiner Zahl
maxX = max(x1, x2); // bestimmt die größere zahl
x = abs(a); // berechnet den Absolutwert
x = constrain(x, a, b); // wird überprüft ob x innerhalb des Wertebereich liegt. Wenn x kleiner als a ist wird a zurückgegeben und wenn x größer als b ist wird b zurückgegeben.
value= map(value, fromLow, fromHigh, toLow, toHigh); // Wertebereich wird angepasst
a = pow(base, exponent); // berechnet die Potenz einer Zahl
a = sqrt(x); // berechnet die Wurzel
a = sin(a); // berechnet den Sinus (Radiant)
a = cos(a); // der Rückgabewert ist auch Radiant
a = tan(a);
randomSeed(analogRead(0)); // Den Zufallsgenerator abhängig von einem Eingang machen. Der Funktionsaufruf muss im setup erfolgen
x = random(300); // eine Zufallszahl zwischen 0 und 299 generieren
x = random(10, 100); // Zufallszahl liegt zwischen 10 und 99);
3.2.7 Externe Interrupts
Ein Interrupt ist wenn ein wichtiges Ereignis eintritt und das Hauptprogramm unterbrochen wird, um das Ereignis zu verarbeiten. Bei dem Arduino Uno gibt es zwei Externe Interrupts an Pin 2 und Pin 3. Man kann den Interrupt auslösen, wenn der Eingang auf LOW ist, sich ändert ( CHANGE ), wechselt auf high ( RISING ) oder auf low wechselt ( FALLING ). Die Interrupt Service Routine ist die Funktion, die den Interrupt verarbeitet. Sie sollte so knapp wie möglich geschrieben werden. Manche Funktionen werden nicht mehr weiterverarbeitet, wenn ein Interrupt aufgerufen wird (serielle Kommunikation). Falls Interrupts bei kritischen Sachen nicht erwünscht sind, kann man sie mit noInterrupts() deaktivieren und mit interrupts() wieder aktivieren.
volatile int state = LOW; // falls die Funktion durch ein Interrupt verändert werden soll muss volatile angegeben werden
void setup()
{
pinMode(13, OUTPUT); // Pins 13 ist ein Ausgang
attachInterrupt(0, blink, CHANGE); // Interrupt 0, Interrupt Service Routine, soll auf ein Wechsel reagieren
delay(60000); // 60 s warten
detachInterrupt(0);
}
void loop()
{
digitalWrite(13, state);
}
void blink() // Interrupt Service Routine
{
state = !state;
}
3.3 Boards
3.3.1 Arduino Uno
Das meist verwendete Board heißt Arduino Uno. Aktuell in der dritten Revision ist es für fast jedes Projekt denkbar. Im Internet kursieren etliche Projekte und Anleitungen im Bezug auf den Arduino Uno. Es gibt zusätzlich einige Shields für dieses Board um die Funktionalität zu erweitern.
Spezifikation Arduino Uno R3
Microcontroller |
ATmega328 |
Operating Voltage |
5V |
Digital I/O Pins |
14 (6 davon haben PWM) |
Analog Input Pins |
6 |
Strom maximal I/O Pin |
40 mA |
Flash Memory |
32 KB (0.5 KB Bootloader) |
SRAM |
2 KB |
EEPROM |
1 KB |
Clock Speed |
16 MHz |
3.3.2 Arduino Mega
Wenn einem die Anschlüsse doch mal nicht reichen sollten dann ist die bessere Wahl der Arduino Mega. Mit zusätzlichem Speicher ist er gut für die großen Projekte ausgerüstet.
Spezifikation Arduino Mega R3
Microcontroller |
ATmega1280 |
Operating Voltage |
5V |
Digital I/O Pins |
54 (15 davon haben PWM) |
Analog Input Pins |
16 |
Strom maximal I/O Pin |
40 mA |
Flash Memory |
128 KB (4 KB Bootloader) |
SRAM |
8 KB |
EEPROM |
4 KB |
Clock Speed |
16 MHz |
3.4 Beispiele
Nun macht es hier wenig Sinn die gleichen Beispiele die es schon genügend im Internet gibt hier noch einmal auszutreten. Unter folgendem Link findet Ihr etliche gut beschriebene Tutorials die sich meist auf den Arduino Uno beziehen aber ohne Probleme auch mit dem Mega realisierbar sind.