Function pointer to member functions

Erstmal hallo zusammen.

Ich bin hier im Forum neu, deshalb einmal kurz ein bisschen was vorne weg zu meinem Wissensstand.
Ich komme eigentlich aus der Welt der PIC-Mikrocontroller und habe mich dort hauptsächlich mit C als Programmiersprache rumgeschlagen.
Ich versuche nun so langsam (auch ein bisschen Getrieben es vernünftig zu machen) mich mit dem Objektorientierten zu beschäftigen. Ich muss dazu sagen, ich bin Maschinenbauer, habe also kein Informatikstudium, von daher die Bitte um etwas Nachsicht, falls ich bestimmte Zusammenhänge nicht ganz so schnell erfassen kann.

Nun zu meinem Problem. Vielleicht erstmal der Code:
header:

#ifndef __STEPPER__
#define __STEPPER__

#include <Arduino.h>

class STEPPER{
  private:
    byte stepPin;
    byte dirPin;
    byte enablePin;
    long stepsPerRevolutiion;
	long oldRoundsPerMinute = 0;
	int edgeState;
	
	void motorStep(void);

  public:
    STEPPER (byte stepPin, byte dirPin, long stepsPerRevolutiion, byte enablePin);
	
    void setMotorSpeed(long roundsPerMinute);
    void enableMotor(void);
    void disableMotor(void);
	void setMotorDir(byte direction);
	void setMotorSpeedInterrupt(long roundsPerMinute);

};

#endif

cpp:

#include "stepperMotor.h"
#include "TimerOne.h"

STEPPER::STEPPER(byte stepPin, byte dirPin, long stepsPerRevolutiion, byte enablePin){
	this -> stepPin = stepPin;
	this -> dirPin = dirPin;
	this -> enablePin = enablePin;
	this -> stepsPerRevolutiion = stepsPerRevolutiion;
	pinMode(this -> stepPin, OUTPUT);
	pinMode(this -> dirPin, OUTPUT);
	pinMode(this -> enablePin, OUTPUT);
}

void STEPPER::setMotorSpeed(long roundsPerMinute){
	long frequency = roundsPerMinute * this -> stepsPerRevolutiion / 60;
	tone(stepPin, frequency);
}

void STEPPER::setMotorSpeedInterrupt(long roundsPerMinute){
	long frequency = roundsPerMinute * this -> stepsPerRevolutiion / 60;
	long interruptTime = 1000000 / (2 * frequency);		//divide by 2 for generating raising and falling edge of one motorstep
	Timer1.initialize(interruptTime);
	Timer1.attachInterrupt(motorStep);
}

void STEPPER::motorStep(void){
	if (edgeState == LOW) {
		edgeState = HIGH;
	} else {
		edgeState = LOW;
	}
  digitalWrite(stepPin, edgeState);
}

void STEPPER::enableMotor(void){
	digitalWrite (dirPin, LOW);
}

void STEPPER::disableMotor(void){
	digitalWrite (dirPin, HIGH);
}

void STEPPER::setMotorDir(byte direction){
	digitalWrite (dirPin, direction);
}

Das Problem ist, dass der Aufruf der Methode motorStep im Rahmen des Timer1 nicht funktioniert.
WAs ich bis jetzt rausbekommen habe ist, dass das Problem wohl darin liegt, dass ein Zeiger auf eine Methode als ein Zeiger auf eine Funktion. Ich habe auch schon versucht mich in dieses Thema einzulesen, ich bekomme es aber einfach nicht hin die Lösungsvorschläge umzusetzen.
Vielleicht kann mir das ja mal jemand an diesem Beispiel erklären, was hier zu tun ist. Super wäre es natürlich wenn es dann auch noch eine Erklärung dazu gibt, was gemacht wurde und warum.
Vielen Dank und schöne Grüße

Wie viele Instanzen der STEPPER Klasse willst du erzeugen?

Wenn mehr als eine (ein 3D Drucker braucht min 4) dann wirst du ALLE diese Instanzen über den Timer Interrupt informieren müssen. Erfinde ein Benachrichtigungssystem.

Also erstmal geht es mir um das Verständnis. Von darher wird es erstmal genau eine Instanz geben. Aber danke für den Tipp. Ich würde diese Problematik, aber dann später angehen.

Warte mal etwas. Ich habe eine Lösung dafür. Ist eigentlich relativ einfach wenn man es mal verstanden hat.

Wobei ich das Konzept nochmal überdenken würde. Es gibt fertige Stepper Bibliotheken dafür. Das muss man nicht selbst implementieren. Auch ist wie gesagt die Idee mit der Verwendung eines Timers innerhalb er Klasse nicht unbedingt gut.

