Forum: Mikrocontroller und Digitale Elektronik C Best Practice für konstante globale Variablen


von Vincent W. (vince6e74)


Lesenswert?

Hallo,

gibt es ein Best Practice in C für konstante Variablen bei 
Microcontroller? Als simples Beispiel definieren welcher Sensor an 
welchen ADC Eingang hängt.

An Möglichkeiten die mir so eingefallen sind:
* Eine Variable in file1.c zu definieren und initialisieren und in 
file2.c mit 'extern' deklarieren,
* mit 'static const' in der Header-Datei initialisieren,
* oder in der Header-Datei deklarieren und in der zugehörigen .c-Datei 
dann initialisieren mit einer init()-Funktion (natürlich hier nicht 
const).


Ich habe alle drei Möglichkeiten schon in verschiedenen Projekten 
verwendet. Jetzt wollte ich einfach mal fragen, ob es dort ein 
"Standard" etabliert hat. Zur kurzen Einordnung und meine bisherige 
Präferenz ist, dass ich immer .c/.h-Dateipaare verwende und, wenn es 
möglich ist, die zweite oder dritte Möglichkeit benutze.

Vielen Dank schon mal für die Antworten.

Gruß Vincent

: Verschoben durch Moderator
von A. S. (Gast)


Lesenswert?

Sorry, keine Möglichkeit ist irgendwie üblich oder gut.

Extern declaration in .h nur dann, wenn in >= 2 c Files verwendet, sonst 
static in dem einen.

Und es gibt für const Variablen in C viel weniger Gründe als in C++.

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


Lesenswert?

Vincent W. schrieb:

> gibt es ein Best Practice in C für konstante Variablen bei
> Microcontroller?

"konstante Variable" ist eigentlich ein Widerspruch in sich. ;-)

(Allerdings ist das C "const" tatsächlich leider genau das.)

> An Möglichkeiten die mir so eingefallen sind:
> * Eine Variable in file1.c zu definieren und initialisieren und in
> file2.c mit 'extern' deklarieren,

Schlechte Idee. Außer mit link-time-Optimization führt das zu unnützem 
Speicher- und Rechenzeitverbrauch, da der Compiler nicht mehr in allen 
Übersetzungseinheiten erkennen kann, dass die Variable tatsächlich ihren 
Wert nie ändert.

> * mit 'static const' in der Header-Datei initialisieren,

Zumindest dahingehend besser.

Für C gerade im Controller-Umfeld wären auch klassische 
Präprozessormakros hier praktikabel, denn auch dort kann der Compiler 
bereits entscheiden, dass bestimmte Code-Teile konstant sind.

> * oder in der Header-Datei deklarieren und in der zugehörigen .c-Datei
> dann initialisieren mit einer init()-Funktion (natürlich hier nicht
> const).

Noch schlechter als die erste Variante.

von Vincent W. (vince6e74)


Lesenswert?

Jörg W. schrieb:

> Schlechte Idee. Außer mit link-time-Optimization führt das zu unnützem
> Speicher- und Rechenzeitverbrauch, da der Compiler nicht mehr in allen
> Übersetzungseinheiten erkennen kann, dass die Variable tatsächlich ihren
> Wert nie ändert.

Sobald es komplett kompiliert ist, müsste es vermutlich nicht so große 
Unterschiede ausmachen?
'extern' verwende ich hauptsächlich für CubeMx-Variablen, die einem 
nicht die Wahl lassen.

> Für C gerade im Controller-Umfeld wären auch klassische
> Präprozessormakros hier praktikabel, denn auch dort kann der Compiler
> bereits entscheiden, dass bestimmte Code-Teile konstant sind.

Die verwende ich auch, allerdings ist das bei komplexeren Dingen, wie 
structs nicht mehr möglich.

> Noch schlechter als die erste Variante.

Ist das nach der Setup-Phase so problematisch?

Da Konstanten in der Setup-Phase erstellt werden, ist es mir nicht so 
wichtig, wie lang der STM32 zum "hochfahren" braucht. Relevanter ist es 
im Loop und in den Interrupts.

Ich habe fast noch nie einen verwendeten STM32 bis zu 100% ausgereizt. 
Ich versuche immer drauf zu achten, dass der Loop und die Interupts 
effizent laufen.

von Hmmm (Gast)


Lesenswert?

