LOCODUINO

Aide
Forum de discussion
Dépôt GIT Locoduino
Flux RSS

mardi 19 mars 2024

Visiteurs connectés : 100

Les interruptions (1)

.
Par : Jean-Luc

DIFFICULTÉ :

La façon dont un Arduino se programme a été présentée dans « La programmation, qu’est ce que c’est » et les articles suivants de la série. Vous savez donc que le logiciel qui vient avec l’Arduino appelle une fois la fonction setup() puis appelle ensuite loop() de manière répétitive.

Généralement, pour mettre en œuvre un système, on va, dans loop(), lire des entrées de l’Arduino, qu’elles soient analogiques ou numériques, exécuter un certain nombre de calculs en fonction de ces entrées puis modifier l’état du système, ce qui aboutira éventuellement à positionner des sorties.

Ces lectures se font donc de manière répétitive. On appelle ceci de la scrutation ou polling en anglais. La période de scrutation dépend du temps que loop() met à s’exécuter et peut d’ailleurs être variable si loop() contient des instructions conditionnelles qui font que d’une itération à l’autre de loop() le nombre d’instructions exécutées n’est pas le même.

Dans certaines circonstances la scrutation n’est pas adaptée car l’entrée qui est scrutée peut changer plusieurs fois de valeur pendant la période de scrutation. Dans ce cas ces changements de valeur ne sont pas vus par le logiciel et le système ne fonctionne pas correctement.

La solution réside dans l’utilisation des interruptions.

<

Article mis à jour le 18 septembre 2021 pour prendre en compte les évolutions de l’API du logiciel Arduino.

Principe de fonctionnement des interruptions

On a vu dans « La programmation, qu’est ce que c’est » que l’Arduino exécutait son programme en séquence, instruction par instruction, dans l’ordre. Certaines instructions permettent de rompre cet ordre : les instructions conditionnelles comme le if ... else ou les instructions de boucle comme le for(...). Mais l’Arduino reste sur ses rails et respecte les instructions qui lui sont données.

Une interruption, comme son nom l’indique, consiste à interrompre momentanément le programme que l’Arduino exécute pour qu’il effectue un autre travail. Quand cet autre travail est terminé, l’Arduino retourne à l’exécution du programme et reprend à l’endroit exact où il l’avait laissé.

Cet autre travail s’appelle le programme d’interruption ou la routine d’interruption ou encore une ISR pour Interrupt Service Routine en anglais.

Il est très important que les ISR aient un temps d’exécution le plus court possible. On ne fera donc aucun calcul compliqué et aucun appel à des fonctions longues comme un affichage sur un écran LCD.

Il existe plusieurs sources d’interruption, on dit également vecteur d’interruption, sur l’AVR ATmega 328 qui équipe l’Arduino Uno, 26 au total. Nous n’allons pas toutes les passer en revue. Nous allons seulement nous concentrer sur celles que nous pouvons utiliser dans nos sketchs. Il s’agit des 5 interruptions liées au changement de la tension présente sur une broche numérique qui sont donc ce que l’on appelle des interruptions externes. Les interruptions liées au timers, des compteurs internes à l’Arduino qui permettent de compter le temps de manière précise sont également intéressantes et feront l’objet d’un autre article.

Que se passe-t-il si une nouvelle interruption survient alors que l’ISR déclenchée par la précédente n’est pas terminée ?

Une ISR n’est pas interrompue par une nouvelle interruption. La nouvelle interruption ne sera prise en compte que lorsque l’ISR en cours se terminera.

Le corollaire est qu’il ne faut pas appeler de fonctions qui se mettent en attente d’une interruption à partir d’une ISR. Comme l’interruption attendue ne peut pas déclencher une nouvelle ISR, la fonction attendra indéfiniment et tout le système se bloquera. C’est ce que l’on appelle un deadlock.

Les fonctions de Serial qui permettent d’afficher, via la ligne série et l’USB dans le moniteur série font exactement cela. Leur appel à partir d’une ISR est donc interdit.

Que se passe-t-il si plusieurs interruptions surviennent en même temps ?

