Keywords: Arduino Programmierung Befehlsübersicht

| ### Arduino von Prof. Jürgen Plate

## Arduino-Programmierung

Beachten Sie bitte auch die Anmerkungen zum Übergang zur Arduino-IDE ab Version 1.0 in der Einführung zum Arduino.

Die Programmiersprache für den Arduino ist eine Untermenge von C. Insofern sollte der mit C vertraute Programmierer kaum Schwierigkeiten haben. Im Gegensatz zum typischen C-Programm fehlt jedoch die Funktion `main()`. An deren Stelle treten die zwei bereits erwähnten Funktionen:

- `init()` wird beim Programmstart einmal ausgeführt und enthält alle Initialisierungen, die das Programm benötigt, z. B. die Festlegung der Datenrichtung bei E/A-Ports etc.

- `loop()` ist eine Endlosschleife, in der sich alle Befehle und Funktionen zur Ablaufsteuerung befinden.

Im Übrigen ist alles wie bei C gewohnt. Es existieren alle Kontrollstrukturen von C, ebenso fast alle Operatoren. Codeblöcke werden wie gewohnt in geschweifte Klammern eingeschlossen. Deshalb wird darauf an dieser Stelle nicht weiter eingegangen. Die Arduino-Datentypen sind an die Gegebenheiten des verwendeten ATmega-Prozessors angepasst:

- Boole'sche Werte benötigen ein Byte Speicherplatz und können true oder false sein.
- char-Variablen benötigen ein Byte Speicherplatz und stellen normalerweise ASCII-Zeichen dar. Sie können entweder als Zahl (char X = 64;) oder als Zeichen, eingeschlossen in Apostrophe, (char x = '@';) definiert werden.
- byte-Variablen belegen ein Byte im Speicher und speichern Werte zwischen 0 und 255.
- Eine int-Variable belegt zwei Byte im Speicher. Sie überstreicht den Wertebereich zwischen -32768 und 32767. Das Gegenstück unsigned int belegt ebenfalls zwei Byte, speichert aber Werte zwischen 0 und 65535.
- long belegt vier Byte im Speicher und speichert Zahlen zwischen -2147483648 und 2147483647. Die vorzeichenlose Variante unsigned long speichert Werte zwischen 0 und 4294967295.
- float und double sind momentan gleich. Sie verwenden diese Typen, um Gleitkommazahlen zu speichern. Beide belegen vier Byte im Speicher und können Werte zwischen -3.4028235E48 und 3.4028235E48 speichern.
- void wird nur für Funktionsdeklarationen benötigt. Es zeigt an, dass eine Funktion keinen Wert zurück gibt. Bei strukturierten Datentypen gibt es lediglich das Array. Structures und Pointer fehlen. Die Arduino-Programmiersprache kennt einige vordefinierte Konstanten:
- TRUE und FALSE definieren boole’sche Werte. FALSE ist einfach als 0 (Null) definiert, während TRUE einen Wert ungleich Null repräsentiert.
- HIGH und LOW definieren Pin-Level und werden beim Lesen oder Schreiben auf Digitalpins verwendet. HIGH ist logisch 1 bzw. ca. 5 Volt, während LOW logisch 0 bzw. ca. 0 Volt ist.
- INPUT/OUTPUT werden von der Funktion `pinMode()` benutzt, um einen digitalen Pin entweder als Eingang oder Ausgang zu definieren.

Im Temp-Ordner von Windows werden jedes Mal, wenn ein Sketch geöffnet oder das erste Mal kompiliert wird, Verzeichnisse angelegt:

- `build<Zahl>.tmp`: Zwischen- und Ergebnis-Dateien des Kompiliervorgangs. Hier liegt im Erfolgsfall auch die erzeugte `.hex`-Datei.
- `console<Zahl>.tmp`: enthält in den Dateien `stdout.txt` und `stderr.txt` die Ausgaben des Compilers.
  Die Standardausgabe `stdout.txt` wird beim Kompilieren auch in der IDE angezeigt.

#### Digital-Funktionen

pinMode(pin,mode)
Wird in setup() verwendet, um einen Pin entweder als Eingang oder Ausgang zu konfigurieren. Als Modus kommen nur INPUT oder OUTPUT in Frage. Die digitalen Pins sind nach dem Einschalten als Eingänge konfiguriert, weshalb sie nicht extra als Eingänge festgelegt werden müssen - es ist aber guter Programmierstil, dies trotzdem zu tun. Als Eingang konfigurierte Pins sind hochohmig. Bei der Ein- und Ausgabefestlegung der Pins gibt es noch eine Besonderheit. Im ATmega können 20-Kiloohm-Pullup-Widerstände per Software zugeschaltet werden, um offene Eingänge auf einen definierten 1-Pegel zu ziehen. Auf diese eingebauten Pullup-Widerstände kann in folgender Weise zugegriffen werden:

    pinMode(pin, INPUT); // setzt "pin" als Eingang
    digitalWrite(pin, HIGH); // schaltet den Pullup-Widerstand ein

Als Ausgang konfigurierte Pins können maximal 40 mA Strom an angeschlossene Elemente liefern. Dies ist genug um eine LED aufleuchten zu lassen (Strombegrenzungswiderstand nicht vergessen), reicht aber nicht, um Relais, Magnetspulen oder Motoren zu betreiben. Kurzschlüsse an den Arduino-Pins können, ebenso wie zu hohe Ströme den Ausgangspin oder gar den ganzen Chip zerstören. Aus dem Grund sollten Ausgangspins mit einem Widerstand von 220 Ohm oder mehr geschützt werden.

digitalRead(pin)
Diese Funktion liest den Wert von einem digitalen Pin aus und liefert als Resultat entweder HIGH oder LOW. Der Pin kann entweder als Variable oder Konstante festgelegt werden (0-13).

digitalWrite(pin,value)
gibt einen Logiklevel (HIGH oder LOW) an einem Pin aus. Der Pin kann als Variable oder Konstante festgelegt werden (0-13). Das folgende Beispiel liest einen Taster an einem digitalen Eingang aus und schaltet eine LED eine wenn der Taster gedrückt wird (auf ein Entprellen das Tasters wird hier verzichtet):

    int led = 13; // LED an Pin 13
    int pin = 7; // Taster an Pin 7
    int value = 0;

    void setup()
      {
      pinMode(led, OUTPUT); // Pin 13 Ausgang
      pinMode(pin, INPUT); // Pin 7 Eingang
      }

    void loop()
      {
      value = digitalRead(pin); // Wert einlesen
      digitalWrite(led, value); // und ausgeben
      }

#### Analog-Funktionen

analogRead(pin)
Liest den Wert eines festgelegten analogen Pins mit einer 10 Bit Auflösung aus. Diese Funktion ist nur für Pins (0-5) verfügbar. Die resultierenden Integer Werte reichen aufgrund der Auflösung von 0 bis 1023. Analoge Pins müssen im Gegensatz zu digitalen nicht zuerst als Eingang oder Ausgang deklariert werden.

analogWrite(pin, value)
Gibt analoge Werte aus. Der Prozessor verwendet dazu hardwarebasierte Pulsweitenmodulation (PWM) am Ausgangspin - es handelt sich also um Rechteckimpulse mit variablem Impuls-Pausenverhältnis. Für einen "echten" A nalogwert müssten noch ein Tefpassfilter nachgeschaltet werden. Auf Arduinos mit dem ATmega 168/368 ist diese Funktion für die Pins 3, 5, 6, 9, 10 und 11 verfügbar. Der Wert kann als Variable oder Konstante im Bereich von 0 bis 255 festgelegt werden. Das folgende Beispiel zeigt, wie man eine LED heller oder dunkler werden lassen kann.

    int ledPin = 13; // PWM Pin fuer die LED
    int ADC0 = 0; // Analogeingang
    int wert, volt;
    void setup()
      {
      Serial.begin(9600);
      }

    void loop()
      {
      wert = analogRead(ADC0); // Geschwindigkeit per Poti am
      volt = wert*5.0/1024.0; // Analogeingang einstellen,
      Serial.println(volt); // Spannung ausgeben
      for (int i=0; i<=255; i++) // heller werden
        {
        analogWrite(ledPin, i);
        delay(wert);
        }
      for (int i=255; i>=0; i--) // dunkler werden
        {
        analogWrite(ledPin, i);
        delay(wert);
        }
      }

