Bibliotheken und Klassen erstellen – Teil I

Über den Beitrag

Nachdem ich in meinem Blog schon eine Reihe von selbst geschriebenen Bibliotheken (siehe auch auf GitHub) vorgestellt habe, bin ich mehrfach gefragt worden, wie man diese eigentlich erstellt. Ein wenig ist es so, als würde man gefragt, wie man einen Arduino Sketch schreibt – darauf gibt es viele Antworten in beliebiger Länge. Zumindest werde ich es nicht schaffen, das Thema in all seinen Facetten abzudecken, obwohl ich zwei Beiträge darauf verwende.

Aber dieser Crashkurs wird es euch hoffentlich erleichtern, in das Thema einzusteigen. Und auch wenn ihr keine eigenen Bibliotheken erstellen wollt, sind gewisse Kenntnisse trotzdem sehr nützlich. Bibliotheken „lesen“ zu können, hilft euch, sie besser zu nutzen oder ggf. Fehlermeldungen zu verstehen.

In diesem ersten Teil erkläre ich die Grundlagen. Die Grammatik sozusagen. Dabei habe ich bewusst ein Beispiel gewählt, das nichts mit Mikrocontrollern zu tun hat. Im zweiten Teil wird es dann sehr konkret, indem ich zeige, wie ihr eine Bibliothek für einen Sensor entwickelt und welche typischen Fragen dabei auftauchen.

Bibliotheken – Grundbegriffe

Ich will nicht zu tief in die allgemeine Theorie einsteigen, aber sicherstellen, dass wir dieselbe Sprache sprechen. Diesen Teil über die Grundbegriffe halte ich kurz, denn ich möchte mich auf die praktische Anwendung konzentrieren. Wem es zu kurz ist, der möge ein wenig googeln. Es gibt dazu schon so viel im Netz, dass ich keinen Mehrwert bieten kann.

Bibliotheken vs. Klassen

Eine Bibliothek ist eigentlich nur eine Sammlung von Programmteilen, die allein nicht lauffähig ist. Die Bibliothek kann Klassen enthalten, das muss sie aber nicht. Für die Klasse ist die Bibliothek sozusagen der Container, in der sie aufbewahrt wird – ggf. neben anderen Komponenten.

Die Arduino Bibliotheken, die nicht zur „Grundausstattung“ gehören, sind im Ordner „Arduino/libraries“ als Unterordner lokalisiert. Die Unterordner tragen den Namen der Bibliothek.

Objekte und objektorientiertes Programmieren

Beim objektorientierten Programmieren (OOP) stellen wir uns die Welt als eine Ansammlung von Objekten vor, die miteinander in Beziehung stehen. Klassen sind die Blaupausen für die Objekte, so wie es z. B. Schaltpläne für Schaltungen sind. Genauso wie man mit einem Schaltplan praktisch unendlich viele Schaltungen erzeugen kann, könnt ihr mit einer Klasse eine unbegrenzte Zahl von Objekten erzeugen. Zumindest so viele, wie es der Speicher euch erlaubt. 

Die Objekte bzw. die Klassen werden so konzipiert, dass sie nach außen nur das Notwendige preisgeben (Prinzip der Kapselung). Die Objekteigenschaften sollen grundsätzlich nur über definierte Methoden verändert werden können. Das bedeutet zunächst einen erhöhten Aufwand, der sich aber später auszahlt. Wer kennt das nicht: Man erweitert ein Programm, das dann nicht mehr funktioniert, weil sich die Programmteile ungewollt beeinflussen. Mittels OOP lassen sich diese Probleme minimieren. 

Neben der Kapselung gibt es weitere grundlegende Prinzipien der OOP. Vor allem sind das Vererbung, Polymorphismus und Abstraktion. Ich gehe aber nicht weiter darauf ein. Interessierte können zum Beispiel hier schauen.

Eigenschaften und Methoden

Objekte haben bestimmte Eigenschaften (Attribute) und Methoden (Funktionen). Ein immer wieder gerne verwendetes Beispiel, um das zu illustrieren, ist das Auto:

Bibliotheken schreiben: Das Auto als Klasse
Das Auto als Klasse

