ARDUINO : les Interruptions
Qu'est-ce qu'une interruption ? c'est un mécanisme présent sur beaucoup de processeurs, qui permet d'exécuter un code particulier, appelé routine d'interruption, quand une entrée change d'état, mais pas seulement dans ce cas.
Une interruption peut également être déclenchée par un timer, ou servir à réveiller un microcontrôleur endormi.
Dans cet article nous allons également aborder un point important : les sections critiques.
1. Les interruptions
1.1. Changement d'état d'une entrée
Le code d'une application peut demander à ce qu'une routine particulière soit exécutée si une entrée digitale change d'état, en attachant une routine à un vecteur d'interruption, à l'aide de la fonction attachInterrupt().
Lorsqu'une interruption survient le code principal de l'application est interrompu, le code de la routine d'interruption est exécuté, puis le code principal de l'application reprend son exécution là où elle s'était arrêtée.
Déclencher une interruption sur changement d'état d'une entrée digitale est facile. Ce code permet de changer l'état d'une LED en fonction des changements d'état de l'entrée D2 :
const byte interruptPin = 2;
volatile byte state = LOW;
void setup()
pinMode(ledPin, OUTPUT);
pinMode(interruptPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(interruptPin), blink, CHANGE);
}
void loop()
digitalWrite(ledPin, state);
}
void blink() {
state = !state;
}
La fonction blink() (c'est elle la routine d'interruption) est appelée à chaque fois que l'entrée change d'état.
Sur un ATMEGA328P seules les entrées 2 et 3 sont utilisables. Sur un MEGA2560 elles sont plus nombreuses : 2, 3, 18, 19, 20, 21.
1.1.1. Front montant ou descendant ?
La routine d'interruption peut être appelée uniquement sur les fronts montants ou descendant du signal, ou les deux, en modifiant le 3ème paramètre de la fonction attachInterrupt() :
- CHANGE : front montant ou descendant
- RISING : front montant uniquement
- FALLING : front descendant uniquement
1.1.2. digitalPinToInterrupt()
A quoi peut bien servir la fonction digitalPinToInterrupt() ? c'est une fonction du core ARDUINO qui retourne le N° de l'interruption correspondant à une broche donnée :
digitalPinToInterrupt(2) retournera 0, et digitalPinToInterrupt(3) retournera 1.
attachInterrupt(digitalPinToInterrupt(2), blink, CHANGE);
Est donc équivalent à :
attachInterrupt(0, blink, CHANGE);
1.1.3. Pin change interrupts
Il faut mentionner aussi les Pin Change Interrupts (PCINT), qui permettent de déclencher une interruption sur une broche digital quelconque :
https://www.robot-maker.com/forum/topic/12765-pin-change-interrupt-sur-arduino-mega-et-uno/
Les PCINT sont un peu plus coûteuses en temps d'exécution si plusieurs entrées peuvent déclencher une interruption, car dans le code de la routine d'interruption il faut déterminer quelle entrée est responsable de l'interruption.
1.2. Timer interrupts
Une interruption peut être générée également sur expiration d'un timer, ou comparaison du compteur d'un timer à une certaine valeur.
On peut bien entendu manipuler directement les registres d'un timer :
https://www.locoduino.org/spip.php?article84
https://www.locoduino.org/spip.php?article88
https://www.locoduino.org/spip.php?article89
https://www.instructables.com/Arduino-Timer-Interrupts/
Heureusement, des librairies, bien plus faciles à utiliser, existent :
Comme je le conseille souvent, essayer les exemples des librairies est un bon point de départ.
ATTENTION : certaines librairies utilisent les timers :
- timer0 (8 bits) compte de 0 à 256 et commande la PWM des broches 5 et 6. Il est aussi utilisé par les fonctions delay(), millis() et micros().
- timer1 (16 bits) compte de 0 à 65535 et est utilisé pour la commande PWM des broches 9 et 10. Il est utilisé également par la libriaire Servo.h
- timer2 (8 bits) est utilisé par la fonction Tone() et la génération de la PWM sur les broches 3 et 11.
Par exemple la librairie IRREMOTE utilise également le timer2, et son utilisation entrera en conflit avec la librairie tone si elles sont utilisées dans le même logiciel :
core.a(Tone.cpp.o): In function tone(unsigned char, unsigned int, unsigned long)': /home/user/arduino-1.0.1/hardware/arduino/cores/arduino/Tone.cpp:230: multiple definition of
__vector_7'
IRremote/IRremote.cpp.o:/home/user/arduino-1.0.1/libraries/IRremote/IRremote.cpp:70: first defined here
1.3. Analog interrupts
On peut même déclencher une interruption à l'aide du comparateur analogique intégré à l'ATMEGA328P :
http://www.gammon.com.au/forum/?id=11916
1.4. Pièges
Piège courant : que signifie cet attribut volatile dans l'exemple ci-dessus ? il faut savoir que lorsque l'on lit dans le programme principal une variable susceptible d'être modifiée par une routine d'interruption cet attribut est indispensable si l'on veut éviter que le compilateur procède à certaines optimisations. L'attribut volatile forcera le code généré par le compilateur à relire la variable à chaque fois.
Dans l'exemple donné ci-dessus au paragraphe 1.1. l'attribut volatile est inutile car la variable state n'est lue qu'une fois dans la fonction loop(). Si elle était lue répétitivement dans une boucle for() ou while() l'attribut volatile serait indispensable. Dans le doute, utiliser volatile systématiquement n'est pas une mauvaise idée.
Deuxième piège : une routine d'interruption doit être la plus courte possible. Dans l'exemple précédent la routine blink() se contente de modifier une variable. Il est hors de question de faire des traitements lourds dans une routine d'interruption (Serial.print() par exemple). Le risque est de rater une nouvelle interruption pendant que l'interruption courante est en cours de traitement.
1.5. Temps d'exécution
Il est possible de surveiller une entrée par une lecture répétitive dans le programme principal, ce que l'on appelle polling, ou à l'aide d'une interruption.
La gestion d'une interruption n'est pas un mécanisme gratuit. Son exécution prend du temps :
- sauvegarde sur la pile du contexte d'exécution de l'application (un certain nombre de registres)
- exécution de l'interruption
- restitution du contexte d'exécution de l'application
Il convient donc d'utiliser les interruptions à bon escient. Il peut arriver que l'exécution répétitive d'interruptions à fréquence élevée consomme plus de temps CPU qu'un traitement classique dans le programme principal.
Il est parfois plus économique et plus efficace de travailler en polling plutôt que sous interruption.
2. Section critique
2.1. Code critique
Comme dit plus haut :
Lorsqu'une interruption survient le code principal de l'application est interrompu, le code de la routine est exécuté, puis le code principal de l'application reprend son exécution.
Il s'en suit donc que l'exécution du code principal peut être perturbé, d'un point de vue temporel, par une interruption. Si la précision d'exécution dans le temps est critique, il convient de protéger le code en désactivant les interruptions. Cela s'appelle une section critique :
void loop()
{
noInterrupts(); // désactivation des interruptions
// portion de code critique, sensible au temps, ici
interrupts(); // réactivation des interruptions
// le reste du code ici
}
Dans l'exemple suivant (la librairie OneWire, écrite par un développeur très compétent), les interruptions sont désactivées pendant la lecture et l'écriture sur le bus, car la communication OneWire doit respecter des timings très précis :
https://github.com/PaulStoffregen/OneWire/blob/master/OneWire.cpp
Il faut savoir que même si le logiciel que l'on est en train d'écrire ne met en place aucune routine d'interruption, certaines routines d'interruption sont déjà installées par la librairie ARDUINO :
- ligne série (Serial)
- timer ZÉRO (utilisé entre autres par millis())
On n'est donc jamais totalement à l'abri.
2.2. Protection de variables
Une section critique peut également être utilisée lorsque l'on désire lire ou modifier des variables pouvant être également modifiées par une interruption.
Première règle : ne jamais travailler directement avec des variables modifiables par une interruption.
Pourquoi ? tout simplement parce que pendant le traitement, la variable peut changer de valeur suite à une interruption.
J'ai déjà travaillé sur un driver LIN (LIN est un protocole de communication largement utilisé dans l'industrie). Chaque trame LIN était reçue sous interruption et l'auteur n'avait pas pris la peine de recopier celle-ci avant de la traiter. Cela semblait fonctionner la plupart du temps, mais de temps à autre une nouvelle trame arrivait avant que la précédente ne soit complètement traitée. Le résultat était catastrophique !
Dans le programme principal il faut donc toujours copier les variables avant de les utiliser, et ceci doit être fait dans une section critique.
Le cas le plus simple est la modification d'une variable sur 32 bits. Si le programme principal consulte une variable de type long ou unsigned long susceptible d'être modifiée par une interruption, une lecture ou une écriture fiable n'est pas garantie.
Le microcontrôleur ATMEGA328P ne possède pas d'instructions de lecture ou d'écriture 32 bits. La lecture ou l'écriture sera donc effectuée en deux instructions. Si une interruption survient entre les deux instructions, le résultat peut être faussé.
- counter = 0x0001FFFF
- copie du LSB : cnt vaut donc 0x0000FFFF
- interruption ! : counter est incrémenté : counter vaut maintenant 0x00020000
- copie du MSB : cnt devient 0x0002FFFF
- le résultat est totalement faux
Une section critique garantira que LSB et MSB soient copiés correctement :
Avec cette modification le déroulement devient :
- counter = 0x0001FFFF
- désactivation des interruptions
- copie du LSB : cnt vaut donc 0x0000FFFF
- pas d'interruption possible
- copie du MSB : cnt devient 0x0001FFFF
- réactivation des interruptions
- le résultat est correct
Bien sûr on pourra objecter que cet exemple est extrême et qu'il y a très peu de chances pour que counter soit égal à 0x0001FFFF et qu'une interruption survienne pendant la copie. Mais sur un temps d'exécution très long cela peut parfaitement arriver. Mieux vaut prévenir ce genre de situation, car en cas de problème il sera très difficile d'en déterminer l'origine.
Prenons un autre exemple, avec deux variables :
Dans cet exemple la routine d'interruption isr() ajoute à un buffer un caractère aléatoire et incrémente un index nommé counter. Dans la réalité ce caractère pourrait parfaitement provenir de la lecture d'un port physique d'entrée / sortie, ou de la lecture d'un timer.
Dans la fonction loop(), si le buffer est à moitié plein il est recopié dans un autre buffer localBuffer pour être traité ensuite, puis counter est remis à ZÉRO.
Une interruption peut survenir pendant que loop() effectue cette opération, ce qui pourrait provoquer une incohérence entre le contenu du buffer et l'index.
Exemple : imaginons que counter soit égal à 55 :
- loop() recopie le buffer
- interruption ! la routine isr() place un octet supplémentaire à l'emplacement 55 et incrémente la variable counter. counter devient égal à 56
- loop() remet counter à ZÉRO
Résultat : avant l'interruption counter vaut 55, et 56 après l'interruption. Après l'interruption, loop() remet la variable counter à ZÉRO. Le caractère à l'emplacement 55 est donc perdu, puisqu'il n'a pas été recopié.
Le remède est l'adoption d'une section critique :
On préservera ainsi la cohérence entre les deux variables.
3. Le mode veille
Le mode veille est utilisé lorsque l'on désire consommer un minimum d'énergie dans une configuration alimentée par piles ou batterie. En pratique seule la carte PRO MINI est adaptée à ce genre d'utilisation, ainsi que certaines cartes ESP8266 et ESP32.
J'en parle ici :
ARDUINO PRO MINI & basse consommation
Consommation d'une carte ARDUINO, ESP8266 ou ESP32
Le mode veille est intimement lié aux interruptions, car lorsque le microcontrôleur est endormi, l'unique moyen de le réveiller est une interruption, et pas n'importe laquelle.
On peut sortir du mode veille en utilisant différentes méthodes :
- réveil au bout d'un temps défini à l'aide du watchdog
- réveil par une entrée digitale
Exemple : on peut parfaitement réveiller un ARDUINO à l'aide d'un capteur de présence sur une entrée digitale.
Sur un ATMEGA328P seules les entrées 2 et 3 sont utilisables pour le réveil. Sur un MEGA2560 elles sont plus nombreuses : 2, 3, 18, 19, 20, 21.
3.1. ESP8266 et ESP32
Contrairement à ce qui se passe sur un ARDUINO, le réveil par GPIO n'existe pas vraiment sur un ESP8266 ou un ESP32, en tous cas pas de la même façon. Un RESET est provoqué et le logiciel doit aller consulter les informations de RESET et lire l'état de la GPIO pour connaître la cause du réveil.
L'ESP32 possède une autre possibilité de réveil : le touchpad
voir cet article : ESP8266 et ESP32 sur batterie
3.2. Tutoriels
On trouve énormément de tutoriels sur le sujet :
TUTORIAL:A GUIDE TO PUTTING YOUR ARDUINO TO SLEEP
Waking up an Arduino with Input from a Sensor
Comme on peut le constater le code n'est pas toujours très abordable pour un débutant.
3.3. Librairie LowPower
Pour ma part j'utilise la librairie LowPower, très simple à mettre en oeuvre (voir les exemples) :
Réveil périodique : idleWakePeriodic
Réveil par une entrée digitale : powerDownWakeExternalInterrupt
Dans ce dernier exemple on aura avantage à remplacer la ligne suivante :
attachInterrupt(0, wakeUp, LOW);
Par :
attachInterrupt(0, wakeUp, RISING);
ou
attachInterrupt(0, wakeUp, FALLING);
Ceci afin de réveiller le microcontrôleur seulement sur un front montant ou descendant.
4. Conclusion
Maîtriser les interruptions peut être un atout dans certaines situations, mais en abuser serait une erreur.
Cordialement
Henri
bonjour,
RépondreSupprimerJ'ai une question sur les interruptions car bloqué dans mon code, est-il possible de coder plus d'une interruption ? Dans mon cas j'en ai codé 2 mais cette 2eme est comme inexistante, elle ne se déclenche absolument pas. Pourtant, comparé à la première interruption codée (qui elle fonctionne a merveille), m'a l'air tout a fait semblable.
Un avis ?
Merci !
Sans le code, difficile de se prononcer.
SupprimerCela va être difficile de poster le code dans les commentaires de ce blog. Sur https://forum.arduino.cc/c/international/francais/49 ce sera plus facile.
Bonjour.
RépondreSupprimerJe réagis a la phrase "Par exemple la librairie IRREMOTE utilise également le timer2, et son utilisation entrera en conflit avec la librairie tone si elles sont utilisées dans le même logiciel".
Comment savoir quelle librairie utilise quel timer, pour vérifier l'absence de conflit ?
J'ai essayé de trouver le fichier source de la librairie et de chercher dedans le mot clef "TCCR", mais je sais pas du tout si c'est une bonne méthode. Pour la librairie servo j'ai assez facilement trouvé, par contre pour les trucs qui sont inclus dans le coeur d'arduino comme par exemple le protocole Serial… c'est beaucoup plus dur de trouver dans quel fichier chercher le mot clef.
Et merci pour tout ces partages.
Mis à part l'examen du code je ne vois pas de solution, à moins que l'auteur ait documenté l'utilisation des interruptions.
SupprimerSinon, la compilation est un bon moyen de mettre en évidence les définitions multiples, et de trouver rapidement les conflits.