Les Interruptions ont chacune une priorité. Par exemple, les interruptions externes sont plus prioritaires que les interruptions des Timers. L’Arduino exécutera les ISR dans leur ordre de priorité. L’ordre de priorité est donné dans la table ci-dessous. La source d’interruption située la plus en haut de la table est la plus prioritaire.

SourceRôle
INT0 Interruption externe sur la broche 2
INT1 Interruption externe sur la broche 3
PCINT0 Interruption externe sur les broches 8 à 13
PCINT1 Interruption externe sur les broches A0 à A5
PCINT3 Interruption externe sur les broches 0 à 7

Les interruptions externes INT0 et INT1

Ces deux interruptions sont les plus faciles à utiliser. INT0 correspond à la broche 2 des Arduino a base d’AVR ATmega 328 et INT1 correspond à la broche 3. Pour accrocher une routine d’interruption à un état ou un changement d’état de l’une de ces broches on va utiliser la fonction attachInterrupt(...).

Cette fonction prend 3 arguments : le numéro d’interruption externe, la fonction à appeler quand l’interruption survient et enfin la condition selon laquelle l’interruption survient. Son prototype est le suivant :

attachInterrupt(numéro, ISR, mode);

numéro est le numéro de l’interruption externe. Il s’agira de 0 ou 1 sur un Arduino Uno/Nano, ce qui correspond respectivement aux broches 2 et 3. Sur un Arduino Mega, les numéros 0 à 5 seront possibles et correspondent, dans l’ordre, aux broches 21, 20, 19, 18, 2 et 3.

Mais on préférera l’appeler en utilisant la fonction d’aide digitalPinToInterrupt qui prend comme argument le numéro de broche et renvoie le numéro d’interruption. Ceci permet de ne pas avoir à se référer à la documentation pour retrouver à quelle broche correspond une interruption. L’appel de attachInterrupt devient donc :

attachInterrupt(digitalPinToInterrupt(broche), ISR, mode);

Détaillons ses arguments.

  • broche est la broche correspondant à l’interruption externe. C’est à dire 2 ou 3 sur un Arduino Uno/Nano et 2, 3, 18, 19, 20 et 21 sur un Arduino Mega.
  • ISR est la routine d’interruption qui sera appelée lorsque l’interruption surviendra. Cette routine d’interruption est une fonction qui ne renvoit rien et qui ne prend aucun argument, comme ceci : void maRoutine() { ... }.
  • mode indique ce qui va provoquer l’interruption. Les valeurs possibles pour mode sont :
    • LOW : l’interruption est déclenchée quand la broche concernée est LOW. Comme il s’agit d’un état et non d’un événement, l’interruption sera déclenchée tant que la broche est LOW. Par conséquent, dès que l’ISR aura terminé son exécution, elle la recommencera. Pendant ce temps, loop() ne sera pas exécuté.
    • CHANGE : l’interruption est déclenchée quand la broche concernée change d’état, c’est à dire passe de LOW à HIGH ou bien de HIGH à LOW. Il s’agit d’un événement.
    • RISING : l’interruption est déclenchée quand la broche concernée passe de LOW à HIGH. Il s’agit également d’un événement.
    • FALLING : l’interruption est déclenchée quand la broche concernée passe de HIGH à LOW. Il s’agit encore d’un événement.

Les modes de déclenchement sont le reflet direct des capacités du matériel et à ce titre permettent la meilleure réactivité. Ayez tout de même à l’esprit qu’il s’écoule presque 3μs entre le déclenchement de l’interruption et l’exécution de la première instruction de l’ISR.

Supposons que l’on veuille provoquer l’exécution de la routine d’interruption maRoutine lorsque le signal sur la broche 2 passe de HIGH à LOW. maRoutine est définie comme suit :

/* Change l'état de la LED embarquée sur la carte */
void maRoutine()
{
    digitalWrite(LED_BUILTIN, ! digitalRead(LED_BUILTIN));
}

On appellera attachInterrupt de la manière suivante :

attachInterrupt(digitalPinToInterrupt(2), maRoutine, FALLING);

Une seconde fonction, detachInterrupt(...) permet de déconnecter l’ISR de la source d’interruption. Son prototype est le suivant :

detachInterrupt(numero);

