Möglichkeiten zur Speicherzuweisung für modulares Firmware-Design in C

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 setzt die primären Parameter, danach wird ein Handler (Zeiger auf eine beschreibende Struktur) an die aufgerufene Funktion innerhalb des Moduls übergeben.

Im Moment frage ich mich, was der beste Ansatz für den Zuweisungsspeicher für die Struktur ist, die ein Modul beschreibt. Wenn möglich, möchte ich Folgendes:

  • Undurchsichtige Struktur, sodass die Struktur nur durch die Verwendung bereitgestellter Schnittstellenfunktionen geändert werden kann
  • Mehrere Instanzen
  • vom Linker zugewiesener Speicher

Ich sehe folgende Möglichkeiten, die alle einem meiner Ziele widersprechen:

globale Erklärung

mehrere Instanzen, allcoted by linker, aber struct ist nicht undurchsichtig

(#includes)
module_struct module;

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

malloc

Undurchsichtige Struktur, mehrere Instanzen, aber Allcotion auf Heap

in module.h:

typedef module_struct Module;

in module.c Init-Funktion, malloc und Rückgabezeiger auf 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();
}

Deklaration im Modul

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

Behalten Sie die gesamte Struktur und den gesamten Speicher im Modul und legen Sie niemals einen Handler oder eine Struktur offen.

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

Gibt es eine Möglichkeit, diese irgendwie für undurchsichtige Strukturen, Linker statt Heap-Zuweisung und mehrere/beliebige Anzahl von Instanzen zu kombinieren?

Lösung

Wie in einigen Antworten unten vorgeschlagen, denke ich, dass der beste Weg ist:

  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 einen Fehler #ifndef MODULE_MAX_INSTANCE_COUNT # zur Kopfzeilendatei des Moduls hinzu, um sicherzustellen, dass der Benutzer des Moduls sich dieser Einschränkung bewusst ist, und definiert die maximale Anzahl von Instanzen, die für die Anwendung gewünscht werden
  4. bei der Initialisierung einer Instanz entweder die Speicheradresse (*void) der beschreibenden Struktur oder den Modulindex zurückgeben (was auch immer Sie mehr mögen)
Die meisten Embedded-FW-Designer vermeiden die dynamische Zuordnung, um die Speichernutzung deterministisch und einfach zu halten. Vor allem, wenn es sich um Bare-Metal handelt und kein zugrunde liegendes Betriebssystem zur Verwaltung des Speichers vorhanden ist.
Genau, deshalb möchte ich, dass der Linker die Zuweisungen durchführt.
Ich bin mir nicht ganz sicher, ob ich das verstehe ... Wie könnten Sie den Speicher vom Linker zuweisen lassen, wenn Sie eine dynamische Anzahl von Instanzen haben? Das erscheint mir recht orthogonal.
Warum lassen Sie den Linker nicht einen großen Speicherpool zuweisen und nehmen daraus Ihre eigenen Zuweisungen vor, was Ihnen auch den Vorteil eines Allokators ohne Overhead bietet. Sie können das Pool-Objekt mit der Zuweisungsfunktion für die Datei statisch machen, sodass es privat ist. In einigen meiner Codes mache ich alle Zuweisungen in den verschiedenen Init-Routinen, dann drucke ich danach aus, wie viel zugewiesen wurde, also setze ich den Pool in der endgültigen Produktionskompilierung auf genau diese Größe.
@LeeDanielCrocker Hm, das ist ein interessanter Ansatz, aber ich kann nicht ganz verstehen, wie ich das machen würde. kannst du irgendwelche ressourcen dazu empfehlen?
@L.Heinrichs, ah, also nicht dynamisch, sondern zur Kompilierzeit entschieden, und Sie möchten dies im aufrufenden Modul tun können und das aufgerufene Modul überhaupt nicht berühren? Warum also nicht die gleichen Konventionen wie bei der zweiten Methode verwenden? Es ist nicht wirklich anonym, aber wenn es Dinge genug für Sie kapselt ...
@jcaron hängt davon ab, wie "dynamisch" die Anwendung ist. Ich kann ein Sensor-Firmware-Modul für zwei verschiedene Anwendungen verwenden, eine mit n und eine mit m Sensoren. In diesem Fall würde ich Speicher für n oder m Instanzen auf Anwendungsebene reservieren. Dies funktioniert nicht, wenn die Firmware die Anzahl der angeschlossenen Sensoren selbst erkennt - außer wenn eine maximale Anzahl angegeben ist und Speicher für diese maximale Anzahl von Instanzen standardmäßig reserviert ist. (edit: einen neuen Kommentar erstellt, weil ich mit dem vorherigen nicht zufrieden war und ihn nicht mehr bearbeiten konnte)
Wenn es sich um eine Entscheidung zur Kompilierzeit handelt, können Sie die Nummer einfach in Ihrem Makefile oder einem Äquivalent definieren, und schon sind Sie fertig. Die Nummer wäre nicht in der Quelle des Moduls, sondern anwendungsspezifisch, und Sie verwenden einfach eine Instanznummer als Parameter.
Ich denke, in deinem Edit von gestern fehlt etwas – ist die Lösung wirklich enter code here?
Oh weh, ich wollte zuerst funktionierenden Code schreiben und testen, bevor ich Code zur Lösung hinzufüge, wurde aber dabei abgelenkt und vergaß es dann. Ich werde die Antwort hinzufügen, sobald ich wieder einen funktionierenden Laptop habe!

