![]() |
Arduinovon Prof. Jürgen Plate |
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-Prozessors angepasst:
Im Temp-Ordner von Windows werden jedes Mal, wenn ein Sketch geöffnet oder das erste Mal kompiliert wird, Verzeichnisse angelegt:
pinMode(pin, INPUT); // setzt "pin" als Eingang digitalWrite(pin, HIGH); // schaltet den Pullup-Widerstand einAls 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
}
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:
while(Serial.read() >= 0);
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);
}
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).
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));
}

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:
while(Serial.read() >= 0);
Die Arduino-Versionen 1.x unterstützten nun Stream-Parsing-Methoden:
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 */
}
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 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)
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. |
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() |
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.
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
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);
}
// 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.
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.
// 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.
// 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:
// 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;
}
}