Forum: Mikrocontroller und Digitale Elektronik Funktionspointer als Parameter


von Alexander H. (ill_son)


Lesenswert?

Hallo,

folgende Frage:

Ich habe einen Kommunikationslayer, der direkt über der Uart angesiedelt 
ist. Nun kann es sein, dass Daten über die eine oder die andere Uart 
ankommen. Ich möchte beim Initialisieren des Kommunikationslayers die zu 
verwendenden Schreibe- u. Lesefunktion der jeweiligen Uart als Parameter 
mitgeben, weiß aber nicht so recht, wie die Parameter der 
Initialisierungsfunktion aussehen müssen.

Momentan ist es fest codiert:
1
void (*kiss_putc)(unsigned char) = &uarte_putc;
2
unsigned int (*kiss_getc)( void ) = &uarte_getc;
3
4
void kiss_init(  )
5
{
6
7
}

Wie muss der Kopf der Initialisierungsfunktion aussehen?

Grüße, Alex

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

1
void kiss_init(void (*put)(unsigned char), int (*get)(void)  )

Übersichtlicher wird es, wenn man für die Typen typedefs einführt.

Die "&" in den Initialisierungen sind überflüssig; das bloße Nennen 
eines Funktionsnamens (ohne runde Klammern danach) ergibt automatisch 
einen Zeiger auf die Funktion.

: Bearbeitet durch Moderator
von Alexander H. (ill_son)


Lesenswert?

Hallo Jörg,

danke für die schnelle Antowrt.

Ich würde es dann so machen:
1
void (*kiss_putc)(unsigned char);
2
unsigned int (*kiss_getc)( void );
3
4
5
void kiss_init( void (*put)(unsigned char), unsigned int(*get)( void ) )
6
{
7
  kiss_putc = put;
8
  kiss_getc = get;
9
}

Ist das korrekt?

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Achso, du willst das in globalen Variablen hinterlegen … ja, sollte 
klappen.

Das sind so Dinge, wo einem natürlich sofort C++ in den Sinn kommt: du 
legst dein Kommunikationsobjekt an, und im Konstruktor übergibst du dem 
Objekt die Zugriffsfunktionen. Sie werden in Objektattributen 
gespeichert und stehen dann später der Instanz zur Verfügung.

Wenn du ein zweites gleichartiges Objekt für eine andere UART brauchst, 
kannst du das dann völlig parallel einrichten. Das geht bei globalen 
Variablen nicht.

von Alexander H. (ill_son)


Lesenswert?

Da ich auch in C# programmiere, wäre objektorientiert natürlich super. 
Geht das unter gcc und Atmel Studio? Als ich meinen ersten Atmel 
programmiert habe, gab's das noch nicht, meine ich.

von devzero (Gast)


Lesenswert?

Ja, mit C++. GCC bringt dafuer g++ mit.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Alexander H. schrieb:
> Als ich meinen ersten Atmel programmiert habe, gab's das noch nicht,
> meine ich.

C++ an sich geht schon sehr lange, nur eine libstdc++ gibt es nicht.

von Einer K. (Gast)


Lesenswert?

Hier mal ein Vorschlag in C++11
1
using PutcPtr = void (*)(unsigned char);
2
using GetcPtr = unsigned int (*)(void);
3
4
void kiss_init(PutcPtr put, GetcPtr get)
5
{
6
  kiss_putc = put;
7
  kiss_getc = get;
8
}

von Oliver S. (oliverso)


Lesenswert?

Und hier mal einer in plain C.
1
typedef void (*PutcPtr)(unsigned char);
2
typedef unsigned int (*GetcPtr)(void);
3
4
void kiss_init(PutcPtr put, GetcPtr get)
5
{
6
  kiss_putc = put;
7
  kiss_getc = get;
8
}

