Comment fonctionnent les interruptions sur l'Arduino Uno et les cartes similaires?


11

Veuillez expliquer comment les interruptions fonctionnent sur l'Arduino Uno et les cartes associées à l'aide du processeur ATmega328P. Des conseils tels que:

  • Uno
  • Mini
  • Nano
  • Pro Mini
  • Lilypad

Veuillez en particulier discuter:

  • Pour quoi utiliser les interruptions
  • Comment écrire une routine de service d'interruption (ISR)
  • Problèmes de timing
  • Sections critiques
  • Accès atomique aux données

Remarque: il s'agit d'une question de référence .

Réponses:


25

TL; DR:

Lors de l'écriture d'une routine de service d'interruption (ISR):

  • Soyez bref
  • Ne pas utiliser delay ()
  • Ne faites pas de tirages en série
  • Rendre les variables partagées avec le code principal volatiles
  • Les variables partagées avec le code principal peuvent devoir être protégées par des "sections critiques" (voir ci-dessous)
  • N'essayez pas d'activer ou de désactiver les interruptions

Que sont les interruptions?

La plupart des processeurs ont des interruptions. Les interruptions vous permettent de répondre à des événements "externes" tout en faisant autre chose. Par exemple, si vous cuisinez le dîner, vous pouvez mettre les pommes de terre à cuire pendant 20 minutes. Plutôt que de regarder l'horloge pendant 20 minutes, vous pouvez régler une minuterie, puis aller regarder la télévision. Lorsque la minuterie sonne, vous "interrompez" votre visionnage pour faire quelque chose avec les pommes de terre.


Exemple d'interruptions

const byte LED = 13;
const byte SWITCH = 2;

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  if (digitalRead (SWITCH) == HIGH)
    digitalWrite (LED, HIGH);
  else
    digitalWrite (LED, LOW);
}  // end of switchPressed

void setup ()
{
  pinMode (LED, OUTPUT);  // so we can update the LED
  pinMode (SWITCH, INPUT_PULLUP);
  attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE);  // attach interrupt handler
}  // end of setup

void loop ()
{
  // loop doing nothing
}

Cet exemple montre comment, même si la boucle principale ne fait rien, vous pouvez allumer ou éteindre la LED de la broche 13, si l'interrupteur de la broche D2 est enfoncé.

Pour tester cela, connectez simplement un fil (ou un interrupteur) entre D2 et la masse. Le pullup interne (activé dans la configuration) force la broche HAUT normalement. Une fois mis à la terre, il devient BAS. Le changement dans la broche est détecté par une interruption CHANGE, ce qui provoque l'appel de la routine de service d'interruption (ISR).

Dans un exemple plus compliqué, la boucle principale pourrait faire quelque chose d'utile, comme prendre des relevés de température, et permettre au gestionnaire d'interruption de détecter un bouton poussé.


Conversion de numéros de broches en numéros d'interruption

Pour simplifier la conversion des numéros de vecteur d'interruption en numéros de broche, vous pouvez appeler la fonction digitalPinToInterrupt()en passant un numéro de broche. Il renvoie le numéro d'interruption approprié, ou NOT_AN_INTERRUPT(-1).

Par exemple, sur l'Uno, la broche D2 de la carte est l'interruption 0 (INT0_vect du tableau ci-dessous).

Ces deux lignes ont donc le même effet:

  attachInterrupt (0, switchPressed, CHANGE);    // that is, for pin D2
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);

Cependant, le second est plus facile à lire et plus portable sur différents types d'Arduino.


Interruptions disponibles

Voici une liste des interruptions, par ordre de priorité, pour l'Atmega328:

 1  Reset
 2  External Interrupt Request 0  (pin D2)          (INT0_vect)
 3  External Interrupt Request 1  (pin D3)          (INT1_vect)
 4  Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
 5  Pin Change Interrupt Request 1 (pins A0 to A5)  (PCINT1_vect)
 6  Pin Change Interrupt Request 2 (pins D0 to D7)  (PCINT2_vect)
 7  Watchdog Time-out Interrupt                     (WDT_vect)
 8  Timer/Counter2 Compare Match A                  (TIMER2_COMPA_vect)
 9  Timer/Counter2 Compare Match B                  (TIMER2_COMPB_vect)
