Éviter les variables globales lors de l'utilisation d'interruptions dans les systèmes embarqués

13

Existe-t-il un bon moyen de mettre en œuvre la communication entre un ISR et le reste du programme pour un système embarqué qui évite les variables globales?

Il semble que le modèle général consiste à avoir une variable globale qui est partagée entre l'ISR et le reste du programme et utilisée comme indicateur, mais cette utilisation de variables globales va à l'encontre de moi. J'ai inclus un exemple simple utilisant des ISR de style avr-libc:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

Je ne peux pas voir de loin ce qui est essentiellement un problème de portée; toutes les variables accessibles à la fois par l'ISR et le reste du programme doivent être intrinsèquement globales, sûrement? Malgré cela, j'ai souvent vu des gens dire des choses comme «les variables globales sont un moyen de mettre en œuvre la communication entre les ISR et le reste du programme» (c'est moi qui souligne), ce qui semble impliquer qu'il existe d'autres méthodes; s'il existe d'autres méthodes, quelles sont-elles?


la source
1
Ce n'est pas nécessairement vrai que TOUT le reste du programme aurait accès; si vous avez déclaré la variable comme statique, seul le fichier dans lequel la variable a été déclarée la verrait. Il n'est pas du tout difficile d'avoir des variables qui sont visibles dans l'ensemble d'un fichier, mais pas le reste du programme et cela peut aider.
DiBosco
1
à côté, l'indicateur doit être déclaré volatile, car vous l'utilisez / le modifiez en dehors du flux de programme normal. Cela oblige le compilateur à n'optimiser aucun indicateur de lecture / écriture et à exécuter l'opération de lecture / écriture réelle.
next-hack
@ next-hack Oui, c'est tout à fait correct, désolé, j'essayais juste de trouver un exemple rapidement.

Réponses:

18

Il existe une méthode standard de facto pour ce faire (en supposant une programmation C):

  • Les interruptions / ISR sont de bas niveau et ne doivent donc être implémentées que dans le pilote lié au matériel qui génère l'interruption. Ils ne devraient pas être situés ailleurs que dans ce pilote.
  • Toutes les communications avec l'ISR sont effectuées uniquement par le conducteur et le conducteur. Si d'autres parties du programme ont besoin d'accéder à ces informations, il doit le demander au pilote via les fonctions setter / getter ou similaires.
  • Vous ne devez pas déclarer de variables "globales". Variables de portée de fichier de signification globale avec liaison externe. C'est-à-dire: des variables qui pourraient être appelées avec un externmot-clé ou simplement par erreur.
  • Au lieu de cela, pour forcer l'encapsulation privée à l'intérieur du pilote, toutes ces variables partagées entre le pilote et l'ISR doivent être déclarées static. Une telle variable n'est pas globale mais limitée au fichier où elle est déclarée.
  • Pour éviter les problèmes d'optimisation du compilateur, ces variables doivent également être déclarées comme volatile. Remarque: cela ne donne pas d'accès atomique ni ne résout la rentrée!
  • Une certaine manière de mécanisme de ré-entrée est souvent nécessaire dans le pilote, au cas où l'ISR écrit dans la variable. Exemples: désactivation d'interruption, masque d'interruption global, sémaphore / mutex ou lectures atomiques garanties.
Lundin
la source
Remarque: vous devrez peut-être exposer le prototype de fonction ISR via un en-tête, afin de le placer dans une table vectorielle située dans un autre fichier. Mais ce n'est pas un problème tant que vous documentez qu'il s'agit d'une interruption et que le programme ne doit pas l'appeler.
Lundin
Que diriez-vous, si le contre-argument était l'augmentation de la surcharge (et du code supplémentaire) de l'utilisation des fonctions setter / getting? J'y suis allé moi-même, en pensant aux normes de code pour nos appareils embarqués 8 bits.
Leroy105
2
@ Leroy105 Le langage C a désormais pris en charge les fonctions en ligne pour une éternité. Bien que même l'utilisation de inlinedevienne obsolète, car les compilateurs optimisent de plus en plus le code. Je dirais que s'inquiéter de la surcharge est une "optimisation prématurée" - dans la plupart des cas, la surcharge n'a pas d'importance, si elle est même présente dans le code machine.
Lundin
2
Cela étant dit, dans le cas de l'écriture de pilotes ISR, 80 à 90% de tous les programmeurs (sans exagérer ici) se trompent toujours. Le résultat est des bogues subtils: drapeaux mal effacés, optimisation incorrecte du compilateur à partir de volatiles manquants, conditions de concurrence, performances en temps réel moche, débordements de pile, etc. encore augmenté. Concentrez-vous sur l'écriture d'un pilote sans bug avant de vous soucier de choses d'intérêt périphérique, comme si les setter / getters introduisent un tout petit peu de surcharge.
Lundin
10
cette utilisation des variables globales va à l'encontre de moi

Tel est le vrai problème. Passer à autre chose.

Maintenant, avant que les genoux ne se plaignent immédiatement de la façon dont cela est impur, permettez-moi de nuancer un peu. Il y a certainement un danger à utiliser des variables globales à outrance. Mais ils peuvent également augmenter l'efficacité, ce qui compte parfois dans les petits systèmes aux ressources limitées.

La clé est de penser à quand vous pouvez raisonnablement les utiliser et ne risquez pas de vous causer des ennuis, par rapport à un bug qui ne fait qu'attendre. Il y a toujours des compromis. Bien qu'éviter généralement les variables globales pour communiquer entre l'interruption et le code de premier plan est une ligne directrice insoutenable, la prendre, comme la plupart des autres lignes directrices, à un extrême religieux est contre-productif.

Voici quelques exemples où j'utilise parfois des variables globales pour transmettre des informations entre le code d'interruption et de premier plan:

  1. Compteurs d'horloge gérés par l'interruption de l'horloge système. J'ai généralement une interruption d'horloge périodique qui s'exécute toutes les 1 ms. Cela est souvent utile pour différents moments du système. Une façon d'extraire ces informations de la routine d'interruption pour que le reste du système puisse les utiliser est de garder un compteur de tick d'horloge global. La routine d'interruption incrémente le compteur à chaque tick d'horloge. Le code de premier plan peut lire le compteur à tout moment. Je le fais souvent pendant 10 ms, 100 ms et même 1 seconde.

    Je m'assure que les graduations de 1 ms, 10 ms et 100 ms sont d'une taille de mot qui peut être lue en une seule opération atomique. Si vous utilisez un langage de haut niveau, assurez-vous d'indiquer au compilateur que ces variables peuvent changer de manière asynchrone. En C, par exemple , vous les déclarez volatiles externes . Bien sûr, c'est quelque chose qui va dans un fichier include en conserve, vous n'avez donc pas besoin de vous en souvenir pour chaque projet.

    Je fais parfois le compteur de ticks 1 s le compteur de temps total écoulé, alors faites 32 bits de large. Cela ne peut pas être lu en une seule opération atomique sur la plupart des petits micro que j'utilise, donc ce n'est pas rendu mondial. Au lieu de cela, une routine est fournie qui lit la valeur de plusieurs mots, traite des mises à jour possibles entre les lectures et renvoie le résultat.

    Bien sûr, il aurait pu y avoir des routines pour obtenir les plus petits compteurs de 1 ms, 10 ms, etc. Cependant, cela ne fait vraiment pas grand-chose pour vous, ajoute de nombreuses instructions au lieu de lire un seul mot et utilise un autre emplacement de pile d'appels.

    Quel est l'inconvénient? Je suppose que quelqu'un pourrait faire une faute de frappe qui écrit accidentellement sur l'un des compteurs, ce qui pourrait alors gâcher un autre timing dans le système. Écrire délibérément sur un compteur n'aurait aucun sens, donc ce type de bogue devrait être quelque chose de non intentionnel comme une faute de frappe. Semble très improbable. Je ne me souviens pas que cela se soit produit dans plus de 100 petits projets de microcontrôleurs.

  2. Valeurs A / N finales filtrées et ajustées. Une chose courante à faire est d'avoir une routine d'interruption gérer les lectures d'un A / D. Je lis généralement les valeurs analogiques plus rapidement que nécessaire, puis j'applique un peu de filtrage passe-bas. Il y a souvent aussi une mise à l'échelle et un décalage qui sont appliqués.

    Par exemple, l'A / D peut lire la sortie 0 à 3 V d'un diviseur de tension pour mesurer l'alimentation 24 V. Les nombreuses lectures sont exécutées à travers un certain filtrage, puis mises à l'échelle de sorte que la valeur finale soit en millivolts. Si l'alimentation est à 24,015 V, la valeur finale est 24015.

    Le reste du système ne voit qu'une valeur actualisée en direct indiquant la tension d'alimentation. Il ne sait pas et n'a pas besoin de se soucier quand exactement cela est mis à jour, d'autant plus qu'il est mis à jour beaucoup plus souvent que le temps d'établissement du filtre passe-bas.

    Encore une fois, une routine d'interface peut être utilisée, mais vous en tirez très peu d'avantages. L'utilisation de la variable globale chaque fois que vous avez besoin de la tension d'alimentation est beaucoup plus simple. N'oubliez pas que la simplicité n'est pas seulement pour la machine, mais que plus simple signifie également moins de risques d'erreur humaine.

Olin Lathrop
la source
J'ai suivi une thérapie, au cours d'une semaine lente, essayant vraiment de toucher mon code. Je vois le point de Lundin sur la restriction de l'accès aux variables, mais je regarde mes systèmes réels et je pense que c'est une possibilité si éloignée QU'UNE PERSONNE pourrait en fait branler une variable globale critique du système. Les fonctions Getter / Setter finissent par vous coûter des frais généraux par rapport à simplement utiliser un global et accepter ce sont des programmes assez simples ...
Leroy105
3
@ Leroy105 Le problème n'est pas que les "terroristes" abusent intentionnellement de la variable globale. La pollution de l'espace de noms pourrait être un problème dans les grands projets, mais cela peut être résolu avec une bonne dénomination. Non, le vrai problème est que le programmeur essaie d'utiliser la variable globale comme prévu, mais ne le fait pas correctement. Soit parce qu'ils ne réalisent pas le problème de condition de concurrence qui existe avec tous les ISR, soit parce qu'ils gâchent la mise en œuvre du mécanisme de protection obligatoire, ou simplement parce qu'ils crachent l'utilisation de la variable globale dans tout le code, créant un couplage étroit et code illisible.
Lundin
Vos points sont valables Olin, mais même dans ces exemples, le remplacement extern int ticks10mspar inline int getTicks10ms()ne fera absolument aucune différence dans l'assemblage compilé, tandis que d'un autre côté, il sera difficile de modifier accidentellement sa valeur dans d'autres parties du programme, et vous permettra également de un moyen de "se connecter" à cet appel (par exemple pour se moquer du temps pendant les tests unitaires, pour consigner l'accès à cette variable, ou autre). Même si vous soutenez que la possibilité pour un programmeur san de changer cette variable à zéro, il n'y a pas de coût pour un getter en ligne.
Groo
@Groo: Cela n'est vrai que si vous utilisez un langage qui prend en charge les fonctions intégrées, et cela signifie que la définition de la fonction getter doit être visible par tous. En fait, lorsque j'utilise un langage de haut niveau, j'utilise davantage les fonctions getter et moins les variables globales. Dans l'assemblage, il est beaucoup plus facile de saisir la valeur d'une variable globale que de s'embêter avec une fonction getter.
Olin Lathrop
Bien sûr, si vous ne pouvez pas vous aligner, le choix n'est pas si simple. Je voulais dire qu'avec les fonctions intégrées (et de nombreux compilateurs antérieurs à C99 supportaient déjà les extensions intégrées), les performances ne peuvent pas être un argument contre les getters. Avec un compilateur d'optimisation raisonnable, vous devriez vous retrouver avec le même assemblage produit.
Groo
2