Antworten (5)

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

Sicher gibt es das. Beachten Sie jedoch zunächst, dass die "beliebige Anzahl" von Instanzen zur Kompilierzeit festgelegt oder zumindest eine Obergrenze festgelegt werden muss. Dies ist eine Voraussetzung dafür, dass die Instanzen statisch zugewiesen werden (was Sie als "Linker-Zuweisung" bezeichnen). Sie können die Zahl ohne Änderung der Quelle anpassen, indem Sie ein Makro deklarieren, das sie angibt.

Dann deklariert die Quelldatei, die die eigentliche Struct-Deklaration und alle zugehörigen Funktionen enthält, auch ein Array von Instanzen mit interner Verknüpfung. Sie stellt entweder ein Array mit externer Verknüpfung von Zeigern auf die Instanzen bereit oder aber eine Funktion zum Zugreifen auf die verschiedenen Zeiger per Index. Die Funktionsvariante ist etwas modularer:

Modul.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];
}

Ich denke, Sie sind bereits damit vertraut, wie der Header die Struktur dann als unvollständigen Typ deklarieren und alle Funktionen deklarieren würde (in Form von Zeigern auf diesen Typ geschrieben). Zum Beispiel:

Modul.h

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

Now ist in anderen Übersetzungseinheiten als , *struct module undurchsichtig und Sie können ohne dynamische Zuordnung auf bis zu der Anzahl von Instanzen zugreifen und diese verwenden, die zur Kompilierzeit definiert wurden.module.c


* Es sei denn, Sie kopieren natürlich seine Definition. Der Punkt ist, dass module.hdas nicht geht.

Ich denke, es ist ein seltsames Design, den Index von außerhalb der Klasse zu übergeben. Wenn ich solche Speicherpools implementiere, lasse ich den Index einen privaten Zähler sein, der für jede zugewiesene Instanz um 1 erhöht wird. Bis Sie "NUM_MODULE_INSTANCES" erreichen, wo der Konstruktor einen Fehler wegen zu wenig Arbeitsspeicher zurückgibt.
Das ist ein fairer Punkt, @Lundin. Dieser Aspekt des Designs 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 trivial, für den Startfall des OP. Eine solche Signifikanz, falls vorhanden, könnte weiter unterstützt werden, indem ein Mittel bereitgestellt wird, um einen Instanzzeiger ohne Initialisierung zu erhalten.
Im Grunde reservieren Sie also Speicher für n Module, egal wie viele verwendet werden, und geben dann einen Zeiger auf das nächste unbenutzte Element zurück, wenn die Anwendung es initialisiert. Ich denke, das könnte funktionieren.
@L.Heinrichs Ja, da eingebettete Systeme deterministischer Natur sind. Es gibt weder „unendliche Menge an Objekten“ noch „unbekannte Menge an Objekten“. Objekte sind oft auch Singletons (Hardwaretreiber), daher ist der Speicherpool oft nicht erforderlich, da nur eine einzige Instanz des Objekts existiert.
Ich stimme für die meisten Fälle zu. Die Frage hatte auch ein gewisses theoretisches Interesse. Aber ich könnte Hunderte von 1-Wire-Temperatursensoren verwenden, wenn genügend IOs verfügbar sind (als einziges Beispiel kann ich mir jetzt einfallen lassen).

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

Was Sie ein Modul nennen, ist eine C++-Klasse, die Daten (entweder extern zugänglich oder nicht) und Funktionen (ebenfalls) enthalten kann. Der Konstruktor (eine dedizierte Funktion) initialisiert es. Der Konstruktor kann Laufzeitparameter oder (meine Lieblings-) Kompilierungsparameter (Vorlagen) annehmen. Die Funktionen innerhalb der Klasse erhalten implizit die Klassenvariable als ersten Parameter. (Oder, oft meine Präferenz, die Klasse kann als verstecktes Singleton fungieren, sodass auf alle Daten ohne diesen Overhead zugegriffen wird).

