Speicherzuweisungsmöglichkeiten für modulares Firmware-Design in C

16

Modulare Ansätze sind im Allgemeinen ziemlich praktisch (portabel und sauber), daher versuche ich, Module so unabhängig wie möglich von anderen Modulen zu programmieren. Die meisten meiner Ansätze basieren auf einer Struktur, die das Modul selbst beschreibt. Eine Initialisierungsfunktion legt die primären Parameter fest. Anschließend wird ein Handler (Zeiger auf die beschreibende Struktur) an die aufgerufene Funktion innerhalb des Moduls übergeben.

Im Moment frage ich mich, was der beste Ansatz für die Zuweisung von Speicher für die Struktur sein kann, die ein Modul beschreibt. Wenn möglich, würde ich mir Folgendes wünschen:

  • Undurchsichtige Struktur, daher kann die Struktur nur durch die Verwendung der bereitgestellten Schnittstellenfunktionen geändert werden
  • Mehrere Instanzen
  • Vom Linker zugewiesener Speicher

Ich sehe folgende Möglichkeiten, die alle mit einem meiner Ziele in Konflikt stehen:

globale Erklärung

mehrere Instanzen, die vom Linker zugeordnet werden, aber struct ist nicht undurchsichtig

(#includes)
module_struct module;

void main(){
   module_init(&module);
}

Malloc

undurchsichtige Struktur, mehrere Instanzen, aber Zuteilung auf Haufen

in module.h:

typedef module_struct Module;

in der Init-Funktion von module.c malloc und Rückgabezeiger auf den zugewiesenen Speicher

module_mem = malloc(sizeof(module_struct ));
/* initialize values here */
return module_mem;

in main.c

(#includes)
Module *module;

void main(){
    module = module_init();
}

Erklärung im Modul

opake Struktur, vom Linker zugewiesen, nur eine vordefinierte Anzahl von Instanzen

Behalten Sie die gesamte Struktur und den gesamten Speicher innerhalb des Moduls und machen Sie niemals einen Handler oder eine Struktur verfügbar.

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

Gibt es eine Möglichkeit, diese für eine undurchsichtige Struktur, einen Linker anstelle einer Heap-Zuordnung und mehrere / beliebig viele Instanzen zu kombinieren?

Lösung

Wie in den folgenden Antworten vorgeschlagen, ist meiner Meinung nach der beste Weg:

  1. Reservieren Sie Speicherplatz für MODULE_MAX_INSTANCE_COUNT Module in der Modulquelldatei
  2. Definieren Sie MODULE_MAX_INSTANCE_COUNT nicht im Modul selbst
  3. Fügen Sie der Modulheaderdatei den Fehler #ifndef MODULE_MAX_INSTANCE_COUNT # hinzu, um sicherzustellen, dass der Modulbenutzer diese Einschränkung kennt und die maximale Anzahl der für die Anwendung gewünschten Instanzen definiert
  4. Bei der Initialisierung einer Instanz wird entweder die Speicheradresse (* void) der beschreibenden Struktur oder der Modulindex (was auch immer Sie mehr wollen) zurückgegeben.
L. Heinrichs
quelle
12
Die meisten eingebetteten FW-Designer verzichten auf die dynamische Zuordnung, um die Speichernutzung deterministisch und einfach zu halten. Insbesondere, wenn es sich um ein Bare-Metal-System handelt und kein zugrunde liegendes Betriebssystem zur Speicherverwaltung vorhanden ist.
Eugene Sh.
Genau deshalb möchte ich, dass der Linker die Zuweisungen vornimmt.
L. Heinrichs
4
Ich bin mir nicht ganz sicher, ob ich das verstehe ... Wie können Sie den Speicher vom Linker zuweisen lassen, wenn Sie eine dynamische Anzahl von Instanzen haben? Das erscheint mir recht orthogonal.
Jcaron
Lassen Sie den Linker einen großen Speicherpool zuweisen und nehmen Sie daraus Ihre eigenen Zuweisungen vor. Dies bietet Ihnen auch den Vorteil eines Zero-Overhead-Zuweisers. Sie können das Pool-Objekt mit der Zuweisungsfunktion für die Datei statisch machen, damit es privat ist. In einem Teil meines Codes mache ich alle Zuweisungen in den verschiedenen Init-Routinen und drucke anschließend aus, wie viel zugewiesen wurde. In der endgültigen Produktionskompilierung habe ich den Pool auf genau diese Größe festgelegt.
Lee Daniel Crocker
2
Wenn es sich um eine Entscheidung zur Kompilierungszeit handelt, können Sie die Nummer einfach in Ihrem Makefile oder einem gleichwertigen Element definieren und fertig. Die Nummer befindet sich nicht in der Quelle des Moduls, sondern ist anwendungsspezifisch. Sie verwenden lediglich eine Instanznummer als Parameter.
Jcaron

Antworten:

4

Gibt es eine Möglichkeit, diese für anonyme Strukturen, Linker anstelle von Heap-Zuweisungen und mehrere / beliebig viele Instanzen zu kombinieren?

Sicher gibt es. Erkennen Sie jedoch zunächst, dass die "beliebige Anzahl" von Instanzen zur Kompilierungszeit festgelegt oder zumindest eine Obergrenze festgelegt werden muss. Dies ist eine Voraussetzung für die statische Zuordnung der Instanzen (was Sie als "Linker-Zuordnung" bezeichnen). Sie können die Zahl ohne Quellenänderung anpassen, indem Sie ein Makro deklarieren, das sie angibt.

Dann deklariert die Quelldatei, die die eigentliche Strukturdeklaration und alle zugehörigen Funktionen enthält, auch ein Array von Instanzen mit interner Verknüpfung. Es bietet entweder ein Array mit externen Verknüpfungen von Zeigern auf die Instanzen oder eine Funktion für den Zugriff auf die verschiedenen Zeiger über den Index. Die Funktionsvariante ist etwas modularer:

module.c

#include <module.h>

// 4 instances by default; can be overridden at compile time
#ifndef NUM_MODULE_INSTANCES
#define NUM_MODULE_INSTANCES 4
#endif

struct module {
    int demo;
};

// has internal linkage, so is not directly visible from other files:
static struct module instances[NUM_MODULE_INSTANCES];

// module functions

struct module *module_init(unsigned index) {
    instances[index].demo = 42;
    return &instances[index];
}

Vermutlich kennen Sie sich bereits damit aus, wie der Header die Struktur als unvollständigen Typ deklariert und alle Funktionen (in Form von Zeigern auf diesen Typ) deklariert . Beispielsweise:

module.h

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

Jetzt struct moduleist es in anderen Übersetzungseinheiten als * undurchsichtig module.c, und Sie können auf die Anzahl der Instanzen zugreifen und sie verwenden, die zum Zeitpunkt der Kompilierung definiert wurden, ohne dass eine dynamische Zuordnung erforderlich ist.


* Natürlich, es sei denn, Sie kopieren die Definition. Der Punkt ist, dass module.hdas nicht geht.

John Bollinger
quelle
Ich finde es seltsam, den Index von außerhalb der Klasse zu übergeben. Wenn ich solche Speicherpools implementiere, lasse ich den Index ein privater Zähler sein, der für jede zugewiesene Instanz um 1 erhöht wird. Bis Sie zu "NUM_MODULE_INSTANCES" gelangen, wo der Konstruktor einen Fehler wegen unzureichendem Arbeitsspeicher zurückgibt.
Lundin
Das ist ein fairer Punkt, @Lundin. Dieser Aspekt des Entwurfs setzt voraus, dass die Indizes eine inhärente Bedeutung haben, was tatsächlich der Fall sein kann oder nicht. Dies ist der Fall, wenn auch in trivialer Hinsicht, für den Ausgangsfall des OP. Eine solche Signifikanz könnte, falls vorhanden, weiter unterstützt werden, indem ein Mittel bereitgestellt wird, um einen Instanzzeiger ohne Initialisierung zu erhalten.
John Bollinger
Im Grunde reservieren Sie also Speicher für n Module, egal wie viele verwendet werden, und geben dann einen Zeiger auf das nächste nicht verwendete Element zurück, wenn die Anwendung es initialisiert. Ich denke, das könnte funktionieren.
L. Heinrichs
@ L.Heinrichs Ja, da eingebettete Systeme deterministischer Natur sind. Es gibt weder eine "endlose Anzahl von Objekten" noch eine "unbekannte Anzahl von Objekten". Objekte sind oftmals auch Singletons (Hardwaretreiber), sodass der Speicherpool häufig nicht benötigt wird, da nur eine einzige Instanz des Objekts vorhanden ist.
Lundin
Ich stimme für die meisten Fälle zu. Die Frage hatte auch theoretisches Interesse. Aber ich könnte Hunderte von 1-Draht-Temperatursensoren verwenden, wenn genügend E / A verfügbar sind (als das einzige Beispiel, das ich jetzt finden kann).
L. Heinrichs
22

Ich programmiere kleine Mikrocontroller in C ++, die genau das erreichen, was Sie wollen.

Was Sie ein Modul nennen, ist eine C ++ - Klasse, die Daten (entweder von außen zugänglich oder nicht) und Funktionen (ebenfalls) enthalten kann. Der Konstruktor (eine dedizierte Funktion) initialisiert sie. Der Konstruktor kann Laufzeitparameter oder (meine bevorzugten) Kompilierungszeitparameter (Vorlagenparameter) verwenden. Die Funktionen innerhalb der Klasse erhalten implizit die Klassenvariable als ersten Parameter. (Oder, oftmals nach meinem Geschmack, kann die Klasse als versteckter Singleton fungieren, sodass auf alle Daten ohne diesen Overhead zugegriffen wird.)

Das Klassenobjekt kann global (damit Sie zum Zeitpunkt der Verknüpfung wissen, dass alles passt) oder stapellokal sein, vermutlich im Hauptbereich. (Ich mag C ++ - Globals wegen der undefinierten globalen Initialisierungsreihenfolge nicht, deshalb bevorzuge ich Stack-Local).

Mein bevorzugter Programmierstil ist, dass Module statische Klassen sind und ihre (statische) Konfiguration durch Vorlagenparameter erfolgt. Dies vermeidet fast alles Überflüssige und ermöglicht eine Optimierung. Kombiniere dies mit einem Tool, das die Stapelgröße berechnet und du kannst ohne Sorgen schlafen :)

Mein Vortrag über diese Art der Codierung in C ++: Objekte? Nein Danke!

Viele Embedded / Mikrocontroller Programmierer scheinen C zu mögen ++ , weil sie denken , dass es sie zwingen würde , verwenden alle von C ++. Das ist absolut nicht nötig und wäre eine sehr schlechte Idee. (Sie verwenden wahrscheinlich auch nicht alle C! Denken Sie an Heap, Fließkomma, Setjmp / Longjmp, Printf, ...)


In einem Kommentar erwähnt Adam Haun RAII und Initialisierung. IMO RAII hat mehr mit Dekonstruktion zu tun, aber sein Punkt ist gültig: Globale Objekte werden vor Ihrem Hauptstart konstruiert, sodass sie möglicherweise auf ungültigen Annahmen basieren (wie eine Haupttaktrate, die später geändert wird). Dies ist ein weiterer Grund, KEINE mit globalem Code initialisierten Objekte zu verwenden. (Ich verwende ein Linker-Skript, das fehlschlägt, wenn ich mit globalem Code initialisierte Objekte habe.) IMO sollten solche "Objekte" explizit erstellt und weitergegeben werden. Dies beinhaltet ein 'Warten'-Objekt, das eine wait () -Funktion bietet. In meinem Setup ist dies "Objekt", das die Taktrate des Chips festlegt.

Apropos RAII: Dies ist eine weitere C ++ - Funktion, die in kleinen eingebetteten Systemen sehr nützlich ist, jedoch nicht aus dem Grund (Speicherfreigabe), für den sie in größeren Systemen am häufigsten verwendet wird (kleine eingebettete Systeme verwenden meist keine dynamische Speicherfreigabe). Denken Sie an das Sperren einer Ressource: Sie können die gesperrte Ressource zu einem Wrapperobjekt machen und den Zugriff auf die Ressource so beschränken, dass er nur über den Sperrwrapper möglich ist. Wenn der Wrapper den Gültigkeitsbereich verlässt, wird die Ressource entsperrt. Dies verhindert den Zugriff ohne Verriegelung und macht es viel unwahrscheinlicher, dass die Entriegelung vergessen wird. mit etwas (Vorlagen-) Magie kann es ein Overhead von Null sein.


Die ursprüngliche Frage erwähnte kein C, daher meine C ++ - zentrierte Antwort. Wenn es wirklich sein muss C ....

Sie können Makrotäuschungen anwenden: Deklarieren Sie Ihre Produkte öffentlich, damit sie einen Typ haben und global zugeordnet werden können. Machen Sie die Namen ihrer Komponenten jedoch unbrauchbar, es sei denn, ein Makro ist anders definiert, wie dies in der .c-Datei Ihres Moduls der Fall ist. Für zusätzliche Sicherheit können Sie die Kompilierungszeit im Mangling verwenden.

Oder haben Sie eine öffentliche Version Ihrer Struktur, die nichts Nützliches enthält, und haben Sie die private Version (mit nützlichen Daten) nur in Ihrer .c-Datei, und behaupten Sie, dass sie dieselbe Größe haben. Ein kleiner Trick beim Erstellen von Dateien könnte dies automatisieren.


@Lundins Kommentar zu schlechten (eingebetteten) Programmierern:

  • Die Art von Programmierer, die Sie beschreiben, würde wahrscheinlich in jeder Sprache ein Chaos anrichten. Makros (in C und C ++ vorhanden) sind ein offensichtlicher Weg.

  • Werkzeuge können in gewissem Maße helfen. Für meine Schüler gebe ich ein Skript an, das keine Ausnahmen, kein rtti, angibt und einen Linker-Fehler ausgibt, wenn entweder der Heap verwendet wird oder Code-initialisierte Globals vorhanden sind. Und es gibt warning = error an und aktiviert fast alle Warnungen.

  • Ich empfehle die Verwendung von Vorlagen, aber mit constexpr und Konzepten ist eine Metaprogrammierung immer weniger erforderlich.

  • "Verwirrte Arduino-Programmierer" Ich würde sehr gerne den Arduino-Programmierstil (Verkabelung, Replikation von Code in Bibliotheken) durch einen modernen C ++ - Ansatz ersetzen, der einfacher, sicherer und schneller und kleinerer Code sein kann. Wenn ich nur die Zeit und Kraft hätte ...

Wouter van Ooijen
quelle
Danke für diese Antwort! Die Verwendung von C ++ ist eine Option, aber wir verwenden C in meiner Firma (was ich nicht explizit erwähnt habe). Ich habe die Frage aktualisiert, damit die Leute wissen, dass ich über C.
L. Heinrichs
Warum benutzt du (nur) C? Vielleicht können Sie sie davon überzeugen, zumindest C ++ in Betracht zu ziehen ... Was Sie wollen, ist das Wesentliche (ein kleiner Teil von C ++), das in C.
Wouter van Ooijen,
In meinem (ersten 'echten' eingebetteten Hobbyprojekt) initialisiere ich einfache Standardeinstellungen im Konstruktor und verwende eine separate Init-Methode für relevante Klassen. Ein weiterer Vorteil ist, dass ich Stichzeiger für Unit-Tests übergeben kann.
Michel Keijzers
2
@Michel für ein Hobbyprojekt kannst du die Sprache frei wählen? Nimm C ++!
Wouter van Ooijen
4
Und während es in der Tat durchaus möglich ist , gute C ++ Programme zu schreiben , für eingebettete, das Problem ist, dass einige> 50% aller Embedded - Systeme Programmierer da draußen sind Scharlatane, verwirrt PC - Programmierer, Arduino Bastler etc etc. Diese Art von Menschen einfach nicht ein verwenden saubere Teilmenge von C ++, auch wenn Sie es ihrem Gesicht erklären. Geben Sie ihnen C ++ und bevor Sie es wissen, werden sie die gesamte STL, Template-Metaprogrammierung, Ausnahmebehandlung, Mehrfachvererbung und so weiter verwenden. Und das Ergebnis ist natürlich kompletter Müll. Leider enden so etwa 8 von 10 eingebetteten C ++ - Projekten.
Lundin
7

Ich glaube, FreeRTOS (vielleicht ein anderes Betriebssystem?) Macht so etwas wie das, wonach Sie suchen, indem Sie 2 verschiedene Versionen der Struktur definieren.
Die "echte", die intern von den Betriebssystemfunktionen verwendet wird, und eine "falsche", die die gleiche Größe wie die "echte" hat, aber keine nützlichen Mitglieder enthält (nur ein paar int dummy1und ähnliche).
Außerhalb des Betriebssystemcodes wird nur die 'Fake'-Struktur angezeigt, und dies wird verwendet, um statischen Instanzen der Struktur Speicher zuzuweisen.
Intern wird beim Aufrufen von Funktionen im Betriebssystem die Adresse der externen 'Fake'-Struktur als Handle übergeben, und diese wird dann als Zeiger auf eine' echte 'Struktur typisiert, damit die Betriebssystemfunktionen das tun können, was sie benötigen tun.

brhans
quelle
Gute Idee, ich denke ich könnte verwenden --- #define BUILD_BUG_ON (Bedingung) ((void) sizeof (char [1 - 2 * !! (Bedingung)]) --- BUILD_BUG_ON (sizeof (real_struct)! = Sizeof ( fake_struct)) ----
L. Heinrichs
2

Anonyme Struktur, daher darf die Struktur nur unter Verwendung der bereitgestellten Schnittstellenfunktionen geändert werden

Meiner Meinung nach ist das sinnlos. Sie können dort einen Kommentar einfügen, aber es macht keinen Sinn, ihn weiter auszublenden.

C wird niemals eine so hohe Isolation liefern, selbst wenn es keine Deklaration für die Struktur gibt, wird es leicht sein, sie versehentlich mit z. B. fehlerhaftem memcpy () oder Pufferüberlauf zu überschreiben.

Geben Sie stattdessen der Struktur einen Namen und vertrauen Sie darauf, dass andere Leute auch guten Code schreiben. Dies erleichtert auch das Debuggen, wenn die Struktur einen Namen hat, auf den Sie verweisen können.

jpa
quelle
2

Reine SW-Fragen werden unter /programming/ besser gestellt .

Das Konzept, eine Struktur eines unvollständigen Typs dem Aufrufer zur Verfügung zu stellen, wird, wie Sie beschreiben, häufig als "undurchsichtiger Typ" oder "undurchsichtige Zeiger" bezeichnet - eine anonyme Struktur bedeutet etwas ganz anderes.

Das Problem dabei ist, dass der Aufrufer keine Instanzen des Objekts zuordnen kann, sondern nur Zeiger darauf. Auf einem PC würden Sie mallocinnerhalb der Objekte "Konstruktor" verwenden, aber Malloc ist ein No-Go in Embedded-Systemen.

In embedded müssen Sie also einen Speicherpool bereitstellen. Sie haben eine begrenzte Menge an RAM, daher ist es normalerweise kein Problem, die Anzahl der Objekte, die erstellt werden können, zu beschränken.

Siehe Statische Zuordnung von undurchsichtigen Datentypen bei SO.

Lundin
quelle
Ou danke für die Klärung der Namensverwirrung an meinem Ende, ich passe das OP an. Ich dachte über einen Stapelüberlauf nach, entschied mich aber dafür, Embedded-Programmierer gezielt anzusprechen.
L. Heinrichs