10  Timer/Counter2 Overflow                         (TIMER2_OVF_vect)
11  Timer/Counter1 Capture Event                    (TIMER1_CAPT_vect)
12  Timer/Counter1 Compare Match A                  (TIMER1_COMPA_vect)
13  Timer/Counter1 Compare Match B                  (TIMER1_COMPB_vect)
14  Timer/Counter1 Overflow                         (TIMER1_OVF_vect)
15  Timer/Counter0 Compare Match A                  (TIMER0_COMPA_vect)
16  Timer/Counter0 Compare Match B                  (TIMER0_COMPB_vect)
17  Timer/Counter0 Overflow                         (TIMER0_OVF_vect)
18  SPI Serial Transfer Complete                    (SPI_STC_vect)
19  USART Rx Complete                               (USART_RX_vect)
20  USART, Data Register Empty                      (USART_UDRE_vect)
21  USART, Tx Complete                              (USART_TX_vect)
22  ADC Conversion Complete                         (ADC_vect)
23  EEPROM Ready                                    (EE_READY_vect)
24  Analog Comparator                               (ANALOG_COMP_vect)
25  2-wire Serial Interface  (I2C)                  (TWI_vect)
26  Store Program Memory Ready                      (SPM_READY_vect)

Les noms internes (que vous pouvez utiliser pour configurer les rappels ISR) sont entre crochets.

Avertissement: si vous mal orthographiez le nom du vecteur d'interruption, même en vous trompant simplement de capitalisation (chose facile à faire), la routine d'interruption ne sera pas appelée et vous n'obtiendrez pas d'erreur de compilation.


Raisons d'utiliser des interruptions

Les principales raisons pour lesquelles vous pouvez utiliser des interruptions sont:

  • Pour détecter les changements de broches (par exemple, encodeurs rotatifs, pressions de bouton)
  • Minuteur de surveillance (par exemple, si rien ne se passe après 8 secondes, interrompez-moi)
  • Interruptions de temporisation - utilisées pour comparer / déborder les temporisations
  • Transferts de données SPI
  • Transferts de données I2C
  • Transferts de données USART
  • Conversions ADC (analogique vers numérique)
  • EEPROM prête à l'emploi
  • Mémoire flash prête

Les «transferts de données» peuvent être utilisés pour laisser un programme faire autre chose pendant l'envoi ou la réception de données sur le port série, le port SPI ou le port I2C.

Réveillez le processeur

Les interruptions externes, les interruptions de changement de broche et l'interruption du minuteur de surveillance peuvent également être utilisées pour réveiller le processeur. Cela peut être très pratique, car en mode veille, le processeur peut être configuré pour utiliser beaucoup moins d'énergie (par exemple, environ 10 microampères). Une interruption ascendante, descendante ou de bas niveau peut être utilisée pour réveiller un gadget (par exemple, si vous appuyez sur un bouton dessus), ou une interruption "horloge de surveillance" peut le réveiller périodiquement (par exemple pour vérifier l'heure ou Température).

Des interruptions de changement de broche peuvent être utilisées pour réveiller le processeur si une touche est appuyée sur un clavier, ou similaire.

Le processeur peut également être réveillé par une interruption de temporisation (par exemple une temporisation atteignant une certaine valeur, ou débordant) et certains autres événements, tels qu'un message I2C entrant.


Activer / désactiver les interruptions

L'interruption "reset" ne peut pas être désactivée. Cependant, les autres interruptions peuvent être temporairement désactivées en désactivant l'indicateur d'interruption global.

Activer les interruptions

Vous pouvez activer les interruptions avec l'appel de fonction "interruptions" ou "sei" comme ceci:

interrupts ();  // or ...
sei ();         // set interrupts flag

Désactiver les interruptions

Si vous devez désactiver les interruptions, vous pouvez "effacer" l'indicateur d'interruption global comme ceci:

noInterrupts ();  // or ...
cli ();           // clear interrupts flag

L'une ou l'autre méthode a le même effet, en utilisant interrupts/ noInterruptsest un peu plus facile de se souvenir de leur chemin.

La valeur par défaut dans l'Arduino est d'activer les interruptions. Ne les désactivez pas pendant de longues périodes ou des choses comme les minuteries ne fonctionneront pas correctement.

Pourquoi désactiver les interruptions?

Il peut y avoir des éléments de code à temps critique que vous ne souhaitez pas interrompre, par exemple par une interruption de minuterie.

De plus, si les champs multi-octets sont mis à jour par un ISR, vous devrez peut-être désactiver les interruptions afin d'obtenir les données "atomiquement". Sinon, un octet peut être mis à jour par l'ISR pendant que vous lisez l'autre.

Par exemple:

noInterrupts ();
long myCounter = isrCounter;  // get value set by ISR
interrupts ();

La désactivation temporaire des interruptions garantit que isrCounter (un compteur défini à l'intérieur d'un ISR) ne change pas pendant que nous obtenons sa valeur.

Avertissement: si vous n'êtes pas sûr si les interruptions sont déjà activées ou non, vous devez enregistrer l'état actuel et le restaurer ensuite. Par exemple, le code de la fonction millis () fait ceci:

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;    // <--------- save status register

  // disable interrupts while we read timer0_millis or we might get an
  // inconsistent value (e.g. in the middle of a write to timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;            // <---------- restore status register including interrupt flag

  return m;
}