Einige Eigenschaften sind unveränderlich, wie beispielsweise die Marke oder die Farbe. Andere Eigenschaften, wie die Geschwindigkeit, sind variabel und werden über Methoden verändert. Methoden wiederum müssen nicht zwingendermaßen eine Eigenschaft steuern, wie hier etwa die Methode „hupen“.

Header- und Quelldateien

In den Bibliotheksordnern befinden sich die zugehörigen Header- (Endung „.h“) und Quelldateien (Endung „.cpp“). In den Headerdateien stehen unter anderem die Klassendeklarationen. Gegebenenfalls sind dort weitere Elemente untergebracht, wie etwa #define und #include Anweisungen oder Enum-Definitionen. In den Quelldateien stehen vor allem die Funktionen.

Bei einfachen Bibliotheken, die lediglich eine einzige Klasse enthalten, haben die Bibliothek, die Klasse und die Header- und Quelldateien häufig denselben Namen.

Wenn ihr Bibliotheken selbst schreibt, dann solltet ihr darauf achten, dass ihr individuelle Namen verwendet. Headerdateien oder Klassen, deren Namen im Library-Verzeichnis doppelt vorkommen, führen zu Problemen. Deshalb habe ich bei (fast) allen von mir auf GitHub veröffentlichten Bibliotheken und Klassen meine Initialen „WE“ im Namen, was also nicht (nur 🙂 ) meinem Ego dient.

Vorbereitungen

Die in diesem Beitrag verwendete Übungs-Bibliothek CoolCarLib könnt ihr von GitHub herunterladen. Folgt diesem Link, dann klickt auf Schaltfäche „Code“ und wählt „Download ZIP“. Speichert die ZIP-Datei in eurem Ordner „Arduino/libraries“ und entpackt sie dort. Ihr solltet dann einen Ordner „CoolCarLib-main“ finden. Die ZIP-Datei braucht ihr jetzt nicht mehr und könnt sie löschen. An die Beispielsketche zur Bibliothek kommt ihr am einfachsten über Datei → Beispiele → CoolCarLib-main.

Für die Erstellung und Bearbeitung von Bibliotheken ist die Arduino IDE als Editor nicht besonders gut geeignet. Ich empfehle das kostenlose Notepad++. Für kleinere Projekte ist das Programm absolut ausreichend und bedarf kaum Einarbeitung.

Eine minimale Klasse – CoolCarBasic

Die erste Klasse, die wir betrachten, beschreibt ein Auto und trägt den Namen CoolCarBasic. Das Auto ist sehr minimalistisch, denn es besitzt lediglich die folgenden Eigenschaften und Methoden:

  • Eigenschaften:
    • Maximale Passagierzahl (maxPassengers)
    • Geschwindigkeit (speed)
  • Methoden
    • Gebe mir die maximale Passagierzahl (getMaxPassengers):
    • Setze die Geschwindigkeit (setSpeed)
    • Gebe mir die aktuelle Geschwindigkeit (getSpeed)
    • Hupe! (hoot!)

CoolCarBasic – Headerdatei

CoolCarLib enthält die beiden Klassen CoolCarBasic und CoolCar. Der folgende Ausschnitt der Headerdatei CoolCarLib.h beinhaltet einen allgemeinen Teil und den Code der CoolCarBasic Klassendeklaration:

#ifndef COOL_CAR_LIB_H_
#define COOL_CAR_LIB_H_

#include <Arduino.h>

/* ###############  CoolCarBasic ############### */

class CoolCarBasic  // Class Declaration
{
    public: 
        CoolCarBasic(uint8_t mP);  // Constructor
    
        uint8_t getMaxPassengers();
        uint16_t getSpeed();
        void setSpeed(uint16_t speed);
        void hoot();
                 
    protected:
        uint8_t maxPassengers;
        uint16_t speed;
};

/* #################  CoolCar ################ 
....
....
....
*/

#endif

Erläuterungen zur Headerdatei

Der Inhalt der Headerdatei ist umfasst von der Konstruktion:

#ifndef COOL_CAR_LIB_H_ 
#define COOL_CAR_LIB_H_ 
.......

#endif

Die mit dem Hash # beginnenden Anweisungen sind Präprozessordirektiven. Das heißt, sie gehören nicht zum eigentlichen Code, sondern bestimmen, was als Code eingelesen wird. Nur wenn COOL_CAR_LIB_H_ noch nicht definiert wurde, wird der Bereich zwischen #ifndef (= if not defined) und #endif gelesen. Damit würde COOL_CAR_LIB_H_ definiert werden. Auf diese Weise wird verhindert, dass der Code zweimal eingelesen wird. 

