Arduino

Recycling mit Struktur – Funktionen

Du schreibst eigene Funktionen, mit Parametern und Rückgabewerten.

Ein Programm im Programm

Eine Funktion ist wie ein kleines Programm innerhalb eines größeren. Sie enthält einen Block von Code, der eine bestimmte Aufgabe erledigt. Funktionen zerlegen ein Programm in kleinere, wiederverwendbare Teile – das verringert den Schreibaufwand und macht Programme lesbarer und leichter zu ändern.

Die Funktion quadrat als Kasten: Der Parameter x = 3 geht hinein, der Rückgabewert 9 kommt heraus
Eine Funktion kann Parameter (Eingabewerte) annehmen und einen Rückgabewert liefern. Über die Parameter verarbeitet sie unterschiedliche Eingabedaten, über den Rückgabewert gibt sie ein Ergebnis an die aufrufende Stelle zurück.

In C++ definierst du eine Funktion mit einem Rückgabetyp, einem Namen, einer Parameterliste und einem Anweisungsblock. Zum Aufrufen schreibst du den Namen und übergibst in Klammern die nötigen Parameter.

setup() und loop() hast du längst mit Leben gefüllt, und Funktionen wie delay() oder drawPixel() benutzt du in jedem Programm. Hier lernst du, eigene zu schreiben.

Tipp: Schreibe eigene Funktionen oberhalb von setup(). Dann steht immer fest, dass sie bekannt sind, wenn sie aufgerufen werden. Die Arduino IDE gleicht die Reihenfolge meist selbst aus. Deshalb funktionieren auch Programme, in denen Funktionen weiter unten stehen, wie etwa das Snake-Spiel. Reines C++ außerhalb der Arduino IDE ist da strenger.

Funktionen ohne Parameter und Rückgabewert

Manche Funktionen brauchen weder Parameter noch Rückgabewert – sie tun einfach immer dasselbe. Das folgende Beispiel zeigt eine Funktion, die ein Raster zeichnet. Das void vor dem Namen bedeutet „nichts“ und zeigt, dass die Funktion keinen Wert zurückgibt. Variablen, die du innerhalb einer Funktion deklarierst, gelten auch nur dort – das i der beiden Schleifen stört also niemanden. Das void in void setup(void) bedeutet „keine Parameter“. Das ist gleichbedeutend mit void setup(), du wirst in diesem Tutorial beide Schreibweisen sehen.

Das Raster als eigene Funktion

#include <U8g2lib.h>
#include <SPI.h>

U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, 10, 8, 9);

void drawGrid() {                         // eigene Funktion für ein Raster
  for (int i = 1; i < 16; i++) {
    u8g2.drawLine(i * 8, 8, i * 8, 56);   // die senkrechten Linien
  }
  for (int i = 1; i < 8; i++) {
    u8g2.drawLine(8, i * 8, 120, i * 8);  // die waagerechten Linien
  }
}

void setup(void) {
  u8g2.begin();
  u8g2.clearBuffer();
  drawGrid();                             // Aufruf der eigenen Funktion
  u8g2.sendBuffer();
}

void loop() {}

Funktionen mit Parametern

Richtig nützlich werden Funktionen, wenn sie von außen Werte bekommen. Die Funktion drawPiece(int xPos, int yPos) zeichnet einen „Spielstein“ an einer übergebenen Position. Der Aufruf drawPiece(1, 2) führt dazu, dass die Parameter xPos und yPos innerhalb der Funktion die Werte 1 und 2 annehmen. Statt fester Zahlen kannst du auch Variablen übergeben, wie im Beispiel unten.

Beachte, dass die Funktion in Rasterfeldern denkt, nicht in Pixeln: Sie rechnet selbst aus, wo das Feld (1, 2) auf dem Display liegt. Genau das ist der Gewinn – die Rechnung steht einmal in der Funktion statt an jeder Aufrufstelle.

Dieselbe Funktion, mehrfach genutzt

#include <U8g2lib.h>
#include <SPI.h>

U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, 10, 8, 9);

void drawGrid() {
  for (int i = 1; i < 16; i++) {
    u8g2.drawLine(i * 8, 8, i * 8, 56);
  }
  for (int i = 1; i < 8; i++) {
    u8g2.drawLine(8, i * 8, 120, i * 8);
  }
}

void drawPiece(int xPos, int yPos) {  // zwei Parameter: die Position im Raster
  u8g2.drawCircle(xPos * 8 + 4, yPos * 8 + 4, 3);
}

void setup(void) {
  u8g2.begin();
  u8g2.clearBuffer();
  drawGrid();
  drawPiece(1, 1);                    // Aufruf mit festen Zahlen
  drawPiece(1, 2);
  int xP = 3;
  int yP = 3;
  drawPiece(xP, yP);                  // Aufruf mit Variablen
  u8g2.sendBuffer();
}

void loop() {}

Funktionen mit Parametern und Rückgabewert

Funktionen, wie du sie aus der Mathematik kennst, liefern meist einen Wert zurück. Hast du beispielsweise die Funktion f(x) = 5 + x, bekommst du beim „Funktionsaufruf“ f(3) den Wert 8 zurück. Mit Parametern und Rückgabewert geht das auch in der Programmierung: Statt void steht dann der Datentyp des Ergebnisses vor dem Namen, und return liefert es zurück.