Notez que les lignes indiquées sauvegardent le SREG (registre d'état) actuel qui comprend l'indicateur d'interruption. Après avoir obtenu la valeur du temporisateur (qui est longue de 4 octets), nous remettons le registre d'état en l'état.


Conseils

Noms des fonctions

Les fonctions cli/ seiet le registre SREG sont spécifiques aux processeurs AVR. Si vous utilisez d'autres processeurs tels que ceux ARM, les fonctions peuvent être légèrement différentes.

Désactiver globalement ou désactiver une interruption

Si vous utilisez, cli()vous désactivez toutes les interruptions (y compris les interruptions de temporisation, les interruptions série, etc.).

Cependant, si vous souhaitez simplement désactiver une interruption particulière, vous devez désactiver l' indicateur d'activation d'interruption pour cette source d'interruption particulière. Par exemple, pour les interruptions externes, appelez detachInterrupt().


Qu'est-ce que la priorité d'interruption?

Puisqu'il y a 25 interruptions (autres que la réinitialisation), il est possible que plusieurs événements d'interruption se produisent à la fois, ou du moins, se produisent avant que le précédent ne soit traité. Un événement d'interruption peut également se produire lorsque les interruptions sont désactivées.

L'ordre de priorité est la séquence dans laquelle le processeur recherche les événements d'interruption. Plus la liste est élevée, plus la priorité est élevée. Ainsi, par exemple, une demande d'interruption externe 0 (broche D2) serait traitée avant la demande d'interruption externe 1 (broche D3).


Des interruptions peuvent-elles se produire alors que les interruptions sont désactivées?

Les événements d' interruption (c'est-à-dire la constatation de l'événement) peuvent se produire à tout moment et la plupart sont mémorisés en définissant un indicateur "événement d'interruption" à l'intérieur du processeur. Si les interruptions sont désactivées, cette interruption sera gérée lorsqu'elles seront réactivées, dans l'ordre de priorité.


Comment utilisez-vous les interruptions?

  • Vous écrivez un ISR (routine de service d'interruption). Ceci est appelé lorsque l'interruption se produit.
  • Vous dites au processeur quand vous voulez que l'interruption se déclenche.

Rédaction d'un ISR

Les routines de service d'interruption sont des fonctions sans arguments. Certaines bibliothèques Arduino sont conçues pour appeler vos propres fonctions, vous n'avez donc qu'à fournir une fonction ordinaire (comme dans les exemples ci-dessus), par exemple.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
 flag = true;
}  // end of switchPressed

Cependant, si une bibliothèque n'a pas déjà fourni de "hook" à un ISR, vous pouvez créer le vôtre, comme ceci:

volatile char buf [100];
volatile byte pos;

// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;
    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

Dans ce cas, vous utilisez la macro "ISR" et fournissez le nom du vecteur d'interruption approprié (à partir du tableau précédent). Dans ce cas, l'ISR gère un transfert SPI terminé. (Remarque, certains anciens codes utilisent SIGNAL au lieu de ISR, mais SIGNAL est obsolète).

Connexion d'un ISR à une interruption

Pour les interruptions déjà gérées par les bibliothèques, vous utilisez simplement l'interface documentée. Par exemple:

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
    {
    char c = Wire.receive ();
    // do something with the incoming byte
    }
}  // end of receiveEvent