Das kompiliert auch unter C++11, C++14, C++17, C++20, nur falls da 
jemand gesteigerten Wert drauf legen würde. Ist allerdings alleine 
deshalb weder sinnvoll noch erforderlich. Die Vorteile von einem in C++ 
möglichen Kommunikationsobjekt haben mit dem syntaktischen Zucker eines 
"using" nichts zu tun.

Oliver

von Einer K. (Gast)


Lesenswert?

Oliver S. schrieb:
> syntaktischen Zucker

> typedef void (*PutcPtr)(unsigned char);
Wenn hier jetzt void und unsigned char auch noch selbst definierte Typen 
wären, dann ist es (was wird denn hier überhaupt definiert?) schon 
schwieriger aufzulösen.

> using PutcPtr = void (*)(unsigned char);
Hier ist viel klarer/offensichtlicher, welches Token definiert wird.

Wenn es auch nur "Zucker" ist.
Auf dem Kuchen mag ich es.

von Alexander H. (ill_son)


Lesenswert?

Danke für die Anregungen. Momentan gibt es nur einen Layer, der entweder 
von der einen oder der anderen Uart Daten bekommt und es steht auch 
schon viel im aktuellen Projekt. Aber beim Nächsten könnte man sich mit 
C++ mal beschäftigen.

Grüße, Alex

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Arduino Fanboy D. schrieb:
> Hier mal ein Vorschlag in C++11

Das ist kein C++, sondern C mit einer anderen Syntax ("using" statt 
"typedef").

Wenn schon C++, dann wie oben skizziert, keine globalen Variablen mehr, 
sondern Objektattribute, die vom Konstruktor intiialisiert werden (und 
danach unveränderlich sind).

von Einer K. (Gast)


Lesenswert?

Jörg W. schrieb:
> sondern C mit einer anderen Syntax
Ein Birnenbaum ist ein Apfelbaum mit anderen Früchten dran.
Nee, oder?

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Arduino Fanboy D. schrieb:
> Ein Birnenbaum ist ein Apfelbaum mit anderen Früchten dran.

Du hast dem Apfelbaum allerdings nur einen Birnbaumanstrich gegeben. Er 
produziert weiterhin fleißig Äpfel.

Der wesentliche Punkt von C++ ist nicht, dass es eine andere (erweiterte 
– "typedef" würde ja trotzdem noch genauso funktionieren) Syntax als C 
hat, sondern dass man damit objektorientiert programmieren kann. Wenn 
man das nicht vorhat, dann kann man auch gleich bei C bleiben, dann gibt 
es nur wenige Gründe für C++. (Echte Konstanten wären vielleicht noch 
einer, denn die kann man bpsw. zur Festlegung von Array-Dimensionen 
benutzen, was in C nicht geht.) Ob ich nun "using" oder "typedef" 
schreibe, spielt doch ansonsten wirklich in diesem Zusammenhang 
überhaupt keine Geige.

: Bearbeitet durch Moderator
von Einer K. (Gast)


Lesenswert?

Jörg W. schrieb:
> sondern dass man damit objektorientiert programmieren kann.
;-) Danke, für diese wichtige Information. ;-)
Die Aussage erscheint mir viel sinnvoller und logischer als:
> Das ist kein C++, sondern C mit einer anderen Syntax

C++ ist eine Multiparadigmen Sprache.
(ist das das richtige Wort?)
> strukturiert, generisch, imperativ,
> prozedural, objektorientiert, funktional
(habe ich noch was vergessen?)

Soviel, zu Äpfeln, Kirschen und Birnen.
Natürlich darfst du das prozedurale Paradigma dem C++ absprechen, oder 
C++ auf OOP reduzieren....
Aber Sinn macht das nicht.

von Bernd K. (prof7bit)


Lesenswert?

Wenn es von beiden immer nur eine Instanz gibt und das sich zur 
Laufzeit nicht ändert dann linke ich solche Sachen statisch.

