Mikrocontroller/ Programmierung

Programmierung des Arduino Mikrocontrollers

In diesem Kapitel soll eine kurze Einführung in die Programmierung des Arduino Boards gegeben werden. Weiterführende Informationen finden sich in der Arduino Referenz und dem Arduino Programming Notebook von Brian W. Evans.

Imperative Programmierung Bearbeiten

Ein Programm in einer imperativen Programmiersprache besteht im Wesentlichen aus Anweisungen, die der Reihe nach ausgeführt werden. Einige Beispiel für Anweisungen:

int counter; // die Variable counter wird als ganze Zahl definiert
int x = 5;   // die Variable x ist eine ganze Zahl und hat den Wert 5

counter = 10 * x; // der Variablen counter wird der Wert 50 zugewiesen
int inputVariable = analogRead(2); // der ganzzahligen Variablen
                                   // inputVariable wird der Wert von
                                   // analogRead(2) zugewiesen
x = x - 3;   // x wird der neue Wert x-3 zugewiesen

Jede Anweisung muss in unserer Programmiersprache (C/C++) mit einem Semikolon „;“ abgeschlossen werden.

Programmplanung und Dokumentation Bearbeiten

Man kann Programme mit Hilfe von Flussdiagrammen planen und darstellen, z.B. als Programmablaufplan. Besser angepasst an (imperative) Programmiersprachen sind Nassi-Shneiderman-Diagramme, die für Kontrollstrukturen spezielle Symbole bereit halten.

Programmablaufplan PAP Bearbeiten

Elemente (Auswahl) Bearbeiten

 
Beispiel eines Programmablaufplans.

Hauptsächlich werden in einem Programmablaufplan die folgenden Elemente verwendet:

  • Kreis; Oval / Rechteck mit gerundeten Ecken: Terminator  
  • Pfeil, Linie: Verbindung zum nächstfolgenden Element  
  • Rechteck: Operation (Tätigkeit)  
  • Rechteck mit doppelten, vertikalen Linien: Unterprogramm  
  • Raute: Verzweigung / Entscheidungen  
  • Parallelogramm: Ein- und Ausgabe  

Nassi-Shneiderman-Struktogramme Bearbeiten

Elemente (Auswahl) Bearbeiten

 
Der euklidische Algorithmus zur Berechnung des größten gemeinsamen Teilers zweier Zahlen als Nassi-Shneiderman Diagramm.
Verzweigung Bearbeiten
 
Zweifache Auswahl

Wenn die Bedingung zutreffend (wahr) ist, wird der Anweisungsblock 1 durchlaufen; trifft die Bedingung nicht zu (falsch), wird der Anweisungsblock 2 durchlaufen (if then else). Ein Anweisungsblock kann aus einer oder mehreren Anweisungen bestehen. Austritt unten nach Abarbeitung des jeweiligen Anweisungsblocks.

Schleife Bearbeiten
 
Zählergesteuerte Schleife

Wiederholungsstruktur, bei der die Anzahl der Durchläufe festgelegt ist (for). Als Bedingung muss eine Zählvariable angegeben und mit einem Startwert initialisiert werden. Ebenso muss ein Endwert und die (Zähl-)Schrittweite angegeben werden. Nach jedem Durchlauf des Schleifenkörpers (Anweisungsblock 1) wird die Zählvariable um die Schrittweite inkrementiert (bzw. bei negativer Schrittweite dekrementiert) und mit dem Endwert verglichen. Ist der Endwert überschritten bzw. unterschritten, wird die Schleife verlassen.


Heute findend vor allem sog. Aktivitätsdiagramme, die (auch) für objektorientierte Sprachen geeignet sind, zusammen mit der Unified Modeling Language (UML) Verwendung.

Kontrollstrukturen Bearbeiten

