Possibilités d'allocation de mémoire pour la conception de firmware modulaire en C

16

les approches modulaires sont assez pratiques en général (portables et propres), donc j'essaie de programmer des modules aussi indépendants que possible des autres modules. La plupart de mes approches sont basées sur une structure qui décrit le module lui-même. Une fonction d'initialisation définit les paramètres principaux, puis un gestionnaire (pointeur vers une structure descriptive) est transmis à la fonction appelée dans le module.

En ce moment, je me demande quelle est la meilleure approche d'allocation de mémoire pour la structure décrivant un module. Si possible, j'aimerais ce qui suit:

  • Structure opaque, donc la structure ne peut être modifiée que par l'utilisation des fonctions d'interface fournies
  • Plusieurs instances
  • mémoire allouée par l'éditeur de liens

Je vois les possibilités suivantes, qui entrent en conflit avec l'un de mes objectifs:

déclaration globale

plusieurs instances, allcoted par l'éditeur de liens, mais la structure n'est pas opaque

(#includes)
module_struct module;

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

malloc

structure opaque, plusieurs instances, mais allcotion sur le tas

dans module.h:

typedef module_struct Module;

dans la fonction module.c init, malloc et pointeur de retour sur la mémoire allouée

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

dans main.c

(#includes)
Module *module;

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

déclaration dans le module

structure opaque, allouée par l'éditeur de liens, uniquement un nombre prédéfini d'instances

garder l'intégralité de la structure et de la mémoire interne au module et ne jamais exposer un gestionnaire ou une struct.

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

Existe-t-il une option pour les combiner d'une manière ou d'une autre pour une structure opaque, un éditeur de liens au lieu d'une allocation de tas et plusieurs / un nombre quelconque d'instances?

Solution

comme proposé dans certaines réponses ci-dessous, je pense que la meilleure façon est de:

  1. réserver de l'espace pour les modules MODULE_MAX_INSTANCE_COUNT dans le fichier source des modules
  2. ne définissez pas MODULE_MAX_INSTANCE_COUNT dans le module lui-même
  3. ajoutez une erreur #ifndef MODULE_MAX_INSTANCE_COUNT # au fichier d'en-tête des modules pour vous assurer que l'utilisateur des modules est conscient de cette limitation et définit le nombre maximal d'instances souhaitées pour l'application
  4. à l'initialisation d'une instance, renvoyez soit l'adresse mémoire (* void) de la structure descriptive, soit l'index des modules (ce que vous préférez)
L. Heinrichs
la source
12
La plupart des concepteurs FW intégrés évitent l'allocation dynamique pour garder l'utilisation de la mémoire déterministe et simple. Surtout s'il est bare-metal et n'a pas de système d'exploitation sous-jacent pour gérer la mémoire.
Eugene Sh.
Exactement, c'est pourquoi je veux que l'éditeur de liens fasse les allocations.
L. Heinrichs
4
Je ne suis pas sûr de comprendre ... Comment pouvez-vous avoir la mémoire allouée par l'éditeur de liens si vous avez un nombre dynamique d'instances? Cela me semble assez orthogonal.
jcaron
Pourquoi ne pas laisser l'éditeur de liens allouer un grand pool de mémoire et faire vos propres allocations à partir de cela, ce qui vous donne également l'avantage d'un allocateur sans frais généraux. Vous pouvez rendre l'objet pool statique au fichier avec la fonction d'allocation afin qu'il soit privé. Dans une partie de mon code, je fais toutes les allocations dans les différentes routines d'initialisation, puis j'imprime ensuite combien a été alloué, donc dans la compilation de production finale, j'ai défini le pool à cette taille exacte.
Lee Daniel Crocker
2
Si c'est une décision au moment de la compilation, vous pouvez simplement définir le nombre dans votre Makefile ou équivalent, et vous êtes tous ensemble. Le numéro ne serait pas dans la source du module, mais serait spécifique à l'application, et vous utilisez simplement un numéro d'instance comme paramètre.
jcaron

Réponses:

4

Existe-t-il une option pour les combiner d'une manière ou d'une autre pour une structure anonyme, un éditeur de liens au lieu d'une allocation de segment de mémoire et plusieurs / un nombre quelconque d'instances?

Bien sûr que oui. Tout d'abord, cependant, reconnaissez que le "n'importe quel nombre" d'instances doit être fixe, ou au moins une limite supérieure établie, au moment de la compilation. Il s'agit d'une condition préalable pour que les instances soient allouées statiquement (ce que vous appelez «allocation de l'éditeur de liens»). Vous pouvez rendre le nombre réglable sans modification de source en déclarant une macro qui le spécifie.

Ensuite, le fichier source contenant la déclaration de structure réelle et toutes ses fonctions associées déclare également un tableau d'instances avec liaison interne. Il fournit soit un tableau, avec liaison externe, de pointeurs vers les instances, soit une fonction pour accéder aux différents pointeurs par index. La variation de fonction est un peu plus modulaire:

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

Je suppose que vous savez déjà comment l'en-tête déclarerait alors la structure comme un type incomplet et déclarerait toutes les fonctions (écrites en termes de pointeurs vers ce type). Par exemple:

module.h

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

Maintenant , struct moduleest opaque dans les unités de traduction autres que module.c, * et vous pouvez accéder et utiliser au nombre de cas définis au moment de la compilation sans allocation dynamique.


* Sauf si vous copiez sa définition, bien sûr. Le fait est que module.hcela ne fait pas cela.

John Bollinger
la source
Je pense que c'est une conception étrange de passer l'index de l'extérieur de la classe. Lorsque j'implémente des pools de mémoire comme celui-ci, je laisse l'index être un compteur privé, augmentant de 1 pour chaque instance allouée. Jusqu'à ce que vous atteigniez "NUM_MODULE_INSTANCES", où le constructeur renverra une erreur de mémoire insuffisante.
Lundin
C'est un bon point, @Lundin. Cet aspect de la conception suppose que les indices ont une signification inhérente, ce qui peut ou non être le cas. Il est le cas, quoique trivialement si, pour le cas à partir de l'OP. Une telle signification, si elle existe, pourrait être davantage soutenue en fournissant un moyen d'obtenir un pointeur d'instance sans initialisation.
John Bollinger
Donc, fondamentalement, vous réservez de la mémoire pour n modules, quel que soit le nombre qui sera utilisé, puis renvoyez un pointeur vers l'élément inutilisé suivant si l'application l'initialise. Je suppose que ça pourrait marcher.
L. Heinrichs
@ L.Heinrichs Oui, car les systèmes embarqués sont de nature déterministe. Il n'y a pas de «quantité infinie d'objets» ni de «quantité inconnue d'objets». Les objets sont souvent aussi des singletons (pilotes matériels), il n'y a donc souvent pas besoin du pool de mémoire, car il n'y aura qu'une seule instance de l'objet.
Lundin
Je suis d'accord pour la plupart des cas. La question avait également un certain intérêt théorique. Mais je pourrais utiliser des centaines de capteurs de température à 1 fil s'il y a suffisamment d'E / S disponibles (comme le seul exemple que je peux trouver maintenant).
L. Heinrichs
22

Je programme de petits micro-contrôleurs en C ++, ce qui permet de réaliser exactement ce que l'on veut.

Ce que vous appelez un module est une classe C ++, il peut contenir des données (accessibles de l'extérieur ou non) et des fonctions (de même). Le constructeur (une fonction dédiée) l'initialise. Le constructeur peut prendre des paramètres d'exécution ou (mon préféré) des paramètres de compilation (modèle). Les fonctions de la classe obtiennent implicitement la variable de classe comme premier paramètre. (Ou, souvent ma préférence, la classe peut agir comme un singleton caché, donc toutes les données sont accessibles sans cette surcharge).

L'objet de classe peut être global (donc vous savez au moment de la liaison que tout ira bien), ou pile-local, vraisemblablement dans le principal. (Je n'aime pas les globaux C ++ en raison de l'ordre d'initialisation global non défini, donc je préfère stack-local).

Mon style de programmation préféré est que les modules sont des classes statiques et que leur configuration (statique) est basée sur des paramètres de modèle. Cela évite presque tout dépassement et permet une optimisation. Combinez cela avec un outil qui calcule la taille de la pile et vous pouvez dormir sans soucis :)

Mon exposé sur cette façon de coder en C ++: Objets? Non merci!

Beaucoup de programmeurs embarqués / microcontrôleurs semblent détester le C ++ car ils pensent que cela les forcerait à utiliser tout le C ++. Ce n'est absolument pas nécessaire et ce serait une très mauvaise idée. (Vous n'utilisez probablement pas tout C non plus! Pensez tas, virgule flottante, setjmp / longjmp, printf, ...)