void setup ()
  {
  Wire.onReceive(receiveEvent);
  }

Dans ce cas, la bibliothèque I2C est conçue pour gérer en interne les octets I2C entrants, puis appeler la fonction fournie à la fin du flux de données entrant. Dans ce cas, receiveEvent n'est pas strictement un ISR (il a un argument) mais il est appelé par un ISR intégré.

Un autre exemple est l'interruption de "broche externe".

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
}  // end of switchPressed

void setup ()
{
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);  // attach interrupt handler for D2
}  // end of setup

Dans ce cas, la fonction attachInterrupt ajoute la fonction switchPressed à une table interne et configure en outre les indicateurs d'interruption appropriés dans le processeur.

Configuration du processeur pour gérer une interruption

L'étape suivante, une fois que vous disposez d'un ISR, consiste à indiquer au processeur que vous souhaitez que cette condition particulière déclenche une interruption.

Par exemple, pour l'interruption externe 0 (l'interruption D2), vous pouvez faire quelque chose comme ceci:

EICRA &= ~3;  // clear existing flags
EICRA |= 2;   // set wanted flags (falling level interrupt)
EIMSK |= 1;   // enable it

Plus lisible serait d'utiliser les noms définis, comme ceci:

EICRA &= ~(bit(ISC00) | bit (ISC01));  // clear existing flags
EICRA |= bit (ISC01);    // set wanted flags (falling level interrupt)
EIMSK |= bit (INT0);     // enable it

L'EICRA (External Interrupt Control Register A) serait défini selon ce tableau de la fiche technique Atmega328. Cela définit le type exact d'interruption que vous souhaitez:

  • 0: Le niveau bas de INT0 génère une demande d'interruption (interruption LOW).
  • 1: Tout changement logique sur INT0 génère une demande d'interruption (interruption CHANGE).
  • 2: Le front descendant de INT0 génère une demande d'interruption (interruption FALLING).
  • 3: Le front montant de INT0 génère une demande d'interruption (interruption RISING).

EIMSK (External Interrupt Mask Register) active réellement l'interruption.

Heureusement, vous n'avez pas besoin de vous souvenir de ces chiffres, car attachInterrupt le fait pour vous. Cependant, c'est ce qui se passe réellement, et pour d'autres interruptions, vous devrez peut-être définir des indicateurs d'interruption "manuellement".


ISR de bas niveau vs ISR de bibliothèque

Pour vous simplifier la vie, certains gestionnaires d'interruption courants se trouvent en fait dans le code de la bibliothèque (par exemple INT0_vect et INT1_vect), puis une interface plus conviviale est fournie (par exemple, attachInterrupt). En fait, attachInterrupt enregistre l'adresse de votre gestionnaire d'interruption souhaité dans une variable, puis l'appelle à partir de INT0_vect / INT1_vect si nécessaire. Il définit également les indicateurs de registre appropriés pour appeler le gestionnaire en cas de besoin.


Les ISR peuvent-ils être interrompus?

En bref, non, sauf si vous le souhaitez.

Lorsqu'un ISR est entré, les interruptions sont désactivées . Naturellement, ils doivent avoir été activés en premier lieu, sinon l'ISR ne serait pas entré. Cependant, pour éviter d'avoir un ISR lui-même interrompu, le processeur désactive les interruptions.

Lorsqu'un ISR se termine, les interruptions sont à nouveau activées . Le compilateur génère également du code à l'intérieur d'un ISR pour enregistrer les registres et les indicateurs d'état, de sorte que tout ce que vous faisiez lorsque l'interruption s'est produite ne sera pas affecté.

Cependant, vous pouvez activer les interruptions à l'intérieur d'un ISR si vous le devez absolument, par exemple.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
  interrupts ();  // allow more interrupts

}  // end of switchPressed

Normalement, vous auriez besoin d'une assez bonne raison pour le faire, car une autre interruption maintenant pourrait entraîner un appel récursif à pinChange, avec des résultats très probablement indésirables.