Toute interruption particulière sera une ressource globale. Parfois, cependant, il peut être utile que plusieurs interruptions partagent le même code. Par exemple, un système peut avoir plusieurs UART, qui doivent tous utiliser une logique d'envoi / réception similaire.

Une bonne approche pour gérer cela consiste à placer les éléments utilisés par le gestionnaire d'interruption, ou des pointeurs vers eux, dans un objet de structure, puis à ce que les gestionnaires d'interruption matériels réels soient quelque chose comme:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Les objets uart1_info, uart2_infoetc. seraient des variables globales, mais ce seraient les seules variables globales utilisées par les gestionnaires d'interruption. Tout le reste que les gestionnaires vont toucher serait traité dans ces limites.

Notez que tout ce qui est accessible à la fois par le gestionnaire d'interruption et par le code de la ligne principale doit être qualifié volatile. Il peut être plus simple de déclarer simplement volatiletout ce qui sera utilisé par le gestionnaire d'interruption, mais si les performances sont importantes, il peut être utile d'écrire du code qui copie les informations dans des valeurs temporaires, les opère, puis les réécrit. Par exemple, au lieu d'écrire:

if (foo->timer)
  foo->timer--;

écrire:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

La première approche peut être plus facile à lire et à comprendre, mais sera moins efficace que la seconde. La question de savoir si cela dépend de l'application.