Bevor wir uns anhand von einigen Programmbeispielen mit der Programmierung unseres Mikrocontrollers vertraut machen, sollen in diesem Abschnitt grundlegende und in fast allen Programmiersprachen vorhandene Kontrollstrukturen vorgestellt werden.

Bedingungen Bearbeiten

Das Durchlaufen von Kontrollstrukturen erfolgt abhängig von Bedingungen (Tests). Bedingungen werden bei uns wie folgt ausgedrückt und sind entweder wahr oder falsch:

(x>5)                   // wahr, wenn x>5 ist, sonst falsch
(x<3)                   // wahr, wenn x<3 ist, sonst falsch
(x==7)                  // wahr, wenn x=7 ist, sonst falsch
(x!=7)                  // wahr, wenn x ungleich 7 ist, sonst falsch
(a>0 && b>0)            // wahr, wenn a>0 und b>0 ist, sonst falsch
(a>0 || b>0)            // wahr, wenn a>0 oder b>0 ist, sonst falsch
((a>0 || b>0) && c==0)  // wahr, wenn a>0 oder b>0 ist und dazu c=0

Verzweigung: if-Abfrage Bearbeiten

if ( <test1> )
{
    <statements1>;
}
else if ( <test2> )     // optional
{
    <statements2>;
}
else                    // optional
{
    <statements3>;
}

Die if-Abfrage erlaubt abhängig von Tests verschiedene Programmteile (Anweisungen) entweder zu durchlaufen d.h. auszuführen oder andernfalls zu überspringen. Diese bedingte Ausführung hat die Abgebildete allgemeine Struktur.

Dabei werden die Anweisungen <statements1> nur ausgeführt, wenn <test1> wahr ist. Ist <test1> falsch, <test2> jedoch wahr, so wird <statements2> ausgeführt. Ist weder <test1> noch <test2> wahr, so werden die Anweisungen in <statements3> ausgeführt.

Schleifen: while, do…while und for Bearbeiten

while-Schleife Bearbeiten

while ( <test> )
{
    <statements>;
}
 
Vorprüfende (kopfgesteuerte) Schleife.

Die Anweisungen <statements> werden so lange (und nur dann) ausgeführt, wie die Bedingung <test> wahr ist.

Die Schleife läuft endlos, wenn <test> nicht innerhalb der Anweisungen <statements> oder aufgrund äusserer Einflüsse (z.B. eines Sensorwerts) verändert und dadurch falsch wird.

do…while-Schleife Bearbeiten

do
{
    <statements>;
}
while ( <test> )
 
Nachprüfende (fußgesteuerte) Schleife.

Die Anweisungen <statements> werden ausgeführt und so lange wiederholt, wie die Bedingung <test> wahr ist.

for-Schleife Bearbeiten

for (<start value>; <test>; <expression>)
{
    <statements>;
}
 
Zählergesteuerte Schleife.

Eine for-Schleife dient dazu, Anweisungen mehrfach zu durchlaufen. Dabei muss <test> wahr sein und die Anweisung <expression> wird nach jedem Schleifendurchlauf ausgeführt.

In den folgenden Abschnitten sind zahlreiche Beispiele für die Verwendung von if-Abfragen und Schleifen zu finden.


Aufgaben:


Das Blink-Programm im Detail Bearbeiten

Betrachten wir ein einfaches Beispielprogramm. Wenn man das folgende Programm auf das Arduino-Board lädt, so blinkt die eingebaute LED an Pin 13. Den grundsätzlichen Aufbau von Arduino-Programmen können wir schon an diesem Beispiel erkennen:

/* Blink
   Turns on a LED for one second, then off for one second,
   repeatedly.  This example code is in the public domain.
*/

void setup() {
  // initialize the digital pin as an output.
  // Pin 13 has an LED connected on most Arduino boards:
  pinMode(13, OUTPUT);
}

void loop() {
  digitalWrite(13, HIGH);   // set the LED on
  delay(1000);              // wait for a second
  digitalWrite(13, LOW);    // set the LED off
  delay(1000);              // wait for a second
}

