Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

techniken:objektorient

Einführung ins objektorientierte Programmieren mit Arduino

Motivation

Schon bald nach Beginn der Programmierarbeiten an eurem Roboter werden euch einige typische Probleme begegnen:

  • Übersichtlichkeit: Das Programm wird immer größer und es wird schwer, den Überblick zu behalten.
  • Entkopplung: Ihr wollt einen Teil des Programms ändern, ohne dass der ganze Rest danach nicht mehr funktioniert.
  • Wiederverwendbarkeit: Ihr habt mehrere gleiche oder ähnliche Teile an eurem Roboter, so dass es Sinn machen würde dafür auch den gleichen Code benutzen.

Ihr ahnt es schon: Diese Probleme sind so alt wie die Programmierpraxis an sich und Ihre Lösung ist eine Kunst und Wissenschaft zugleich.

Fast immer hilft es jedoch, das Programm in sinnvolle Untereinheiten aufzuteilen, die relativ unabhängig voneinander sind.

Praktisches Beispiel: Motorsteuerung

Mit dem Konzept der Funktion habt ihr bereits ein enorm nützliches Werkzeug zur Strukturierung eures Codes kennengelernt. Mit ihnen lässt sich eine Tätigkeit bzw. Operation, die aus mehreren anderen Operationen besteht unter einem neuen Namen zusammenfassen, so dass sie immer wieder durchgeführt werden kann, ohne dass der Code dafür kopiert werden oder auch nur bekannt sein muss.

so könnte z.B. eine Funktion für das Einstellen der Geschwindigkeit eines Motors, der an eine H-Brücke angeschlossen ist, so aussehen:

void setMotorSpeed(int newSpeed, int forwardPin, int reversePin, int throttlePin){
  // sollen es vorwärts oder rückwärts gehen?
  if(newSpeed>0){
    //schalte auf vorwärts
    digitalWrite(reversePin,LOW);
    digitalWrite(forwardPin,HIGH);
  }else{
    //schalte auf vorwärts
    digitalWrite(forwardPin,LOW);
    digitalWrite(reversePin,HIGH);
  }
  //stelle die Geschwindigkeit ein:
  analogWrite(throttlePin, newSpeed);
}

In Zukunft brauchen wir dann nur noch eine einzige Zeile zu schreiben, um z.B. die Geschwindigkeit des linken Rades einzustellen:

setMotorSpeed(speed, leftForwardPin, leftReversePin, leftThrottlePin); //die Pinnummern sind in Variablen gespeichert.

Das spart schon eine Menge Tipperei.

Aber eines stört immernoch: Jedes Mal, wenn wir die Geschwindigkeit ändern wollen, müssen wir die Pinnummern wissen!

Das ist besonders lästig, wenn die Funktionsaufrufe quer über das Programm verteilt sind, oder wir Programmmodule erstellen wollen, die auf beliebige Motoren zugreifen können sollen, ohne vorher zu wissen, welche dies sein werden. So könne es z.B. eine H-Brücke geben, die statt 3 nur 2 Pins als Eingänge (Richtung, PWM) hat. Wegen einer kleinen Hardwareänderung wie dieser an vielen Stellen Code ändern zu müssen, der sich mit ganz anderen Dingen (z.B. der Navigation in einem Labyrinth) beschäftigt, führt zu viel verschwendeter Zeit…

Ideal wäre also etwas, das 'nach außen' die Funktionen zur Verfügung stellt, die uns wirklich interessieren (Gas geben) und alle dazu nötigen technischen Details (irgendwelche Pins an- und ausschalten) 'intern' regelt.

Eine Lösung: Daten und Code im Paket: Klassen und Objekte

Die Sprache C++ und damit auch Arduino stellt dafür ein geeignetes Werzeug bereit: Klassen und Objekte.

Eine Klasse legt die gemeinsamen Eigenschaften einer Gruppe von Objekten fest. In unserem Fall also ist darin z.B. festgelegt, dass alle Motoren die Möglichkeit haben, unterscheidlich viel Gas ('throttle')zu geben. Im Programm sieht das dann z.B. so aus:

class Motor{
  public: //die Folgenden Eigenschaften und Methoden sind nach außen sichtbar:
  void setThrottle(int newThrottle); // jeder Motor hat eine Möglichkeit 'Gas zu geben'
}

Jetzt weiss der Compiler, dass es eine Klasse (class) mit dem Namen Motor gibt, die eine Funktion setThrottle bereitstellt, welche mit einer Zahl (int) als Argument aufgerufen wird und nichts (void) zurückgibt.

Klassen sind Datentypen

Die oben neu eingeführte Klasse 'Motor' können wir genauso verwenden, wie wir es mit anderen Datentypen (z.B. int, long, float) tun könnten. Wenn wir also z.B. zwei Motoren brauchen können wir folgendes schreiben:

Motor leftMotor; // deklariere ein Objekt vom Typ 'Motor' mit dem Namen 'leftMotor'
Motor rightMotor; // deklariere ein Objekt vom Typ 'Motor' mit dem Namen 'rightMotor'

Aufruf von Klasseneigenen Funktionen (Methoden)

Wenn wir jetzt z.B. auf der linken Seite Vollgas geben und die rechte Seite anhalten wollen, können wir das so schreiben:

//der Name des Objekts 'leftMotor' und die darin aufgerufene Funktion 'setThrottle' werden durch einen Punkt getrennt:
leftMotor.setThrottle(255); 
rightMotor.setThrottle(0);

Wie ihr seht, führt diese Syntax auch gleich zu einem gut lesbaren Programm, wenn die Objekte und derem Methoden sprechende Namen haben.

Klassen können andere Variablen beeinhalten, auf die dann von 'innen' zugegriffen werden kann.

Aber woher wissen jetzt die Objekte linkerMotor und rechterMotor, welche Pins sie an- und ausschalten wollen?

Die Klasse Motor ist bisher ja so abstrakt gehalten, dass sie darüber gar keine Informationen enthält! Eine Klasse, die wirklich etwas konkretes tun soll, braucht also Variablen, in denen sie die Pinnummern speichern kann und Code, der beschreibt, was zu tun ist.
Beides lässt sich ganz einfach in die Definition der Klasse hineinschreiben:

class MotoMamaMotor {
...
  private: // alles was folgt, ist nur für den internen Gebrauch durch die Klasse selbst:
  int forwardPin; //jedes Objekt vom Typ MotoMamaMotor hat seinen eigenen forwardPin
  int reversePin;
  int throttlePin;
...
};

Und wie kommen jetzt die Pinnummern in das Objekt hinein, wenn sie nicht von außen sichtbar sind?

Ganz einfach: Wir schreiben auch dafür eine setup Funktion in die Klasse1) :

class MotoMamaMotor{
  private:
...    
  public: // wir wollen, dass das 'setup' von außen zugänglich ist.
  void setup(int newForwardPin, int newReversePin, int newThrottlePin){
    // wir merken uns die Pins für die spätere Verwendung
    forwardPin=newForwardPin;
    reversePin=newReversePin;
    throttlePin=newThrottlePin;
 
    // und initialiseren auch gleich die Ausgänge !
    digitalWrite(throttlePin,LOW); // damit uns die Kiste nicht gleich losfährt...
 
    pinMode(forwardPin,OUTPUT);
    pinMode(reversePin,OUTPUT);
    pinMode(throttlePin,OUTPUT);
  }
...
};

Ein komplettes Beispielprogramm

Jetzt müssen wir nur noch die bereits oben beschriebene Funktion für das Einstellen der Geschwindigkeit in die Klasse einfügen und wir haben ein komplettes Beispiel:

// Definition einer Klasse zur Motoransteuerung
class MotoMamaMotor {
  private: // alles was folgt, ist nur für den internen Gebrauch durch die Klasse selbst:
  int forwardPin; //jedes Objekt vom Typ MotoMamaMotor hat seinen eigenen forwardPin
  int reversePin;
  int throttlePin;
 
  public: //alles was folgt, ist auch nach außen sichtbar
 