Combien de temps faut-il pour exécuter un ISR?

Selon la fiche technique, la durée minimale de maintenance d'une interruption est de 4 cycles d'horloge (pour pousser le compteur de programme actuel sur la pile) suivi du code s'exécutant maintenant à l'emplacement du vecteur d'interruption. Cela contient normalement un saut à l'endroit où se trouve réellement la routine d'interruption, ce qui correspond à 3 autres cycles. L'examen du code produit par le compilateur montre qu'un ISR réalisé avec la déclaration "ISR" peut prendre environ 2,625 µs à exécuter, plus ce que le code lui-même fait. Le montant exact dépend du nombre de registres à sauvegarder et à restaurer. La quantité minimale serait de 1,1875 µs.

Les interruptions externes (où vous utilisez attachInterrupt) font un peu plus et prennent environ 5,125 µs au total (fonctionnant avec une horloge de 16 MHz).


Combien de temps avant que le processeur commence à entrer dans un ISR?

Cela varie quelque peu. Les chiffres cités ci-dessus sont les chiffres idéaux où l'interruption est immédiatement traitée. Quelques facteurs peuvent retarder cela:

  • Si le processeur est endormi, il y a des heures de "réveil" désignées, qui peuvent durer quelques millisecondes, tandis que l'horloge est remontée à la vitesse. Cette durée dépendrait des réglages du fusible et de la profondeur du sommeil.

  • Si une routine de service d'interruption est déjà en cours d'exécution, aucune autre interruption ne peut être entrée tant qu'elle n'est pas terminée ou qu'elle ne s'auto-interrompt pas. C'est pourquoi vous devriez garder chaque routine de service d'interruption courte, car chaque microseconde que vous passez dans une, vous retardez potentiellement l'exécution d'une autre.

  • Certains codes désactivent les interruptions. Par exemple, appeler millis () désactive brièvement les interruptions. Par conséquent, le délai de traitement d'une interruption serait prolongé de la durée de désactivation des interruptions.

  • Les interruptions ne peuvent être traitées qu'à la fin d'une instruction, donc si une instruction particulière prend trois cycles d'horloge et vient de démarrer, alors l'interruption sera retardée d'au moins quelques cycles d'horloge.

  • Un événement qui rétablit les interruptions (par exemple le retour d'une routine de service d'interruption) est garanti pour exécuter au moins une instruction supplémentaire. Ainsi, même si un ISR se termine et que votre interruption est en attente, il doit toujours attendre une instruction de plus avant d'être réparée.

  • Étant donné que les interruptions ont une priorité, une interruption de priorité plus élevée peut être traitée avant l'interruption qui vous intéresse.


Considérations sur les performances

Les interruptions peuvent augmenter les performances dans de nombreuses situations, car vous pouvez poursuivre le «travail principal» de votre programme sans avoir à constamment tester pour voir si des commutateurs ont été enfoncés. Cela dit, la surcharge de service d'une interruption, comme discuté ci-dessus, serait en fait plus que de faire une "boucle serrée" interroger un seul port d'entrée. Vous pouvez à peine répondre à un événement dans, disons, une microseconde. Dans ce cas, vous pouvez désactiver les interruptions (par exemple, les minuteries) et simplement boucler en recherchant la broche à changer.


Comment les interruptions sont-elles mises en file d'attente?

Il existe deux types d'interruptions:

  • Certains définissent un indicateur et ils sont traités par ordre de priorité, même si l'événement qui les a provoqués s'est arrêté. Par exemple, une interruption de niveau montant, descendant ou changeant sur la broche D2.

  • D'autres ne sont testés que s'ils se produisent "en ce moment". Par exemple, une interruption de bas niveau sur la broche D2.

Ceux qui définissent un indicateur peuvent être considérés comme étant mis en file d'attente, car l'indicateur d'interruption reste défini jusqu'à ce que la routine d'interruption soit entrée, moment auquel le processeur efface l'indicateur. Bien sûr, puisqu'il n'y a qu'un seul indicateur, si la même condition d'interruption se reproduit avant que le premier ne soit traité, il ne sera pas traité deux fois.