supercat
la source
0

Voici trois idées:

Déclarez la variable indicateur comme statique pour limiter la portée à un seul fichier.

Rendez la variable d'indicateur privée et utilisez les fonctions getter et setter pour accéder à la valeur d'indicateur.

Utilisez un objet de signalisation tel qu'un sémaphore au lieu d'une variable indicateur. L'ISR définirait / publierait le sémaphore.

kkrambo
la source
0

Une interruption (c'est-à-dire le vecteur qui pointe vers votre gestionnaire) est une ressource globale. Donc, même si vous utilisez une variable sur la pile ou sur le tas:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

ou du code orienté objet avec une fonction 'virtuelle':

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

… La première étape doit impliquer une variable globale réelle (ou au moins statique) pour atteindre ces autres données.

Tous ces mécanismes ajoutent une indirection, ce qui n'est généralement pas le cas si vous souhaitez extraire le dernier cycle du gestionnaire d'interruption.

CL.
la source
vous devez déclarer l'indicateur comme volatile int *.
next-hack
0

Je suis en train de coder pour Cortex M0 / M4 en ce moment et l'approche que nous utilisons en C ++ (il n'y a pas de balise C ++, donc cette réponse pourrait être hors sujet) est la suivante:

Nous utilisons une classe CInterruptVectorTablequi contient toutes les routines de service d'interruption qui sont stockées dans le vecteur d'interruption réel du contrôleur:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