Zum Beispiel erwartet meine plattformunabhängige iolink.c daß irgendwo 
eine handvoll Funktionen iolink_uart_****() existieren die es aufrufen 
kann, sie kennt aber nur deren Prototypen, implementieren tut sie sie 
selber nicht.

Je nach verwendetem Controller, UART oder IO-Link-Transceiver linke ich 
dann einen von meinen hardwarespezifischen Treibern iolink-uart-xxxx.c 
hinzu und diese implementieren jeweils genau diese Funktionen. Der 
Linker fügt es automatisch zusammen.

Umgekehrt rufen alle meine iolink-uart-xxxx.c Dateien alle eine Handvoll 
dort nicht existierender iolink_uart_hook_****() auf (sie kennen auch 
nur die Prototypen) aber da gleichzeitig immer auch die iolink.c 
mitgelinkt wird in der diese hooks alle implementiert sind kann der 
Linker das auflösen.

Für Hook-Funktionen deren Verwendung optional ist baue ich zusätzlich 
noch eine WEAK Implementation ein die nichts tut und wenn der Anwender 
beschließt diese Hook-Funktion zu implementieren dann wird eben diese 
gelinkt anstelle des weak defaults, in etwa so wie bei den 
Interrupt-Handlern.

Die Prototypen für diese "externen" Funktionen kommen alle in den Header 
des Moduls das sie aufrufen will ganz unten gesondert dokumentiert mit 
dem Hinweis daß ein anderes Modul diese implementieren muss oder 
implementieren kann. So lässt sich jedes Modul getrennt für sich immer 
fehlerfrei kompilieren.

Wenn man sich bei den Namensgebung solcher Funktionen an ein striktes 
Schema hält sieht man auf den ersten Blick wer die Funktion aufruft 
und/oder in welcher Datei sie implementiert ist.

Die meisten Sachen kann ich so erschlagen. So hab ich zum Beispiel einen 
USB-Treiber der würde eine usb_hook_led(bool on) aufrufen wenn irgendwo 
im Projekt eine solche existiert und wenn nicht dann blinkt eben nichts 
und der Compiler generiert keine einzige Instruktion. Der gcc kann sowas 
hervorragend über Modulgrenzen hinweg(!) inlinen und optimieren, also 
kann man großzügig mit sowas umgehen bei null Laufzeitoverhead und man 
erspart sich einen Haufen Boilerplate, hässliche 
Funktionszeiger-Typdefs, Überprüfung und Behandlung von Nullpointern und 
sonstiges mühselige Geschreibsel.

Nur wenn es die Natur der Sache erfordert daß man mehrere Instanzen der 
selben Klasse gleichzeitig haben könnte muss man zur Laufzeit binden.

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Bernd K. schrieb:
> Wenn es von beiden immer nur eine Instanz gibt und das sich zur
> Laufzeit nicht ändert dann linke ich solche Sachen statisch.
>
> Zum Beispiel erwartet meine plattformunabhängige iolink.c daß irgendwo
> eine handvoll Funktionen iolink_uart_****() existieren die es aufrufen
> kann, sie kennt aber nur deren Prototypen, implementieren tut sie sie
> selber nicht.
>
> Je nach verwendetem Controller, UART oder IO-Link-Transceiver linke ich
> dann einen von meinen hardwarespezifischen Treibern iolink-uart-xxxx.c
> hinzu und diese implementieren jeweils genau diese Funktionen. Der
> Linker fügt es automatisch zusammen.

Das kann auch optimierungstechnisch besser sein als indirekte Aufrufe 
via Funktionszeiger:  Wenn mit LTO o.a. optimiert wird, ermöglicht das 
Inlining über Modulgrenzen hinweg.

Mit Funktionszeigern ist diese Optimierung i.d.R. nicht möglich, weil 
der Wert so eines des Zeigers wesentlich schwerer zu eruieren ist als 
ein Symbol.

Allerdings könnte weak hier dem Compiler / LTO reingrätschen und solche 
Optimierungen verhindern, müsste man sich mal genau anschauen.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.