Das Klassenobjekt kann global sein (damit Sie zur Verbindungszeit wissen, dass alles passt) oder Stack-lokal, vermutlich hauptsächlich. (Ich mag C++ Globals wegen der undefinierten globalen Initialisierungsreihenfolge nicht, also bevorzuge ich Stack-Local).

Mein bevorzugter Programmierstil ist, dass Module statische Klassen sind und ihre (statische) Konfiguration durch Template-Parameter erfolgt. Dies vermeidet fast alle Überarbeitungen und ermöglicht eine Optimierung. Kombinieren Sie dies mit einem Tool, das die Stapelgröße berechnet, und Sie können unbesorgt schlafen :)

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

Viele Embedded-/Mikrocontroller-Programmierer scheinen C++ nicht zu mögen, weil sie denken, dass es sie dazu zwingen würde, C++ vollständig zu verwenden . Das ist absolut nicht notwendig und wäre eine sehr schlechte Idee. (Sie verwenden wahrscheinlich auch nicht das gesamte 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 konstruiert, bevor Ihr Hauptstart beginnt, sodass sie möglicherweise mit ungültigen Annahmen arbeiten (wie einer Haupttaktgeschwindigkeit, die später geändert wird). Das ist ein weiterer Grund, keine globalen Code-initialisierten Objekte zu verwenden. (Ich verwende ein Linker-Skript, das fehlschlägt, wenn ich globale Code-initialisierte Objekte habe.) IMO sollten solche "Objekte" explizit erstellt und weitergegeben werden. Dies schließt ein „Warte“-Einrichtungs-„Objekt“ ein, das eine wait()-Funktion bereitstellt. In meinem Setup ist dies ein 'Objekt', das die Taktrate des Chips einstellt.

Apropos RAII: Das ist eine weitere C++-Funktion, die in kleinen eingebetteten Systemen sehr nützlich ist, wenn auch nicht aus dem Grund (Speicherfreigabe), für den sie am häufigsten in größeren Systemen verwendet wird (kleine eingebettete Systeme verwenden meistens keine dynamische Speicherfreigabe). Denken Sie an das Sperren einer Ressource: Sie können die gesperrte Ressource zu einem Wrapper-Objekt machen und den Zugriff auf die Ressource so einschränken, dass er nur über den Sperr-Wrapper 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 (Template-)Magie kann es ohne Overhead sein.


In der ursprünglichen Frage wurde C nicht erwähnt, daher meine C++-zentrierte Antwort. Wenn es wirklich C sein muss....

Sie könnten Makrotricks anwenden: Deklarieren Sie Ihre Stucts öffentlich, damit sie einen Typ haben und global zugewiesen werden können, aber verstümmeln Sie die Namen ihrer Komponenten über die Verwendbarkeit hinaus, es sei denn, ein Makro ist anders definiert, was in der .c-Datei Ihres Moduls der Fall ist. Für zusätzliche Sicherheit können Sie die Kompilierzeit beim Mangeln 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 bisschen Make-File-Trick 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 (vorhanden in C und C++) sind ein offensichtlicher Weg.

  • Werkzeuge können bis zu einem gewissen Grad helfen. Für meine Studenten gebe ich ein gebautes Skript vor, das keine Ausnahmen und kein RTI 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 Metaprogrammierung immer weniger erforderlich.

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

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, um die Leute wissen zu lassen, dass ich über C spreche.
Warum verwendest du (nur) C? Vielleicht gibt Ihnen das die Chance, sie davon zu überzeugen, zumindest C++ in Betracht zu ziehen ... Was Sie wollen, ist im Wesentlichen (ein kleiner Teil von) C++, das in C realisiert ist.
Was ich in meinem (ersten „echten“ eingebetteten Hobbyprojekt) tue, ist, einen einfachen Standardwert im Konstruktor zu initialisieren und eine separate Init-Methode für relevante Klassen zu verwenden. Ein weiterer Vorteil ist, dass ich Stub-Zeiger für Unit-Tests übergeben kann.
@Michel für ein Hobbyprojekt kannst du die Sprache frei wählen? Nimm C++!
Ich programmiere in C, weil wir FreeRTOS verwenden, das in C geschrieben ist. Wenn ich C++ verwenden wollte, müsste ich herumgehen und eine ganze Menge Zeug mit Wrappern usw. optimieren
Warum? Sie können problemlos C-Code aus C++ aufrufen, wenn Sie die "C"-Verknüpfung angeben.
@WoutervanOoijen Das tue ich, der einzige Nachteil ist, dass STM32/CubeMX standardmäßig C-Code generiert und die Header-Datei überschreibt (und das manuelle Zusammenführen von main.c mit main.cpp erforderlich ist).
OK. ein Grund mehr für mich, IDEs zu hassen, die für Sie entscheiden, was Ihr Ziel sein soll (von welchem ​​Hersteller) und wie Ihre App gebaut werden soll.
Beachten Sie, dass RAII in einer MCU aufgrund von Initialisierungsabhängigkeiten etwas kompliziert sein kann – zum Beispiel wäre es sehr natürlich, ein Modul in seinem Konstruktor zu initialisieren, aber der Konstruktor könnte aufgerufen werden, bevor die Systemuhren eingerichtet wurden.
Ich habe der Antwort einen Text hinzugefügt.
Wenn Sie diese Konzepte verwenden möchten, aber C++ nicht mögen, versuchen Sie es mit Rust - es hat die gleichen Funktionen, aber mit einer viel schöneren Syntax!
Die Frage bezieht sich auf C, also ist diese ganze C++-Werbung nicht zum Thema.
Und obwohl es durchaus möglich ist, gute C++-Programme für Embedded zu schreiben, besteht das Problem darin, dass etwa 50 % aller Programmierer von Embedded-Systemen da draußen Quacksalber, verwirrte PC-Programmierer, Arduino-Hobbyisten usw. usw. sind. Diese Art von Leuten kann a einfach nicht verwenden saubere Teilmenge von C++, auch wenn Sie es ihnen ins Gesicht erklären. Geben Sie ihnen C++ und ehe Sie sich versehen, werden sie die gesamte STL, Template-Metaprogrammierung, Ausnahmebehandlung, Mehrfachvererbung und so weiter verwenden. Und das Ergebnis ist natürlich kompletter Müll. So enden leider etwa 8 von 10 eingebetteten C++-Projekten.
@Lundin: Text hinzugefügt :)