#include <Arduino.h> bindet – nicht überraschend – die Arduino Bibliothek ein. Was hingegen schon überrascht, ist, dass ihr Arduino.h einlesen müsst. Wenn ihr einen einfachen Sketch schreibt, wird Arduino.h ja schließlich auch ohne diese Direktive automatisch eingebunden. Die Notwendigkeit begründet sich in der Reihenfolge, in der die Programmteile gelesen werden. Wenn eure Bibliothek vor der Arduino Bibliothek verarbeitet wird, dann stoppt der Compiler mit einer Fehlermeldung, falls er auf eine Arduino-spezifische Funktion wie etwa digitalWrite() oder Serial.print() stößt.

Darauf folgt die Klassendeklaration, eingeleitet von dem Schlüsselwort class. Der Inhalt der Klassendeklaration ist in geschweifte Klammern gefasst. Der Abschluss erfolgt durch ein Semikolon. Ohne das Semikolon gibt es Fehlermeldungen, die leider nicht direkt auf die Ursache schließen lassen.

Alle Funktionen (Methoden) und Variablen (Eigenschaften), die unter public: stehen, sind öffentlich zugänglich. Der Zugriff erfolgt über den Objektnamen und den Punktoperator. Alles, was sich unter protected: befindet, steht nur der Klasse selbst zur Verfügung. Das heißt, wenn ihr ein CoolCarBasic Objekt mit dem Namen myCar erzeugt, dann ist die Anweisung myCar.setSpeed(50) erlaubt. Eine Zuweisung myCar.speed = 50 ist hingegen unzulässig. Eine Alternative zu protected ist private. Der Unterschied ist, dass private Funktionen und Variablen nicht vererbt werden können.

Die erste Funktion der Klasse ist CoolCarBasic(uint8_t);. Dabei handelt es sich um den Konstruktor, den wir weiter unten besprechen.

Bei Variablendefinitionen ganzer Zahlen solltet ihr den Variablentyp in der Schreibweise (u)intx_t angeben. Dabei steht „int“ für Integer, „u“ für unsigned und „x“ für die Größe in Bits. Auf einem Arduino entspricht beispielsweise ein uint16_t einem unsigned int. Es gibt aber auch Systeme, wie etwa den ESP32, auf denen ein Integer größer als 16 Bit ist. Deshalb ist uint16_t eine eindeutigere Definition.  

CoolCarBasic – Quelldatei

Jetzt kommen wir zur Quelldatei CoolCarLib.cpp. Hier erst einmal der für CoolCarBasic relevante Code:

#include <CoolCarLib.h>

/* ###############  CoolCarBasic ############### */

/************  Constructor ************/

CoolCarBasic::CoolCarBasic(uint8_t mP){
  maxPassengers = mP;
  speed = 0;
}

/**********  Public Functions **********/

uint8_t CoolCarBasic::getMaxPassengers(){    
    return maxPassengers;
}

uint16_t CoolCarBasic::getSpeed(){    
    return speed;
}

void CoolCarBasic::setSpeed(uint16_t sp){    
    speed = sp;
}

void CoolCarBasic::hoot(){
  Serial.println("beep! beep! beep!");
}

/* #################  CoolCar ################ 
............
*/

Erläuterungen zur Quelldatei

In der ersten Zeile wird CoolCarBasic.h eingebunden. Die Anweisung wirkt überflüssig, da wir die Datei in den Sketchen gleich als Erstes einbinden. Aber ohne geht es nicht – das sind die Eigenheiten des Präprozessors.

Darauf folgen die Definitionen der Funktionen, die zuvor in der Headerdatei deklariert wurden. Was dabei auffällt ist, dass allen Funktionsnamen der Klassenname vorangestellt ist, getrennt durch den doppelten Doppelpunkt ::. Dieses Zeichen trägt den sperrigen Namen „Bereichsauflösungsoperator“ (engl.: „scope resolution operator“). CoolCarBasic::xxx() bedeutet, dass die Funktion xxx() zur Klasse CoolCarBasic gehört. Woher sollte der Compiler sonst wissen, welcher Klasse er die Funktion zuordnen soll!?