Il faut savoir que ces indicateurs peuvent être définis avant d'attacher le gestionnaire d'interruption. Par exemple, il est possible qu'une interruption de niveau montant ou descendant sur la broche D2 soit "signalée", puis dès que vous effectuez un attachInterrupt, l'interruption se déclenche immédiatement, même si l'événement s'est produit il y a une heure. Pour éviter cela, vous pouvez effacer manuellement le drapeau. Par exemple:

EIFR = bit (INTF0);  // clear flag for interrupt 0
EIFR = bit (INTF1);  // clear flag for interrupt 1

Cependant, les interruptions de "bas niveau" sont vérifiées en permanence, donc si vous ne faites pas attention, elles continueront à se déclencher, même après l'appel de l'interruption. Autrement dit, l'ISR se terminera, puis l'interruption se déclenchera immédiatement à nouveau. Pour éviter cela, vous devez effectuer un detachInterrupt immédiatement après avoir su que l'interruption s'est déclenchée.


Conseils pour la rédaction des ISR

Bref, restez court! Lorsqu'un ISR exécute d'autres interruptions ne peuvent pas être traitées. Ainsi, vous pourriez facilement manquer les pressions sur les boutons ou les communications série entrantes si vous essayez d'en faire trop. En particulier, vous ne devez pas essayer de déboguer des "impressions" à l'intérieur d'un ISR. Le temps nécessaire pour les réaliser est susceptible de causer plus de problèmes qu’ils n'en résolvent.

Une chose raisonnable à faire est de définir un indicateur à un octet, puis de tester cet indicateur dans la fonction de boucle principale. Ou, stockez un octet entrant d'un port série dans un tampon. La minuterie intégrée interrompt le suivi du temps écoulé en tirant à chaque fois que la minuterie interne déborde, et vous pouvez donc calculer le temps écoulé en sachant combien de fois la minuterie a débordé.

N'oubliez pas qu'à l'intérieur d'un ISR, les interruptions sont désactivées. Ainsi, l'espoir que le temps renvoyé par les appels de fonction millis () changera conduira à la déception. Il est valable d' obtenir l'heure de cette façon, sachez simplement que le temporisateur n'incrémente pas. Et si vous passez trop de temps dans l'ISR, le temporisateur peut manquer un événement de débordement, ce qui rend le temps renvoyé par millis () devenu incorrect.

Un test montre que, sur un processeur Atmega328 16 MHz, un appel à micros () prend 3,5625 µs. Un appel à millis () prend 1,9375 µs. L'enregistrement (sauvegarde) de la valeur actuelle du temporisateur est une chose raisonnable à faire dans un ISR. La recherche des millisecondes écoulées est plus rapide que les microsecondes écoulées (le décompte des millisecondes est simplement extrait d'une variable). Cependant, le comptage en microsecondes est obtenu en ajoutant la valeur actuelle du temporisateur Timer 0 (qui continuera à augmenter) à un "décompte de débordement Timer 0" enregistré.

Avertissement: étant donné que les interruptions sont désactivées dans un ISR et que la dernière version de l'Arduino IDE utilise des interruptions pour la lecture et l'écriture en série, ainsi que pour incrémenter le compteur utilisé par "millis" et "delay", vous ne devriez pas essayer d'utiliser ces fonctions à l'intérieur d'un ISR. Pour le dire autrement:

  • N'essayez pas de retarder, par exemple: delay (100);
  • Vous pouvez obtenir le temps d'un appel en millis, mais il n'augmentera pas, alors n'essayez pas de retarder en attendant qu'il augmente.
  • Ne faites pas d'impression en série (par exemple. Serial.println ("ISR entered");)
  • N'essayez pas de faire une lecture en série.

Interruptions de changement de broche

Il existe deux façons de détecter des événements externes sur les broches. Le premier est les broches spéciales "interruption externe", D2 et D3. Ces événements d'interruption discrets généraux, un par broche. Vous pouvez y accéder en utilisant attachInterrupt pour chaque broche. Vous pouvez spécifier une condition de hausse, de baisse, de modification ou de bas niveau pour l'interruption.