numero est le numéro d’interruption, 0 ou 1 sur un Arduino Uno, 0 à 5 sur un Arduino Mega.

Ici aussi, il est préférable d’utiliser digitalPinToInterrupt pour se dispenser d’avoir à se rappeler de la correspondance broche → numéro d’interruption. Il est donc recommandé d’appeler detachInterrupt de la manière suivante :

detachInterrupt(digitalPinToInterrupt(broche));

Dominique a présenté dans L’Arduino et le système de commande numérique DCC la façon dont l’information est transmise en DCC et notamment le fait que les bits d’une trame DCC étaient codés via le temps qui s’écoule entre deux fronts descendants du signal. La façon adéquate est donc de connecter le signal DCC, une fois mis en forme entre 0 et 5v, ou 0 et 3,3v pour un Arduino a base de micro-contrôleur ARM, sur une entrée d’interruption externe et d’attacher une ISR dont le mode est FALLING. À chacune de ses exécutions, l’ISR mesurera le temps écoulé depuis son exécution précédente et déterminera ainsi la valeur du bit qui vient d’être transmis.

Deux broches d’interruptions externes ne permettent pas de mettre en œuvre des systèmes de grande taille. Heureusement, toutes les broches numériques de l’Arduino Uno peuvent servir comme broches d’interruptions externes [1].

Les interruptions externes PCINT0, PCINT1 et PCINT2

Ces interruptions externes offrent moins de possibilités que celles que nous avons vues au paragraphe précédent.

Tout d’abord, les 3 sources PCINT0, PCINT1 et PCINT2 sont partagées par plusieurs broches. PCINT0 correspond aux interruptions externes sur les broches 8 à 13, PCINT1 aux interruptions externes sur les broches A0 à A5 et PCINT3 aux interruptions externes sur les broches 0 à 7. On peut noter que les broches 2 et 3 sont donc partagées entre PCINT3 et INT0 et INT1. On ne peut donc à la fois bénéficier de INT0 et INT1 et des interruptions PCINT3 sur les broches 2 et 3

Comme la même ISR est exécutée pour toutes les broches qui partagent sa source d’interruption, la discrimination de la broche qui a engendré l’interruption, ou des broches en cas de changements simultanés, doit être faite dans la routine d’interruption.

Ensuite, la richesse des modes de déclenchement de INT0 et INT1 n’existent pas pour les sources PCINTx. Le seul mode est l’interruption sur changement d’état, LOW vers HIGH et HIGH vers LOW. Par conséquent si seul l’un des changements d’état intéresse le programme, il faut également discriminer en allant lire l’état de la broche qui a déclenché l’interruption.

Heureusement il existe une bibliothèque qui gère toutes ces situations et qui permet, d’une part, de retrouver une souplesse et une richesse comparable à celle de INT0 et INT1 et, d’autre part, de masquer la complexité du matériel.

Il faut toutefois garder à l’esprit que, comparé aux interruptions INT0 et INT1, l’usage des interruptions PCINTx conduira à une exécution plus fréquente de l’ISR et à une réactivité moindre puisque la bibliothèque prendra du temps pour déterminer la broche source et le type de changement avant d’appeler votre fonction. On privilégiera donc les premières. Le corollaire est que la fonction que vous écrivez n’est pas directement une ISR mais une fonction appelée par l’ISR. En effet, il n’y a que 3 ISR, une pour chacune des trois sources PCINT0, PCINT1 et PCINT2, mais vous pouvez définir autant de fonctions qu’il y a de broches grâce à la bibliothèque.

La bibliothèque PinChangeInt

Avant d’utiliser cette bibliothèque, il faut l’installer. L’installation est classique. Comme d’habitude, il faut veiller à ce que les noms des dossiers soient cohérents. Pour ma part, j’ai installé la version 2.19beta que l’on trouve ici : http://code.google.com/p/arduino-pi.... Une fois l’archive zip décompressée, donnant un dossier nommé pinchangeint-v2.19beta. À l’intérieur, vous trouverez un dossier PinChangeInt qu’il faut déplacer dans le dossier bibliothèque de votre dossier de travail. Relancez ensuite l’IDE Arduino.

La bibliothèque fournit les fonctions :