Jetzt kommen wir zum Konstruktor CoolCarBasic::CoolCarBasic(uint8_t){...}. Der Konstruktor ist die Funktion, mit der ihr eure Objekte erzeugt. Als Funktion ist der Konstruktor insofern besonders, als er keinen Rückgabewert hat. Es ist aber erlaubt, dem Konstruktor Werte zu übergeben. In unserem Beispiel erhält der Konstruktor die maximale Anzahl der Passagiere und weist sie der privaten Variable maxPassengers zu. Außerdem wird der Geschwindigkeit speed der Ausgangswert 0 zugeordnet. Typischerweise ist das aber etwas, was man nicht im Konstruktor, sondern in einer init() oder begin() Funktion unterbringt.

Die Abfrage der maximalen Anzahl der Passagiere erfolgt mit getMaxPassengers(). Da maxPassengers eine für das Auto unveränderliche Größe ist, gibt es keine setMaxPassengers() Funktion. Anders sieht es mit der Geschwindigkeit speed aus. Für sie gibt es eine get- und eine set-Funktion.

Die Funktion hoot() (hupe!) ist eine Methode, die keine Variable verändert. Damit beim Aufruf irgendetwas passiert, habe ich ihr eine Serial.println() Anweisung spendiert. Normalerweise würde ich das Ausgabemedium in einer Klasse nicht definieren, sondern immer nur Werte zurückgeben – wie diese ausgegeben werden, soll der Anwender selbst entscheiden.

CoolCarBasic – Verwendung

cool_car_basic_test.ino – Testsketch

Und so könnte dann ein Sketch aussehen, der CoolCarBasic verwendet:

#include <CoolCarBasic.h>

CoolCarBasic myCar = CoolCarBasic(7); // 7 passengers is maximum

void setup() {
  Serial.begin(9600);
  //delay(200); // uncomment for ESP32 / ESP8266 Boards
  
  byte passengerLimit = myCar.getMaxPassengers();
  unsigned int currentSpeed = myCar.getSpeed();
  
  Serial.print("Max. number of passengers: ");
  Serial.println(passengerLimit);
  
  Serial.print("CurrentSpeed [km/h]: ");
  Serial.println(currentSpeed);
  
  myCar.setSpeed(50);
  currentSpeed = myCar.getSpeed();
  Serial.print("CurrentSpeed [km/h]: ");
  Serial.println(currentSpeed);
  
  myCar.hoot();  
}

void loop() {}

Die Zeile 3 ruft den Konstruktor auf und erzeugt das Objekt myCar. Man sagt auch, dass eine Instanz der Klasse CoolCarBasic erzeugt wird. Dabei übergebt ihr dem Objekt den Wert für maxPassengers.

Das delay(200); in Zeile 7 entkommentiert ihr, wenn ihr ein ESP32 oder ESP8266 basiertes Board verwendet. Es könnte sonst sein, dass ihr nach dem Hochladen des Sketches nichts auf dem seriellen Monitor seht.

Der Rest des Sketches dürfte selbsterklärend sein.

Ausgabe von cool_car_basic_test.ino

Die Ausgabe ist nicht sonderlich überraschend:

Ausgabe von cool_car_basic_test.ino
Ausgabe von cool_car_basic_test.ino

Eines könntet ihr noch ausprobieren: Macht aus der Variable speed eine öffentliche Variable, indem ihr ihre Deklaration in den Bereich „public“ verschiebt. Ihr werdet sehen, dass ihr dann eine Zuweisung wie etwa myCar.speed = 150 vornehmen könnt. Oder ihr lasst euch den Wert per Serial.print(myCar.speed) anzeigen. Und warum tut man das nicht, sondern macht sich die Mühe, set- und get-Funktionen zu schreiben? Die Antwort lautet: Wegen des Grundprinzips der Kapselung. Und warum ist Kapselung wichtig? Sie ermöglicht euch Kontrolle! So lassen sich beispielsweise Checks in die set-Funktion einbauen, die die Eingabe unzulässiger Werte verhindern.

Eine erweiterte Klasse – CoolCar

