Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

skript:klassen-arduino

Klassen in Arduino/C++

In dieser Aufgabe gehen wir davon aus, dass ihr Grundkenntnisse über Klassen in Java/Processing habt im Umfang der 3. Woche des Crashkurses sowie Grundkenntnisse über Arrays in C++.

In Arduino kann man auch mit Klassen arbeiten. Im Folgenden wollen wir auf dem Piezo unterschiedliche Melodien abspielen und diese mit Hilfe einer Klasse Song modellieren. Baut eine Schaltung mit dem Piezo auf.

1. Deklaration

Jeder Song soll zwei Eigenschaften haben: die Liste von abzuspielenden Noten in Form eines int-Arrays und der Pin, welcher mit dem Piezo verbunden ist.

Eine Klasse in C++ besteht in der Regel aus zwei Dateien: einer Header-Datei .h, die alle Variablen, Konstruktoren und Methoden benannt werden, und einer .cpp-Datei, die die eigentliche Impementieriung der Methoden beihnaltet. In unserem Beispiel kann die Klasse so aussehen:

Song.h
class Song {
  public:
    Song(int noteList[], int arraySize, int pinNumber); // Konstruktor
    void play(); // Methode zum Abspielen der Melodie
  private:
    int *notes; // Noten-Frequenzen in Hz; ist ein Zeiger, da Array-Laenge noch nicht bekannt
    int arraySize; // Laenge des Noten-Arrays
    int pin; // Piezo-Pin
};
Song.cpp
#include "Song.h"
Song::Song(int noteList[], int arraySize, int pinNumber) {
  // TODO: Implementierung des Konstruktors fehlt
}
 
void Song::play() {
  // TODO: Implementierung der Funktion fehlt
}

Diese Aufteilung klingt vielleicht verwirrend am Anfang. Gründe dafür sind die historische Entwicklung von C++ und die Idee, die Schnittstelle von der Implementierung zu trennen. Z.B. wenn man im Team arbeitet, kann man die Header-Dateien als Schnittstellen an Andere übergeben, während an der Implementierung noch gearbeitet wird. Theoretisch kann man auch alles in einer Datei schreiben; Die Aufteilung in mehrere Dateien ist für die Übersichtlichkeit empfohlen.

Ihr sieht oben, dass die Header-Datei zwei Blöcke hat, die mit public und private anfangen. Diese Schlüsselworte weisen auf die Sichtbarkeit der Variablen und Funktionen hin. Public bedeutet, dass die Variable/Funktion nur innerhalb der Klasse sichtbar ist, protected innerhalb der Klasse und ihrer Unterklassen, public überall. (Ähnliche Ebenen gibt es übrigens auch in Java.) Der Faustregel ist, die Sichtbarkeit möglichst begrenzt zu wählen. Die Begründung ist die gleiche wie bei globalen und lokalen Variablen: Je mehr Stellen haben Zugriff auf die Variable, desto mehr fehleranfällig ist der Code. Wir machen den Konstruktor und die Funktion play() public, damit wir darauf von unserem Haupt-Tab zugreifen können. Die beiden Variablen sind private, sie sollen nur innerhalb der Klasse verfügbar sein. Es ist üblich, in der Header-Datei den public-Block ganz oben zu platzieren.

Wenn in C++ Arrays als Parameter an eine Funktion übergeben werden, wird nur der Zeiger auf das erste Array-Element übergeben. D.h. die Funktion bekommt keine Kopie des ganzen Arrays, sondern nur die Adresse des ersten Elementes. Eine Lösung ist, die Array-Länge als einen weiteren Parameter (arraySize in unserem Beispiel) zu übergeben. In der Liste der Membervariablen wird ebenso nur ein Zeiger int *notes verwendet, da die Deklaration eines Arrays ohne seiner Länge nicht möglich ist.

2. Implementierung

Im Beispiel oben steht ein Konstruktor ohne Parameter Song() als Platzhalter – das wollen wir jetzt ändern. Wenn ein Song erzeugt wird, soll ihm die Liste der Noten sowie die Pinnummer übergeben werden. Also wir wollen sowas:
Song(int noteList[], int pinNumber). In diesem Fall besteht aber das Problemm, dass in C++ Arrays nicht als Parameter an eine Funktion übergeben werden können. Stattdessen wird nur der Zeiger auf das erste Array-Element übergeben, die Iteration durch das Array ist nicht möglich. Die Lösung ist, zusätzlich noch die Array-Länge zu übergeben:

Song.cpp
//[...]
// Konstruktor; noteList: Notenabfolge, arraySize: Laenge des Noten-Arrays, pin: Piezo-Pin
Song::Song(int noteList[], int arraySize, int pinNumber) {
  this->arraySize = arraySize;
  this->notes = noteList;
  this->pin = pinNumber;
}
 
//[...]

Genauso wie in Java wird this benutzt, um auf die Instanz der Klasse (d.h. auf die gerade erzeugte Objekt vom Typ Song) zu zeigen. Achtung: in C++ wird statt Punkt ein Pfeil verwendet.

Jetzt implementieren wir auch die Funktion play(). Wenn wir tone() verwenden wollen, bekommen wir erstmal einen Fehler, da sie nicht zu unserer Klasse gehört. Die Lösung ist, die Klasse Arduino.h miteinzuschließen – sie macht die Arduino-spezifische Funktionalität verfügbar.

Song.cpp
#include "Arduino.h"
//[...]
// Spielt die Melodie ab
void Song::play() {
  for (int i = 0; i < arraySize ; i++) {
    tone(pin, notes[i]);
    delay(500);
  }
  noTone(pin);
}
//[...]

Komplette Datei Song.cpp

Komplette Datei Song.cpp

Song.cpp
#include "Song.h"
#include "Arduino.h"
 
// Konstruktor; noteList: Notenabfolge, arraySize: Laenge des Noten-Arrays, pin: Piezo-Pin
Song::Song(int noteList[], int arraySize, int pinNumber) {
  this->arraySize = arraySize;
  this->notes = noteList;
  this->pin = pinNumber;
}
 
// Spielt die Melodie ab
void Song::play() {
  for (int i = 0; i < arraySize ; i++) {
    tone(pin, notes[i]);
    delay(500);
  }
  noTone(pin);
}


3. Aufruf

Nun werden wir einen Song im Haupt-Tab erstellen und aufrufen, z.B. so (Piezo auf Pin 2):

variante1
#include "Song.h"
 
void setup() {
  int notes[] = {262, 294, 330, 349, 392}; // Notenfrequenzen
  Song mySong (notes, sizeof(notes) / sizeof(notes[0]), 2); // oder: Song mySong(notes, 5, 2);
  mySong.play();
}
 
void loop() { 
}

Achtung: wir müssen dem Compiler mit #include Song.h Bescheid sagen, dass eine Klasse aus einer anderen Datei verwendet wird. Falls wir auf die Variable mySong sowohl von setup() als auch von loop() zugreifen wollen, können wir diese inkl. Konstruktoraufruf auch global haben:

variante2
#include "Song.h"
int notes[] = {262, 294, 330, 349, 392};
Song mySong (notes, sizeof(notes) / sizeof(notes[0]), 2); // oder: Song mySong(notes, 5, 2);
 
void setup() {
  mySong.play(); 
}
 
void loop() {
  mySong.play();
}

Der Konstruktor hier wird aufgerufen, bevor setup() anfängt. Das kann ungünstig sein im Fall, wenn der Konstruktor Parameter hat, deren Werte am Anfang des Programms noch nicht bekannt sind (z.B. wenn sie von Sensormessungen abhängen, die in setup() gemacht werden). Anders als in Java, können wir nicht einfach Song mySong; in Zeile 3 schreiben. Das liegt daran, dass mySong eine Referenz ist, und Referenzen dürfen nicht gleich NULL sein.

Die Unterscheidung zwischen Referenzen und Zeigern (Englisch: pointers) in C++ ist ein separates großes Thema – bei Interesse lesst diesen Link oder sucht Informationen dazu online. Wir werden dieses Thema hier nicht angehen.

Wir werden stattdessen einen Zeiger nutzen – man kann das so verstehen, dass, statt des Objektes an sich, haben wir nur einen Verweis darauf, wo dieses Objekt sich befindet. Ein Zeiger wird mit einem * markiert, und der Zugriff auf Funktionen und Variablen geht über .

variante3
#include "Song.h"
Song* mySong; // Zeiger
 
void setup() {
  int notes[] = {262, 294, 330, 349, 392};
  mySong = new Song(notes, sizeof(notes) / sizeof(notes[0]), 2); // oder: Song mySong(notes, 5, 2);
  mySong->play(); 
}
 
void loop() {
  mySong->play();
}
skript/klassen-arduino.txt · Zuletzt geändert: 2021/06/11 11:28 von d.golovko