Ich glaube, FreeRTOS (vielleicht ein anderes Betriebssystem?) tut so etwas wie das, wonach Sie suchen, indem es zwei verschiedene Versionen der Struktur definiert.
Das 'echte', das intern von den Betriebssystemfunktionen verwendet wird, und ein 'gefälschtes', das die gleiche Größe wie das 'echte' hat, aber keine nützlichen Elemente enthält (nur ein paar int dummy1und ähnliche).
Nur die „gefälschte“ Struktur wird außerhalb des Betriebssystemcodes offengelegt, und dies wird verwendet, um statischen Instanzen der Struktur Speicher zuzuweisen.
Wenn Funktionen im Betriebssystem aufgerufen werden, wird ihnen intern die Adresse der externen „falschen“ Struktur als Handle übergeben, und diese wird dann als Zeiger auf eine „echte“ Struktur typisiert, damit die Betriebssystemfunktionen das tun können, was sie tun müssen tun.

Gute Idee, ich denke, ich könnte --- #define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)])) --- BUILD_BUG_ON(sizeof(real_struct) != sizeof( fake_struct)) ----

Anonyme Struktur, dh die Struktur darf nur durch die Verwendung bereitgestellter Schnittstellenfunktionen geändert werden

Meiner Meinung nach ist dies sinnlos. Sie können dort einen Kommentar einfügen, aber es hat keinen Sinn, ihn weiter zu verstecken.

C wird niemals eine so hohe Isolation bieten, selbst wenn es keine Deklaration für die Struktur gibt, wird es leicht sein, sie versehentlich mit zB einem irrtümlichen memcpy() oder einem Pufferüberlauf zu überschreiben.

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

Reine SW-Fragen werden besser unter https://stackoverflow.com gestellt .

Das Konzept, dem Aufrufer eine Struktur mit unvollständigem Typ zur Verfügung zu stellen, wie Sie es beschreiben, wird oft als "undurchsichtiger Typ" oder "undurchsichtige Zeiger" bezeichnet - anonyme Struktur bedeutet etwas ganz anderes.

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

Was Sie also in Embedded tun, ist, einen Speicherpool bereitzustellen. Sie haben nur eine begrenzte Menge an RAM, daher ist es normalerweise kein Problem, die Anzahl der Objekte zu beschränken, die erstellt werden können.

Siehe Statische Zuordnung von undurchsichtigen Datentypen drüben bei SO.

Vielen Dank, dass Sie die Namensverwirrung meinerseits geklärt haben. Passen Sie das OP schlecht an. Ich dachte an einen Stapelüberlauf, entschied mich aber, mich speziell an eingebettete Programmierer zu wenden.