Mit CoolCarBasic habt ihr das Grundgerüst für eine Klasse kennengelernt. Ein paar wichtige Dinge, die ihr typischerweise benötigt, fehlen aber noch. Diese möchte ich anhand der zweiten Klasse der CoolCarLib erklären, nämlich CoolCar. Eigentlich wäre das auch eine gute Gelegenheit, das Thema Vererbung zu behandeln, aber ich muss mich hier thematisch ein wenig beschränken.

CoolCar besitzt die folgenden Eigenschaften und Methoden:

  • Eigenschaften:
    • Maximale Passagierzahl
    • Maximale Geschwindigkeit
    • Aktuelle Geschwindigkeit
    • Länge
    • Stufe der Klimaanlage
  • Methoden:
    • Initialisierung
    • Gebe mir die maximale Passagierzahl / maximale Geschwindigkeit / Länge / aktuelle Geschwindigkeit
    • Beschleunige / bremse um den Wert x
    • Hupe!
    • Gebe mir / setze die Stufe der Klimaanlage

CoolCar – Headerdatei

Hier der relevante Teil der Headerdatei:

/* #################  CoolCar ################ */

enum cc_ac_level{   
    CC_AC_OFF, CC_AC_LOW, CC_AC_MEDIUM, CC_AC_HIGH, CC_AC_MAX
};

class CoolCar
{
    public: 
        CoolCar(const uint8_t mP, const uint16_t mSp, const float len = 4.2)
        : maxPassengers{mP}, maxSpeed{mSp}, length{len} { /*empty */ }
        
        void init();
        uint8_t getMaxPassengers();
        uint16_t getMaxSpeed();
        float getLengthInMeters();
        void hoot();
        bool accelerate(uint16_t accVal);
        void brake(uint16_t brakeVal);
        uint16_t getCurrentSpeed();
        void setAirConLevel(cc_ac_level acLevel);
        cc_ac_level getAirConLevel();
            
    protected:
        uint8_t maxPassengers;
        uint16_t maxSpeed;
        float length; 
        int16_t currentSpeed;
        cc_ac_level airConLevel;
        
        int16_t calculateNewSpeed(int16_t value);
};

Erklärungen zur Headerdatei

Gebrauch von Enum Aufzählungen

Unser CoolCar Auto besitzt nun eine Klimaanlage, die in fünf Stufen einstellbar sein soll, und zwar von „Aus“ (CC_AC_OFF) bis „Maximal“ (CC_AC MAX). Dazu benutzen wir die private Variable airConLevel, die als Enum-Aufzählung definiert ist. Die Einstellung erfolgt über setAirConLevel(). Natürlich könnte man für die Stufen einfach auch Integerwerte von 0 bis 4 verwenden. Aber beispielsweise kann man sich unter „MEDIUM“ mehr vorstellen als unter „Stufe 2“.

Die Bezeichnungen der Enum-Elemente sollten individuell sein. Deshalb habe ich ihnen ein „CC_AC_“ (Cool Car Air Condition) vorangestellt. Wenn ihr euch nicht an diese Regel haltet und der Bezeichner schon einmal an anderer Stelle für eine globale Definition benutzt wurde, dann gibt es eine Fehlermeldung. Ihr könnt das ausprobieren, indem Ihr versucht, CC_AC_HIGH in ein einfaches HIGH umzubenennen, was ja bekanntermaßen schon für den Logiklevel vergeben ist.

Empfohlen: Enum-Klassen

Eigentlich wird empfohlen, anstelle der einfachen Enums enum class zu verwenden:

enum class cc_ac_level : uint8_t {   
    CC_AC_OFF, CC_AC_LOW, CC_AC_MEDIUM, CC_AC_HIGH, CC_AC_MAX
};

Wenn ihr die Elemente verwenden wollt, dann müsst ihr ihnen den Namen der Enum-Klasse, getrennt durch den Bereichsauflösungsoperator ::, voranstellen, also beispielsweise so: cc_ac_level::CC_AC_MEDIUM. Der Vorteil ist zum einen geringeres Risiko von Namenskollisionen, zum anderen werden implizite Typenumwandlungen vermieden. Für mehr Details schaut z.B. hier.

In den meisten Arduino Bibliotheken trifft man auf die einfachen Enums, so auch in meinen, wie ich gestehen muss.

Übergabe mehrerer Parameter an den Konstruktor

