mikrocontroller.net

Forum: Compiler & IDEs C: Verschiedene Varianten/Build options aber 1 header


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von JK (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Hallo zusammen,

ich versuche gerade, etwas Struktur in meinen Code und Buildprozess zu 
bekommen. Dafür möchte ich allgemeine Funktionen von 
Versionsspezifischen trennen.

Allgemeine Funktionen sind z.B. UART oder ADC Treiber. Diese können 
"instantiiert" werden (wir sind in C, aber es gibt struct Zeiger, die 
als "this" Zeiger verwendet werden).

Die Pins der UART bswp. unterscheiden sich aber, ebenso Versionsnummern 
oder Identifikationsstrings, Clock Inits, einige defines usw. Das sind 
versionsspezifische oder von mir aus auch produktspezifische 
Einstellungen.

Ich verwende momentan Keil uVision und habe da mehrere Build options 
angelegt. Diese unterscheiden sich teilweise durch ein/ausgeschaltete 
Features per Build defines und der verwendeten MCU (STM32F103 und 
STM32F100).

Mein Fernziel ist, einen IDE unabhängigen Build Prozess zu erstellen, 
aber bis dahin ist noch ein weiter Weg.

Erstmal geht es um eine ganz konkrete Frage/Problem. Ich möchte eine Art 
Interface erstellen, aber mit Implementation. Nur die Daten kommen aus 
einer versionsabhängigen C Datei.

Konkrete Ausgangslage:
// "Interface" maintenance.h
uint32_t GetDeviceId();
void     GetDeviceName(const char **str, size_t *len);
void     ConfigureClocks();
// "Implementation" maintenance_configuration1.c
#include "maintenance.h"

// configuration/product specific settings
const uint32_t deviceId = 0x1234;
const char     deviceName[] = "myDevicename1";

// function implementation
uint32_t GetDeviceId()
{
    return deviceId;
}

void GetDeviceName(const char **str, size_t *len)
{
    *str = deviceName;
    *len = strlen(deviceName);
}

void ConfigureClocks()
{
    // board/configuration specific clocks
}

Dann gibt es noch weitere "maintenance_configuration2.c" und ein paar 
mehr.
Durch die build options der IDE wird im Moment sichergestellt, dass 
lediglich eine der configurations files compiliert und gelinkt wird.

Ich möchte nun allgemeine Funktionen, wie z.B. GetDeviceName (die 
Funktion ist in Wirklichkeit etwas komplizierter) bereits im "Interface" 
implementieren, aber Daten aus der c. Datei verwenden. Also andersrum 
als sonst üblich.

Momentan funktioniert das, indem ich die Funktion als inline im header 
implementiere und die Daten per extern deklariere:
// maintenance.h
extern const char deviceName[];

__inline void GetDeviceName(str, len)
{
    *str = deviceName;
    *len = strlen(deviceName);
}
// maintenanc_configuration1.c
#include maintenance.h

const char     deviceName[] = "myDevicename1";

extern void GetDeviceName(str, len)

Inline deswegen, weil ich hoffe, dass damit die konstanten Werte zur 
Compilezeit ersetzt werden und mir das gleichzeitig die mehrfache 
identische (!) Implementation in den .c Dateien erspare.
Nachteil ist, dass durch das extern eigentlich die Daten jetzt eben auch 
external linkage haben, man aber nicht direkt darauf zugreifen soll.

Kann man das so machen? Ist das sinnvoll? Wie würde man das sonst 
machen?

Meine Ziele
- weg von großen ifdef Orgien
- spezifizieren der configuration durch hinzulinken der entsprechenden c 
file (oder lauer ich hier dem Trugschluss auf, dass das später die 
einfachste Variante in einer makefile ist?)
- keine Mehrfachimplementierung identischer Funktionalität, nur weil 
sich Daten ändern
- effiziente Implementierung, z.B. soll bei den Konstanten zur 
Compilezeit einfach der Wert ersetzt werden.

Vielen Dank für jeglichen Input!

von A. S. (achs)


Bewertung
0 lesenswert
nicht lesenswert
Jede .c wird für sich compiliert. Daten in einer .c ersetzen nichts in 
einer anderen .c.

Wenn Du in my.c XYZ durch "device 7" ersetzt haben möchtest, dann 
entweder in einer Header oder im makefile (was aber nicht über 1 
hinausgehen sollte).

Du kannst z.b. je Plattform ein anderes Header Verzeichnis einbinden 
oder je Plattform ein #define oder ein #if verwenden. Entsprechend 
hierarchisch kannst Du mit 1 insgesamt (je Plattform) für alle sourcen 
auskommen, wenn das Dein Ziel ist. #if-Orgien müssen nicht sein, sind 
aber manchmal lesbarer als Zwiebelschichten und indirekt.

: Bearbeitet durch User
von Eric B. (beric)


Bewertung
0 lesenswert
nicht lesenswert
Wenn's nur 1 Device im Projekt gibt zur "Maintenance" könnte man die 
Konfiguration, inklusive Code, in eine Header-Datei auslagern und per 
command-line define includieren

maint_if.h
#ifndef MAINT_IF_H
#define MAINT_IF_H
uint32_t maint_getDeviceId();
void     maint_getDeviceName();
void     maint_configClocks(); 
#endif
main_config1.h
#define DEVICE_ID 0x12345678
#define DEVICE_NAME "myDevice1"
#define DEVICE_CONFIGCLOCK { \
  /*insert clock configuration code here */ \
  }
