Embedded C-Datenspeichermoduldesign

8

Ich entwerfe ein eingebettetes C-Datenspeichermodul. Es wird von Dateien / Modulen eingeschlossen, die Zugriff auf diese "gemeinsamen" systemweiten Daten haben wollen. Mehrere Aufgaben aggregieren Dutzende von Eingaben (GPIO-, CAN-, I2C- / SPI- / SSP-Daten usw.) und speichern diese Werte mithilfe der API. Dann können andere Aufgaben über die API sicher auf die Daten zugreifen. Das System ist eine eingebettete App mit einem RTOS, daher werden Mutexe zum Schutz der Daten verwendet. Diese Ideen werden unabhängig von der Implementierung verwendet.

Ich habe in der Vergangenheit so etwas entworfen, und ich versuche, es zu verbessern. Ich bin gerade auf halbem Weg durch eine neue Implementierung und ich stolperte in ein paar Schluckauf und würde wirklich von einer neuen Perspektive profitieren.

Kurze Übersicht über die Anforderungen dieses Moduls:

  • Idealerweise würde es eine Schnittstelle geben, die auf die Variablen zugreifen kann (ein get, ein Satz).
  • Ich möchte verschiedene Variablentypen (Floats, Ints usw.) zurückgeben. Dies bedeutet, dass Makros wahrscheinlich benötigt werden.
  • Ich bin nicht auf Coderaum gedrängt, aber es ist immer ein Anliegen
  • Schnelle gets / sets sind absolut vorrangig (was bedeutet, in Strings zu speichern, ala xml / json ist out)
  • Zur Laufzeit müssen keine neuen Variablen hinzugefügt werden. Beim Booten ist alles statisch definiert

    Die Frage ist, wie würdest du so etwas gestalten? Aufzählungen, Strukturen, Accessoren, Makros usw.? Ich suche hier keinen Code, ich diskutiere nur allgemeine allgemeine Designideen. Wenn es eine Lösung im Internet gibt, die diese Art von Dingen anspricht, ist vielleicht sogar nur ein Link ausreichend.

        
  • Jeff Lamb 25.01.2011, 20:48
    quelle

    4 Antworten

    0

    Ich habe eine meiner nur noch nicht akzeptierten Fragen aktualisiert. Hier ist meine letzte Implementierung. Benutze dies seit über einem Jahr und es funktioniert fantastisch. Sehr einfach, Variablen hinzuzufügen und sehr wenig Overhead für den Vorteil, den es uns gibt.

    lib_data.h:

    %Vor%

    lib_data.c:

    %Vor%     
    Jeff Lamb 25.04.2012, 17:09
    quelle
    5

    Ich war selbst einige Male in dieser Situation. Jedes Mal, wenn ich damit fertig bin, "mein eigenes zu rollen", und ich leide definitiv nicht am Not Invented Here (NIH) -Syndrom. Manchmal machen der Platzbedarf, die Bearbeitungszeit oder Zuverlässigkeits- / Wiederherstellbarkeitsanforderungen nur den am wenigsten schmerzhaften Pfad.

    Also, anstatt den großen amerikanischen Roman zu diesem Thema zu schreiben, werde ich hier nur ein paar Gedanken aussprechen, da Ihre Frage ziemlich weit gefasst ist (aber danke, dass Sie zumindest eine Frage stellen und Hintergrund liefern). p>

    Ist C ++ auf dem Tisch? Inline-Funktionen, Vorlagen und einige der Boost-Bibliotheken könnten hier nützlich sein. Aber ich schätze, das ist gerade C.

    Wenn Sie C99 verwenden, können Sie zumindest Inline-Funktionen verwenden, die einen Schritt über die Makros hinausgehen, wenn es um die Typsicherheit geht.

    Vielleicht möchten Sie darüber nachdenken, mehrere Mutexe zu verwenden, um verschiedene Teile der Daten zu schützen. Auch wenn die Aktualisierungen kurz sind, sollten Sie die Daten in Abschnitte unterteilen (z. B. Konfigurationsdaten, Initdaten, Fehlerprotokolldaten, Trace-Daten usw.) und jedem einen eigenen Mutex geben, um die Trichter- / Drosselpunkte zu reduzieren / p>

    Sie könnten auch in Erwägung ziehen, den gesamten Zugriff auf die Daten durch eine Serveraufgabe zu führen. Alle liest & amp; Schreibvorgänge durchlaufen eine API, die mit der Server-Task kommuniziert. Die Serveraufgaben ziehen Lese- & amp; schreibt Anfragen in der Reihenfolge ihrer Warteschlange, behandelt sie schnell, indem sie in einen RAM-Spiegel schreibt, Antworten sendet, wenn nötig (zumindest für Leseanforderungen), und puffert dann Daten im Hintergrund an NVM, falls erforderlich. Klingt schwergewichtig im Vergleich zu einfachen Mutexen, hat aber in bestimmten Anwendungsfällen Vorteile. Ich weiß nicht genug über Ihre Bewerbung, um zu wissen, ob dies eine Möglichkeit ist.

    Eine Sache, die ich sagen werde, ist die Idee, mit einem Tag (zB eine Liste von enums wie CONFIG_DATA, ADDRESS_DATA, etc.) zu erhalten / setzen, ist ein großer Schritt vorwärts von der direkten Adressierung von Daten (zB "gib mir die 256 Bytes bei Adresse ox42000). Ich habe gesehen, dass viele Shops große Schmerzen erleiden, wenn das ganze physikalische Adressierungsschema endlich zusammenbricht und sie müssen neu facettieren / neu gestalten. Versuchen Sie, das "was" von dem "entkoppelt" zu halten. wie "- Kunden sollten nicht wissen oder sich darum kümmern müssen, wo die Dinge gespeichert sind, wie groß sie sind, etc. (Sie wissen vielleicht schon alles, tut mir leid, wenn das so ist, ich sehe es ständig ...)

    >

    Eine letzte Sache. Du hast Mutexe erwähnt. Vorsicht vor Inversion der Priorität ... das kann dazu führen, dass diese "schnellen Zugriffe" in manchen Fällen sehr lange dauern. Bei den meisten Kernel-Mutex-Implementierungen können Sie dies berücksichtigen, aber oft ist es nicht standardmäßig aktiviert. Nochmal, Entschuldigung, wenn das alte Nachrichten sind ...

        
    Dan 26.01.2011 00:45
    quelle
    2

    Einige Ansätze, mit denen ich Erfahrungen gemacht habe und die ich für ihre eigenen Bedürfnisse gefunden habe. Ich schreibe nur meine Gedanken zu diesem Thema, hoffe, das gibt Ihnen einige Ideen, mit denen Sie gehen können ...

    • Für komplexere Daten mit vielen Abhängigkeiten und Einschränkungen habe ich festgestellt, dass ein Modul auf höherer Ebene in der Regel vorzuziehen ist, selbst wenn es auf Kosten der Geschwindigkeit geht. Es erspart Ihnen Kopfschmerzen, und es ist normalerweise die Weg zu gehen in meiner Erfahrung, wenn Sie wirklich harte Bedingungen haben. Die Sache ist, den Großteil Ihrer "statischen" Daten zu speichern, die sich in dieser Art von Modul nicht viel ändern, und von der Benutzerfreundlichkeit und den "komplexeren" Funktionen (z. B. Datenvalidierung, referenzielle Integrität, Abfragen) zu profitieren , Transaktionen, Privilegien usw.). Wenn die Geschwindigkeit in einigen Bereichen jedoch entscheidend ist, möchten Sie sie möglicherweise aus diesem Modul heraushalten oder ein einfaches "Mirroring" -Schema implementieren (bei dem Sie beispielsweise regelmäßig zwischen Ihren Lightweight-Mirrors und der Hauptkonfigurations-DB hin- und herwechseln könnten) einige Leerlaufaufgabe). Da ich mit Embedded-Systemen einige Erfahrungen mit diesem Problem gemacht habe, habe ich festgestellt, dass sqlite am einfachsten zu verwenden ist. Wenn ich Ihre Kommentare sehe, kann ich feststellen, dass die Portierung kein großer Deal ist, wenn Sie die C-Standardbibliothek für Ihre Plattform unterstützt haben. Ich habe das System in etwa einer Woche auf eine PowerPC-Architektur portiert, und wir hatten reinen internen proprietären Code. Es ging hauptsächlich darum, die OS-Abstraktionsschicht herauszufinden und von dort aus reibungslos zu segeln. Ich empfehle sqlite sehr für diesen .
    • Wenn das Obige immer noch zu schwer ist (entweder in Bezug auf die Leistung oder vielleicht nur zu viel), können Sie die meisten wichtigen Vorteile durch die Verwendung eines grundlegenden Schlüssel / Wert-Systems erzielen. Die Idee ist ähnlich zu dem, was hier zuvor erwähnt wurde: Definieren Sie Ihre Schlüssel über Enums, speichern Sie Ihre Daten in einer Tabelle von gleich großen (z. B. 64 Bit) Werten und lassen Sie jede Variable in einer Zelle residieren. Ich finde, dass dies für einfachere Projekte funktioniert, mit nicht so vielen Arten von Konfigurationseinträgen, während Sie Ihre Flexibilität behalten und das System immer noch leicht zu entwickeln und zu verwenden ist. Ein paar Hinweise dafür:
      • Teilen Sie Ihre Listen in Gruppen oder Kategorien auf, die jeweils einen eigenen Wertebereich haben. Ein Schema, das ich mag, besteht darin, die Gruppen selbst in einer Enumeration zu definieren und diese Werte zum Starten des ersten Wertes jeder der Gruppen (EntryInGroupID = (groupid << 16) | serial_entry_id) .
      • zu verwenden
      • Es ist einfach, " Tabellen " mit diesem Schema zu implementieren (wo Sie mehr als einen Eintrag für jede Anforderung oder Konfiguration haben, z. B. Routing-Tabellen). Sie können das Design verallgemeinern, indem Sie alles zu einer "Tabelle" machen. Wenn Sie einen einzelnen Wert definieren möchten, erstellen Sie einfach eine Tabelle mit einem Eintrag. In eingebetteten Systemen würde ich empfehlen, alles im Voraus zuzuordnen, entsprechend der maximalen Anzahl von Einträgen, die für diese Tabelle möglich sind, und "einen Teil davon verwenden". Auf diese Weise haben Sie den schlimmsten Fall abgedeckt und sind deterministisch.
      • Eine andere Sache, die ich sehr nützlich fand, war Hooks für alles. Diese Hooks können unter Verwendung von Function Pointers (für jedes Verhalten / jede Strategie / Operation) implementiert werden, die durch eine "descriptor" -Struktur gegeben werden, die zum Definieren des Dateneintrags verwendet wird. Normalerweise verwenden Ihre Einträge die "Standard" Hook-Funktionen zum Lesen / Schreiben (z. B. direkten Speicherzugriff oder Dateizugriff) und NULL-out die anderen möglichen Hooks (z. B. "validieren", "Standardwert", "Benachrichtigung für Änderung " und derartige). Hin und wieder, wenn Sie einen komplizierteren Fall haben, können Sie das Haken-System, das Sie eingepflanzt haben, mit geringen Entwicklungskosten nutzen. Dieses Hook-System kann Ihnen inhärent eine "Hardware-Abstraktionsschicht" für die Speicherung der Werte selbst liefern und auf die Daten aus dem Speicher, den Registern oder Dateien mit der gleichen Leichtigkeit zugreifen. Eine "notify for changes" -Registrierungs- / Aufhebensregistrierungsfunktion kann auch wirklich helfen, Aufgaben über Konfigurationsänderungen zu melden und ist nicht so schwer hinzuzufügen.
    • Für Ultra-Echtzeit-Anforderungen würde ich Ihnen nur empfehlen, es einfach zu halten und Ihre Daten in Strukturen zu speichern . Es ist so einfach wie möglich und meistert alle Ihre Bedürfnisse. Es ist jedoch schwierig, die Parallelität in diesem System zu handhaben, so dass Sie entweder alle Zugriffe mit Mutexen, wie Sie es vorgeschlagen haben, einbinden müssen oder einfach einen Mirror für jede "Aufgabe" behalten müssen. Wenn dies tatsächlich Ultra-Echtzeit ist, würde ich für die Spiegelung mit periodischen Synchronisierungen im gesamten System gehen. Sowohl dieses System als auch das oben vorgeschlagene System machen es sehr einfach, Ihre Daten zu serialisieren (da sie immer im Binärformat gespeichert werden), was die Dinge vereinfacht und die Dinge einfach hält (alles, was Sie für die Serialisierung benötigen, ist ein Mempy mit eine Besetzung oder zwei). Es kann auch hilfreich sein, von Zeit zu Zeit Verbindungen zu verwenden, und einige Optimierer gehen gut damit um, aber es hängt wirklich von der Architektur und der Toolchain ab, mit der Sie arbeiten.
    • Wenn Sie wirklich besorgt sind, dass Sie Ihre Daten für neue Schemas aktualisieren müssen, sollten Sie ein " upgrader-Modul " in Betracht ziehen, das mit mehreren Schemas konfiguriert werden kann und verwendet werden kann alte Formate beim Aufruf in das neue konvertieren. Sie können Codegenerierungstechniken aus XML-Schemas verwenden, um die Datenbank selbst zu generieren (z. B. in Structs und Enums), und XML-Tools zum Ausführen des "Conversion-Codes" nutzen, indem Sie XSLT oder einfach nur Python-artige Skripts schreiben. Möglicherweise möchten Sie die Konvertierung außerhalb des Ziels (z. B. am Host) mit einem Skript auf höherer Ebene durchführen, anstatt diese Logik in das Ziel selbst zu stopfen.
    • Für Datentyp Sicherheit können Sie viele Template- und Meta-Programmier-Assistenten verwenden. Obwohl es als Framework-Coder schwer zu pflegen ist, erleichtert es den Benutzern des Systems die Arbeit. Wenn richtig codiert (was schwierig ist!), Kann es Ihnen viele Zyklen sparen und nicht viel Code-Overhead mit sich bringen (mit Inlining, expliziter Template-Instantiierung und viel Sorgfalt). Wenn Ihr Compiler nicht so gut ist (was bei eingebetteten Plattform-Compilern normalerweise der Fall ist), verwenden Sie einfach Wrapper-Makros für die Konvertierung.
    • Über das Concurrency-Problem finde ich im Allgemeinen, je nach dem Gesamtdesign des Systems, auf eine der folgenden Methoden zurückzugreifen
      • Wie Sie vorgeschlagen haben, wickeln Sie das Ganze mit einem Mutex zusammen und hoffen auf das Beste. Sie können mehr Präzision erreichen, indem Sie Sperren pro Tabelle oder sogar pro Eintrag hinzufügen, aber dies hat seine eigenen Probleme und ich würde empfehlen, es einfach zu halten und bei einem globalen Mutex zu bleiben. Wenn Sie Replikationen für den harten Echtzeit-Kram haben, wird dies wahrscheinlich Ihre Leistung nicht beeinträchtigen, da Sie die Synchronisierung nur gelegentlich durchführen.
      • Entwerfen Sie Ihre Datenbank als " single task " und übertragen Sie den gesamten Zugriff darauf über Nachrichten (MQs, Sockets, was auch immer). Dies funktioniert besser für die unkritischen Pfaddaten (wieder) und ist eine einfache Lösung. Wenn es nicht zu viele gleichzeitige Zugriffe gibt, könnte dies der richtige Weg sein. Beachten Sie, dass die Leistung von Lese- / Schreibvorgängen nicht so teuer ist (da Sie keine Daten kopieren), und wenn Sie mit den Prioritäten Ihrer Aufgabe richtig spielen, kann dies der Fall sein eine einfache Lösung, die die meisten Ihrer Bedürfnisse löst (durch Vermeidung des Problems des gleichzeitigen Zugriffs).

    Die meisten Systeme, mit denen ich gearbeitet habe, benötigten irgendeine Art von Konfigurationsmodul, normalerweise über flüchtige und nichtflüchtige Speicher. Es ist sehr einfach (und sogar verlockend), dies zu überdenken, oder eine Overkill-Lösung zu bringen, wo sie nicht benötigt wird. Ich habe festgestellt, dass der Versuch, zu diesen Themen "zukunftssicher" zu sein, in vielen Fällen nur eine Verschwendung von Zeit sein wird. Normalerweise ist der einfachste Weg das Beste (in Echtzeit und eingebetteten Systemen sowieso). Außerdem treten Leistungseinbußen auf, wenn Sie das System skalieren, und manchmal bleibt es bei der einfacheren und schnelleren Lösung.

    Angesichts der von Ihnen gelieferten Informationen und der obigen Analyse empfehle ich Ihnen daher, mit dem Schlüssel-Wert- oder Direkten-Struktur-Ansatz zu gehen. Ich bin ein persönlicher Fan des "single task" -Ansatzes für meine Systeme, aber es gibt wirklich keine absolute Antwort dafür, und es bedarf einer tieferen Analyse. Für diese Lösungen, die ich nach einer "generischen" und gebrauchsfertigen Lösung gesucht habe, finde ich mich immer selbst in 1-2 Wochen implementiert und erspare mir viele Kopfschmerzen.

    Ich hoffe, die Antwort war nicht übertrieben: -)

        
    scooz 13.02.2011 00:33
    quelle
    1

    Normalerweise verwende ich eine einfache wörterbuchähnliche API mit einem int als Schlüssel und einem festen Wert. Dies wird schnell ausgeführt, verwendet eine sehr kleine Menge an Programm-RAM und hat eine vorhersehbare Daten-RAM-Nutzung. Mit anderen Worten, die API der untersten Ebene sieht folgendermaßen aus:

    %Vor%

    Tasten werden zu einer Liste von Konstanten:

    %Vor%

    Sie umgehen verschiedene Datentypen durch Casting. Sucks, aber Sie können Makros schreiben, um den Code ein wenig sauberer zu machen:

    %Vor%

    Das Erzielen der Typensicherheit ist schwierig ohne das Schreiben einer separaten Makro- oder Zugriffsfunktion für jedes Element. Bei einem Projekt benötigte ich eine Validierung der Eingabedaten, und dies wurde zum Typ-Sicherheitsmechanismus.

    Wie Sie die physische Speicherung von Daten strukturieren, hängt davon ab, wie viel Datenspeicher, Programmspeicher, Zyklen und separate Schlüssel Sie haben. Wenn Sie viel Programmplatz haben, hacken Sie den Schlüssel, um einen kleineren Schlüssel zu erhalten, den Sie direkt in einem Array nachschlagen können. Normalerweise mache ich den zugrunde liegenden Speicher wie folgt:

    %Vor%

    und iteriere durch. Für mich war das sogar auf sehr kleinen (8-Bit) Mikrocontrollern schnell genug, obwohl es für Sie möglicherweise nicht passt, wenn Sie viele Elemente haben.

    Denken Sie daran, dass Ihr Compiler die Schreibvorgänge wahrscheinlich inline oder optimiert, sodass Zyklen pro Zugriff möglicherweise niedriger sind als erwartet.

        
    Ian Howson 27.01.2011 23:13
    quelle

    Tags und Links