Der Konstruktor der Klasse CoolCar bekommt die folgenden Parameter übergeben:

  • Maximale Passagierzahl
  • Höchstgeschwindigkeit
  • Länge des Autos

Da sich diese Eigenschaften nicht ändern, übergibt man sie am besten als Konstanten.

Je größer die Zahl der Parameter, desto höher die Wahrscheinlichkeit, dass sich der Anwender in Reihenfolge vertut. Deshalb sollte man es damit nicht übertreiben.

Wie bei gewöhnlichen Funktionen können Parameter für den Konstruktor auch als optional eingerichtet werden, indem man sie vordefiniert. In diesem Beispiel machen wir das für die Länge len. Wird die Länge nicht übergeben, dann greift die Voreinstellung „4.2“.

Durch die Schreibweise, die ich hier verwende, werden den Objekteigenschaften hier schon in der Headerdatei die übergebenen Parameter zugewiesen.

Auch ist es möglich, den Konstruktor zu überladen, d.h. ihn mehrfach mit unterschiedlichen Parametern zu deklarieren.

CoolCar – Quelldatei

Hier der relevante Teil der Quelldatei:

/**********  Public Functions **********/

void CoolCar::init(){    
    currentSpeed = 0;
    airConLevel = CC_AC_OFF;
}

uint8_t CoolCar::getMaxPassengers(){    
    return maxPassengers;
}

uint16_t CoolCar::getMaxSpeed(){    
    return maxSpeed;
}

float CoolCar::getLengthInMeters(){
    return length;
}

uint16_t CoolCar::getCurrentSpeed(){
    return currentSpeed;
}

void CoolCar::hoot(){
    Serial.println("beep! beep! beep!");
}

bool CoolCar::accelerate(uint16_t accVal){
    bool noLimitViolation = true;
    uint16_t newSpeed = static_cast<uint16_t>(calculateNewSpeed(accVal));
    if(newSpeed > maxSpeed){
        currentSpeed = maxSpeed;
        noLimitViolation = false;
    }
    else{
        currentSpeed = newSpeed;
    }
    return noLimitViolation;
}

void CoolCar::brake(uint16_t brakeVal){
    int16_t newSpeed = calculateNewSpeed(brakeVal * (-1));
    if(newSpeed <= 0){
        currentSpeed = 0;
    }
    else{
        currentSpeed = (uint16_t)newSpeed;
    }
}

void CoolCar::setAirConLevel(cc_ac_level level){
    airConLevel = level;
}

cc_ac_level CoolCar::getAirConLevel(){
    return airConLevel;
}
        
/*********  Private Functions *********/

int16_t CoolCar::calculateNewSpeed(int16_t value){
    int16_t speed = currentSpeed + value;
    return speed;   
}

 

Erklärungen zur Quelldatei

Konstruktor

Der Konstruktor muss dieses Mal nicht noch einmal in der Quelldatei aufgeführt werden. Die Parameter sind schon übergeben worden.

Init() Funktion

Viele Arduino Klassen besitzen eine init() oder begin() Funktion, in der Objekteigenschaften einen bestimmten Startwert zugewiesen bekommen. In unserem Fall betrifft das die Einstellung der Klimaanlage und die aktuelle Geschwindigkeit.

Wenn die Klassen Bauteile wie Sensoren oder Steuerungen repräsentieren, wird in init() auch häufig geprüft, ob die Bauteile korrekt angeschlossen wurden.

Geschwindigkeitskontrolle

Im Gegensatz zum letzten Beispiel steuern wir die Geschwindigkeit über die Beschleunigung accelerate() und über Bremsvorgänge brake(). Versucht ihr die Höchstgeschwindigkeit zu überschreiten, dann wird die Geschwindigkeit auf die Höchstgeschwindigkeit begrenzt. Überdies hat die Funktion accelerate() einen Rückgabewert (noLimitViolation). Ist er true, dann ist alles in Ordnung. Ist er hingegen false, habt ihr versucht, über die Höchstgeschwindigkeit hinaus zu beschleunigen.

Für das Bremsen gilt, dass es nicht zu negativen Geschwindigkeiten führen darf. brake() enthält eine entsprechende Kontrollfunktion. Auf einen Rückgabewert habe ich aber verzichtet.

Vorsicht mit den Vorzeichen

