Arduino


von Prof. Jürgen Plate

Arduino

Notizen und Anmerkungen

Programmierung

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:

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-Pro­zessors angepasst:

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 Ele­mente 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" Analogwert 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);
    }
  }

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:

Serielle Kommunikation

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. Wird serielle Kom­munikation verwendet, können die digitalen Pins 0 (RX) und 1 (TX) nicht gleichzeitig für andere Zwecke verwendet werden. Es sind folgende Datenraten (Bit/s) möglich: 300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600 und 115200. Während der seriellen übertragung blinkern die LEDs "TX" und "RX" auf dem Board.

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);
    }
  }

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: 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: 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;
  }

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:

TimerPin Standardeinstellung
05, 61000 Hz
19, 10500 Hz
23, 11500 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.

BezeichnungBedeutungFunktion
TCCRxA Timer Counter Control Register Alegt Betriebsart fest
TCCRxB Timer Counter Control Register Blegt Prescaler fest
TCNTx Timer Counter RegisterTimer-Zähler
OCRxA Output Compare Register ARegister für Interrupt-Auslösung
OCRxB Output Compare Register BRegister für Interrupt-Auslösung
TIMSKx Register Timer Counter Interrupt-MaskierungBedingungen für Interrupt-Auslösung
TIFRx Timer Counter 0 Interrupt Flag RegisterAnzeige 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);
  }

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:

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.

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:

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 © FH München, FB 04, Prof. Jürgen Plate