Programmkopf und Kommentare Bearbeiten

Die Zeilen 1 bis 5 stellen den sogenannten „header“ (engl. „Kopfteil“) dar. Sie beschreiben das Programm und enthalten weitere wichtige Informationen wie z.B. die Lizenz, unter der das Programm steht. Alle diese Angaben sind in die Kommentarzeichen /* und */ eingefasst. Aller Text zwischen diesen Zeichen spielt für den Ablauf des Programms keine Rolle, ist aber für den Programmierer umso wichtiger. Einzelne Zeilen(-teile) werden mit // wie z.B. Zeile 8, 9 bzw. in 14 ff. „auskommentiert“.

Funktionen: setup() und loop() Bearbeiten

Nach dem „header“ beginnt das auf dem Mikroprozessor ausgeführte Programm mit einer Funktion. Beim Programmieren stellen Funktionen, die manchmal auch Unterprogramme genannt werden, ein wesentliches Element dar. Wir kennen Funktionen aus der Mathematik:  . Die Funktion heißt  ,   ist der Name der Funktion. Die Funktion bekommt als Argument die Variable   übergeben und gibt   zurück. D.h. wenn wir z.B.   an die Funktion   übergeben, bekommen wir   zurück. Im Gegensatz dazu müssen Funktionen in Programmen keine Argumente übergeben bekommen. Auch müssen sie nichts zurückgeben. Beides ist bei Programm-Funktionen völlig unabhängig voneinander.

Unser einfaches Programm besteht aus zwei Funktionen, die in jedem Arduino-Programm vorhanden sein müssen. Betrachten wir die erste Funktion, definiert in den Zeilen 7 bis 11. Funktionsdefinitionen sind immer gleich aufgebaut: Vor dem Funktionsnamen setup wird der Rückgabetyp der Funktion angegeben, in unserem Fall „void“ (engl. leer, nichts). Nach dem Funktionsnamen werden in runden Klammern die übergebenen Argumente angegeben. Bei uns wird nichts übergeben, die Klammern sind leer. Anschließend folgt in geschweiften Klammern der Funktionsblock: Alle hier gemachten Anweisungen werden zum Programmstart ausgeführt.

In unserem Fall wird, wie aus dem Kommentar ersichtlich, Pin 13 als OUTPUT (Ausgang) definiert. Alle digitalen Ports des Arduinos können entweder als Eingang (INPUT) oder Ausgang (OUTPUT) verwendet werden. OUTPUT bedeutet wir wollen ein Signal ausgeben, verwenden wir INPUT, so lesen wir ein Signal ein.

Hat der Mikrocontroller die Funktion setup() abgearbeitet, setzt er seine Arbeit mit der Funktion loop() (engl. Schleife), definiert in den Zeilen 13 bis 18, fort. Auch diese Funktion gibt nichts zurück (Rückgabetyp „void“) und bekommt auch keine Argumente übergeben: Die runden Klammern sind leer: (). Was passiert, wenn loop() ausgeführt wird? Wie aus den Kommentaren ersichtlich wird die LED eingeschaltet (HIGH), dann 1000 ms gewartet, anschließend wieder abgeschaltet (LOW) und wieder 1 s gewartet.

Sobald die Funktion loop() beendet wurde, wird sie sofort wieder aufgerufen und erneut ausgeführt: Die LED blinkt.

Das Digital/AnalogReadSerial-Programm im Detail Bearbeiten

Nachdem wir sowohl digitale als auch analoge Signale erzeugen können, schauen wir uns nun zwei Programme an, mit denen wir die eingelesenen Signale am Bildschirm ausgeben können. Dies ist insbesondere zum Testen von Schaltungen bzw. Sensoren hilfreich.

Betrachten wir das DigitalReadSerial-Programm, das ein digitales Signal einliest und über die serielle Schnittstelle ausgibt. Die Anweisungen sind in Kommentaren erläutert:

/* DigitalReadSerial
   Reads a digital input on pin 2, prints the result to the serial
   monitor.  This example code is in the public domain.
 */

void setup() {
  Serial.begin(9600);  // Serielle Schnittstelle wird bereitgestellt
  pinMode(2, INPUT);   // Pin 2 wird als Eingang definiert
}

void loop() {
  int sensorValue = digitalRead(2);  // Pin 2 wird ausgelesen und der
                                     // (ganzzahligen) Variablen
                                     // sensorValue zugewiesen.
  Serial.println(sensorValue, DEC);  // Der Wert von sensorValue wird
                                     // ausgegeben.
}

Das AnalogReadSerial-Programm funktioniert genau gleich wie das DigitalReadSerial-Programm, nur dass nun ein analoges Signal eingelesen wird:

/* AnalogReadSerial
   Reads an analog input on pin 0, prints the result to the serial
   monitor.  This example code is in the public domain.
 */

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

void loop() {
  int sensorValue = analogRead(A0);  // Analoger Eingang 0 wird
                                     // ausgelesen.
  Serial.println(sensorValue, DEC);
}

Vom Blinklicht zum Lauflicht Bearbeiten

Wir wollen nun das Blink-Programm durch Ansteuerung mehrerer LEDs zu einem Lauflicht erweitern. Im ersten Schritt erweitern wir unser Blink-Programm und fügen weitere LEDs hinzu. Dazu müssen wir die entsprechenden Pins als Ausgänge definieren. Unsere Funktion setup() sieht dann folgendermaßen aus:

void setup() {
  // initialize the digital pins as output:
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(12, OUTPUT);
  pinMode(13, OUTPUT);
}

Da sich Zeilen 3 bis 7 nur durch die Pin-Nummer unterscheiden, bietet sich die Verwendung einer for-Schleife an:

void setup() {
  // initialize the digital pins as output:
  for ( int i = 9; i != 14; i++ ){
     pinMode(i, OUTPUT);
  }
}

Die Schleife startet mit der ganzen Zahl   und wird solange durchlaufen, wie   ist. Bei jedem Durchlauf wird   um eins erhöht. Statt   kann man auch   schreiben.

Betrachten wir nun die Funktion loop(). In einem ersten Schritt schalten wir mit einer Schleife alle LEDs der Reihe nach an:

void loop() {
  for ( int i = 9; i != 14; i++ ){
    digitalWrite(i, HIGH);    // set the LED i on
    delay(1000);              // wait for a second
  }
}

Nun soll aber, wenn z.B. die LED an Pin 10 angeschaltet wird die LED an Pin 9 ausgehen, also immer die im Schleifendurchlauf zuvor angeschaltete LED soll wieder ausgeschaltet werden. Probieren wir folgendes:

void loop() {
  for ( int i = 9; i != 14; i++ ){
    digitalWrite(i, HIGH);    // set the LED i on
    digitalWrite(i-1, LOW);   // set the LED i-1 off
    delay(1000);              // wait for a second
  }
}

Unser Programm funktioniert nun schon halbwegs, allerdings leuchtet die letzte LED permanent. Da   in unserer Schleife maximal bis   läuft, wird die LED   ausgeschaltet, jedoch nie LED 13. Wir lösen dieses Problem, indem wir die Schleife bis 14 laufen lassen. (Man beachte, dass dadurch ein fiktiver Pin 14 HIGH geschaltet wird, genauso wie Pin 8 schon im ersten Schleifendurchlauf LOW geschaltet wurde. Will man diese Pins verwenden, so muss das Programm an dieser Stelle verbessert werden!):

void loop() {
  for ( int i = 9; i != 15; i++ ){
    digitalWrite(i, HIGH);    // set the LED i on
    digitalWrite(i-1, LOW);   // set the LED i-1 off
    delay(1000);              // wait for a second
  }
}