Unser Auto kann nur vorwärtsfahren. Also müsste die Geschwindigkeit immer positiv sein. Trotzdem gibt calculateNewSpeed() ein int16_t zurück. Der Grund ist leicht einzusehen: Die Prüfung auf negative Werte erfolgt erst nach der Rückgabe an brake(). Der Rückgabewert muss explizit in ein uint16_t gewandelt werden. Insbesondere beim Arbeiten mit Registern (siehe Teil 2 des Beitrages), vergisst man gerne mal zu berücksichtigen, dass diese gegebenenfalls auch negative Werte enthalten können. 

Viele Autoren nutzen zur Typumwandlung den sogenannten C-Cast, also beispielsweise (uint16_t)value. Man sollte stattdessen static_cast<uint16_t>(value) verwenden – daran halte ich mich aber auch oft nicht.

CoolCar – Verwendung

cool_car_test.ino – Testsketch

Hier ein kleiner Anwendungssketch:

#include <CoolCarLib.h>

CoolCar myCar = CoolCar(5, 180, 3.5); 
// Alternative: CoolCar myCar(5, 180); 

void setup() {
  Serial.begin(9600);
  //delay(200); // uncomment for ESP32 / ESP8266
  myCar.init();
  Serial.print("Max. number of passengers: ");
  Serial.println(myCar.getMaxPassengers());
  Serial.print("Max. Speed [km/h]: ");
  Serial.println(myCar.getMaxSpeed());
  Serial.print("Length [meters]: ");
  Serial.println(myCar.getLengthInMeters());
  myCar.hoot();
  Serial.print("Speed: ");
  Serial.println(myCar.getCurrentSpeed());

  if(!myCar.accelerate(50)){
    Serial.println("Speed Limit Warning!"); 
  }
  Serial.print("New Speed [km/h]: ");
  Serial.println(myCar.getCurrentSpeed());

  if(!myCar.accelerate(150)){
    Serial.println("Acceleration warning!!"); 
  }
  Serial.print("New Speed [km/h]: ");
  Serial.println(myCar.getCurrentSpeed());

  Serial.print("Air Conditioning Level: ");
  Serial.println(myCar.getAirConLevel());

  myCar.brake(60);
  Serial.print("New Speed [km/h]: ");
  Serial.println(myCar.getCurrentSpeed());

  myCar.setAirConLevel(CC_AC_MEDIUM);
  Serial.print("Air Con Level [num]: ");
  Serial.println(myCar.getAirConLevel());
  printAirConLevel();
}

void loop() {} 

void printAirConLevel(){
  cc_ac_level acLevel = myCar.getAirConLevel(); 
  Serial.print("Air Con Level [level]: ");
  switch(acLevel){
    case CC_AC_OFF:
      Serial.println("off");
      break;
    case CC_AC_LOW:
      Serial.println("low");
      break;
    case CC_AC_MEDIUM:
      Serial.println("medium");
      break;
    case CC_AC_HIGH:
      Serial.println("high");
      break;
    case CC_AC_MAX:
      Serial.println("maximum");
      break; 
    default:
      Serial.print("couldn't detect");
  }
}

 

Ausgabe von cool_car_test.ino

Auch diese Ausgabe dürfte nicht unerwartet sein:

Ausgabe von cool_car_test.ino
Ausgabe von cool_car_test.ino

An dem Beispiel erkennt ihr einen Nachteil von einfachen Enums und Enum-Klassen. Fragt ihr den airConLevel mit getAirConLevel() ab, dann bekommt ihr nur die nackte Zahl, aber nicht den Namen des enum-Elements. Erst eine Konstruktion wie printAirConLevel() übersetzt den Rückgabewert in etwas Verständliches.

Keyword Highlighting mit keywords.txt

Um Tippfehler zu vermeiden bzw. sie leichter zu erkennen, ist es hilfreich, wenn die Schlüsselwörter, also Funktionen, Variablen, enum-Elemente usw. farblich durch die Arduino IDE hervorgehoben werden. Dazu erstellt ihr eine Datei namens keywords.txt, die ihr in das Bibliotheksverzeichnis kopiert. In die Datei schreibt ihr die hervorzuhebenden Namen. Dahinter schreibt ihr, getrennt durch einen Tab(!), KEYWORD1, KEYWORD2 oder LITERAL. Das #-Zeichen kennzeichnet Kommentare. Andere IDEs haben da smartere Lösungen!