Arduino 1.x führt eine Reihe von Verbesserungen und Änderungen bei der Serial-Bibliothek ein:

- `Serial.flush` wartet nun, bis alle ausgehenden Daten gesendet wurden, statt empfangene Daten einfach zu ignorieren. Die folgende Anweisung löscht alle Daten aus dem Empfangspuffer:

      while(Serial.read() >= 0);

- `Serial.write` und `Serial.print` blockieren nun nicht mehr. Die Daten von `Serial.write` werden nun per Interrupt im Hintergrund übertragen. Manchmal muss man nun warten, bis alle Zeichen gesendet wurden. Gegebenenfalls ruft man `Serial.flush()` gleich nach `Serial.write()` auf.
- Die Funktion `Serial.peek()` wurde hinzugefügt, damit man sich das nächste Zeichen im Empfangspuffer ansehen kann, ohne es einzulesen. Im Gegensatz zu `Serial.read` wird das Zeichen mit Serial.peek nicht aus dem Puffer entfernt.

#### Port-Manipulation

Bei manchen Anwendungen sind die Arduino-Input/Output-Funktionen wie analogRead() oder digitalWrite() zu langsam. In solchen Fällen kann man die Ports des ATmega 328 direkt manipulieren. Für die unsprüngliche Zielgruppe waren die Einzelbit-Befehle sicher isdeal, denn beim Zugriff auf die 8-Bit-Ports sind unter Umständen Bitmanipulationsoperationen wie AND, OR oder XOR notwendig um einzelne Bits zu ändern. So ist der Durchgriff auf die Ports ca. 50 mal schneller. Bei Arduino können drei Ports genutzt werden:

Port D des ATmega führt auf die digitalen Pins 0 bis 7, Port B auf die digitalen Pins 8 bis 13 und Port C auf die analogen Pins 0 bis 5. Die Ports werden von 3 Registern gesteuert, die mit "DDRx", "PORTx" und "PINx" bezeichnet werden. Diese drei Register existieren für jeden Port, es gibt also zum Beispiel für Port B ein DDRB-, ein PORTB- und ein PINB-Register. Das jeweilige DDRx-Register steuert, ob die Pins als Input oder Output konfiguriert sein sollen (DDR = Data Direction Register), das PORT-Register ist ein Ausgang, es legt fest, ob die Pins HIGH oder LOW sind und das PIN-Register gibt den Zustand der Pins an, die im DDR-Register auf Input gesetzt wurden.

Ist im DDRx-Register ein Bit auf 1 gesetzt, so wird das korrespondierende Bit des PORTx (Ausgang) auf den Anschlusspin geschaltet, ist das DDRx-Bit dagegen auf 0 gesetzt, wird der entsprechende Anschlusspin an das zugehörige PINx-Bit geleitet.

Die einzelnen Register sind in der Arduino-Entwicklungsumgebung bereits als Namen vordefiniert, man kann also sofort loslegen. Es muss lediglich die Headerdatei `avr/io.h` eingebunden werden .Beispielsweise setzt der folgende Befehl D0, D2, D6 und D7 als Eingänge sowie D1, D3, D4 und D5 als Ausgänge.

    DDRD = B00111010;

Ähnlich funktionieren Ein- und Ausgabe. Auch hier werden alle Bits gleichzeitig ein- oder ausgegeben, z. B.:

    PORTB = B00001100;

    #include <avr/io.h>

    void setup()
      {
      DDRB = B11111111;      // alle Bits als Ausgang
      }

    // Wechselblinker mit allen Ausgaengen
    void loop()
      {
      PORTB = B10101010;
      delay(300);
      PORTB = B01010101;
      delay(300);
      }

#### Zeitmessung

millis()
Gibt den (long-)Wert in Millisekunden zurück, die seit dem Programmstart vergangen sind. Dieser Wert läuft nach etwa 9 Stunden über und die Zählung beginnt dann wieder bei Null.

Das folgende Programm zeigt, wie man mit Hilfe von millis eine LED blinken lassen kann, ohne `delay` zu verwenden. Praktischerweise verwende ich die LED, die sich schon auf dem Board befindet.

    const int ledPin =  13;         // LED auf dem Board

    int ledState = LOW;             // LED-Status
    long previousMillis = 0;        // Zeitpunkt des letzten LED-
                                    // Status-Wechsels
    long interval = 1000;           // Blink-Intervall (Millisekunden)

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

    void loop()
      {
      unsigned long currentMillis = millis();  // aktueller Wert
      if (currentMillis - previousMillis > interval)
        {
        previousMillis = currentMillis;   // aktuellen Wert speichern
        ledState = ((ledState == LOW)? HIGH : LOW); // toggeln
        digitalWrite(ledPin, ledState);   // LED ansteuern
        }
      }

Bei jedem Schleifendurchlauf wird also geprüft, ob es Zeit ist, die LED umzuschalten - wenn also die Zeitdifferenz zwischen aktuellem Wert und dem Zeitpunkt des letzten Wechsels größer als die vorgegebenen Differenz ist. Dieses Schema kann durchaus mit mehreren Vorgängen und unterschiedlichen Zeiten verwendet werden. Es gibt nur zwei kleine Nachteile. Erstens gibt die Zeit für einen Schleifendurchlauf den minimalen Abstand zwischen zwei Aktionen vor und zweitens muss man eventuell überlegen, was nach knapp 50 Tagen passiert. Da ist nämlich gegebenenfalls der aktuelle Millis-Wert nicht größer, sondern sehr viel kleiner als der aktuelle und das Blinken endet scheinbar unmotiviert.

micros()
Gibt den (long-)Wert in Mikrosekunden zurück, die seit dem Programmstart vergangen sind. Dieser Wert läuft nach etwa 70 Minuten über und die Zählung beginnt dann wieder bei Null. Bei einem mit 16 MHz getakteten Prozessor beträgt die Auflösung 4 Mikrosekunden, bei langsameren Boards entsprechend mehr.

delay(ms)
Wartet für die Dauer der angegebenen Zeit in Millisekunden.

delayMicroseconds(us)
Wartet für die Dauer der angegebenen Zeit in Mikrosekunden. Relativ genaue Zeiten erhält man nur bei Werten bis 16383 (kann sich bei späteren Versionen ändern).

#### Mathematische Funktionen

min(x, y)
Berechnet das Minimum von zwei Werten und gibt den kleineren Wert zurück.

max(x, y)
Berechnet das Maximum von zwei Werten und gibt den höheren Wert zurück.

sin(x), cos(x), tan(x)
Trigonometrische Funktionen für Sinus, Cosinus und Tangens. Der Parameter wird jeweils im Bogenmass angegeben.

abs(x)
Absolutwert von x.

sqrt(x)
Quadratwurzel von x.

pow(x, exponent)
Berechnung von xexponent.

constrain(x, min, max)
Begrenzung einer Zahl auf einen Bereich. x ist der zu begrenzende Parameter. für den Rückgabewert gilt:
x: wenn x zwischen min und max liegt,
min: wenn x < min ist,
max: wenn x > max ist.

map(x, low1, high1, low2, high2)
Abbildung eines Wertes x aus dem Bereich [low1..high1] auf den Bereich [low2..high2]. Es wird keine Begrenzung des Parameters x auf den Bereich [low1..high1] vorgenommen. Die map()-Funktion verwendet Integer-Arithmetik! Ist die untere Grenze eines Bereichs größer als die Obergrenze, wird der Wert invertiert. Auch negative Grenzen sind erlaubt, zum Beispiel:

    y = map(x, 1, 100, 100, 1);
    y = map(x, 1, 100, 100, -100);

randomSeed(seed)
Setzt einen Startwert (engl. "Seed" = "Saat") als Ausgangspunkt für die Funktion `random()`. Der Arduino kann keine wirklichen Zufallswerte produzieren - er generiert vielmehr Pseudo-Zufallsfolgen. Mit `randomSeed()` kann man bessere Zufallsergebnisse erhalten. Als Seed können so zum Beispiel `millis()` oder `analogRead()` verwendet werden.