maintenance.c
#include maint_if.h
#include MAINT_CONFIG
inline uint32_t maint_getDeviceId() {
  return DEVICE_ID;
}

inline char *maint_getDeviceName () {
  return DEVICE_NAME;
}

void maint_configClocks() {
  DEVICE_CONFIGCLOCK
}
compile:
gcc -o maintenance.o -DMAINT_CONFIG="""main_config1.h""" maintenance.c
(ja, dreifach " auf der Windows cmd-line)

von JK (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Vielen Dank für deine Antwort.

Mir ist bewusst, dass jede translation Unit erstmal für sich alleine 
steht. Daher ja die getter.

Die Grundidee ist: ein generisches config Interface, als zentrale Datei. 
Alle darin enthaltenen Funktionen müssen implementiert werden. 
Identische Funktionen können und sollten eben dort aber auch schon 
implementiert sein. Daten kommen aus der “Spezialisierung”, die bedingt 
kompiliert werden.

Deswegen die Idee mit dem Header und der inline Funktionen.

Die Hoffnung war, dass durch lto und co kein overhead entsteht. Genauso, 
als würde ich einfach mit globalen defines arbeiten, z.B. Für den 
deviceName.

von Eric B. (beric)


Bewertung
0 lesenswert
nicht lesenswert
Bei mehrere gleichzeigie Instanzen würde ich den Ansatz ändern:

maint_if.h
#ifndef MAINT_IF_H
#define MAINT_IF_H

typedef uint mainthndl;
mainthndl maint_defineDevice(uint32 id, char *name, void (*clockcfg)(void));  
uint32_t maint_getDeviceId(mainthndl dev);
void     maint_getDeviceName(mainthndl dev);
void     maint_configClocks(mainthndl dev); 
#endif
maintenance.c
#include maint_if.h

typedef struct {
  uint32 id;
  char *name;
  void (*clockcfg)(void);
} maint_s;

maint_s devices[MAX_NR_OF_DEVICES];

mainthndl maint_defineDevice(uint32 id, char *name, void (*clockcfg)(void)) {
  // search an empty entry in the 'devices' array
  devices[idx].id = id;
  devices[idx].name = name;
  devices[idx].clockcfg = clockcfg;
  return (mainthndl) idx;
}

// Attention! Sanity checks missing!!
uint32_t maint_getDeviceId(mainthndl dev) {
  return devices[dev].id;
}

char *maint_getDeviceName (mainthndl dev) {
  return devices[dev].name;
}

void maint_configClocks(mainthndl dev) {
  devices[dev].clockcfg();
}
device1.h
#ifndef DEVICE1_H
#define DEVICE1_H
extern void dev1_init();
#endif
device1.c
#include maint_if.h

mainthndl self;

static void dev1_clock_config();

void dev1_init() {
  self = maint_defineDevice(
    0x12345678,
    "myDevice1",
    dev1_clock_config);
}

static void device1_clock_config()
{
  // device specific clock configuration code
}

von JK (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Hi,

vielen Dank für den Code. Ich habe aber das Gefühl, dass es eher 
komplexer als einfacher wird.

Es gibt nur ein config zur Compilezeit. Im obigen Fall ein device. Die 
configs sind alles Konstanten, daher würde ich gerne zur Laufzeit 
erstellte structs o.ä. vermeiden, falls diese nicht wegoptimiert werden.

Oder hab ich was missverstanden?

von A. S. (achs)


Bewertung
0 lesenswert
nicht lesenswert
JK schrieb:
> Genauso, als würde ich einfach mit globalen defines arbeiten, z.B. Für
> den deviceName.

Hast Du Dich schon Mal mit Funktionspointer beschäftigt? Einen Großteil 
Deiner Aufgaben lösen die (fast) ohne Overhead und vor allem mit 
optionaler Implementierung (also kommt z.b. "No device" zurück, wenn das 
Modul den getter noch nicht implementiert hat.

von JK (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Ja, aber aber die sind noch schwieriger zu optimieren und zu inlinen. 
Die oben genannte deviceId und der deviceName werden häufig in anderen 
Modulen benötigt und sind Konstanten. Also wäre der ideale Fall, dass 
sie auch zur Compilezeit als solche ersetzt werden.

Ich setze aber ein Testprojekt auf.

von DPA (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Ich verstehe ja nicht, warum man einerseitz schön dynamisch configs in 
.c Dateien zur link- & run-time configuration haben will, aber 
andererseits alles möglichst inlinen und und absurd zur compiletime 
vor-optimieren will. Entweder man geht den Weg, oder eben halt nicht, 
man kann nicht beides haben.

JK schrieb:
> spezifizieren der configuration durch hinzulinken der entsprechenden c
> file (oder lauer ich hier dem Trugschluss auf, dass das später die
> einfachste Variante in einer makefile ist?)

Ich denke, ob du ne Liste mit Files, oder ne Liste mit Optionen änderst, 
ist make ziemlich egal. Und wie man die Konfiguration dann umsetzt und 
organisiert, da sind der Fantasie auch keine Grenzen gesetzt. Die Frage 
ist eher, wie willst du, dass die Konfiguration nachher aussieht.

Linux und U-Boot verwenden Kconfig, aber da gibt es nicht wirklich eine 
offizielles standalone variante davon, und es ist GPL (wie funktioniert 
das Lizenztechnisch überhaupt bei U-Boot?).

Man könnte auch makefiles für diverse Componenten machen, eins welches 
die Komponenten für eine Version in ner variable auflistet, und dann 
eines das dann die makefiles der restlichen komponenten einbindet, und 
weiss, wie man die baut. etc.

Oder man macht was anderes. Überleg die nicht, wie du das umsetzen 
willst, überleg dir, wie das nachher verwendet werden soll, und überleg 
dir danach, wie man da das umsetzen kann.

von Eric B. (beric)


Bewertung
0 lesenswert
nicht lesenswert
JK schrieb:
> Es gibt nur ein config zur Compilezeit.

Dann passt die erste Variante: Je Device braucht du nur die DEFINEs im 
maint_configX.h zu setzen und dem Kompiler über die -D Option 
mitzuteilen welche Headerdatei includiert werden soll.

von Eric B. (beric)


Bewertung
0 lesenswert
nicht lesenswert
Eric B. schrieb:
> #include MAINT_CONFIG
...
> compile:gcc -o maintenance.o -DMAINT_CONFIG="""main_config1.h"""
> maintenance.c(ja, dreifach " auf der Windows cmd-line)

Oder
#define xstr(s) str(s)
#define str(s) #s
#include xstr(MAINT_CONFIG)
und dann geht das compilieren mit -DMAINT_CONFIG=main_config1.h, ohne 
allen Gänsefüsschen.

von A. S. (achs)


Bewertung
0 lesenswert
nicht lesenswert
Vielleicht machst Du mal ein Beispiel, wo Du Probleme siehst. Ansonsten 
stimme ich DPA zu

DPA schrieb:
> Ich denke, ob du ne Liste mit Files, oder ne Liste mit Optionen änderst,
> ist make ziemlich egal. Und wie man die Konfiguration dann umsetzt und
> organisiert, da sind der Fantasie auch keine Grenzen gesetzt. Die Frage
> ist eher, wie willst du, dass die Konfiguration nachher aussieht.

Direkt straight wäre je Plattform eine Header (und ggf. ein C-File) und 
gut ist. C können C-Programmierer und IDEs lesen, Dein Make nicht 
unbedingt, wenn es abgefahren wird.

Ob Du nun alle Header einbindest und den sowieso vorhanden include-Guard 
erweiterst, oder ob Du in einer Header die richtige per #ifdef wählst 
oder ob Du die richtige Einkommentierst, ist am Ende Egal. Auch der 
Trick mit der Ersetzung geht, aber da verwirrst Du schon den ersten 
Maintainer, wenn er die include-Datei gar nicht findet im Quelltext.

 Irgendwo musst Du die 1-aus-n Auswahl treffen, die Du builden willst, 
dass kann gerne im Makefile sein.

von Oliver S. (oliverso)


Bewertung
0 lesenswert
nicht lesenswert
DPA schrieb:
> Ich verstehe ja nicht, warum man einerseitz schön dynamisch
> configs in
> .c Dateien zur link- & run-time configuration haben will, aber
> andererseits alles möglichst inlinen und und absurd zur compiletime
> vor-optimieren will. Entweder man geht den Weg, oder eben halt nicht,
> man kann nicht beides haben.

Da es um „compiletime-dynamische“ Daten geht, kann und will man das.

Oliver

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.