Cependant, il existe également des interruptions de «changement de broches» pour toutes les broches (sur l'Atmega328, pas nécessairement toutes les broches des autres processeurs). Ceux-ci agissent sur des groupes de broches (D0 à D7, D8 à D13 et A0 à A5). Ils sont également moins prioritaires que les interruptions d'événements externes. Cependant, ils sont un peu plus compliqués à utiliser que les interruptions externes car ils sont regroupés en lots. Donc, si l'interruption se déclenche, vous devez déterminer dans votre propre code quelle broche a causé l'interruption.

Exemple de code:

ISR (PCINT0_vect)
 {
 // handle pin change interrupt for D8 to D13 here
 }  // end of PCINT0_vect

ISR (PCINT1_vect)
 {
 // handle pin change interrupt for A0 to A5 here
 }  // end of PCINT1_vect

ISR (PCINT2_vect)
 {
 // handle pin change interrupt for D0 to D7 here
 }  // end of PCINT2_vect


void setup ()
  {
  // pin change interrupt (example for D9)
  PCMSK0 |= bit (PCINT1);  // want pin 9
  PCIFR  |= bit (PCIF0);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE0);   // enable pin change interrupts for D8 to D13
  }

Pour gérer une interruption de changement de broche, vous devez:

  • Spécifiez quelle broche dans le groupe. Il s'agit de la variable PCMSKn (où n est 0, 1 ou 2 dans le tableau ci-dessous). Vous pouvez avoir des interruptions sur plusieurs broches.
  • Activez le groupe approprié d'interruptions (0, 1 ou 2)
  • Fournissez un gestionnaire d'interruption comme indiqué ci-dessus

Tableau des broches -> changer les noms / masques des broches

D0    PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1    PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2    PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3    PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4    PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5    PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6    PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7    PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8    PCINT0  (PCMSK0 / PCIF0 / PCIE0)
D9    PCINT1  (PCMSK0 / PCIF0 / PCIE0)
D10   PCINT2  (PCMSK0 / PCIF0 / PCIE0)
D11   PCINT3  (PCMSK0 / PCIF0 / PCIE0)
D12   PCINT4  (PCMSK0 / PCIF0 / PCIE0)
D13   PCINT5  (PCMSK0 / PCIF0 / PCIE0)
A0    PCINT8  (PCMSK1 / PCIF1 / PCIE1)
A1    PCINT9  (PCMSK1 / PCIF1 / PCIE1)
A2    PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3    PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4    PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5    PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Traitement du gestionnaire d'interruption

Le gestionnaire d'interruption devra déterminer quelle broche a provoqué l'interruption si le masque en spécifie plusieurs (par exemple, si vous souhaitez des interruptions sur D8 / D9 / D10). Pour ce faire, vous devez stocker l'état précédent de cette broche et déterminer (en faisant un digitalRead ou similaire) si cette broche particulière a changé.


Vous utilisez probablement des interruptions de toute façon ...

Un environnement Arduino "normal" utilise déjà des interruptions, même si vous ne tentez pas personnellement. Les appels de fonction millis () et micros () utilisent la fonction "dépassement de temporisation". L'un des temporisateurs internes (temporisateur 0) est configuré pour interrompre environ 1000 fois par seconde et incrémenter un compteur interne qui devient effectivement le compteur millis (). Il y a un peu plus que cela, car le réglage est fait pour la vitesse d'horloge exacte.

La bibliothèque série matérielle utilise également des interruptions pour gérer les données série entrantes et sortantes. Ceci est très utile car votre programme peut faire d'autres choses pendant le déclenchement des interruptions et remplir un tampon interne. Ensuite, lorsque vous cochez Serial.available (), vous pouvez découvrir ce qui, le cas échéant, a été placé dans ce tampon.


Exécuter l'instruction suivante après avoir activé les interruptions

Après un peu de discussion et de recherche sur le forum Arduino, nous avons clarifié exactement ce qui se passe après avoir activé les interruptions. Il y a trois façons principales de penser que vous pouvez activer les interruptions, qui n'étaient pas activées auparavant:

  sei ();  // set interrupt enable flag
  SREG |= 0x80;  // set the high-order bit in the status register
  reti  ;   // assembler instruction "return from interrupt"