Schalten, Verzögerung und Glück oder Pech Bearbeiten

Auf Knopfdruck soll das Lauflicht, vergleichbar mit einem Glücksrad das sich dreht, langsamer wird und schließlich in einem Feld stehen bleibt, langsamer werden und schließlich bei einer LED anhalten. Abhängig von der LED soll dann eine Melodie ertönen.

Beginnen wir mit dem Einlesen eines digitalen Signals, das wir mit einem einfachen Taster erzeugen (siehe Abschnitt {digiSig}). Dazu benötigen wir einen Input-Pin:

void setup() {
  // initialize the digital pins as output:
  for ( int i = 9; i != 14; i++ ){
    pinMode(i, OUTPUT);
  }
  pinMode(3, INPUT);  // initialize the digital pin 3 as input
}

In der loop-Funktion fragen wir Pin 3 ab. Wird der Taster gedrückt (Eingang Pin 3 ist HIGH d.h. es liegen   am Pin an) wird das Lauflicht bis zum Anhalten verzögert, andernfalls läuft es einfach wie zuvor weiter:

void loop() {
  // Lese den digitalen Eingang 3 aus und speichere den
  // Wert (0 oder 1) in der Variable 'sensorValue':
  int sensorValue = digitalRead(3);
  if (sensorValue == 1){
    for (int p = 10; p < 300; p = p + 30){
      // Schleife Wartezeit
      for ( int i = 9; i != 15; i++ ){
        // Schleife Lauflicht
        digitalWrite(i, HIGH);    // set the LED i on
        digitalWrite(i-1, LOW);   // set the LED i-1 off
        delay(p);                 // wait for p milliseconds
      }
    }
  }else{
    for ( int i = 9; i != 15; i++ ){
      digitalWrite(i, HIGH);    // set the LED i on
      digitalWrite(i-1, LOW);   // set the LED i-1 off
      delay(1000);              // wait for a second
    }
  }
}

Nun soll das Lauflicht beim letzten Durchlauf bei einer zufällig gewürfelten LED anhalten. Dazu verwenden wir die Funktion random(min,max), die eine Zufallszahl zwischen min und max-1 zurückgibt. Im letzten Durchlauf halten wir bei der gewürfelten LED an und machen eine längere Pause. Je nachdem ob die „Glücks-LED“ gewürfelt wurde, wir entweder die Funktion happy() oder aber sad() aufgerufen:

void loop() {
  // Lese den digitalen Eingang 3 aus und speichere den
  // Wert (0 oder 1) in der Variable 'sensorValue':
  int sensorValue = digitalRead(3);
  if (sensorValue == 1){
    int num = random(9,15);  // Zufallszahl 'num' zwischen 9 und 14
    for (int p = 10; p < 300; p = p + 30){
      // Schleife Wartezeit
      for ( int i = 9; i != 15; i++ ){
        // Schleife Lauflicht
        if ( p > 269 & num == i ){  // Wenn 'letzte Runde' und
          digitalWrite(i-1, LOW);   // LED 'i' = Zufallszahl 'num'
          digitalWrite(i, HIGH);
          if (num == 11){
            happy();
            delay(1000);
          }else{
            sad();
            delay(1000);
          }
        }
        digitalWrite(i, HIGH);    // set the LED i on
        digitalWrite(i-1, LOW);   // set the LED i-1 off
        delay(p);                 // wait for p milliseconds
      }
    }
  }else{
    for ( int i = 9; i != 15; i++ ){
      digitalWrite(i, HIGH);    // set the LED i on
      digitalWrite(i-1, LOW);   // set the LED i-1 off
      delay(1000);              // wait for a second
    }
  }
}

Die Funktionen happy() und sad() Bearbeiten

Die Funktionen happy() und sad() sollen eine Melodie spielen. Wie funktioniert das? Betrachten wir das Beispielprogramm toneMelodie:

/*
 Melody

 Plays a melody,  circuit: 8-ohm speaker on digital pin 8
 by Tom Igoe
 This example code is in the public domain.

*/

#include "pitches.h";

// notes in the melody:
int melody[] = {NOTE_C4, NOTE_G3,NOTE_G3, NOTE_A3,
                              NOTE_G3,0, NOTE_B3, NOTE_C4};

// note durations: 4 = quarter note, 8 = eighth note, etc.:
int noteDurations[] = {4, 8, 8, 4,4,4,4,4 };

void setup() {
  // iterate over the notes of the melody:
  for (int thisNote = 0; thisNote < 8; thisNote++) {

    // to calculate the note duration, take one second
    // divided by the note type.
    //e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
    int noteDuration = 1000/noteDurations[thisNote];
    tone(4, melody[thisNote],noteDuration);

    // to distinguish the notes, set a minimum time between them.
    // the note's duration + 30% seems to work well:
    int pauseBetweenNotes = noteDuration * 1.30;
    delay(pauseBetweenNotes);
    // stop the tone playing:
    noTone(4);
  }
}

void loop() {
  // no need to repeat the melody.
}

Anhand dieses Beispiels können wir happy() und sad() implementieren:

void happy() {
  // notes in the melody:
  int melody[] = {NOTE_C4, NOTE_G3,NOTE_G3, NOTE_A3,
                              NOTE_G3,0, NOTE_B3, NOTE_C4};

  // note durations: 4 = quarter note, 8 = eighth note, etc.:
  int noteDurations[] = {4, 8, 8, 4,4,4,4,4 };

  // iterate over the notes of the melody:
  for (int thisNote = 0; thisNote < 8; thisNote++) {

    // to calculate the note duration, take one second
    // divided by the note type.
    //e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
    int noteDuration = 1000/noteDurations[thisNote];
    tone(4, melody[thisNote],noteDuration);

    // to distinguish the notes, set a minimum time between them.
    // the note's duration + 30% seems to work well:
    int pauseBetweenNotes = noteDuration * 1.30;
    delay(pauseBetweenNotes);
    // stop the tone playing:
    noTone(4);
  }
}

void sad() {
  for (int f = 1000; f > 100; f = f - 5) {
    tone(4, f, 10);
    delay(8);
  }
}

Nicht zu vergessen ist die Einbindung der Datei pitches.h am Anfang des Programms. Die Datei pitches.h ordnet den Noten Frequenzen zu.

Ansteuerung einer LCD-Anzeige Bearbeiten

/*
  LiquidCrystal Library - Hello World

 Demonstrates the use a 16x2 LCD display.  The LiquidCrystal library
 works with all LCD displays that are compatible with the Hitachi
 HD44780 driver. There are many of them out there, and you can usually
 tell them by the 16-pin interface.

 This sketch prints &quot;Hello World!&quot; to the LCD and shows the time.

  The circuit:
 * LCD RS pin to digital pin 12   * LCD Enable pin to digital pin 11
 * LCD D4 pin to digital pin 5    * LCD D5 pin to digital pin 4
 * LCD D6 pin to digital pin 3    * LCD D7 pin to digital pin 2
 * LCD R/W pin to ground
 * 10K resistor: ends to +5V and ground, wiper to LCD VO pin (pin 3)

 This example code is in the public domain.
 http://www.arduino.cc/en/Tutorial/LiquidCrystal
 */
// include the library code:
#include <LiquidCrystal.h>;
// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

void setup() {
  // set up the LCD's number of columns and rows:
  lcd.begin(16, 2);
  // Print a message to the LCD.
  lcd.print("hello, world!");
}
void loop() {
  // set the cursor to column 0, line 1
  // (note: line 1 is the second row, since counting begins with 0):
  lcd.setCursor(0, 1);
  // print the number of seconds since reset:
  lcd.print(millis()/1000);
}