Dies ist eine alte Version des Dokuments!
Schon bald nach Beginn der Programmierarbeiten an eurem Roboter werden euch einige typische Probleme begegnen:
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.
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.
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.
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'
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.
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); } ... };
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 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.