random(min, max)
Diese Funktion erlaubt die Erzeugung von Pseudo-Zufallszahlen innerhalb eines definierten Bereiches. y = random(100, 200); besetzt beispielsweise y mit einer Zufallszahl zwischen 100 und 200. Wird der Minimalwert weggelassen, setzt die Funktion ihn auf 0. Das folgende Beispiel simuliert Kerzenlicht. Dazu "flackern" zwei gelbe und eine rote LED mit zufälliger Helligkeit:

    int gelb1 = 9;
    int gelb2 = 10;
    int rot = 11;
    void setup()
      {
      pinMode(gelb1, OUTPUT);
      pinMode(rot, OUTPUT);
      pinMode(gelb2, OUTPUT);
      }

    void loop()
      {
      // LEDs mit zufälliger Helligkeit, aber mindestens etwa halbhell
      analogWrite(gelb1, random(115,255);
      analogWrite(rot, random(115,255);
      analogWrite(gelb2, random(115,255);
      // etwas warten
      delay(random(100));
      }

#### Konvertierung von Zahlen

Für das, was man bei C mittels Typecast erledigt, hat der Arduino passende Funktionen:

- char() liefert das char-Äquivalent von x zurück
- byte() liefert das byte-Äquivalent von x zurück
- int() liefert das int-Äquivalent von x zurück
- word() liefert das word-Äquivalent von x zurück
- long() liefert das longint-Äquivalent von x zurück
- float() liefert das floate-Äquivalent von x zurück

#### Serielle Kommunikation

Die serielle Schnittstelle dient der Kommunikation zwischen dem Arduino-Board und einem Computer oder anderen Geräten. Alle Arduinos haben mindestens eine serielle Schnittstelle (UART/USART). Sie kommuniziert auf den digitalen Pins 0 (RX) und 1 (TX), aber auch mit dem Computer (PC) über USB. Wird die serielle Schnittstelle verwendet, können die digitalen Pins 0 (RX) und 1 (TX) nicht gleichzeitig für andere Zwecke (z. B. als Digitalausgang) verwendet werden. Das folgende Bild zeigt die Beschaltung auf dem Arduino-Board:

Während der seriellen Übertragung blinkern die LEDs "TX" und "RX" auf dem Board. Der Arduino Mega besitzt drei weitere serielle Schnittstellen: Serial1 (Pins 19 (RX) und 18 (TX)), Serial2 (Pins 17 (RX) und 16 (TX)) sowie Serial3 (Pins 15 (RX) und 14 (TX)).

Sie können den in der Arduino-Umgebung integrierten serielle Monitor verwenden, um mit einem Arduino-Board zu kommunizieren. Klicken Sie auf die serielle Monitor-Taste in der Werkzeugleiste und wählen Sie die gleiche Baudrate wie im Arduino-Programm. Durch die Kopplung über die 1-Kiloohm-Widerstände kann der serielle USB-Anschluss auch zum "Sniffern" der seriellen Übertragung auf den Pins 0 und 1 dienen, die dadurch auch Priorität besitzen. Auch wäre es prinzipiell möglich, auf Pin 0 serielle Daten zu empfangen und andererseits Daten seriell an den PC zu senden.

Serial.begin(rate)
öffnet den seriellen Port und setzt die Baud Rate (Datenrate) für die serielle Übertragung fest. Die typische Baud Rate mit dem Computer ist 9600 Baud. Es sind folgende Datenraten (Bit/s) möglich: 300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600 und 115200.

Serial.print (data)
sendet Daten zum seriellen Port,

Serial.println(data)
sendet Daten zum seriellen Port, gefolgt von einem automatischen Zeilenumbruch als Carrier Return und Linefeed.

Die beiden Ausgabeanweisungen senden numerische Daten als Dezimalzahl über die Schnittstelle. Es kann ein optionaler zweiter Parameter als Formatangabe angegeben werden: DEC entspricht dem dezimalen Defaultwert, HEX erzeugt eine Hexadezimalzahl, OCT eine Oktalzahl und BIN erzeugt einen 8-Bit-Binärwert. Strings werden als ASCII-Zeichen geschickt, wobei auch die bei C üblichen Ersatzdarstellungen für Tabulator, Linefeed, Return usw. verwendet werden können. Weitere Informationen finden Sie auf der Arduino-Website.

Serial.write(data), Serial.write(buf,len)
sendet Daten binär zum seriellen Port. Bei Byte-Varablen wird genau ein Byte gesendet, bei Strings der komplette String. Bei zwei Parametern ist buf ein Array, das byteweise gesendet wird. len legt die Anzahl der zu sendenden Bytes fest.

Serial.available()
gibt an, ob sich Zeichen im Empfangspuffer befinden.

Serial.read()
liest ein Byte von der seriellen Schnittstelle.

Serial.flush()

Löscht den seriellen Puffer.

Serial.end()

schließt die serielle Schnittstelle.

Beispiel:

    byte eingabe;
    void setup()
      {
      Serial.begin(9600);
      }

    void loop()
      {
      if (Serial.available() > 0)
        {
        eingabe = Serial.read();
        Serial.write(eingabe);
        }
      }

Arduino 1.x führt eine Reihe von Verbesserungen und Änderungen bei der Serial-Bibliothek ein:

- `Serial.flush` wartet nun, bis alle ausgehenden Daten gesendet wurden, statt empfangene Daten einfach zu ignorieren. Die folgende Anweisung löscht alle Daten aus dem Empfangspuffer:

      while(Serial.read() >= 0);

- `Serial.write` und `Serial.print` blockieren nun nicht mehr. Die Daten von `Serial.write` werden nun per Interrupt im Hintergrund übertragen. Manchmal muss man nun warten, bis alle Zeichen gesendet wurden. Gegebenenfalls ruft man `Serial.flush()` gleich nach `Serial.write()` auf.
- Die Funktion `Serial.peek()` wurde hinzugefügt, damit man sich das nächste Zeichen im Empfangspuffer ansehen kann, ohne es einzulesen. Im Gegensatz zu `Serial.read` wird das Zeichen mit Serial.peek nicht aus dem Puffer entfernt.

Die Arduino-Versionen 1.x unterstützten nun Stream-Parsing-Methoden:

- `boolean find(char *target)` liest aus dem Stream, bis das angegebene Ziel (target) gefunden wurde. Gibt `true` zurück, wenn der Zielstring gefunden wurde. Der Stream wird beim Parsing nur einmal verarbeitet, man kann also nicht zurückgehen, um etwas anderes zu suchen.
- `boolean findUntil(char *target, char *terminate)` ähnelt der vorhergehenden Methode, hier endet die Suche jedoch erst, wenn der Terminierungsstring gefunden wird. Gibt `true` zurück, wenn das Ziel gefunden wurde.
- `long parseInt()` gibt den ersten gültigen (langen) Integerwert zurück. Führende Zeichen, die keine Ziffern und kein Minus- oder Pluszeichen sind, werden übersprungen. Der Integerwert wird bei der ersten auf die Zahl folgenden Nicht-Ziffer abgeschlossen. Werden keine Ziffern gefunden, gibt die Funktion 0 zurück.
- `long parseInt(char skip)` arbeitet wie `parseInt()`, das angegebene Zeichen `skip` wird innerhalb des numerischen Wertes ignoriert. Verwendbar, um einen einzelnen numerischen Wert einzulesen, der durch Satzzeichen getrennt ist (1,234,555).
- `float parseFloat()` arbeitet wie `parseInt()`, jedoch für Gleitpunktzahlen.
- `size_t readBytes(char *buffer, size_t length)` liest eingehende Zeichen in den angegebenen Puffer ein, bis es zum Timeout kommt oder die angegebene Anzahl Zeichen (`length`) eingelesen wurde. Gibt die Anzahl der im Puffer abgelegten Zeichen zurück.

#### Daten im Programmspeicher (FLASH) ablegen

Ab Arduino IDE Version 1.0 wurde die F()-Syntax eingeführt. Mit ihr kann man Strings im Flash-Speicher anstatt im RAM ablegen und so Speicherplatz im RAM gewinnnen. Zum Beispiel:

    Serial.println(F("This string will be stored in flash memory"));

Mittels `PROGMEM` kann ebenfalls eine Variable im Programmspeicher abgelegt werden, z. B.:

    const char satz[] PROGMEM = "Laber laber rhabarber rhabarber.";

Das Schlüsselwort "PROGMEM" ist ein Variablen-Modifikator. Es sollte daher nur mit den in `pgmspace.h` definierten Datentypen verwendet werden. Es weist den Compiler an, die Informationen im Flash-Speicher anstatt in SRAM abzulegen. PROGMEM ist Teil der Bibliothek `pgmspace.h`, die nur in der AVR-Architektur zur Verfügung steht, weshalb die Headerdatei ins Programm mittels `#include` eingebunden werden muss. Für die Syntax gibt es zwei Varianten:

    const Datentype Variablenname[] PROGMEM = {};
    const PROGMEM Datentype Variablenname[] = {};

Da PROGMEM nur auf einzelne Variablen angewendet werden kann, ist es den Aufwand nur Wert, wenn Sie einen größeren Block von Daten speichern wollen - das ist in der Regel am einfachsten als Array realisierbar. Das Verfahren über PROGMEM ist zweistufig: es sind spezielle Methoden (Funktionen) zum Auslesen der Daten aus dem Programmspeicher zurück in das SRAM notwendig, die ebenfalls in der o. g. Bibliothek stehen. Dazu ein Beispiel:

    #include <avr/pgmspace.h>

    /* Datentyp unsigned int */
    const PROGMEM  uint16_t Data[]  = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 32768};

    /* Datentyp String */
    const char Message[] PROGMEM  = {"Between two evils, I always pick the one I never tried before. -- Mae West"};

    unsigned int Show; /* zum Auslesen 16-Bit-Integer */
    char ch;           /* zum Auslesen String */
    int i;             /* Schleifenzaehler */
    int len;           /* Stringlaenge */

    void setup()
      {
      /* seriellen Monitor starten */
      Serial.begin(9600);
      while (!Serial);

      /* Beispielcode hier, damit er nur einmal ausgefuehrt wird */

      /* 16-Bit-Integer auslesen */
      for (i = 0; i <= 12; i++)
        {
        Show = pgm_read_word_near(Data + i);
        Serial.println(Show);
        }
      Serial.println();

      /* String auslesen */
      len = strlen_P(Message);
      for (i = 0; i < len; i++)
        {
        ch =  pgm_read_byte_near(Message + i);
        Serial.print(ch);
        }

      Serial.println();
      }

    void loop()
      {
      /* bleibt leer */
      }

#### Sonstige Funktionen

shiftOut(dataPin, clockPin, bitOrder, value)
arbeitet als Software-Schieberegister, wobei der Byte-Wert über den dataPin hinausgeschoben wird. Der zugehörige Schiebetakt wird an clockPin erzeugt. BitOrder gibt die Reihenfolge an: MSBFIRST oder LSBFIRST (Most Significant Bit First oder Least Significant Bit First).

tone(pin, frequenz), tone(pin, frequenz, dauer)
erzeugt ein Rechtecksignal (50% Tastverhältnis) mit der gewünschten Frequenz auf dem angegebenen Pin. Wird keine Dauer angegeben, endet der Ton erst wieder, wenn die Funktion noTone() aufgerufen wird. Am entsprechenden Pin kann ein Piezosummer angeschlossen werden. Es kann immer nur ein Ton erzeugt werden, ein Aufruf von tone() mit einen anderen Pin hat keine Wirkung. Die Funktion beeinflusst die PWM-Ausgabe auf den Pins 3 und 11!

noTone()
stoppt die Erzeugung eines Tons.

Das folgende Beispiel erzeugt einen Sirenenton:

    int Speaker = 9, i; // Lautsprecherport, Zaehlvariable

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

    void loop()
      {
      // ansteigender Ton
      for(i = 120; i < 160; i++)
        {
        tone(Speaker,500,i);
        delay(1);
        }
      noTone();
      delay(100);
      }

### Interrupts

Bei einem Interrupt wird durch ein Ereignis (z. B. Timer, Signal an einem Pin etc.) die Hauptschleife `loop()` unterbrochen und ein vorher festgelegtes Unterprogramm ausgeführt. Nach Abarbeitung des Unterprogrammes wird das Programm an der ursprünglichen Stelle fortgesetzt. Das gewöhnliche Arduino-Board kann zwei Interrupts erfassen:

- Interrupt 0 am digitalen Port 2 und
- Interrupt 1 am digitalen Port 3.

Der Arduino Mega hat sogar vier zusätzliche externe Interrupts. Daneben gibt es diverse interne Interrupts von Timer, seriellen Schnittstellen, I2C, SPI usw., die durch die Arduino-Software genutzt werden, ohne dass der Anwender dies wahrnimmt. Man kann diese Voreinstellungen ändern, muss dann aber in den C-Code der Arduino-Libraries und in das Core-File eingreifen.

Interrupts werden bei Eintritt in die Interrupt Service Routine automatisch gesperrt und nach Rückkehr ins Hauptprogramm wieder freigegeben. Variablen aus dem Hauptprogramm, die in der Interrupt Service Routine geändert werden sollen müssen als "volatile" deklariert werden. Andernfalls nimmt der Compiler an, dass sich diese Variablen niemals ändern und er ersetzt sie schon beim Kompilieren durch Konstante. Beispiel:

    volatile unsigned int Counter;

Damit der Arduino weiss, wie er auf Interrupts reagieren und welches Unterprogramm er beim Auftreten eines Interrupts ausführen soll, muss dessen Adresse festgelegt werden. Dies erledigt man im `setup()` mit der Methode `attachInterrupt()`. Die Interrupt-Funktionen sind: attachInterrupt(interrupt, function, mode)
legt eine Interrupt-Serviceroutine (ISR) fest. Die meisten Arduinos erlauben zwei externe Interrupts: Int 0 auf Digital-Pin 2 und Int 1 auf Digital-Pin 3 (beim "Mega" kommen noch die Pins 4 und 5 hinzu). Der Parameter interrupt legt die Nummer des Interrupts fest, function ist die Adresse der ISR (einfach den Funktionsnamen angeben) und mode definiert die Auslösebedingung:

- LOW: Interrupt, wenn der Pin Low-Pegel hat,
- CHANGE: Interrupt, wenn der Pin seinen Wert wechselt,
- RISING: Interrupt bei steigender Flanke am Pin,
- FALLING: Interrupt bei fallender Flanke am Pin.

Beim Arduino Due gibt es noch den Mode HIGH, wobei die Modi HIGH und LOW in den seltensten Fällen zur Anwendung kommen.

Innerhalb der ISR funktioniert delay() nicht und der Ergebniswert von millis() bleibt konstant. Serielle Daten können verloren gehen. Die ISR sollte, wie bei Assembler, so kurz wie möglich gehalten werden. Alle globalen Variablen, die in der ISR verändert werden, müssen alsvolatile deklariert werden.

detachInterrupt(interrupt)
Schaltet den per `attachInterrupt()` aktivierten Interrupt wieder aus.

interrupts()
Erlaubt Interrupts (wird nach einem Aufruf von `noInterrupts()` benötigt).

noInterrupts()
Alle Interrupts abschalten.

Da, wie schon erwähnt, die `delay()`-Funktion, `millis()` sowie die serielle Schnittstelle auch mit Interrupts arbeiten, können diese Funktionen nicht innerhalb einer Interruptserviceroutine verwendet werden. Überhaupt sollte die Routine so kurz und einfach wie möglich gehalten werden (eventuell nur Setzen von Flag-Variablen, die dann in `loop()` entsprechende Aktionen auslösen.

Ein Beispiel soll das Ganze etwas weiter verdeutlichen:

    // irgendein Sensor ist an D2 angeschlossen
    #define Sensor 2
    // Ausgabe-LED
    #define LED 13

    volatile int zustand = LOW; // Kommunikation zwischen ISR und Hauptprogramm

    void setup()
      {
      pinMode(Sensor, INPUT);
      digitalWrite(Sensor, HIGH); // schaltet den Pullup-Widerstand ein
      pinMode(LED, OUTPUT);

      // Interrupt aktivieren
      attachInterrupt(0, MachWas, CHANGE);
      }

    void loop()
      {
      digitalWrite(ledPin, zustand);
      }

    // Interrupt-Service-Routine
    void MachWas()
      {
      zustand = ! zustand;
      }

Interrupt | Bezeichnung | Funktion
--- | --- | ---
Int0: INT0 | External Interrupt Request 0 | Externer Interrupt an PD2 (Arduino Pin 2). Der Interrupt kann über attachInterrupt() mit einer Funktion verbunden werden. detachInterrupt() hebt diese Verbindung wieder auf.
Int1: INT1 | External Interrupt Request 1 | Externer Interrupt an PD3 (Arduino Pin 3). Der Interrupt kann über attachInterrupt() mit einer Funktion verbunden werden. detachInterrupt() hebt diese Verbindung wieder auf.
Int18: USART RX | USART Rx Complete | Serieller Empfang. Ein empfangenes Zeichen wird in den Empfangspuffer übertragen. Methode der Klasse Serial.
Int19: USART UDRE | USART, Data Register Empty | Seriell Senden. Das nächste Zeichen wird dem Ausgabepuffer entnommen und an den UART übergeben. Methode der Klasse Serial.

### Timer

Timer steuern zeitliche Abläufe im Prozessor, beispielsweise für die Zeitmessung, PWM (Pulsweitenmodulation) oder die Erzeugung von Tönen. Durch geschickte Programmierung und Abfrage der `millis()` lassen sich zwar viele Dinge ohne Timer erledigen. Wenn es aber auf exakte Zeiten ankommt, geht es nicht ohne Timer. Der Arduino besitzt einen 16-MHz-Quarz, weshalb der interne Taktgeber alle 62,5 Nanosekunden einen Impuls liefert. Für die meisten Anwendungen ist das zu schnell. Deshalb gibt es den so genannten Prescaler, mit dem die diese Zeitbasis reduziert werden kann. Dies ist ganz einfach ein binärer Teiler für die Taktfrequenz. Ein Prescaler-Wert von 8 erhöht die Pulsdauer auf 16/8 = 2, also eine halbe Mikrosekunde. Nach dem Systemstart werden alle Timer mit einem Prescaler-Wert von 64 initialisiert.

Der Arduino hat, wie schon erwähnt, drei Timer. Jeder dieser Timer ist zwei PWM-fähigen Ausgangs-Pins zugeordnet:

Timer | Pin | Standardeinstellung
0 | 5, 6 | 1000 Hz
1 | 9, 10 | 500 Hz
2 | 3, 11 | 500 Hz

Es ist ohne großen Aufwand möglich, die PWM-Frequenz unseren konkreten Anforderungen anzupassen, indem die entsprechenden Register des Mikroprozessors direkt angesprochen werden. Achtung: Der Timer 0 ist u.a. um die Funktionen `millis()` und `delay()` zuständig. Eine Änderung der Werte für den Timer 0 sollte also nur durchgeführt werden, wenn man genau weiss, was man tut. Es folgt eine Übersicht der Timer-Register. Das Zeichen "x" ist ein Platzhalter für die Nummer des Timers.

Bezeichnung | Bedeutung | Funktion
--- | --- | ---
TCCRxA | Timer Counter Control Register A | legt Betriebsart fest
TCCRxB | Timer Counter Control Register B | legt Prescaler fest
TCNTx | Timer Counter Register | Timer-Zähler
OCRxA | Output Compare Register A | Register für Interrupt-Auslösung
OCRxB | Output Compare Register B | Register für Interrupt-Auslösung
TIMSKx | Register Timer Counter Interrupt-Maskierung | Bedingungen für Interrupt-Auslösung
TIFRx | Timer Counter 0 Interrupt Flag Register | Anzeige ob Trigger-Bedingung vorliegt

Wer tiefer in die Timer-Programmierun einsteigen will, kommt um die Beschäftigung mit den o. a. Registern nicht herum. Für einfache Aufgaben gibt es die TimerOne-Library ( http://playground.arduino.cc/uploads/Code/TimerOne.zip). Die Bibliothek wird zunächst in das Libraries-Verzeichnis entpackt und anschließend mit `#include` in den Quelltext eingebunden. Wenn Sie TimerOne verwenden, funktioniert `analogWrite()` für die digitalen Pins 9 and 10 nicht mehr. Sie ist relativ übersichtlich und besteht aus folgenden Methoden:

initialize(period)
Diese Methode muss vor allen anderen in `setup()` aufgerufen werden. Optional kann die Periodendauer des Timers in Mikrosekunden angegeben werden. Per Default ist er auf 1 Sekunde eingestellt.

setPeriod(period)
Setzt die Periodendauer des Timers in Mikrosekunden. Der Wert muss zwischen 1 und 8388480 (ca. 8,3 Sekunden) liegen. Beim Aufruf wird auch der Interrupt und die PWM-Frequenz bzw. das Tastverhältnis geändert.

pwm(pin, duty, period)
Erzeugt ein PWM-Signal am angegebenen Pin (für Timer 1 sind dies PORT B 1 und 2, entsprechend den Pins 9 und 10). der zweite Parameter "duty" legt das Tastverhältnis fest, das zwischen 0 und 1023 liegen darf. Optional kann noch die Periodendauer in Mikrosekunden angegeben werden.

attachInterrupt(function, period)
Ruft immer wieder im durch "period" (in Mikrosekunden) Zeitabstand, die Funktion auf, deren Name als "function" übergeben wird. Denken Sie bitte daran, dass die Interruptserviceroutine innerhalb des durch "period" angegebenen Zeitraums abgearbeitet und wieder beendet sein muss. Andernfalls wird die `loop()` niemals mehr durchlaufen. Fehlt der zeite Parameter, wird die vorherige Einstellung übernommen.

setPwmDuty(pin, duty)
Erlaubt das einfache Ändern des Tastverhältnisses. `pwm()` muss davor mindestens einmal aufgerufen worden sein. Danach kann man dann das Tastverhältnis mit `setPwmDuty()` schneller ändern.

detachInterrupt()
Disabled den mit `attachInterrupt()` initiierten Interrupt.

disablePwm(pin)
Schaltet die PWM-Ansteuerung des entsprechenden Pin ab.

read()
Gibt die Zeit in Mikrosekunden seit dem letzten Timerüberlauf zurück.

Das folgende Beispiel aus der Timer-Bibliothek erzeugt ein PWM-Signal an Pin 9 mit 50% Tastverhältnis und erzeugt einen Timer-Interrupt der den Pin 10 alle 500 ms umschaltet:

    #include "TimerOne.h"

    #define LED 13

    void setup()
      {
      pinMode(LED, OUTPUT);
      Timer1.initialize(500000);         // initialize timer1, and set a 1/2 second period
      Timer1.attachInterrupt(callback);  // attaches callback() as a timer overflow interrupt
      Timer1.pwm(9, 512);                // setup pwm on pin 9, 50% duty cycle
      }

    void callback()
      {
      // Die LED blinkt unabhängig vom Programmcode in loop()
      digitalWrite(LED, digitalRead(LED) ^ 1); // EXOR invertiert
      }

    void loop()
      {
      // Der ganze Rest des Programms
      }

Alternativ kann man das Tastverhältnis per Potentiometer am analogen Eingang steuern.

    #include

    // LED an Pin 9
    #define PWMout 9
    // Analogeingang
    #define PWMsoll A0

    int PWMsollValue = 0;     // aktueller Poti-Wert
    int t = 1000;             // t in Mikrosekunden

    void setup()
      {
      Timer1.pwm(PWMout, PWMsollValue, t);  // PWM initiieren
      }

    void loop()
      {
      // Einlesen des Potis (0..1023)
      PWMsollValue = analogRead(PWMsoll);

      // PWM-Tastverh. aendern
      setPwmDuty(PWMout, PWMsollValue);
      }

Int16: TIMER0 OVF | Timer / Counter0 Overflow | Zeitmessung. Die Lib wiring.c wird immer eingebunden. Dies geschieht über die dort definierte Funktion init(), die vom Arduino-Framework noch vor setup() aufgerufen wird. millis(), micros(), delay(), analogWrite()
Int7: TIMER2 COMPA | Timer/Counter2 Compare Match A | Tonerzeugung. Funktionen tone() und noTone()

### Bibliotheken, Header-Dateien

Wie schon erwähnt, lehnt sich die Arduino-Programmiersprache an C bzw. C++ an - nicht zuletzt, weil genau diese Sprachen auch "im Hintergrund" wirken. So ist es jederzeit möglich, auf C oder C++ auszuweichen und den erzeugten Binärcode in den Arduino zu laden. Etliche der zahlreichen Bibliotheken (Libraries) des Arduino sind in C, C++ oder Assembler geschrieben. Wie man eigene Bibliotheken erstellt schildert die Dokumentation in einer Schritt-für-Schritt-Anleitung. Sie können auch eine Bibliothek hinzufügen, die Sie beispielsweise aus dem Internet herunter geladen haben. Der entpackte Ordner der Bibliothek muss in den "Libraries"-Ordner im Sketchbook kopiert werden. Gegebenfalls muss man ihn anlegen. Nach dem Neustart der IDE steht die Bibliothek zum Import bereit.

Meist müssen Sie auch nicht selbst programmieren, denn neben der Standard avr-libc gibt es u. a. Bibliotheken für:

- EEPROM: Zugriff auf das interne EEPROM
- Ethernet: benötigt ein Ethernet-Erweiterungsboard
- LiquidCrystal: Anbindung von LCDs
- SD: Anbindung von SD-Karten
- Servo: Modellbau-Servos steuern
- SPI: Zugriff auf den SPI-Bus
- SoftwareSerial: emulierte serielle Schnittstelle auf digitalen Ports
- Stepper: Steuern von Schrittmotoren
- Wire: Zugriff auf die I2C-Schnittstelle
- Matrix: Steuern von LED-Matritzen
- Sprite: Animationen auf LED-Matritzen

Egal was man mit dem Arduino anfangen möchte, es gibt eigentlich immer schon eine komplette Bibliothek dazu. Das vereinfacht vor allem das Programmieren für diejenigen, die bisher noch keine Erfahrung mit Mikrocontrollern haben.

An dieser Selle solle es beispielhaftum die Anwendung solcher Bibliotheken gehen. Will man Bibliotheken verwenden, müssen sie, wie bei C üblich, ins Programm eingefügt werden. Im Hauptmenü der IDE finden Sie unter "Sketch" den Befehl "Import Library". Hier wählt man einfach die Bibliothek aus, die man verwenden will und im Sketch erscheint die Include-Zeile, etwa:

    #include <Servo.h>

Hiermit wird auf die in der Arduino IDE enthaltene Servo-Library zurückgegriffen. Modellbau-Servos sind eine geschlossene Motoreinheit, deren Achse sich in einem Winkel von 180 oder 270 Grad bewegen lässt. Ein auf der Achse sitzender Hebel betätigt dann die Ruder oder Klappen des Modells proportional zur Auslenkung des Steuerknüppels der Fernsteuerung. Ein Servo enthält somit einem internen Regelkreis. Als Stellgröße wird alle 20 ms ein Impuls mit einer Dauer von 1 ms bis 2 ms gesendet (Ruder auf Links- bzw. Rechtsanschlag). Im folgenden Beispiel wird eine servoPulse-Funktion genutzt um das Servo von 1 bis 179 Grad und wieder zurück zu bewegen (das Beispiel findet sich auch in der IDE unter "File" → "Examples" → "Servo"):

    // Headerfile für die Servosteuerung einbinden
    #include <Servo.h>

    // Erzeugen eines Servo-Objekts
    Servo myservo;

    int pos = 0; // Speichern der Position

    void setup()
      { myservo.attach(9); } // Servo-Datenleitung an Pin 9

    void loop()
      {
      // mit mittlerer Geschwindigkeit nach rechts drehen
      for(pos = 1; pos < 180; pos++)
        {
        myservo.write(pos);
        delay(15);
        }
      // mit mittlerer Geschwindigkeit nach links drehen
      for(pos = 179; pos > 0; pos--)
        {
        myservo.write(pos);
        delay(15);
        }
      }

Das Servo verträgt Steuerwerte zwischen 0 und 180, was bei einen 180-Grad-Servo exakt dem Winkel der Servoachse entspricht. Im folgenden Beispiel wird eine per A/D-Wandler gelesene Eingangsspannung in eine Servoauslenkung transponiert.

    #include <Servo.h>

    Servo myservo;   // neues Servo-Objekt erzeugen
    int potpin = 0;  // Analogpin, an dem das Potenziometer haengt
    int val;         // Hilfsvariable pin

    void setup()
      {
      myservo.attach(9);  // ordnet Pin 9 der Servo-Steuerleitung zu
      }

    void loop()
      {
      val = analogRead(potpin);         // Poti einlesen (0 - 1023)
      val = map(val, 0, 1023, 0, 179);  // Skalierung auf zulaessige
                                        // Servo-Werte (max. 180)
      myservo.write(val);               // Servo positionieren
      delay(15);                        // etwas warten
      }

Beachten Sie, dass die Servo-Library Timer 1 verwendet und Sie deshalb keine PWM-Ausgabe bei den Pins 9 und 10 tätigen können.

Die Bibiothek `WProgram.h` wurde ab IDE 1.0 durch die `Arduino.h` ausgetauscht. Wenn eine Zusatz-Bibiothek unter Arduino 1.0 die Fehlermeldungen liefert, das `WProgramm.h` nicht vorhanden sei, kann man folgendes an den Anfang der betreffenden Bibiothek schreiben. So funktioniert sie unter der alten und neuen IDE:

    #if defined(ARDUINO) && ARDUINO >= 100
    #include "Arduino.h"
    #else
    #include "WProgram.h"
    #endif

### Beispiele

#### Temperaturerfassung

Als erstes Beispiel soll die Erfassung der Temperatur dienen. Halbleiter-Temperatursensoren geben ein Ausgangssignal ab, das proportional zur Umgebungstemperatur ist. Weit verbreitet sind die Typen LM35 bis LM335, die ein Ausgangssignal zur Verfügung stellen, das direkt proportional zur Celsius-Skala ist. Die Bausteine werden in verschiedenen Genauigkeitsklassen über verschiedene Temperaturbereiche bereits auf dem Wafer abgeglichen, sodass ein externer Abgleich nicht vorgesehen werden muss. Insofern soll nur der preiswerte Typ LM335 betrachtet werden, der sich auf den absoluten Nullpunkt (-273,15 °C) bezieht und eine Spannung von 10 mV/K am Ausgang liefert. Bei 25° Celsius sollte sich also eine Spannung von etwa 2,98 V am Ausgang messen lassen. Diese liegt etwa in der Mitte des Messbereichs des Arduino und daher kann der Sensor direkt angeschlossen werden. Zum Betrieb benötigt er nur noch einen Widerstand von 2 Kiloohm gegen +5 V.

Für die Umrechung des Spannungswertes in die Temperatur gilt

temp = Uein * 100 - 273,15

Da der A/D-Wandler jedoch nicht die Eingangsspannung, sondern einen Wert von 0 bis 1023 liefert, der den Spannungsbereich von 0 bis 5 V entspricht, muss der Arduino folgendermaßen rechnen:

temp = Vcc * Uein / 1024.0 * 100.0 - 273.15

Für die Gleitpunktarithmetik mit begrenzter Stellenzahl ist die Formel jedoch ungünstig, da erst durch 1024 geteilt und dann mit 100 multipliziert wird. Der bei der Division eventuell entstehende Rundungsfehler multipliziert sich anschließend auch mit dem Faktor 100. Also stellt man besser um:

temp = (Vcc * Uein * 100 / 1024.0) - 273.15 ' temp = (Vcc * Uein / 10.24) - 273.15

Beim preiswerten LM335 kann es vorkommen, dass das Sensorausgangssignal von der tatsächlichen Temperatur etwas abweicht. Hier kann entweder per Hardware am Eingang adj des LM335 die Ausgangsspannung korrigiert werden oder man verwendet im Programm einem Korrekturfaktor. Beim Einlesen von Sensordaten stellen sich auch immer leichte Schwankungen der gelesenen Werte ein, die beispielsweise durch elektromagnetische Störeinflüssen oder leichte Schwankungen der Messgröße hervorgerufen werden. Um den Eingangswert zu stabilisieren, kann man gegebenenfalls mehrmals den Wert erfassen und dann einen Mittelwert bilden. Das erste Testprogramm liest den Temperaturwert und gibt ihn auf der seriellen Schnittstelle aus.

    // Analog input pin
    #define analogInPin A0

    // Referenzspannung A/D-Wandler
    #define Vcc 5.0

    int sensorValue = 0;         // Sensorwert
    float temp;                  // Temperaturwert

    void setup()
      {
      // serielle Schnittstelle einschalten und auf 9600 bps setzen
      Serial.begin(9600);
      }

    void loop()
      {
      // Analogwert lesen
      sensorValue = analogRead(analogInPin);
      // in Celsiusgrade umrechnen
      temp = (Vcc * sensorValue / 10.24) - 273.15;

      // Ausgabe auf dem seriellen Monitor
      Serial.print("Sensor = " );
      Serial.print(sensorValue);
      Serial.print("\t Temperatur = ");
      Serial.println(temp);

      // 1/2 Sekunde warten
      delay(500);
      }

#### Balkenanzeige

Für eine autarke Temperaturanzeige könnte man eine Siebensegmentanzeige ansteuern. Wenn es nicht auf einen exakten Zahlenwert, sondern nur auf eine ungefähre Angabe ankommt, bieten sich auch andere, recht übersichtliche Anzeigemöglichkeiten an - etwa die lineare Anzeige eines herkömmlichen Themometers. Das folgende Programm zeigt das Prinzip unter Verwendung der Digitalports des Arduino. Deren Zahl ist allerdings nicht groß genug für eine halbwegs genaue Darstellung des aktuellen Tempera-turwertes. Man müsste für eine praxistaugliche Lösung eine Erweiterung der Digital-ports vornehmen, etwa über Schieberegister. Beim folgenden Beispiel fehlen die serielle Ausgabe und die Umrechnung in Celsiusgrade. Die Digitalpins werden über ein Array adressiert, was das Programm beim Setup wie auch in der Hauptschleife wesentlich verkürzt, da sie per Schleife angesprochen werden:

    // Analog input pin
    #define analogInPin A0

    // Referenzspannung A/D-Wandler
    #define Vcc 5.0

    int sensorValue = 0;             // Sensorwert
    int pin[] = {10,9,8,7,6,5,4,3};  // Digital-Ausgänge
    int i;                           // Zähler

    void setup()
      {
      for (i = 0; i < 8; i++)
        pinMode(pin[i], OUTPUT);
      }

    void loop()
      {
      // Analogwert lesen
      sensorValue = analogRead(analogInPin);
      // auf den Bereich 0 .. 8 skalieren
      sensorValue = map(sensorValue, 0, 1023, 0, 8);

      // Balkenanzeige
      for (i = 0; i < 8; i++)
        digitalWrite(pin[i],(i < sensorValue));

      // 1/2 Sekunde warten
      delay(500);
      }

Die Funktion `map()` bildet den Analogbereich von 0 bis 1023 auf die Zahl der LEDs (0 bis 8) ab. Bei der Balkenanzeige wird auch ein Programmiertrick verwendet: Der Ausdruck `(i < sensorValue)` ist entweder TRUE oder FALSE, was den binären Ausgabewerten LOW und HIGH entspricht. Angenommen der (gemappte) Sensorwert ist 4, dann sind die LEDs 0, 1, 2, 3 eingeschaltet und 4, 5, 6, 7 ausgeschaltet.

#### Analogeingabe

Den Beschleunigungssensor ADXL 335 entstammt einer ganzen Familie von Beschleunigungssensoren, die zwei oder drei Achsen auswerten können und unterschiedliche Maximalbeschleunigungen "vertragen". Auch ist die gesamte analoge Elektronik bereits auf dem Chip realisiert, sodass es nunr noch für jede Achse einen Ausgang gibt, dessen Spannung der gemessenen Beschleunigung proportional ist. Der Ausgangsspannungsbereich passt ideal zum Arduino-Eingang.

Zum Anschluß an den Arduino werden die Anschlüsse Vcc, GND, X, Y und Z des Beschleunigungssensors mit kurzen Kabelstücken versehen. An diese Kabel wird eine Stiftleiste gelötet, die zur Belegung des Arduino passt: X, Y und Z werden mit den Analog-Eingängen 1, 2 und 3 des Arduino verbunden. Der Anschluss ST (Self Test) am Sensor wurde nicht belegt. Die Versorgungsspannung für den Beschleunigungssensor ist laut Datenblatt mit 1,8 bis 3,6 V angegeben, der Sensor muss also aus dem 3,3-V-Anschluss des Arduino versorgt werden (nicht an 5 V!). Je nachdem, wie Sie das Board neigen, erhalten Sie entsprechende Werte des ADXL335. Die Ausgabespannung des Sensors ist recht gut mit dem Controller kompatibel, sodass keine weitere Hardware nötig ist. Der ADXL335 misst Beschleunigungen bis maximal 3 g. Braucht man andere Maximalwerte kann man u.a. ADXL322 (2 g), ADXL321 (18 g) oder ADXL 305 (5 g) verwenden. Das Testprogramm gestaltet sich recht kurz:

    // Pinbelegung
    const int xpin = 1;     // X-Achse
    const int ypin = 2;     // Y-Achse
    const int zpin = 3;     // Z-Achse

    void setup()
      {
      Serial.begin(9600);
      }

    void loop()
      {
      // Sensorwerte ausgeben
      Serial.print("X: ");
      Serial.print(analogRead(xpin));
      Serial.print(", Y: ");
      Serial.print(analogRead(ypin));
      Serial.print(", Z: ");
      Serial.println(analogRead(zpin));
      // etwas warten
      delay(200);
      }

Im seriellen Monitor können jetzt die Werte für die drei Achsen X, Y, und Z abgelesen werden. Dank der Erdbeschleunigung von 9.81 m/s2 kann die Funktion des Sensors ohne weitere Hilfsmittel überprüft werden. Liegt beispielsweise die X-Achse genau waagrecht (parallel zum Erdboden, stellt sich ein Ausgangswert um 512 ein, bei nega-tivem Winkel wird der Wert größer, bei positiver Neigung kleiner. Der Arduino mit diesem Sensor würde sich also auch als X-Y-Z-"Wasserwaage" eignen. Kombiniert mit der Datenaufzeichnung auf SD-Karte (siehe "Datalogger") und per Akku gespeist kann man den Arduino auch im Paket auf Reisen schicken und anschließend feststellen, wie gut oder schlecht das Paket behandelt wurden und wie lange es in irgend einem Lager herumlag.

#### Soft-Blinker

Dies Beispiel realisiert einen "Soft-Blinker", indem die LED per PWM mit einem sinusförmigen Signal angesteuert wird. Per Programm wird in 1-Grad-Schritten der Sinus im Bereich von 0 bis 180° berechnet und mit 255 multipliziert. Da der Sinus in diesem Bereich nur Werte zwischen 0 und 1 annehmen kann, ergibt dass die ideale Ansteuerung für den Analogausgang.

    // Pin der roten LED
    #define LEDRot 9

    // eine bekannte Konstante
    #define Pi 3.14159265

    int wert;      // Ausgabewert
    int i;         // Schleifenzaehler

    void setup()
      {
      // muss nicht sein, kann aber ...
      pinMode(ledPin, OUTPUT);
      }

    void loop()
      {
      for (i = 0; i < 180; i++)
        {
        // Sinus berechnen (Achtung: Bogenmass!)
        wert = int(sin(i*Pi/180)*255);
        analogWrite(ledPin, wert);
        delay(10);
        }
      }

Das Programm belastet den Prozessor durch die Sinusberechnung relativ stark. Eine Alternative wäre ein Array mit den 180 Sinuswerten (gleich mit 255 multipliziert), auf das dann mit Index i zugegriffen wird.

#### Farbwechsel

Nachdem das so schön klappt, werden im nächsten Programm alle drei LEDs eingesetzt und ein Farbwechsel realisiert. In der Funktion setLeds() gibt es eine sinnvolle Anwendung der Speicherklasse static. Die aktuellen LED-Farbintensitäten werden in den Variablen red, green und blue von Aufruf zu Aufruf weitergegeben. Ohne static funktioniert es nicht, es würde immer mit Rot begonnen werden. Die Funktion kann mit Werten zwischen 0 und 764 aufgerufen werden.

    // Pinzuordnung der LEDs
    #define LEDBlau  11
    #define LEDGruen 10
    #define LEDRot   9

    void setup()
      {
      pinMode(LEDBlau, OUTPUT);
      pinMode(LEDGruen, OUTPUT);
      pinMode(LEDRot, OUTPUT);
      }

    void loop()
      {
      int licht;
      for (licht = 0; licht < 765; licht++)
        {
        setLEDs(licht);
        delay(20);
        }
      }

    void setLEDs(int i)
      {
      // Farbwerte mit Vorbesetzung, begonnen wird mit rot
      static int red   = 255;
      static int green = 0;
      static int blue  = 0;

      if (i < 255)      // Phase 1: von rot nach grün
        {
        red--;       // red down
        green++;     // green up
        blue = 0;    // blue low
        }
      else if (i < 510) // Phase 2: von grün nach blau
        {
        red = 0;     // red low
        green--;     // green down
        blue++;      // blue up
        }
      else if (i < 766) // Phase 3: von blau nach rot
        {
        red++;       // red up
        green = 0;   // green low
        blue--;      // blue down
        }
      analogWrite(LEDRot,   red);
      analogWrite(LEDGruen, green);
      analogWrite(LEDBlau,  blue);
      }

In vorangegangenen Beispiel sind die Farbübergänge zwischen den LEDs nicht gleichmäßig. Das liegt daran, dass das Auge die Lichtintensität nicht linear, sondern eher logarithmisch wahrnimmt. Ist die LED ganz dunkel, werden kleine Änderungen gut wahrgenommen, ist die LED dagegen schon ziemlich hell, merkt man die Zu- oder Abnahme erst nach mehreren Schritten.

Um das auszugleichen, kann bei der Ausgabe der Farbwert entsprechend angepasst werden. Dies erfolgt am Besten mithilfe einer Umrechnungstabelle, die in einem Array gespeichert wird. So ist es im folgenden Programm realisiert. Die Umcodierung erfolgt in der Funktion setColor(), in der auch die Ausgabe erfolgt.

In setLEDs() wird dann der auszugebende Farbwert berechnet. Im Gegensatz zum vorhergehenden Programm kann hier jeder individuelle Wert gesetzt werden. Für die Eingabewerte von 0 bis 255 durchläuft die Funktion einmal den Farbkreis.

    // Pinzuordnung der LEDs
    #define LEDBlau  9
    #define LEDGruen 10
    #define LEDRot   11

    void setup()
      {
      pinMode(LEDBlau, OUTPUT);
      pinMode(LEDGruen, OUTPUT);
      pinMode(LEDRot, OUTPUT);
      }

    void loop()
      {
      int licht;
      for (licht = 0; licht < 256; licht++)
        {
        setLEDs(licht);
        delay(100);
        }
      }

    // Wählt für einen Wert (0 .. 255) eine Farbe aus dem Farbkreis
    void setLEDs(int value)
      {
      int red, green, blue;

      if(value < 64)           // rot nach gruen
        {
        red = 63 - value;
        green = value;
        blue = 0;
        }
      else if(value < 128)     // gruen nach blau
        {
        red = 0;
        green = 127 - value;
        blue = value - 64;
        }
      else if(value < 192)     // blau nach rot
        {
        red = value - 128;
        green = 0;
        blue = 191 - value;
        }
      else                     // rot nach weiss
        {
        red = 63;
        green = 255 - value;
        blue = 255 - value;
        }
      setColor(red, green, blue);
      }

    // Stellt die LED-Helligkeiten logarithmisch ein
    void setColor(int red, int green, int blue)
      {
      // Tabelle der Helligkeitswerte,
      // logarithmisch ansteigend
      int logValue[64] =
        { 0,  1,  2,  3,  4,  5,  6,  7,
          8,  9, 10, 11, 12, 13, 14, 16,
         18, 20, 22, 25, 28, 30, 33, 36,
         39, 42, 46, 53, 56, 60, 64, 68,
         72, 77, 81, 86, 90, 95,100,105,
         110,116,121,127,132,138,144,150,
         156,163,169,176,182,189,196,203,
         210,218,225,233,240,248,253,255  };

      analogWrite(LEDBlau, logValue[blue]);
      analogWrite(LEDGruen, logValue[green]);
      analogWrite(LEDRot, logValue[red]);
      }

Manchmal ist das RGB-Farbschema jedoch hinderlich. Es wäre viel praktischer, wenn man im Programm nur einen einzigen Wert für die Farbe hätte und nicht drei. Genau das erreichen Sie mit dem HSV-Farbschema. Der HSV-Farbraum beschreibt eine Farbe mit Hilfe des Farbtons (englisch hue), der Farbsättigung (saturation) und des Hellwerts (value).

Bei der Farbdarstellung wird der HSV-Farbraum gegenüber den Alternativen RGB (Rot, Grün, Blau) und CMYK (Cyan, Magenta, Yellow, Black) bevorzugt, weil es leichter fällt, eine Farbe zu finden: Man kann für die Farbmischung unmittelbar den Farbton wählen und dann entscheiden, wie gesättigt und wie hell (oder dunkel) dieser sein soll.

Für die Beschreibung des Farbortes im HSV-Farbraum werden folgende Parameter benutzt:

- Farbton (hue) als Winkel H auf dem Farbkreis (z. B. 0° = Rot, 120° = Grün, 240° = Blau). Die Farbtoneinstellung erfolgt durch Weiterdrehen auf einen der benachbarten Farbwerte. So kann z.B. ein Blau in Richtung Cyan und Grün oder in Richtung Violett und Rot verschoben werden. Der Farbton wird als Position auf dem Stan-dard-Farbkreis angegeben und daher in Werten zwischen 0° und 360° ausgedrückt.
- Sättigung (saturation) S in Prozent (0 % = Neutralgrau, 50 % = wenig gesättigte Farbe, 100 % = gesättigte, reine Farbe). Alternativ wird ein Intervall von 0 bis 1 verwendet. Vom Sättigungsgrad einer Farbe ist es abhängig, ob wir einen Farbton als satt und kräftig oder als matt und schwach empfinden. Sie beschreibt also das Verhältnis zwischen Farbe und Grauanteil. Auf dem Farbkreis nimmt die Sättigung vom Rand zur Mitte hin ab.
- Helligkeit (value oder brightness) V als Prozentwert (0 % = keine Helligkeit, 100 % = volle Helligkeit). Alternativ wird auch hier ein Intervall von 0 bis 1 verwendet, das auch "Dunkelstufe" genannt wird. Das folgende Programm rechnet die HSV-Angaben in Werte für Rot, Grün und Blau um, die dann zur Ansteuerung der LEDs dienen.

Das Programm weicht vom oben angegebenen Schema insofern ab, als dass anstelle der Prozentangaben bereits die bei der Ausgabe möglichen Werte 0 bis 255 verwendet werden. Auf diese Weise kommt das Programm auch mit Integer-Arithmetik aus, was der Rechenzeit und dem Speicherbedarf zu Gute kommt.

    // Pinzuordnung der LEDs
    #define LEDBlau  9
    #define LEDGruen 10
    #define LEDRot   11

    void setup()
      {
      pinMode(LedBlau, OUTPUT);
      pinMode(LedGruen, OUTPUT);
      pinMode(LedRot, OUTPUT);
      }

    void loop()
      {
      int licht;
      for (licht = 0; licht < 360; licht++)
        { // kompletten Farbkreis durchlaufen
        setLED(licht,255);
        delay(100);
        }
      }

    void setLED(int hue, int l)
      {  // LED-Farben festlegen nach HUE und Intensität.
         // Sättigung ist hier immer auf Maximum gesetzt
      int col[3] = { 0,0,0 };

      getRGB(hue, 255, l, col);             // HSV in RGB umrechen
      analogWrite(LedRot, 255 - col[0]);    // und ausgeben
      analogWrite(LedGruen, 255 - col[1]);
      analogWrite(LedBlau, 255 - col[2]);
      }

    void getRGB(int hue, int sat, int val, int colors[3])
      {  // Diese Funktion rechnet einen HSV-Wert in die ensprechenden
         // RGB-Werte um. Diese werden im Array ‚colors' zurückgegeben
         // colors[0] = ROg, colors[1]} = Gruen, colors[2] = Blau
         // hue: 0 - 359, saturation: 0 - 255, val (lightness): 0 - 255
      int r, g, b, base;

      if (sat == 0)
        { // Sättigung = 0 --> Grauwert
        colors[0] = val;
        colors[1] = val;
        colors[2] = val;
        }
      else
        {
        base = ((255 - sat) * val) >> 8;
        if (hue < 60)
          {
          red = val;
          green = (((val - base)*hue)/60) + base;
          blue = base;
          }
        else if (hue < 120)
          {
          red = (((val - base)*(60-(hue%60)))/60) + base;
          green = val;
          blue = base;
          }
        else if (hue < 180)
          {
          red = base;
          green = val;
          blue = (((val - base)*(hue%60))/60) + base;
          }
        else if (hue < 240)
          {
          red = base;
          green = (((val - base)*(60 - (hue%60)))/60) + base;
          blue = val;
          }
        else if (hue < 300)
          {
          red = (((val - base)*(hue%60))/60) + base;
          green = base;
          blue = val;
          }
        else if (hue < 360)
          {
          red = val;
          green = base;
          blue = (((val - base)*(60 - (hue%60)))/60) + base;
          }
        colors[0] = red;
        colors[1] = green;
        colors[2] = blue;
        }
      }

###### Copyright © Hochschule München, FK 04, Prof. Jürgen Plate und die Autoren
Letzte Aktualisierung: