Les affectations de pointeurs de fonction sont-elles atomiques dans Arduino?

11

Les extraits suivants proviennent du code source de la bibliothèque TimerOne :

// TimerOne.h:
void (*isrCallback)();

// TimerOne.cpp:
ISR(TIMER1_OVF_vect) // interrupt service routine that wraps a user defined function supplied by attachInterrupt
{
  Timer1.isrCallback();
}

// TimerOne.cpp:
void TimerOne::attachInterrupt(void (*isr)(), long microseconds)
{
  if(microseconds > 0) setPeriod(microseconds);
  isrCallback = isr; // register the user's callback with the real ISR
  TIMSK1 = _BV(TOIE1); // sets the timer overflow interrupt enable bit
  resume();                                                                                            
}

La question: si la minuterie est déjà en cours d'exécution et que le programme principal appelle attachInterrupt(), l'interruption de la minuterie pourrait-elle s'y produire pendant l'affectation du pointeur de fonction isrCallback = isr;? Ensuite, avec un timing chanceux, le Timer1.isrCallback();pointeur de fonction serait composé en partie de l'ancienne et en partie de la nouvelle adresse, provoquant le saut ISR vers un faux emplacement?

Je suppose que cela pourrait être le cas, car les pointeurs de fonction sont certainement plus larges que 1 octet, et l'accès aux données> 1 octet n'est pas atomique. Les solutions de contournement possibles pourraient être:

  • Appelez toujours detachInterrupt()pour vous assurer que le minuteur ne fonctionne pas, avant d'appeler attachInterrupt(), c'est-à-dire clarifiez les documents Timer1.
  • Ou, modifiez Timer1, désactivant temporairement les interruptions de dépassement de minuterie juste avant isrCallback = isr;

Est-ce que cela a du sens, ou y a-t-il quelque chose dans les Timer1sources ou les affectations de pointeurs de fonction que j'ai manqué?

Joonas Pulakka
la source

Réponses:

7

Jetez un œil au code pour attachInterrupt () et detachInterrupt () dans /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/cores/arduino/WInterrupts.c(enfin, c'est là qu'ils sont sur un Mac, de toute façon. La structure des fichiers Arduino sur les autres OS semble probablement similaire dans les niveaux inférieurs du chemin).

Il semble que attachInterrupt () suppose que l'interruption en question n'est pas encore activée car elle écrit le pointeur de fonction sans prendre de précautions. Notez que detachInterrupts () désactive l'interruption cible avant d'écrire un pointeur NULL sur son vecteur. Donc j'utiliserais au moins une paire detachInterrupt()/attachInterrupt()

Je voudrais exécuter un tel code dans une section critique, moi-même. Il semble que votre première façon (détacher, puis attacher) fonctionnerait, bien que je ne puisse pas être sûr qu'il ne puisse pas manquer une interruption malheureusement chronométrée. La fiche technique de votre MCU pourrait avoir plus à dire à ce sujet. Mais je ne suis pas non plus sûr à ce stade, qu'un global cli()/ sei()ne le manquerait pas non plus. La feuille de données ATMega2560, section 6.8, dit: "Lorsque vous utilisez l'instruction SEI pour activer les interruptions, l'instruction suivant SEI sera exécutée avant toutes les interruptions en attente, comme indiqué dans cet exemple", ce qui semble impliquer qu'elle peut mettre en mémoire tampon une interruption pendant que les interruptions sont éteints.

JRobert
la source
Il est en effet utile de plonger vers les sources :) Le mécanisme d'interruption attach / detach de TimerOne semble être fait de la même manière que le standard (WInterrupt) et a donc les mêmes "fonctionnalités".
Joonas Pulakka
0

On dirait que vous avez raison. La chose logique à faire serait de désactiver les interruptions de manière à ne pas les réactiver si elles ont été désactivées en premier lieu. Par exemple:

  uint8_t oldSREG = SREG;  // remember if interrupts are on
  cli();                   // make the next line interruptible
  isrCallback = isr;       // register the user's callback with the real ISR
  SREG = oldSREG;          // turn interrupts back on, if they were on before

"Lorsque vous utilisez l'instruction SEI pour activer les interruptions, l'instruction suivant SEI sera exécutée avant toute interruption en attente, comme illustré dans cet exemple"

L'intention est de vous permettre d'écrire du code comme ceci:

  sei ();         // enable interrupts
  sleep_cpu ();   // sleep

Sans cette disposition, vous pourriez obtenir une interruption entre ces deux lignes, et donc dormir indéfiniment (car l'interruption qui allait vous réveiller s'est produite avant de dormir). La disposition dans le processeur selon laquelle l'instruction suivante, après que les interruptions sont activées si elles ne l'ont pas été auparavant, est toujours exécutée, évite cela.

Nick Gammon
la source
Ne serait-il pas plus efficace de le faire TIMSK1=0; TIFR1=_BV(TOV1); isrCallback=isr; TIMSK1=_BV(TOIE1);? Il épargne un registre CPU et ne contribue pas à la latence d'interruption.
Edgar Bonet
Qu'en est-il des autres bits, comme ICIE1, OCIE1B, OCIE1A? Je comprends la latence, mais les interruptions devraient pouvoir faire face à quelques cycles d'horloge de non-disponibilité. Il peut y avoir une condition de concurrence. L'interruption peut déjà être déclenchée (c'est-à-dire que le drapeau de la CPU est défini) et la désactivation de TIMSK1 peut ne pas empêcher sa gestion. Vous devrez peut-être également réinitialiser TOV1 (en écrivant 1 dans TIFR1) pour vous assurer que cela ne se produit pas. L'incertitude m'amène à penser que désactiver les interruptions à l'échelle mondiale est la solution la plus sûre.
Nick Gammon