La classe CInterruptVectorTableimplémente une abstraction des vecteurs d'interruption, vous pouvez donc lier différentes fonctions aux vecteurs d'interruption pendant l'exécution.

L'interface de cette classe ressemble à ceci:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

Vous devez créer les fonctions qui sont stockées dans la table vectorielle staticcar le contrôleur ne peut pas fournir de thispointeur car la table vectorielle n'est pas un objet. Donc, pour contourner ce problème, nous avons le pThispointeur statique à l'intérieur du CInterruptVectorTable. En entrant dans l'une des fonctions d'interruption statiques, il peut accéder au pThispointeur pour accéder aux membres du même objet CInterruptVectorTable.


Maintenant, dans le programme, vous pouvez utiliser le SetIsrCallbackfunctionpour fournir un pointeur de fonction à une staticfonction qui doit être appelée lorsqu'une interruption se produit. Les pointeurs sont stockés dans le InterruptVectorTable_t virtualVectorTable.

Et l'implémentation d'une fonction d'interruption ressemble à ceci:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

Donc, cela appellera une staticméthode d'une autre classe (qui peut être private), qui peut alors contenir un autre static thispointeur pour accéder aux variables membres de cet objet (un seul).

Je suppose que vous pourriez construire et interfacer comme IInterruptHandleret stocker des pointeurs vers des objets, donc vous n'avez pas besoin du static thispointeur dans toutes ces classes. (peut-être essayons-nous cela dans la prochaine itération de notre architecture)

L'autre approche fonctionne bien pour nous, car les seuls objets autorisés à implémenter un gestionnaire d'interruption sont ceux à l'intérieur de la couche d'abstraction matérielle, et nous n'avons généralement qu'un seul objet pour chaque bloc matériel, donc cela fonctionne bien avec les static thispointeurs. Et la couche d'abstraction matérielle fournit une autre abstraction aux interruptions, appelée ICallbackqui est ensuite implémentée dans la couche de périphérique au-dessus du matériel.


Accédez-vous aux données globales? Bien sûr, mais vous pouvez rendre privées la plupart des données globales nécessaires, comme les thispointeurs et les fonctions d'interruption.

Ce n'est pas à l'épreuve des balles, et cela ajoute des frais généraux. Vous aurez du mal à implémenter une pile IO-Link en utilisant cette approche. Mais si vous n'êtes pas extrêmement serré avec les timings, cela fonctionne très bien pour obtenir une abstraction flexible des interruptions et de la communication dans les modules sans utiliser de variables globales accessibles de partout.

Arsenal
la source
1
"afin que vous puissiez lier différentes fonctions aux vecteurs d'interruption pendant l'exécution" Cela semble être une mauvaise idée. La «complexité cyclomatique» du programme ne ferait que passer par le toit. Toutes les combinaisons de cas d'utilisation devraient être testées de manière à ce qu'il n'y ait ni conflits de synchronisation ni d'utilisation de pile. Beaucoup de maux de tête pour une fonctionnalité avec une utilité très limitée IMO. (Sauf si vous avez un cas de chargeur de démarrage, c'est une autre histoire) Dans l'ensemble, cela sent la méta-programmation.
Lundin
@Lundin Je ne vois pas vraiment votre point. Nous l'utilisons pour lier par exemple l'interruption DMA au gestionnaire d'interruption SPI si le DMA est utilisé pour le SPI et au gestionnaire d'interruption UART s'il est utilisé pour l'UART. Les deux gestionnaires doivent être testés, bien sûr, mais ce n'est pas un problème. Et cela n'a sûrement rien à voir avec la méta-programmation.
Arsenal le
Le DMA est une chose, l'attribution au moment de l'exécution des vecteurs d'interruption en est une autre. Il est logique de laisser une configuration de pilote DMA être variable, au moment de l'exécution. Une table vectorielle, pas tellement.
Lundin
@Lundin Je suppose que nous avons des points de vue différents à ce sujet, nous pourrions commencer une discussion à ce sujet, car je ne vois toujours pas votre problème avec cela - il se peut donc que ma réponse soit si mal écrite que tout le concept soit mal compris.
Arsenal