Ein Wert zurück an das Hauptprogramm

Hinweis: Dieses Beispiel gibt nur Daten über die serielle Schnittstelle aus. Öffne also den seriellen Monitor, wie im Kapitel Variablen beschrieben.

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

int quadrat(int x) {  // "int" vorn: die Funktion gibt eine ganze Zahl zurück
  int q = x * x;
  return q;           // dieser Wert landet an der Aufrufstelle
}

void loop() {
  for (int i = 0; i < 100; i++) {
    Serial.print("Das Quadrat von ");
    Serial.print(i);
    Serial.print(" ist ");
    Serial.println(quadrat(i));  // der Aufruf steht, wo sonst eine Zahl stünde
    delay(100);
  }
}

Funktionen mit Rückgabewert

Manchmal ist es sinnvoll, Funktionen zu schreiben, die ohne Parameter einen Wert liefern – etwa einen, der sich aus globalen Variablen ergibt. Im folgenden Beispiel beantwortet outsideSquare() die Frage „Ist der Punkt außerhalb des Rahmens?“ mit true oder false. Weil die Funktion einen Wahrheitswert zurückgibt, kannst du ihren Aufruf direkt als Bedingung in ein if schreiben.

Prüfen, ob der Punkt den Rahmen verlässt

#include <U8g2lib.h>
#include <SPI.h>

#define KEY_RIGHT 2
#define KEY_DOWN 3
#define KEY_LEFT 4
#define KEY_UP 1
#define KEY_CENTER 0

U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, 10, 8, 9);

int xPos;
int yPos;

void setup(void) {
  u8g2.begin();
  u8g2.clearBuffer();
  u8g2.sendBuffer();
  u8g2.setFont(u8g2_font_glasstown_nbp_tf);

  pinMode(KEY_RIGHT, INPUT_PULLUP);
  pinMode(KEY_DOWN, INPUT_PULLUP);
  pinMode(KEY_LEFT, INPUT_PULLUP);
  pinMode(KEY_UP, INPUT_PULLUP);
  pinMode(KEY_CENTER, INPUT_PULLUP);

  xPos = 64;
  yPos = 32;
}

boolean outsideSquare() {       // kein Parameter, aber ein Rückgabewert
  boolean out = false;          // Annahme: der Punkt ist drin
  if (xPos < 20) {
    out = true;
  }
  if (xPos > 106) {             // der Rahmen reicht von x = 20 bis x = 106
    out = true;
  }
  if (yPos < 10) {
    out = true;
  }
  if (yPos > 52) {              // und von y = 10 bis y = 52
    out = true;
  }
  return out;
}

void loop() {
  if (digitalRead(KEY_UP) == LOW) {
    yPos = yPos - 1;
  }
  if (digitalRead(KEY_DOWN) == LOW) {
    yPos = yPos + 1;
  }
  if (digitalRead(KEY_LEFT) == LOW) {
    xPos = xPos - 1;
  }
  if (digitalRead(KEY_RIGHT) == LOW) {
    xPos = xPos + 1;
  }
  if (digitalRead(KEY_CENTER) == LOW) {
    u8g2.clearBuffer();
    xPos = 64;
    yPos = 32;
  }

  delay(50);
  u8g2.clearBuffer();
  u8g2.drawFrame(20, 10, 87, 43);     // Rahmen ab (20,10): 87 breit, 43 hoch
  u8g2.drawPixel(xPos, yPos);
  if (outsideSquare()) {              // der Funktionsaufruf als Bedingung
    u8g2.drawStr(36, 34, "Come back in!");
  }
  u8g2.sendBuffer();
}

Parameter sind Kopien

Eine Funktion bekommt nur eine Kopie des übergebenen Wertes. Ändert sie ihren Parameter, bleibt die Variable an der Aufrufstelle, wie sie war. Soll ein Ergebnis zurück, gibst du es mit return zurück und speicherst es:

void verdoppeln(int zahl) {
  zahl = zahl * 2;            // ändert nur die Kopie in zahl
}

int doppelt(int zahl) {
  return zahl * 2;            // liefert das Ergebnis an die Aufrufstelle
}

// So sieht das beim Aufruf aus:
int wert = 5;
verdoppeln(wert);             // wert ist immer noch 5
wert = doppelt(wert);         // wert ist jetzt 10

Noch etwas zu return: Der Befehl beendet die Funktion sofort. Was dahinter steht, wird nicht mehr ausgeführt. Deshalb kann eine Funktion auch mehrere return enthalten, wie crash() im Snake-Spiel.

Übung

Schreibe die vier Joystick-Abfragen aus loop() in eine eigene Funktion readJoystick() um. loop() wird dadurch deutlich kürzer und besser lesbar.

Die Funktion braucht weder Parameter noch Rückgabewert, denn xPos und yPos sind globale Variablen und lassen sich direkt ändern. Sie beginnt also mit void readJoystick(). In loop() steht dann an der Stelle der vier Abfragen nur noch der Aufruf readJoystick();.