Dans tous les cas, le processeur garantit que l' instruction suivante après que les interruptions sont activées (si elles étaient précédemment désactivées) sera toujours exécutée, même si un événement d'interruption est en attente. (Par "suivant", j'entends le suivant dans la séquence du programme, pas nécessairement celui qui suit physiquement. Par exemple, une instruction RETI revient à l'endroit où l'interruption s'est produite, puis exécute une autre instruction).

Cela vous permet d'écrire du code comme ceci:

sei ();
sleep_cpu ();

Si ce n'est pas le cas pour cette garantie, l'interruption peut se produire avant que le processeur ne dorme, puis elle peut ne jamais se réveiller.


Interruptions vides

Si vous voulez simplement une interruption pour réveiller le processeur, mais ne faites rien de particulier, vous pouvez utiliser la définition EMPTY_INTERRUPT, par exemple.

EMPTY_INTERRUPT (PCINT1_vect);

Cela génère simplement une instruction "reti" (retour de l'interruption). Comme il n'essaie pas de sauvegarder ou de restaurer les registres, ce serait le moyen le plus rapide d'obtenir une interruption pour le réveiller.


Sections critiques (accès aux variables atomiques)

Il existe quelques problèmes subtils concernant les variables qui sont partagées entre les routines de service d'interruption (ISR) et le code principal (c'est-à-dire le code qui n'est pas dans un ISR).

Étant donné qu'un ISR peut se déclencher à tout moment lorsque les interruptions sont activées, vous devez être prudent quant à l'accès à ces variables partagées, car elles peuvent être mises à jour au moment même où vous y accédez.

Tout d'abord ... quand utilisez-vous des variables "volatiles"?

Une variable ne doit être marquée comme volatile que si elle est utilisée à la fois à l'intérieur d'un ISR et à l'extérieur.

  • Les variables utilisées uniquement en dehors d'un ISR ne doivent pas être volatiles.
  • Les variables utilisées uniquement à l' intérieur d'un ISR ne doivent pas être volatiles.
  • Les variables utilisées à l'intérieur et à l'extérieur d'un ISR doivent être volatiles.

par exemple.

volatile int counter;

Marquer une variable comme volatile indique au compilateur de ne pas "mettre en cache" le contenu des variables dans un registre de processeur, mais de toujours le lire dans la mémoire, si nécessaire. Cela peut ralentir le traitement, c'est pourquoi vous ne faites pas que chaque variable volatile, quand elle n'est pas nécessaire.

Désactiver les interruptions lors de l'accès à une variable volatile

Par exemple, pour comparer countà un certain nombre, désactivez les interruptions pendant la comparaison dans le cas où un octet de counta été mis à jour par l'ISR et non l'autre octet.

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // end of TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // end of setup

void loop ()
  {
  noInterrupts ();    // <------ critical section
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();      // <------ end critical section
  } // end of loop

Lisez la fiche technique!

Plus d'informations sur les interruptions, les minuteries, etc. peuvent être obtenues à partir de la fiche technique du processeur.

http://www.atmel.com/images/Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete.pdf


D'autres exemples

Des considérations d'espace (limite de taille de poste) empêchent ma liste d'exemples de code. Pour plus d'exemples de code, consultez ma page sur les interruptions .


Une référence très utile - c'était une réponse incroyablement rapide.
Dat Han Bag

C'était une question de référence. J'avais préparé la réponse et cela aurait été encore plus rapide si la réponse n'avait pas été trop longue, j'ai donc dû l'élaguer. Voir le site lié pour plus de détails.
Nick Gammon

À propos du "mode veille", est-il efficace de faire dormir l'Arduino pendant, disons, 500 ms?
Dat Ha

@ Nick Gammon Je suppose que la mise sous tension ou hors tension (avec ou sans automatisation) du CPU peut être définie comme une interruption non conventionnelle, si vous le souhaitez. "J'avais préparé la réponse" - vous venez de retirer toute la magie de ce moment que je pensais avoir.
Dat Han Bag

1
J'ai bien peur que ce ne soit pas vrai. J'ai un exemple qui utilise des interruptions de changement de broche pour sortir du mode hors tension. Aussi, comme je le mentionne sur ma page sur les interruptions, Atmel a confirmé que toute interruption externe réveillera le processeur (c'est-à-dire montée / descente / changement et faible).
Nick Gammon
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.