WAs ich bis jetzt rausbekommen habe ist, dass das Problem wohl darin liegt, dass ein Zeiger auf eine Methode als ein Zeiger auf eine Funktion.

Ja. Wenn du das schon mal erkannt hast, gut. Und was ist der Unterschied zwischen Funktionen und Methoden? Der versteckte this-Zeiger auf das aktuelle Objekt. Die Signatur der Funktionen/Methoden ist also unterschiedlich

Es gibt (auf den AVR Arduinos) zwei Möglichkeiten das zu lösen: 1.) Eine Wrapper Funktion die einen globalen Zeiger auf das Objekt verwaltet und über den man die Methode aufrufen kann 2.) Eine anonyme Lambda-Funktion (dank C++11). Dadurch kann der Zeiger lokal sein

Letzteres ist das aller schönste:

//Test Klasse nur um das Prinzip zu zeigen
class Timer
{
public:
  typedef void(*functionPointer)();

  void setFunction(functionPointer func)
  {
    this->func = func;
  }

  void tick()
  {
    if (func)
      func();
  }
private:
  functionPointer func;
};

class TestClass
{
public:
  //im Konstruktor damit es direkt aufgerufen wird. Geht natürlich auch in einer Methode!!
  TestClass()
  {
    static TestClass* obj = this;   //Zeiger auf aktuelles Objekt. Muss static sein, damit die Variable keinem Objekt zugerordnet ist

    //Lambda Funktion. Ruft die Setter Funktion auf und übergibt die aufzurufende Methode
    timer.setFunction(
      []() {
        obj->doSomething();
      }
    );

    //Funktion einmal per Hand aufrufen damit was sieht. Auch das geschieht mit dem richtigen Timer natürlich automatisch
    timer.tick();
  }

  void doSomething()
  {
    Serial.println("Do Something");
  }

private:
  Timer timer;
};

void setup()
{
  Serial.begin(9600);

  TestClass test;
}

void loop()
{
}

Die Timer Klasse ist eine primitive Test Klasse. Sie hat einen Funktionszeiger wie der echte Timer und eine Funktion um diesen zu setzen. Die tick() Methode existiert nur damit man per Hand einen Timer Tick auslösen kann. Darin wird dann der Funktionszeiger aufgerufen.

In der anderen Test Klasse habe ich die Logik in den Konstruktor gepackt damit es einfacher wird. Das geht natürlich auch in einer Methode

Die Magie findet hier statt:

    static TestClass* obj = this; 

    timer.setFunction(
      []() {
        obj->doSomething();
      }
    );

Hier wird erst mal ein Zeiger auf das aktuelle Objekt in eine statische Variable geschrieben. Diese ist keinem Objekt zugeordnet. Dann erstellt man eine Lambda Funktion die man an die Setter-Methode des Timers übergibt. Diese mag keinen this-Zeiger, was man eben durch die Verwendung des statischen Zeigers umgangen hat.

Mit "normalen" Mitteln würde das so nicht gehen. Man bräuchte eine extra Methode (der Wrapper aus Punkt 1), um diesen Aufruf zu tätigen.

Lambda Funktionen sind eng mit Funktionsobjekten verwandt. Im Hintergrund wird da automatisch ein Funktionsobjekt erstellt. Und Lambdas mit leerer Capture- und Input-Liste ( gibt an dass die Funktion keinen Zugriff auf Variablen des Objekts hat und keine Parameter hat ) sind automatisch mit klassischen C Funktionszeigern kompatibel. Deshalb schluckt die Setter-Methode das einfach so.

timer.Tick() wird danach einmal per Hand aufgerufen, nur damit was sieht! Das muss da nicht hin!!

Die Timer Klasse verwaltet halt genau einen Funktionszeiger. Auch das ist der Grund weshalb das so nicht mit mehreren Instanzen geht. Er zeigt auf die Methode eines Objekts und kann dann nicht die Methode eines anderen Objekts aufrufen.

In vollständigem C++ gibt es dafür std::function was das ganze nochmal ein ganzes Stück einfacher macht. Dadurch braucht man diesen Workaround nicht. Aber auf dem AVR gibt es die STL nicht. Auf dem ESP8266 geht das übrigens!

Sh4rkY: Aber danke für den Tipp.

Gerne.

Sh4rkY: Ich würde diese Problematik, aber dann später angehen.