Vincent W. schrieb:
> Sobald es komplett kompiliert ist, müsste es vermutlich nicht so große
> Unterschiede ausmachen?

Ohne LTO sieht der Compiler immer nur das jeweilige Source-File. Er muss 
also davon ausgehen, dass die externe Variable auch wirklich variabel 
ist, und Du willst bestimmt nicht, dass bei I/O-Zugriffen etwas wie 
1<<somebit tatsächlich zu unnötigen Bitschiebereien führt.

von A. S. (Gast)


Lesenswert?

Vincent W. schrieb:
> Da Konstanten in der Setup-Phase erstellt werden, ist es mir nicht so
> wichtig, wie lang der STM32 zum "hochfahren" braucht.

Du müsstest ein typisches Beispiel / Anwendungsfall beschreiben.

Und wenn der gleiche Speicherbereich für einige const ist, für andere 
variabel, gibt es u.U plattformspezifische Einschränkungen. Am 
einfachsten ist das mit einer static Variablen/Struktur und dann nach 
extern nur ein const XYtype * const ptrXY.

von Udo K. (Gast)


Lesenswert?

Vincent W. schrieb:
> gibt es ein Best Practice in C für konstante Variablen bei
> Microcontroller? Als simples Beispiel definieren welcher Sensor an
> welchen ADC Eingang hängt.

The "best practice" in C ist der Preprozessor.
Funktioniert seit 50 Jahren ohne Probleme.
Funktioniert auch mit Structs, Strings, und "inline" Funktionen.
Die Defines kommen in "config.h", das die C Files inkludieren.

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


Lesenswert?

Udo K. schrieb:
> The "best practice" in C ist der Preprozessor.

Nein, der ist eher "best compromise" als "best practice".

Wenn es eine Lösung ohne Präprozessor gibt, die das gleiche Ergebnis 
erzielt, würde ich sie jederzeit bevorzugen.

von Udo K. (Gast)


Lesenswert?

Unter Best Practice versteht man nicht die denkbar beste Lösung,
sonder die gängige Praxis, die funktioniert.

Unter C ist der Präprozessor ein wichtiger Sprachbestandteil,
ohne den es nicht geht.  Der ist superflexibel, und dabei einfach
in der Benutzung.  Kein Vergleich zu den Templates in C++,
die in ihrer Komplexität einfach nur krank sind.

Aber wer meint, der Präprozessor ist zu kompliziert, oder altbacken,
der kann "static int i = 100" ins Header File schreiben.
Oder die Weicheier schreiben "static const int i = 100".
Dem Compiler ist es wurscht.

von Stefan F. (Gast)


Lesenswert?

Vincent W. schrieb:
> gibt es ein Best Practice in C für konstante Variablen bei
> Microcontroller?

Konstanten definiere ich in C im #define, vorzugsweise in einer 
separaten Datei, die ich gerne hardware.h oder config.h nenne.

Das sind keine Variablen, und zwar mit Absicht, damit sie kein RAM 
belegen. Beim arm-gcc sorgt das Schlüsselwort const schon dafür, dass 
kein RAM belegt wird. Aber beim avr-gcc geht das nicht so einfach, 
deswegen #define.

Was nicht konstante Variablen angeht: Diese vermeide ich wo immer es 
geht, da man sich danach durch copy/paste Aktionen sehr schnell 
Konflikte/Fehler einhandelt die erst später auffallen. In meinen 
Projekten findest du globale Variablen fast ausschließlich in der 
main.c.

Ab und zu nutze ich statische globale Variablen in Bibliotheken und eine 
dazu passende öffentlich sichtbare getter-Funktion, zum Beispiel um den 
Status einer Resource bekannt zu geben (z.B. ob eine SD Karte gerade 
bereit oder belegt ist).

Udo K. schrieb:
> The "best practice" in C ist der Preprozessor.

C ist ein bisschen angestaubt und der Preprozessor noch mehr. Ich habe 
15 Jahre PC's in C programmiert ohne zu wissen, was ein Preprozessor 
ist. Ich kannte nur #define, #ifdef und #include. Durch dieses Forum 
hier wurde ich darauf aufmerksam, dass man sehr viel mehr mit dem 
Preprozessor machen kann und dies auch in übermäßigem Eifer in einem 
meiner komplexesten Projekte intensiv genutzt - und wieder bereut.