PCintPort::attachInterrupt(pin, ISR, mode);

On voit que alors que les fonctions attachInterrupt et detachInterrupt relatives à INT0 et INT1 prennent comme premier argument le numéro de l’interruption, PCintPort::attachInterrupt et PCintPort::detachInterrupt prennent comme premier argument le numéro ou l’identifiant de la broche. Les autres arguments sont identiques.

et

PCintPort::detachInterrupt(pin);

où l’unique argument est la broche.

Un exemple est fourni dans le programme ci-dessous où les broches auxquelles les ISR sont attachées sont A1, A2, A3 et A4. Une exploration plus détaillée de cette bibliothèque fera l’objet d’un futur article.

Un exemple d’utilisation des interruptions PCINTx est la surveillance d’un grand nombre de pédales qui sont fréquemment mises en œuvre par des ILS. Voici par exemple un programme test qui gère 4 ILS. Les ILS sont connectés sur les entrées A1 à A4 programmées en entrées numériques avec pull-up. Voici une implantation sur breadboard de ce montage très simple.

PNG - 230.5 kio

Le programme installe 4 fonctions qui seront appelées par les ISR des vecteurs PCINTx, une pour chaque ILS. Les ILS tirant l’entrée à 0V quand ils collent, le mode de déclenchement choisi est FALLING. Chaque fonction incrémente une variable. Il faut toujours que les fonctions exécutées lors d’une interruption soient les plus courtes possibles.

Les ILS étant des dispositifs mécaniques, ils engendrent des rebonds quand ils collent ou décollent. C’est à dire que, au lieu de géner un seul FALLING, ils vont en générer plusieurs. La réactivité des ISR est grande, elles sont capables de capturer des événements qui se succèdent toutes les 10µs environ, Les rebonds sont plus lents, on est dans la centaine de µs. Il est donc nécessaire de filtrer les déclenchements trop rapprochés qui correspondent aux rebonds. La manière habituelle de faire consiste à enregistrer la date de déclenchement et, quand une seconde ISR survient, de n’en tenir compte que si le temps écoulé depuis la dernière date est supérieur à un seuil. 1ms permet de gommer les rebonds tout en conservant une bonne réactivité.

Voici par exemple la fonction correspondant à un déclenchement sur le premier ILS :

void interruptILS1()
{
  static unsigned long dateDernierChangement = 0;
  
  unsigned long date = millis();
  if ((date - dateDernierChangement) > dureeAntiRebond) {
    comptageILS1++;
    dateDernierChangement = date;
  }
}

Afin de visualiser l’activation des ILS, un LCDKeypad shield est employé. Dans loop(), le programme affiche, toutes les 20 ms, la valeur des 4 variables mémorisant le nombre de déclenchements sur chaque ILS.

#include <LiquidCrystal.h>
#include <LCDKeypad.h>
#include <PinChangeInt.h>
 
LCDKeypad lcd;
 
const byte pinILS1 = A1;
const byte pinILS2 = A2;
const byte pinILS3 = A3;
const byte pinILS4 = A4;
 
byte comptageILS1 = 0;
byte comptageILS2 = 0;
byte comptageILS3 = 0;
byte comptageILS4 = 0;
 
const unsigned long dureeAntiRebond = 1;
 
void interruptILS1()
{
  static unsigned long dateDernierChangement = 0;
  
  unsigned long date = millis();
  if ((date - dateDernierChangement) > dureeAntiRebond) {
    comptageILS1++;
    dateDernierChangement = date;
  }
}
 
void interruptILS2()
{
  static unsigned long dateDernierChangement = 0;
  
  unsigned long date = millis();
  if ((date - dateDernierChangement) > dureeAntiRebond) {
    comptageILS2++;
    dateDernierChangement = date;
  }
}
 
void interruptILS3()
{
  static unsigned long dateDernierChangement = 0;
  
  unsigned long date = millis();
  if ((date - dateDernierChangement) > dureeAntiRebond) {
    comptageILS3++;
    dateDernierChangement = date;
  }
}
 
void interruptILS4()
{
  static unsigned long dateDernierChangement = 0;
  
  unsigned long date = millis();
  if ((date - dateDernierChangement) > dureeAntiRebond) {
    comptageILS4++;
    dateDernierChangement = date;
  }
}
 