  //Diese Setup-Funktion muss aufgerufen werden, bevor die Motorsteuerung verwendet wird:
  void setup(int newForwardPin, int newReversePin, int newThrottlePin){
    // wir merken uns die Pins für die spätere Verwendung
    forwardPin=newForwardPin;
    reversePin=newReversePin;
    throttlePin=newThrottlePin;
 
    // und initialiseren auch gleich die Ausgänge
    digitalWrite(throttlePin,LOW); // damit uns die Kiste nicht gleich losfährt...
    pinMode(forwardPin,OUTPUT);
    pinMode(reversePin,OUTPUT);
    pinMode(throttlePin,OUTPUT);
  }
 
  //Mit dieser Fukntion lässt sich die Geschwindigkeit des Motors regeln
  void setThrottle(int newThrottle){
    if(newThrottle>0){ // sollen es vorwärts oder rückwärts gehen?
      //schalte auf vorwärts
      digitalWrite(reversePin,LOW);
      digitalWrite(forwardPin,HIGH);
    }else{
      //schalte auf vorwärts
      digitalWrite(forwardPin,LOW);
      digitalWrite(reversePin,HIGH);
    }
    //stelle die Geschwindigkeit ein:
    analogWrite(throttlePin, newThrottle);
  }
};
 
// Erzeuge zwei getrennte Objekte vom Typ MotoMamaMotor. Diese lassen sich wie ganz normale Variablen verwenden.
MotoMamaMotor leftMotor;
MotoMamaMotor rightMotor;
 
void setup(){
  leftMotor.setup(3,4,5);
  rightMotor.setup(6,7,8);
};
 
void loop(){
  // Fange rückwärts an und fahre langsam immer schneller vorwärts.
  for (int i =-255, i<=255;i++){
    leftMotor.setThrottle(i);
    rightMotor.setThrottle(i);
  };
 
  // Fange vorwärts an und fahre langsam immer schneller rückwärts.
  for (int i =255, i>=-255;i--){
    leftMotor.setThrottle(i);
    rightMotor.setThrottle(i);
  };
};

Um das Programm übersichtlich zu halten, können Klassen in eigene Dateien ausgelagert werden

Um eine Datei zu eurem Arduino-Programm hinzuzufügen, clickt ihr auf das Symbol mit dem Dreieck Pfeil am rechten Rand des Fensters und wählt „neuer Tab“.

Am unteren Rand des Fensters könnt Ihr dann einen Dateinamen eingeben. Dieser muss aus .h enden, damit es klappt ((Arduino ist bei der Unterstützung von allem, was etwas fortgeschrittener ist, ziemlich zurückhaltend…). In unserem Fall habe ich die Datei MotorControl.h genannt.

In die neu erstellte Datei kommt als erstes die Zeile

#include "Arduino.h" 

Sie sorgt dafür, dass in dieser Datei Arduino-spezifische Funktionen verwendet werden können. Danach könnt ihr den Code für eure Klasse einfügen.

Damit euer Programm den Code aus der neu angelegten Datei verwenden kann, muss dort widerum eine Zeile mit dem Inhalt

#include "MotorControl.h" 

an den Anfang.

Teil 2: Mehr Details zum Konzept

Klassen können aufeinander aufbauen (Vererbung)

Zu Beginn des Artikels hatten wir versprochen, dass es Möglich sein sollte, verschiedene Motortreibervarianten gegeneinander auszutauschen, ohne am Rest des Programmes etwas zu ändern.

Klassen können andere Objekte als Membervariablen enthalten

In C++ können Klassendeklarationen und der Code für die darin angekündigten Funktionen in unterschiedlichen Dateien liegen

Wer mehr wissen möchte, ließt am besten den Artikel über das Erstellen von Libraries in Arduino.



1) Eine schönere Alternative wäre es, das im Konstruktor zu erledigen. Leider initialisiert Arduino die Hardware erst nachdem die Konstruktoren global deklarierter Klassen aufgerufen wurden, so dass Konstruktoren, die auf Hardware zugreifen, dort Probleme verursachen - hier ein Forumseintrag, der sich mit diesem Problem beschäftigt.
techniken/objektorient.txt · Zuletzt geändert: 2016/01/21 12:45 (Externe Bearbeitung)