Ich kann nur davon abraten, Makros tief zu verschachteln, weil man ihre 
Wirkung dann nur noch mühsam nachvollziehen kann. Hier wurde mal ein 
Konstrukt für AVR veröffentlicht, dass Aufrufe wie "SET(PD0,1)" 
ermöglichte. So elegant das nach außen hin aussah, so schrecklich 
komplex war das unter der Haube. Wenn komplex verschachtelte Makros mal 
eine Syntax-Fehler im Compiler auslösen, bekommt man oft sehr 
unverständliche Fehlermeldungen.

Und bitte immer daran denken, dass der GNU C Compiler inzwischen 
ziemlich gut optimieren kann. Einfache C-Funktionen wie:
1
void set_led(uint8_t value)
2
{
3
    if (value)
4
        PORTD |= (1<<4);
5
    else
6
        PORTD &= ~(1<<4);
7
}
werden in den allermeisten Fällen auf den simplen Port-Zugriff 
reduziert, so dass im Maschinencode weder ein Funktionsaufruf noch die 
if()-Abfrage stattfindet.

Deswegen ist einiges an traditioneller Marko-Magie, was früher mal der 
manuellen Optimierung diente, schlicht überflüssig geworden.

von P. S. (namnyef)


Lesenswert?

Unter Berücksichtigung, dass im Header-File nur eine Bekanntmachung 
stattfinden sollte, würde ich es so machen:

*.c: const (Definition und Initialisierung)
*.h: extern const

Im Header-File mit "static const" initialisieren ist imho Murks ;). 
Datenstrukturen sollten schon in dem Modul definiert und initialisiert 
werden, dem sie logisch/funktional zugeordnet werden können. Und dann 
eben im Header-File dieses Moduls lediglich der Öffentlichkeit bekannt 
gemacht werden.
Den Präprozessor würde ich, wenn möglich, vermeiden z.B. wegen fehlender 
Typ-Überprüfung.

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

P. S. schrieb:
> Unter Berücksichtigung, dass im Header-File nur eine Bekanntmachung
> stattfinden sollte, würde ich es so machen:
>
> *.c: const (Definition und Initialisierung)
> *.h: extern const
>
> Im Header-File mit "static const" initialisieren ist imho Murks ;).

Das gibt dem Compiler allerdings die Möglichkeit, die Variable 
rauszuoptimieren, weil der Compiler dann bei jeder Benutzung den Wert 
kennt. Bei deinem Vorschlag muss da immer ein extra Speicherzugriff 
durchgeführt und dieser gelesene Wert dann verwendet werden. Der oben 
genannte shift (1<<pin_number) wäre so ein Beispiel. Auf einem AVR muss 
der zu schiebende Wert aus dem Speicher gelesen und dann mangels 
Schiebe-Instruktion in einer Schleife bitweise mit sich selbst addiert 
werden, statt dass der Shift vollständig zur Compilezeit berechnet und 
nur noch das Ergebnis eingetragen wird.

> Den Präprozessor würde ich, wenn möglich, vermeiden z.B. wegen fehlender
> Typ-Überprüfung.

Allerdings gibt es wiederum auch Stellen, wo es gar nicht anders geht, 
weil eine echte Compilezeit-Konstante benötigt wird, und eine 
const-Variable ist auf C-Ebene keine solche.

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


Lesenswert?

Udo K. schrieb:
> Unter C ist der Präprozessor ein wichtiger Sprachbestandteil,
> ohne den es nicht geht.

Sicher, schon allein wegen der #includes.

Allerdings ist der Präprozessor eben auch für manch leidige Überraschung 
zuständig. Unsere Compiler sind mittlerweile viel leistungsfähiger als 
vor 50 Jahren, sie erkennen viel mehr Optimierungsfälle. Daher kann man 
vieles heutzutage ohne Präprozessor schreiben, was zu Zeiten von Brian 
Kernighan und Dennis Ritchie so nicht denkbar war.

von A. S. (Gast)


Lesenswert?

Jörg W. schrieb:
> Daher kann man
> vieles heutzutage ohne Präprozessor schreiben, was zu Zeiten von Brian
> Kernighan und Dennis Ritchie so nicht denkbar war.

Ganz richtig. Nur muss man für die meisten Ersatzkonstrukte bei jeder 
Plattform in jeder Version schauen, ob denn der Ballast wirklich 
weggelassen wird. Also, dass z.B.
1
static const struct sMyCfg MyCfg={1,2,3,4,5...}