Wenn man von vornherein eine Klasse als Singleton konzipiert, dann handelt man sich meist mehr Probleme ein, als man hinterher bereit ist, zu beheben. Das Singelton Dasein ist die Ausnahme, welche für eine konkrete Anwendung angemessen sein mag, aber gerade Stepper, die bilden gerne Rudel. Der Timer1 ist eine begrenzte Ressource, das mag Sinn machen, ihn als Singleton auszubilden. Aber andere AVRs haben durchaus mehrere 16Bit Timer.

Tipp: Es ist meist sinnvoller, sofort das richtige zu lernen, als hinterher erst das falsche ausradieren zu müssen, und danach dann doch noch das richtige oben drauf zu lernen. Ins Besondere, wo das mit dem "ausradieren" sowieso nicht wirklich klappt.

Siehe dazu:

Es besteht die große Gefahr, durch exzessive Verwendung von Singletons quasi ein Äquivalent zu globalen Variablen zu implementieren und damit dann prozedural anstatt objektorientiert zu programmieren

Aus Wikipedia: Singleton_(Entwurfsmuster)

Und, von der prozeduralen C Programmierung willst du ja weg. Also solltest du es vermeiden, diese "Denke" ins C++ ein zu schleppen. :o :o :o


was man eben durch die Verwendung des statischen Zeigers umgangen hat.

Danke.

Oder wenn man es praktischer sehen will: wenn das ganze später mit mehreren Instanzen laufen soll, bringt es nichts das jetzt mit einer Instanz zu implementieren. Vor allem weil man eben eine völlig andere Vorgehensweise braucht. Selbst wenn man jedem Motor einen Timer spendiert (auf dem Mega ginge das mit 1, 3, 4 und 5), hat man dann keine externen Timer-Bibliotheken, sondern programmiert die Timer per Hand. Wobei das sehr wahrscheinlich gar nicht nötig ist. Ein Timer sollte ausreichen. Aber die Logik muss anders sein als einfach eine vorgegebene Methode aufzurufen

@Serenifly Vielen Lieben Dank für die ausführliche Antwort. Ich werde mir das nachher nochmal versuchen genau im Detail zu veranschaulichen und nachzuvollziehen.

Mir fehlt jetzt gerade leider ein bisschen die Zeit. Es werden aber mit Sicherheit noch fragen zu diesem Thema folgen ;-)

Das ganze soll aktuell aber erstmal mit nur einer Instanz laufen, da eigentlich angedacht ist eine Servosteuerung zu implementieren, die genauen einen Motor pro board treibt.

@combie ja ich gebe dir Recht. Ich werde mich damit auch noch im Detail auseinandersetzen. Mir hilft es nur meist die Sachen zu verstehen, an denen ich gerade scheitere. So ein Problem kommt mit Sicherheit nochmal vor.

Noch eine kleine Schönheitssache: Klassen-Namen fangen mit Großbuchstaben an, aber werden nicht komplett groß geschrieben. Das verwendet man normal für Konstanten (im Gegensatz zu Variablen).

Nachtrag: Um zu sehen wie ein Timer mehrere Objekte verwalten kann,solltest du dir mal die Servo Library anschauen. Da werden die Informationen (nur der verwendeter Pin und eine Zeit) in einem statischen Array gespeichert. Damit ist das Array für alle Objekte gleich. Dann iteriert die ISR über das Array um die Informationen für das aktuelle Objekt zu finden. Ist allerdings etwas verwirrend, da der Code recht allgemein ist und darauf ausgelegt ist optional andere Timer zu verwenden

Ich weiß aber nicht ob das auch hier was taugt. Es gibt natürlich noch andere Optionen, wie den Timer außerhalb der Klasse zu haben und einfach ab und zu nachschauen ob was zu tun ist.

Mir scheint, im Moment versuchst du deine bekannte C Denke in C++ zu gießen. Aber OOP, kommt dabei wohl nicht raus. Irgendwann wirst du deine Denke umstellen müssen. Sonst wirst du mit C++ nicht glücklich. Und meine Meinung ist: Je früher, desto besser.

Nunja, ich kann dich nicht hindern den schweren Weg zu gehen. Aber solange du in die falsche Richtung läufst, kann ich dich da auch nicht unterstützen.

@serenifly vielen Dank für den Tipp, werde ich mal reinschauen.

@combie ja du hast recht. Ich tue mich halt noch echt schwer mit dem OOP. Ich war ja schon froh als ich die normale C-Denke drin hatte. Da tut man sich irgendwann echt schwer mit der Umstellung.

Werde ich wohl aber direkt angehen. Dann ist die Klasse wenigstens auch später noch in anderen Projekten ohne Probleme einsetzbar.