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.

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();.