nicht zu einer Instanz je c-File führt und nicht zu 
Flash-Read-Sonderfunktionen zur Laufzeit, wenn ein Element gebraucht 
wird.

Während der Präprozessor relativ verlässlich ist, sind es Optimierungen 
nicht. Und in den seltensten Fällen geht (in C):
1
int myXYArray[MyCfg.nXY]

von P. S. (namnyef)


Lesenswert?

Rolf M. schrieb:
> Das gibt dem Compiler allerdings die Möglichkeit, die Variable
> rauszuoptimieren, weil der Compiler dann bei jeder Benutzung den Wert
> kennt. Bei deinem Vorschlag muss da immer ein extra Speicherzugriff
> durchgeführt und dieser gelesene Wert dann verwendet werden.

Die oberste Prämisse sollte aber Wartbarkeit sein. Code zu warten, bei 
dem der Autor offensichtlich viel Freude daran hatte jede unnötige 
Instruction rauszuoptimieren, ist meist ein Alptraum. Und wenn man sich 
das Disassembly anschaut, waren die meisten "Optimierungen" eh erfolglos 
oder - unter Berücksichtigung der zur Verfügung stehenden Ressourcen - 
völlig unnötig.

Heutige Compiler und Architekturen sind gut genug, sodass man da i. d. 
R. nicht selber Hand anlegen braucht.

von A. S. (Gast)


Lesenswert?

Rolf M. schrieb:
> Das gibt dem Compiler allerdings die Möglichkeit, die Variable
> rauszuoptimieren, weil der Compiler dann bei jeder Benutzung den Wert
> kennt

Ein C++ Compiler darf und kann das auch so. Wenn man sowas will, dann 
einfach C++ nehmen, egal ob mit oder ohne OOP, Templates oder was auch 
immer.

von Rolf M. (rmagnus)


Lesenswert?

A. S. schrieb:
> Rolf M. schrieb:
>> Das gibt dem Compiler allerdings die Möglichkeit, die Variable
>> rauszuoptimieren, weil der Compiler dann bei jeder Benutzung den Wert
>> kennt
>
> Ein C++ Compiler darf und kann das auch so.

Es geht aber um C. Wobei auch ein C++-Compiler zumindest mal LTO 
braucht, um das hinzubekommen. Denn auch der kann einen Wert, den er 
nicht kennt, nicht direkt einfügen. :)

von Falk B. (falk)


Lesenswert?

P. S. schrieb:
> Die oberste Prämisse sollte aber Wartbarkeit sein. Code zu warten, bei

https://www.mikrocontroller.net/articles/AVR-GCC-Codeoptimierung#Prinzipien_der_Optimierung

von A. S. (Gast)


Lesenswert?

Rolf M. schrieb:
> A. S. schrieb:
>> Rolf M. schrieb:
>>> Das gibt dem Compiler allerdings die Möglichkeit, die Variable
>>> rauszuoptimieren, weil der Compiler dann bei jeder Benutzung den Wert
>>> kennt
>>
>> Ein C++ Compiler darf und kann das auch so.
>
> Es geht aber um C. Wobei auch ein C++-Compiler zumindest mal LTO
> braucht, um das hinzubekommen. Denn auch der kann einen Wert, den er
> nicht kennt, nicht direkt einfügen. :)

Das ist richtig. Du wirst aber zugegben, dass Dein Konzept hier ein 
gefährlicher Ritt auf der der Rasierklinge ist: Eine Struktur X-fach 
lokal zu instanziieren erlaubt es dem Compiler, dies auch X-fach zu tun. 
Dann hast Du auf einmal 1000 statt 1 Instanz, mit 1000 statt einer 
Adresse (wenn man Pointer darauf bildet und die verwendet). Und das nur, 
damit der Compiler prinzipiell die Möglichkeit hat, den Wert direkt zu 
benutzten und den Rest gar nicht erst anzulegen.

von Rolf M. (rmagnus)


Lesenswert?

P. S. schrieb:
> Rolf M. schrieb:
>> Das gibt dem Compiler allerdings die Möglichkeit, die Variable
>> rauszuoptimieren, weil der Compiler dann bei jeder Benutzung den Wert
>> kennt. Bei deinem Vorschlag muss da immer ein extra Speicherzugriff
>> durchgeführt und dieser gelesene Wert dann verwendet werden.
>
> Die oberste Prämisse sollte aber Wartbarkeit sein. Code zu warten, bei
> dem der Autor offensichtlich viel Freude daran hatte jede unnötige
> Instruction rauszuoptimieren, ist meist ein Alptraum.

