Gestion des interruptions dans les microcontrôleurs et exemple FSM

9

Question initiale

J'ai une question générale sur la gestion des interruptions dans les microcontrôleurs. J'utilise le MSP430, mais je pense que la question peut être étendue à d'autres uC. Je voudrais savoir si c'est une bonne pratique d'activer / désactiver fréquemment les interruptions le long du code. Je veux dire, si j'ai une partie de code qui ne sera pas sensible aux interruptions (ou pire encore, ne doit pas écouter les interruptions, pour une raison quelconque), est-il préférable de:

  • Désactivez les interruptions avant, puis réactivez-les après la section critique.
  • Placez un indicateur à l'intérieur de l'ISR respectif et (au lieu de désactiver l'interruption), définissez l'indicateur sur false avant la section critique et réinitialisez-le sur true, juste après. Pour empêcher l'exécution du code de l'ISR.
  • Aucun des deux, les suggestions sont donc les bienvenues!

Mise à jour: interruptions et diagrammes d'état

Je vais fournir une situation spécifique. Supposons que nous voulons implémenter un graphique d'état, qui est composé de 4 blocs:

  1. Transitions / effet.
  2. Conditions de sortie.
  3. Activité d'entrée.
  4. Faites de l'activité.

C'est ce qu'un professeur nous a enseigné à l'université. Probablement, la meilleure façon de le faire est de suivre ce schéma:

while(true) {

  /* Transitions/Effects */
  //----------------------------------------------------------------------
  next_state = current_state;

  switch (current_state)
  {
    case STATE_A:
      if(EVENT1) {next_state = STATE_C}
      if(d == THRESHOLD) {next_state = STATE_D; a++}
      break;
    case STATE_B:
      // transitions and effects
      break;
    (...)
  }

  /* Exit activity -> only performed if I leave the state for a new one */
  //----------------------------------------------------------------------
  if (next_state != current_state)
  {
    switch(current_state)
    {
      case STATE_A:
        // Exit activity of STATE_A
        break;
      case STATE_B:
        // Exit activity of STATE_B
        break;
        (...)
    }
  }

  /* Entry activity -> only performed the 1st time I enter a state */
  //----------------------------------------------------------------------
  if (next_state != current_state)
  {
    switch(next_state)
    {
      case STATE_A:
        // Entry activity of STATE_A
        break;
      case STATE_B:
        // Entry activity of STATE_B
        break;
      (...)
    }
  }

  current_state = next_state;

  /* Do activity */
  //----------------------------------------------------------------------
  switch (current_state)
  {
    case STATE_A:
      // Do activity of STATE_A
      break;
    case STATE_B:
      // Do activity of STATE_B
      break;
    (...)
  }
}

Supposons également que, par exemple STATE_A, je veux être sensible à une interruption provenant d'un ensemble de boutons (avec système debouce, etc., etc.). Lorsque quelqu'un appuie sur l'un de ces boutons, une interruption est générée et l'indicateur lié au port d'entrée est copié dans une variable buttonPressed. Si le rebounce est réglé sur 200 ms d'une manière ou d'une autre (horloge de surveillance, minuterie, compteur, ...), nous sommes sûrs qu'il buttonPressedne peut pas être mis à jour avec une nouvelle valeur avant 200 ms. C'est ce que je vous demande (et moi-même :) bien sûr)

Dois-je activer l'interruption de l'activité DO STATE_Aet la désactiver avant de quitter?

/* Do activity */
//-------------------------------------
switch (current_state)
{
  case STATE_A:
    // Do activity of STATE_A
    Enable_ButtonsInterrupt(); // and clear flags before it
    // Do fancy stuff and ...
    // ... wait until a button is pressed (e.g. LPM3 of the MSP430)
    // Here I have my buttonPressed flag ready!
    Disable_ButtonsInterrupt();
    break;
  case STATE_B:
    // Do activity of STATE_B
    break;
  (...)
}

D'une manière que je suis sûr que la prochaine fois que j'exécute le bloc 1 (transition / effets) à la prochaine itération, je suis sûr que les conditions vérifiées le long des transitions ne proviennent pas d'une interruption ultérieure qui a écrasé la valeur précédente de buttonPressedcelle-ci. besoin (bien qu'il soit impossible que cela se produise car 250 ms doivent s'écouler).

Umberto D.
la source
3
Il est difficile de faire une recommandation sans en savoir plus sur votre situation. Mais parfois, il est nécessaire de désactiver les interruptions dans les systèmes embarqués. Il est préférable de ne désactiver les interruptions que pendant de courtes périodes afin de ne pas manquer les interruptions. Il peut être possible de désactiver uniquement certaines interruptions plutôt que toutes les interruptions. Je ne me souviens pas avoir utilisé la technique du drapeau à l'intérieur de l'ISR comme vous l'avez décrit, donc je suis sceptique quant à savoir si c'est votre meilleure solution.
kkrambo

Réponses:

17

La première tactique consiste à concevoir le micrologiciel global de manière à ce que les interruptions se produisent à tout moment. Devoir désactiver les interruptions pour que le code de premier plan puisse exécuter une séquence atomique doit être fait avec parcimonie. Il y a souvent un chemin architectural autour.

Cependant, la machine est là pour vous servir, et non l'inverse. Les règles générales sont uniquement destinées à empêcher les mauvais programmeurs d'écrire du code vraiment mauvais. Il est de loin préférable de comprendre exactement comment fonctionne la machine, puis de concevoir un bon moyen d'exploiter ces capacités pour effectuer la tâche souhaitée.

Gardez à l'esprit qu'à moins que vous ne soyez vraiment serré sur les cycles ou les emplacements de mémoire (cela peut certainement arriver), sinon vous souhaitez optimiser la clarté et la maintenabilité du code. Par exemple, si vous avez une machine 16 bits qui met à jour un compteur 32 bits dans une interruption d'horloge, vous devez vous assurer que lorsque le code de premier plan lit le compteur, ses deux moitiés sont cohérentes. Une façon consiste à désactiver les interruptions, à lire les deux mots, puis à les réactiver. Si la latence d'interruption n'est pas critique, c'est parfaitement acceptable.

Dans le cas où vous devez avoir une latence d'interruption faible, vous pouvez, par exemple, lire le mot haut, lire le mot bas, relire le mot haut et répéter s'il a changé. Cela ralentit un peu le code de premier plan, mais n'ajoute aucune latence d'interruption. Il existe plusieurs petits trucs. Un autre peut être de définir un indicateur dans la routine d'interruption qui indique que le compteur doit être incrémenté, puis de le faire dans la boucle d'événement principale du code de premier plan. Cela fonctionne bien si le taux d'interruption du compteur est suffisamment lent pour que la boucle d'événement fasse l'incrémentation avant que l'indicateur ne soit à nouveau défini.

Ou, au lieu d'un indicateur, utilisez un compteur d'un mot. Le code de premier plan conserve une variable distincte qui contient le dernier compteur pour lequel il a mis à jour le système. Il effectue une soustraction non signée du compteur en direct moins la valeur enregistrée pour déterminer le nombre de ticks à gérer à la fois. Cela permet au code de premier plan de manquer jusqu'à 2 N -1 événements à la fois, où N est le nombre de bits dans un mot natif que l'ALU peut gérer atomiquement.

Chaque méthode a ses propres avantages et inconvénients. Il n'y a pas de bonne réponse unique. Encore une fois, comprenez comment fonctionne la machine, alors vous n'aurez pas besoin de règles générales.

Olin Lathrop
la source
7

Si vous avez besoin d'une section critique, vous devez vous assurer que l'opération protégeant votre section critique est atomique et ne peut pas être interrompue.

Ainsi, la désactivation des interruptions, qui est le plus souvent gérée par une instruction à processeur unique (et appelée à l'aide d'une fonction intrinsèque du compilateur), est l'un des paris les plus sûrs que vous puissiez prendre.

Selon votre système, il peut y avoir des problèmes avec cela, comme une interruption peut être manquée. Certains microcontrôleurs définissent les indicateurs quel que soit l'état de l'activation globale des interruptions, et après avoir quitté la section critique, les interruptions sont exécutées et sont simplement retardées. Mais si vous avez une interruption qui se produit à un rythme élevé, vous pouvez manquer une deuxième fois l'interruption s'est produite si vous bloquez les interruptions pendant trop longtemps.

Si votre section critique ne nécessite qu'une seule interruption pour ne pas être exécutée, mais que d'autres doivent être exécutées, l'autre approche semble viable.

Je me retrouve à programmer des routines de service d'interruption aussi courtes que possible. Ils ont donc simplement défini un indicateur qui est ensuite vérifié pendant les routines de programme normales. Mais si vous faites cela, méfiez-vous des conditions de course en attendant que ce drapeau soit défini.

Il y a beaucoup d'options et sûrement pas une seule réponse correcte à cela, c'est un sujet qui nécessite une conception soignée et mérite un peu plus de réflexion que d'autres choses.

Arsenal
la source
5

Si vous avez déterminé qu'une section de code doit s'exécuter sans interruption, sauf dans des circonstances inhabituelles, vous devez désactiver les interruptions pendant la durée minimale possible pour terminer la tâche et les réactiver par la suite.

Placez un indicateur à l'intérieur de l'ISR respectif et (au lieu de désactiver l'interruption), définissez l'indicateur sur false avant la section critique et réinitialisez-le sur true, juste après. Pour empêcher l'exécution du code de l'ISR.

Cela permettrait quand même une interruption, un saut de code, une vérification, puis un retour. Si votre code peut gérer autant d'interruptions, vous devriez probablement simplement concevoir l'ISR pour définir un indicateur plutôt que d'effectuer la vérification - il serait plus court - et gérer l'indicateur dans votre routine de code normale. Cela ressemble à quelqu'un qui a mis trop de code dans l'interruption et qui utilise l'interruption pour effectuer des actions plus longues qui devraient avoir lieu dans le code normal.

Si vous traitez avec du code où les interruptions sont longues, un indicateur comme vous le suggérez pourrait résoudre le problème, mais il sera toujours préférable de simplement désactiver les interruptions si vous ne pouvez pas re-factoriser le code pour éliminer le code excessif dans l'interruption .

Le principal problème avec la méthode du drapeau est que vous n'exécutez pas du tout l'interruption - ce qui peut avoir des répercussions plus tard. La plupart des microcontrôleurs suivent les indicateurs d'interruption même lorsque les interruptions sont globalement désactivées, puis exécutent l'interruption lorsque vous réactivez les interruptions:

  • Si aucune interruption ne s'est produite pendant la section critique, aucune n'est exécutée après.
  • Si une interruption se produit pendant la section critique, une est exécutée après.
  • Si plusieurs interruptions se produisent pendant la section critique, une seule est exécutée après.

Si votre système est complexe et doit suivre les interruptions plus complètement, vous devrez concevoir un système plus compliqué pour suivre les interruptions et fonctionner en conséquence.

Cependant, si vous concevez toujours vos interruptions pour effectuer le travail minimum nécessaire pour atteindre leur fonction et retardez tout le reste au traitement normal, les interruptions auront rarement un impact négatif sur votre autre code. Demandez à l'interruption de capturer ou de libérer des données si nécessaire, ou définissez / réinitialisez les sorties, etc. selon les besoins, puis demandez au chemin de code principal de prêter attention aux indicateurs, aux tampons et aux variables que l'interruption affecte afin que le long traitement puisse être effectué dans la boucle principale, plutôt que l'interruption.

Cela devrait éliminer toutes les situations, sauf quelques très rares, où vous pourriez avoir besoin d'une section de code ininterrompue.

Adam Davis
la source
J'ai mis à jour le message pour mieux expliquer la situation dans laquelle je travaille :)
Umberto D.
1
Dans votre exemple de code, j'envisagerais de désactiver l'interruption de bouton spécifique lorsqu'elle n'est pas nécessaire et de l'activer en cas de besoin. Faire cela fréquemment n'est pas un problème de par sa conception. Laissez les interruptions globales activées pour pouvoir ajouter d'autres interruptions au code plus tard si nécessaire. Alternativement, réinitialisez simplement le drapeau lorsque vous passez à l'état A et sinon, ignorez-le. Si le bouton est enfoncé et le drapeau réglé, peu importe? Ignorez-le jusqu'à ce que vous reveniez à l'état A.
Adam Davis
Oui! Cela peut être une solution car dans la conception actuelle, j'utilise souvent LPM3 (MSP430) avec des interruptions globales activées et je quitte LPM3, reprenant l'exécution, dès qu'une interruption est détectée. Donc, une solution est celle que vous avez présentée qui, je pense, est rapportée dans la deuxième partie du code: activez les interruptions dès que je commence à effectuer l'activité do d'un état qui en a besoin et désactivez-la avant de passer au bloc de transitions. Une autre autre solution possible consiste-t-elle à désactiver l'interruption juste avant de quitter le "bloc d'activité Do" et à la réactiver quelque temps (quand?) Après?
Umberto D.
1

Mettre un drapeau dans l'ISR comme vous le décrivez ne fonctionnera probablement pas car vous ignorez essentiellement l'événement qui a déclenché l'interruption. La désactivation globale des interruptions est généralement un meilleur choix. Comme d'autres l'ont dit, vous ne devriez pas avoir à le faire très souvent. Gardez à l'esprit que toute lecture ou écriture effectuée via une seule instruction ne devrait pas avoir besoin d'être protégée car l'instruction s'exécute ou non.

Cela dépend beaucoup du type de ressources que vous essayez de partager. Si vous alimentez des données de l'ISR vers le programme principal (ou vice-versa), vous pouvez implémenter quelque chose comme un tampon FIFO. La seule opération atomique serait la mise à jour des pointeurs de lecture et d'écriture, ce qui minimise le temps que vous passez avec les interruptions désactivées.

Adam Haun
la source
0

Il y a une différence subtile dont vous devez tenir compte. Vous pouvez choisir de «retarder» le traitement d'une interruption ou de «ne pas tenir compte» et d'interrompre.

Souvent, nous disons dans le code que nous désactivons l'interruption. Ce qui se passera probablement, en raison des fonctions matérielles, c'est qu'une fois que nous aurons activé les interruptions, cela se déclenchera. Cela retarde en quelque sorte l'interruption. Pour que le système fonctionne de manière fiable, nous devons connaître la durée maximale pendant laquelle nous pouvons retarder le traitement de ces interruptions. Et puis assurez-vous que tous les cas où les interruptions sont désactivées seront terminés dans un temps plus court.

Parfois, nous voulons ignorer les interruptions. La meilleure façon pourrait être de bloquer l'interruption au niveau matériel. Il y a souvent un contrôleur d'interruption ou similaire où l'on pourrait dire quelles entrées devraient générer des interruptions.

ghellquist
la source