void setup()
{
  lcd.begin(16, 2);
  lcd.clear();
  lcd.print("Test PCINT");
 
  /*
   * Programme les broches des ILS en entree
   * et active le pullup interne
   */
  pinMode(pinILS1, INPUT_PULLUP);
  pinMode(pinILS2, INPUT_PULLUP);
  pinMode(pinILS3, INPUT_PULLUP);
  pinMode(pinILS4, INPUT_PULLUP);
  
  /*
   * accroche les ISR aux pins
   */
  PCintPort::attachInterrupt(pinILS1, interruptILS1, FALLING);
  PCintPort::attachInterrupt(pinILS2, interruptILS2, FALLING);
  PCintPort::attachInterrupt(pinILS3, interruptILS3, FALLING);
  PCintPort::attachInterrupt(pinILS4, interruptILS4, FALLING);
}
 
void loop()
{
  lcd.setCursor(0,1);
  lcd.print(comptageILS1);
  
  lcd.setCursor(4,1);
  lcd.print(comptageILS2);
 
  lcd.setCursor(8,1);
  lcd.print(comptageILS3);
  
  lcd.setCursor(12,1);
  lcd.print(comptageILS4);
 
  delay(20);  
}

Voici une vidéo montrant cette application de test en action.

[1C’est également le cas pour les autres Arduino à base d’AVR ATMega 328 et pour les Arduino à base de micro-contrôleurs ARM. Ce n’est pas le cas des Arduino à base d’ATMega 32u4 comme le Leonardo ou le Pro

66 Messages

Réagissez à « Les interruptions (1) »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Programmation »

Le monde des objets (1)

Le monde des objets (2)

Le monde des objets (3)

Le monde des objets (4)

Les pointeurs (1)

Les pointeurs (2)

Les Timers (I)

Les Timers (II)

Les Timers (III)

Les Timers (IV)

Les Timers (V)

Bien utiliser l’IDE d’Arduino (1)

Bien utiliser l’IDE d’Arduino (2)

Piloter son Arduino avec son navigateur web et Node.js (1)

Piloter son Arduino avec son navigateur web et Node.js (2)

Piloter son Arduino avec son navigateur web et Node.js (3)

Piloter son Arduino avec son navigateur web et Node.js (4)

Comment gérer le temps dans un programme ?

La programmation, qu’est ce que c’est

Types, constantes et variables

Installation de l’IDE Arduino

Répéter des instructions : les boucles

Instructions conditionnelles : le if ... else

Instructions conditionnelles : le switch ... case

Comment concevoir rationnellement votre système

Comment gérer l’aléatoire ?

Calculer avec l’Arduino (1)

Calculer avec l’Arduino (2)

Les structures

Systèmes de numération

Les fonctions

Trois façons de déclarer des constantes

Transcription d’un programme simple en programmation objet

Ces tableaux qui peuvent nous simplifier le développement Arduino

Les chaînes de caractères

Trucs, astuces et choses à ne pas faire !

Processing pour nos trains

Arduino : toute première fois !

Démarrer en Processing (1)

TCOs en Processing (1)

TCOs en Processing (2)

L’assembleur (1)

L’assembleur (2)

L’assembleur (3)

L’assembleur (4)

L’assembleur (5)

L’assembleur (6)

L’assembleur (7)

L’assembleur (8)

L’assembleur (9)

Les derniers articles

L’assembleur (9)


Christian

L’assembleur (8)


Christian

L’assembleur (7)


Christian

L’assembleur (6)


Christian

L’assembleur (5)


Christian

L’assembleur (4)


Christian

L’assembleur (3)


Christian

L’assembleur (2)


Christian

L’assembleur (1)


Christian

TCOs en Processing (2)


Pierre59

Les articles les plus lus

Les Timers (I)

Les interruptions (1)

Instructions conditionnelles : le if ... else

Comment gérer le temps dans un programme ?

Les structures

Bien utiliser l’IDE d’Arduino (1)

Ces tableaux qui peuvent nous simplifier le développement Arduino

Les chaînes de caractères

Les Timers (II)

Calculer avec l’Arduino (1)