Das kommt drauf an. Super wartbarer Code, der aber zu langsam ist und 
damit die Echtzeit-Vorgaben verletzt oder der zu groß ist und daher 
nicht in den Speicher passt, bringt einem auch nichts.
Abgesehen davon finde ich sowas wie ein
1
#define LED_PORT PORTC
nicht wirklich unwartbar - auch nicht, wenn es in einem Header steht.

A. S. schrieb:
> Das ist richtig. Du wirst aber zugegben, dass Dein Konzept hier ein
> gefährlicher Ritt auf der der Rasierklinge ist: Eine Struktur X-fach
> lokal zu instanziieren erlaubt es dem Compiler, dies auch X-fach zu tun.
> Dann hast Du auf einmal 1000 statt 1 Instanz, mit 1000 statt einer
> Adresse (wenn man Pointer darauf bildet und die verwendet).

Naja, wenn du 1000 separate Source-Files hast, die alle auf die selbe 
globale Variable (bzw. Konstante) zugreifen und die das alle jeweils 
unbedingt über einen Zeiger tun müssen, hast du ganz andere Probleme.

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

Rolf M. schrieb:
> und die das alle jeweils unbedingt über einen Zeiger tun müssen, hast du
> ganz andere Probleme.

Das ist richtig. Aber zum einen ging es dem TO um eine Art config, und 
nicht um einzelne skalare, zum anderen fällt man halt bei einem Element 
nach Jahren drauf rein.

Rolf M. schrieb:
> Super wartbarer Code, der aber zu langsam ist und damit die
> Echtzeit-Vorgaben verletzt oder der zu groß ist und daher nicht in den
> Speicher passt, bringt einem auch nichts.
Meist ist wartbarer Code auch schneller, lesbarer und zuverlässiger.

Dein define-beispiel ist doch meist ein gutes Beispiel.

von Toby P. (Gast)


Lesenswert?

Kann man die globale nicht als Rückgabewert einer Funktion definieren.

Entweder als Wert oder als Pointer?

von Stefan F. (Gast)


Lesenswert?

Toby P. schrieb:
> Kann man die globale nicht als Rückgabewert einer Funktion definieren.
> Entweder als Wert oder als Pointer?

Kann man, habe ich doch oben geschrieben.

von W.S. (Gast)


Lesenswert?

Vincent W. schrieb:
> gibt es ein Best Practice in C für konstante Variablen bei
> Microcontroller? Als simples Beispiel definieren welcher Sensor an
> welchen ADC Eingang hängt.

Das sind zwei völlig verschiedene Dinge.

1. konstante Variablen: die fügt man am ehesten als const ...= wert; ein 
und verweist per .h auf diese typisierte Konstante.. WENN: es wirklich 
eine sein muß. Wenn nicht, dann nimm eher ein #define xxx in der .h 
Datei.

2. Welcher Sensor an welchem ADC-Eingang hängt, sollte eben nicht über 
irgend welche 'konstanten Variablen', sondern über einen passenden 
Low-Level-Treiber erledigt werden, denn der Wert des Sensors ist ja 
nicht wertfrei, sondern hat irgend eine Bedeutung, eine Kalibrierung, 
ergibt irgend einen Systemzustand der behandelt werden muß oder nicht 
usw. Das wiederum ist Sache des Treibers.

W.S.

von Rolf M. (rmagnus)


Lesenswert?

W.S. schrieb:
> 2. Welcher Sensor an welchem ADC-Eingang hängt, sollte eben nicht über
> irgend welche 'konstanten Variablen', sondern über einen passenden
> Low-Level-Treiber erledigt werden, denn der Wert des Sensors ist ja
> nicht wertfrei, sondern hat irgend eine Bedeutung, eine Kalibrierung,
> ergibt irgend einen Systemzustand der behandelt werden muß oder nicht
> usw. Das wiederum ist Sache des Treibers.

Ob du das  C-File, das den Wert einliest, jetzt "Treiber" nennst oder 
nicht, ändert an der Frage erst mal nichts. Auch bei einem Treiber 
möchte man ggf. konfigurieren können, an welchem Port der Sensor hängt.

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.