Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

techniken:objektorient

Dies ist eine alte Version des Dokuments!




Übersichtlichkeit und wiederverwendbare Module durch objektorientiertes Programmieren

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.

Darum geht es hier.

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 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:

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);
  }
};
 
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

Klassen können aufeinander aufbauen (Vererbung)



Klassen und Objekte

Auch bei Arduino kann man objektorientiert programmieren. Hier ist ein guter Link, der alles sehr schön beschreibt. Auch bei diesem Link könnt ihr nachsehen. Dort ist ein Beispiel für eine Klasse. Hinter diesem Link könnt ihr auch noch einmal eine ausführlichere Beschreibung finden.

Wichtig dabei ist, dass es eine Header Datei gibt, die wie eure Klasse heißt und „.h“ als Ende hat. Dann könnt ihr (auch in der gleichen Datei) eure Funktionen deklarieren mit:

void/int/... Klassenname::Funktionsname(Parameterliste)
{
    was die Funktion tun soll
}

Auch muss eure Datei einen sogenannten Konstruktor haben:

Klassenname::Klassenname(Parameterliste zur Instanziierung)
{
    Werte setzen, die zum Erstellen von späteren Objekten wichtig sind
}

Wenn ihr dann ein Objekt von dieser Klasse erstellen wollt, könnt ihr diese mit:

#include <Klassenname.h>

am Anfang eures Programmes und dann:

Klassenname Objektname (Parameter);

erstellen. Mit

Objektname.funktionsaufruf
Objektname.variable

könnt ihr dann auf Funktionen im Objekt oder auf Variablen zugreifen.

Wer mehr wissen möchte, ließt am besten den Artikel.

1) Eine svchöne 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 Vorumseintrag, der sich mit diesem Problem beschäftigt.
techniken/objektorient.1397240490.txt.gz · Zuletzt geändert: 2016/01/21 12:45 (Externe Bearbeitung)