Condition de course du sommeil du microcontrôleur

11

Étant donné un microcontrôleur qui exécute le code suivant:

volatile bool has_flag = false;

void interrupt(void) //called when an interrupt is received
{
    clear_interrupt_flag(); //clear interrupt flag
    has_flag = true; //signal that we have an interrupt to process
}

int main()
{
    while(1)
    {
        if(has_flag) //if we had an interrupt
        {
            has_flag = false; //clear the interrupt flag
            process(); //process the interrupt
        }
        else
            sleep(); //place the micro to sleep
    }
}

Supposons que la if(has_flag)condition soit fausse et que nous soyons sur le point d'exécuter l'instruction sleep. Juste avant d'exécuter l'instruction sleep, nous recevons une interruption. Après avoir quitté l'interruption, nous exécutons l'instruction sleep.

Cette séquence d'exécution n'est pas souhaitable car:

  • Le microcontrôleur s'est endormi au lieu de se réveiller et d'appeler process().
  • Le microcontrôleur peut ne jamais se réveiller si aucune interruption n'est reçue par la suite.
  • L'appel à process()est reporté à la prochaine interruption.

Comment le code peut-il être écrit pour empêcher cette condition de concurrence critique de se produire?

Éditer

Certains microcontrôleurs, comme à l'ATMega, ont un bit d'activation de veille qui empêche cette condition de se produire (merci Kvegaoro de l'avoir signalé). JRoberts propose un exemple d'implémentation qui illustre ce comportement.

D'autres micros, tels que les PIC18, n'ont pas ce bit et le problème persiste. Cependant, ces micros sont conçus de telle sorte que les interruptions peuvent toujours réveiller le cœur indépendamment du fait que le bit d'activation d'interruption globale soit défini (merci supercat de l'avoir signalé). Pour de telles architectures, la solution consiste à désactiver les interruptions globales juste avant de s'endormir. Si une interruption se déclenche juste avant l'exécution de l'instruction sleep, le gestionnaire d'interruption ne sera pas exécuté, le cœur se réveillera et une fois les interruptions globales réactivées, le gestionnaire d'interruption sera exécuté. En pseudo-code, l'implémentation ressemblerait à ceci:

int main()
{
    while(1)
    {
        //clear global interrupt enable bit.
        //if the flag tested below is not set, then we enter
        //sleep with the global interrupt bit cleared, which is
        //the intended behavior.
        disable_global_interrupts();

        if(has_flag) //if we had an interrupt
        {
            has_flag = false; //clear the interrupt flag
            enable_global_interrupts();  //set global interrupt enable bit.

            process(); //process the interrupt
        }
        else
            sleep(); //place the micro to sleep
    }
}
TRISAbits
la source
Est-ce une question pratique ou théorique?
AndrejaKo
en théorie, vous utilisez une minuterie qui vous réveille une fois tous les (entrez une valeur acceptable) ms, puis se rendort si rien n'est nécessaire.
Grady Player
1
Je ferais interrupt_flagcomme un int, et je l'incrémenterais chaque fois qu'il y a une interruption. Puis changez le if(has_flag)en while (interrupts_count)puis dormez. Néanmoins, l'interruption peut se produire une fois que vous avez quitté la boucle while. Si c'est un problème, alors le traitement dans l'interruption lui-même?
angelatlarge
1
Eh bien, cela dépend du micro que vous utilisez .. Si c'était un ATmega328, vous pourriez éventuellement désactiver le mode veille sur l'interruption, donc si la condition de course que vous décrivez se produit, la fonction veille serait annulée, rebouclée et vous traiteriez l'interruption avec une petite latence. Mais également utiliser une minuterie pour se réveiller à un intervalle égal ou inférieur à votre latence maximale serait également une excellente solution
Kvegaoro
1
@TRISAbits: Sur le PIC 18x, l'approche que j'ai décrite dans ma réponse fonctionne très bien (c'est ma conception normale lorsque j'utilise cette partie).
supercat

Réponses:

9

Il existe généralement une sorte de support matériel pour ce cas. Par exemple, l' seiinstruction des AVR d'activer les interruptions permet d'activer jusqu'à ce que l'instruction suivante soit terminée. Avec cela, on peut faire:

forever,
   interrupts off;
   if has_flag,
      interrupts on;
      process interrupt;
   else,
      interrupts-on-and-sleep;    # won't be interrupted
   end
end

L'interruption qui aurait été manquée dans l'exemple serait dans ce cas suspendue jusqu'à ce que le processeur termine sa séquence de mise en veille.

JRobert
la source
Très bonne réponse! L'algorithme que vous fournissez fonctionne vraiment très bien sur un AVR. Merci pour la suggestion.
TRISAbits
3

Sur de nombreux microcontrôleurs, en plus d'être en mesure d'activer ou de désactiver des causes d'interruption particulières (généralement dans un module de contrôleur d'interruption), il existe un indicateur maître dans le cœur du processeur qui détermine si les demandes d'interruption seront acceptées. De nombreux microcontrôleurs quitteront le mode veille si une demande d'interruption atteint le cœur, que le cœur soit disposé à l'accepter ou non.

Sur une telle conception, une approche simple pour obtenir un comportement de sommeil fiable consiste à faire en sorte que la vérification de la boucle principale efface un indicateur, puis vérifie s'il connaît une raison pour laquelle le processeur devrait être éveillé. Toute interruption qui se produit pendant ce temps et qui pourrait affecter l'une de ces raisons devrait mettre le drapeau. Si la boucle principale n'a trouvé aucune raison de rester éveillée, et si l'indicateur n'est pas défini, la boucle principale devrait désactiver les interruptions et vérifier à nouveau l'indicateur [peut-être après quelques instructions NOP s'il est possible qu'une interruption qui devienne en attente pendant une instruction de désactivation-interruption peut être traitée après que la récupération d'opérande associée à l'instruction suivante a déjà été effectuée]. Si le drapeau n'est toujours pas réglé, alors allez vous coucher.

Dans ce scénario, une interruption qui se produit avant que la boucle principale ne désactive les interruptions définira l'indicateur avant le test final. Une interruption trop tardive pour être réparée avant l'instruction de mise en veille empêchera le processeur de se mettre en veille. Les deux situations sont très bien.

La mise en veille à la sortie est parfois un bon modèle à utiliser, mais toutes les applications ne lui conviennent pas vraiment. Par exemple, un appareil doté d'un écran LCD économe en énergie peut être plus facilement programmé avec un code qui ressemble à:

void select_view_user(int default_user)
{
  int current_user;
  int ch;
  current_user = default_user;
  do
  {
    lcd_printf(0, "User %d");
    lcd_printf(1, ...whatever... );
    get_key();
    if (key_hit(KEY_UP)) {current_user = (current_user + 1) % MAX_USERS};
    if (key_hit(KEY_DOWN)) {current_user = (current_user + MAX_USERS-1) % MAX_USERS};
    if (key_hit(KEY_ENTER)) view_user(current_user);
  } while(!key_hit(KEY_EXIT | KEY_TIMEOUT));
}

Si aucun bouton n'est enfoncé et que rien d'autre ne se produit, il n'y a aucune raison pour que le système ne s'endorme pas pendant l'exécution de la get_keyméthode. Bien qu'il soit possible que des clés déclenchent une interruption et gèrent toutes les interactions de l'interface utilisateur via la machine d'état, un code comme celui-ci est souvent le moyen le plus logique de gérer les flux d'interface utilisateur hautement modaux typiques des petits microcontrôleurs.

supercat
la source
Merci supercat pour la bonne réponse. Désactiver les interruptions puis se mettre en veille est une solution fantastique à condition que le cœur se réveille de toutes les sources d'interruption, que le bit d'interruption global soit activé / désactivé. J'ai jeté un coup d'œil au schéma du matériel d'interruption PIC18, et cette solution fonctionnerait.
TRISAbits
1

Programmez le micro pour qu'il se réveille en cas d'interruption.

Les détails spécifiques varieront en fonction du micro que vous utilisez.

Modifiez ensuite la routine main ():

int main()
{
    while(1)
    {
        sleep();
        process(); //process the interrupt
    }
}
jwygralak67
la source
1
L'architecture de réveil par interruption est supposée dans la question. Je ne pense pas que votre réponse résout la question / le problème.
angelatlarge
@angelatlarge Point accepté. J'ai ajouté un exemple de code qui, je pense, aide.
jwygralak67
@ jwygralak67: Merci pour la suggestion, mais le code que vous fournissez déplace simplement le problème dans la routine process (), qui doit maintenant tester si l'interruption s'est produite avant d'exécuter le corps de process ().
TRISAbits
2
Si l'interruption ne s'était pas produite, pourquoi sommes-nous réveillés?
JRobert
1
@JRobert: Nous pourrions être réveillés d'une interruption précédente, terminer la routine process (), et lorsque nous terminons le test if (has_flag) et juste avant le sommeil, nous obtenons une autre interruption, ce qui provoque le problème que j'ai décrit dans le question.
TRISAbits