Dans un commentaire, Adam Haun mentionne RAII et l'initialisation. IMO RAII a plus à voir avec la déconstruction, mais son point est valable: les objets globaux seront construits avant le démarrage de votre principal, donc ils pourraient fonctionner sur des hypothèses invalides (comme une vitesse d'horloge principale qui sera modifiée ultérieurement). C'est une raison de plus de NE PAS utiliser d'objets globaux initialisés par code. (J'utilise un script de l'éditeur de liens qui échoue lorsque j'ai des objets globaux initialisés par du code.) IMO de tels «objets» doivent être explicitement créés et transmis. Cela inclut un «objet» d'installation «en attente» qui fournit une fonction wait (). Dans ma configuration, c'est un «objet» qui définit la vitesse d'horloge de la puce.

Parler de RAII: c'est une fonctionnalité C ++ de plus qui est très utile dans les petits systèmes embarqués, bien que ce ne soit pas pour la raison (désallocation de mémoire) qu'elle soit la plus utilisée dans les systèmes plus grands (les petits systèmes embarqués n'utilisent généralement pas de désallocation de mémoire dynamique). Pensez à verrouiller une ressource: vous pouvez faire de la ressource verrouillée un objet wrapper et restreindre l'accès à la ressource uniquement via le wrapper de verrouillage. Lorsque l'encapsuleur sort de la portée, la ressource est déverrouillée. Cela empêche l'accès sans verrouillage et rend beaucoup plus improbable d'oublier le déverrouillage. avec un peu de magie (modèle), cela peut être nul.


La question d'origine ne mentionnait pas C, d'où ma réponse centrée sur C ++. S'il doit vraiment être C ....

Vous pouvez utiliser la macro astuce: déclarez vos structures publiquement, afin qu'elles aient un type et puissent être allouées globalement, mais modifiez les noms de leurs composants au-delà de l'utilisabilité, à moins qu'une macro ne soit définie différemment, ce qui est le cas dans le fichier .c de votre module. Pour plus de sécurité, vous pouvez utiliser le temps de compilation dans le mangling.

Ou avoir une version publique de votre structure qui n'a rien d'utile, et avoir la version privée (avec des données utiles) uniquement dans votre fichier .c, et affirmer qu'elles sont de la même taille. Un peu de ruse de création de fichier pourrait automatiser cela.


@Lundins commente les mauvais programmeurs (intégrés):

  • Le type de programmeur que vous décrivez gâcherait probablement dans n'importe quelle langue. Les macros (présentes en C et C ++) sont un moyen évident.

  • L'outillage peut aider dans une certaine mesure. Pour mes étudiants, je mandat un script construit qui spécifie pas d'exceptions, pas de rtti, et donne une erreur de l'éditeur de liens lorsque le tas est utilisé ou des globaux initialisés par code sont présents. Et il spécifie warning = error et active presque tous les avertissements.

  • J'encourage l'utilisation de modèles, mais avec constexpr et les concepts, la métaprogrammation est de moins en moins requise.

  • "programmeurs Arduino confus" Je voudrais beaucoup remplacer le style de programmation Arduino (câblage, réplication de code dans les bibliothèques) par une approche C ++ moderne, qui peut être plus simple, plus sécurisée et produire un code plus rapide et plus petit. Si seulement j'avais le temps et le pouvoir ...

Wouter van Ooijen
la source
Merci pour cette réponse! L'utilisation de C ++ est une option, mais nous utilisons C dans mon entreprise (ce que je n'ai pas mentionné explicitement). J'ai mis à jour la question pour que les gens sachent que je parle de C.
L. Heinrichs
Pourquoi utilisez-vous (uniquement) C? Peut-être que cela vous donne la chance de les convaincre de considérer au moins le C ++ ... Ce que vous voulez, c'est essentiellement (une petite partie du) C ++ réalisé en C.
Wouter van Ooijen
Ce que je fais dans mon (premier «vrai» projet de hobby embarqué) est l'initialisation d'un défaut simple dans le constructeur, et j'utilise une méthode Init distincte pour les classes pertinentes. Un autre avantage est que je peux passer des pointeurs de talon pour des tests unitaires.
Michel Keijzers du
2
@Michel pour un projet hobby vous êtes libre de choisir la langue? Prenez C ++!
Wouter van Ooijen
4
Et s'il est en effet parfaitement possible d'écrire de bons programmes C ++ pour embarqués, le problème est que plus de 50% de tous les programmeurs de systèmes embarqués sont des charlatans, des programmeurs PC confus, des amateurs d'Arduino, etc.Ce genre de personnes ne peut tout simplement pas utiliser un sous-ensemble propre de C ++, même si vous l'expliquez en face. Donnez-leur du C ++ et avant de le savoir, ils utiliseront l'ensemble de la STL, la métaprogrammation des modèles, la gestion des exceptions, l'héritage multiple, etc. Et le résultat est bien sûr une poubelle complète. C'est malheureusement ainsi que 8 projets C ++ embarqués sur 10 finissent.
Lundin
7

Je crois que FreeRTOS (peut-être un autre système d'exploitation?) Fait quelque chose comme ce que vous recherchez en définissant 2 versions différentes de la structure.
Le `` vrai '', utilisé en interne par les fonctions du système d'exploitation, et un `` faux '' qui est de la même taille que le `` vrai '', mais n'a pas de membres utiles à l'intérieur (juste un tas de int dummy1et similaire).
Seule la structure «fausse» est exposée en dehors du code du système d'exploitation, et elle est utilisée pour allouer de la mémoire aux instances statiques de la structure.
En interne, lorsque des fonctions du système d'exploitation sont appelées, l'adresse de la structure externe `` fausse '' est transmise en tant que poignée, puis transtypée en tant que pointeur vers une structure `` réelle '' afin que les fonctions du système d'exploitation puissent faire ce dont elles ont besoin pour faire.

brhans
la source
Bonne idée, je suppose que je pourrais utiliser --- #define BUILD_BUG_ON (condition) ((void) sizeof (char [1 - 2 * !! (condition)]))) BUILD_BUG_ON (sizeof (real_struct)! = Sizeof ( fake_struct)) ----
L. Heinrichs
2

Structure anonyme, la structure ne peut donc être modifiée que par l'utilisation des fonctions d'interface fournies

À mon avis, c'est inutile. Vous pouvez y mettre un commentaire, mais inutile d'essayer de le cacher davantage.

C ne fournira jamais une isolation aussi élevée, même s'il n'y a pas de déclaration pour la structure, il sera facile de l'écraser accidentellement avec par exemple memcpy () erroné ou un dépassement de tampon.

Au lieu de cela, donnez simplement un nom à la structure et faites confiance à d'autres personnes pour écrire également du bon code. Cela facilitera également le débogage lorsque la structure a un nom que vous pouvez utiliser pour y faire référence.

jpa
la source
2

Il est préférable de poser des questions sur Pure SW sur /programming/ .

Le concept consistant à exposer une structure de type incomplet à l'appelant, comme vous le décrivez, est souvent appelé "type opaque" ou "pointeurs opaques" - une structure anonyme signifie tout autre chose.

Le problème avec cela est que l'appelant ne pourra pas allouer des instances de l'objet, seulement des pointeurs vers celui-ci. Sur un PC, vous utiliseriez mallocà l'intérieur du "constructeur" des objets, mais malloc est un no-go dans les systèmes embarqués.

Donc, ce que vous faites dans l'embarqué, c'est de fournir un pool de mémoire. Vous avez une quantité limitée de RAM, donc restreindre le nombre d'objets pouvant être créés n'est généralement pas un problème.

Voir Allocation statique des types de données opaques sur SO.

Lundin
la source
Ou merci d'avoir clarifié la confusion de nommage de ma part, mal régler l'OP. Je pensais aller au débordement de pile, mais j'ai décidé que je voudrais cibler spécifiquement les programmeurs intégrés.
L. Heinrichs