#######################################
# Syntax Coloring Map For CoolCarLib
#######################################

#######################################
# Datatypes (KEYWORD1)
#######################################

CoolCar	KEYWORD1
CoolCarBasic	KEYWORD1

# ENUM TYPES
cc_ac_level	KEYWORD1

#######################################
# Methods and Functions (KEYWORD2)
#######################################

getMaxPassengers	KEYWORD2
getSpeed	KEYWORD2
setSpeed	KEYWORD2
hoot	KEYWORD2
maxPassengers	KEYWORD2
speed	KEYWORD2
init	KEYWORD2
getMaxSpeed	KEYWORD2
getLengthInMeters	KEYWORD2
accelerate	KEYWORD2
brake	KEYWORD2
getCurrentSpeed	KEYWORD2
setAirConLevel	KEYWORD2
getAirConLevel	KEYWORD2
maxSpeed	KEYWORD2
length	KEYWORD2
currentSpeed	KEYWORD2
calculateNewSpeed	KEYWORD2

#######################################
# Constants (LITERAL1)
#######################################

# ENUM VALUES
CC_AC_OFF	LITERAL1
CC_AC_LOW	LITERAL1
CC_AC_MEDIUM	LITERAL1
CC_AC_HIGH	LITERAL1
CC_AC_MAX	LITERAL1

 

Fazit und Ausblick

Mit diesem Beitrag habt ihr die Grundlagen zur Erstellung von Bibliotheken erlernt. Allerdings hatte dieses Beispiel noch keinen wirklichen Bezug zu Mikrocontrollern. Solltet ihr eine Bibliothek für typische Arduino Bauteile erstellen wollen, stellen sich weitere Fragen. Zum Beispiel: Wie verändere ich selektiv bestimmte Bits in einem Register? Oder: Wie lese ich Werte aus, die über mehrere Register verteilt sind? Oder: wie übergebe ich ein SPI- oder Wire-Objekt? Diese und andere Fragen werden im zweiten Teil dieses Beitrages anhand eines praktischen Beispiels beantwortet.

2 thoughts on “Bibliotheken und Klassen erstellen – Teil I

  1. Deine Ausführungen „Bibliotheken und Klassen“ werden mich noch eine Weile beschäftigen – das liegt aber mehr an meiner langen Leitung als an deinen sehr instruktiven Erläuterungen. Danke.
    Eine Bemerkung verunsichert mich etwas: „Das delay(200); in Zeile 7 entkommentiert ihr, wenn ihr ein ESP32 oder ESP8266 basiertes Board verwendet. Es könnte sonst sein, dass ihr nach dem Hochladen des Sketches nichts auf dem seriellen Monitor seht.“. Was kann die Ursache sein für diese für mich unerklärliche Auswirkung auf die ESP’s? Mir passier es öfters bei ESP’s, dass ich keine Ausgabe auf dem SeriMoni bekomme. Aber mit hterm.exe klappt es dann meistens. Hat das eine Bewandnis mit deiner Feststellung zum delay?

    1. Hallo Andreas,

      ich weiß nicht genau, wo der Grund liegt, dass manches Serial.print() zu Beginn eines Sketches verschluckt wird. Ich muss dem irgendwann noch einmal auf den Grund gehen. Tatsache ist, dass bei einem einfachen Sketch wie diesem hier:

      void setup(){
      Serial.begin(9600);
      //delay(200);
      Serial.println(„Hallo“);
      }

      void loop(){}

      ….das „Hallo“ nach dem Hochladen auf den ESP32 nur dann auf dem seriellen Monitor (den ich vorher geöffnet habe) erscheint, wenn das delay() nicht auskommentiert ist. Wenn man den ESP32 rebootet, dann ist es da – mit oder ohne delay(). Das Problem tritt nur nach dem Hochladen auf.

      Das kann sehr verwirrend sein und viele denken sie hätten etwas falsch gemacht. Deshalb weise ich lieber darauf hin. Leider kann ich aber nur die Lösung, nicht aber den genauen Grund nennen.

      Interessant, dass es mit hterm wohl besser geht. Das muss ich auch nochmal gelegentlich ausprobieren.

      Wenn irgendein anderer Leser den genauen Grund kennt, immer heraus damit!

      VG, Wolfgang

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert