Arduino, fins de course et interruptions

Salut à tous les arduinophiles, arduinomanes et arduinophones.

Quand on veut automatiser des modèles Meccano avec un arduino, la question des fins de course se pose souvent : on veut arrêter des moteurs à courant continu lorsqu’ils sont arrivés à un point d’arrêt. La solution est d’employer un détecteur de présence qui doit déclencher l’arrêt ou l’inversion du sens du moteur.

Le détecteur de présence le plus couramment utilisé est un microrupteur (microswitch) mécanique, c’est-à-dire un petit levier actionné par l’objet en mouvement qui déclenche soit une rupture de contact soit un contact entre deux de ses bornes (voir photo).

Mais il existe aussi des fourches optiques qui détectent la présence d’un objet opaque entre les fourches. Le principe est celui d’une barrière lumineuse : une branche de la fourche contient une LED (le plus souvent infrarouge) et l’autre branche contient un petit récepteur infrarouge. Le petit circuit électronique de la fourche provoque la mise au niveau logique HIGH de la broche de sortie lorsque un objet opaque est entre les fourches.

A vrai dire, il est aisé de fabriquer une barrière lumineuse soi-même avec des composants élémentaires, mais on perd les bénéfices de la miniaturisation industrielle. Il existe aussi d’autres détecteurs de présence (magnétiques ou capacitifs), mais leur précision est généralement moindre que ceux présentés ci-dessus.

L’objet de cet article est de comparer les deux solutions (mécanique et optique) et de présenter une méthode de programmation avec un arduino en utilisant les interruptions.

Utilisation des microrupteurs mécaniques

Une solution sans arduino !

Si l’on n’utilise l’arduino que pour arrêter un moteur en fin de course, on peut se passer d’arduino. Une solution très simple (mais qui fait perdre un peu de puissance au moteur) est proposée au début de l’article d’Eric Champleboux dans le magazine du CAM n° 155 p. 61. Une solution améliorée en utilisant des relais est proposée en fin de l’article.

On peut aussi utiliser le montage d’Eric avec un arduino : le programme se contente de déclencher l’alimentation du moteur au moment voulu, l’arrêt se fera tout seul en fin de course. Après avoir lancé le moteur, l’arduino peut donc faire autre chose sans se soucier des fins de course. Cette manière de procéder simplifie la programmation de l’arduino. En revanche il vous faudra souder quelques diodes (dans le bon sens et capables de supporter le courant du moteur), suivant les schémas proposés par Eric.

La solution précédente est parfois insuffisante…

Souvent, on désire que l’arduino gère les fins de course parce que le signal d’un fin de course doit non seulement arrêter un moteur mais aussi déclencher d’autres actions (changer le sens du moteur, allumer des LED, incrémenter un compteur ou d’autres choses encore). Dans ce cas, la consultation par l’arduino de l’état du fin de course doit se faire aussi souvent possible afin arrêter ou d’inverser le moteur à temps ! Nous reviendrons plus loin sur les deux manières de programmer ce « aussi souvent possible ».

Câblage d’un microrupteur sur un arduino

La liaison entre le microrupteur et l’arduino ne nécessite que deux fils. Quand on utilise un arduino pour détecter la fin de course et agir en conséquence, il faut considérer le microrupteur comme un bouton poussoir ordinaire (en montage pullup ou pulldown). Le montage pullup est préférable car il évite le soudage d’une résistance pulldown, puisque dans le montage pullup, on peut utiliser la résistance pullup interne de l’arduino en déclarant dans la fonction setup le mode d’utilisation de la broche du microrupteur avec l’instruction suivante :
    pinMode(n°BrocheMicrorupteur,INPUT_PULLUP).
L’autre broche du microrupteur est connectée au GND. Pour la programmation, il faudra se souvenir qu’avec le montage pullup, la broche du microrupteur est en permanence au niveau HIGH et qu’elle passe au niveau LOW lorsque le microrupteur est activé.

Les rebondissements d’un microrupteur :

Les microrupteurs mécaniques ont un gros défaut : les contacts du microrupteur rebondissent parfois une ou deux fois, voire plus. On peut s’en rendre compte avec un oscilloscope branché sur le microrupteur. L’arduino peut donc percevoir plusieurs fois le changement d’état du contact de fin de course !
On peut régler plus ou moins le problème du rebondissement en soudant un condensateur de quelques nF (nanoFarad) aux bornes du microrupteur. Cette solution est simple mais elle retarde un peu le changement d’état de la broche : plus la capacité du condensateur est grande, plus le retard du changement d’état de la broche est important.
Bon nombre de programmeurs préfèrent régler le problème des rebondissements de manière informatique : le principe est de consulter l‘état de la broche du fin de course un certain nombre de fois ou pendant un certain nombre de millisecondes avant de prendre en compte l’état stabilisé de la broche (ces nombres, décidés par le programmeur, dépendent de la qualité des microrupteurs). Cette méthode, tout aussi incertaine que la précédente, retarde donc aussi un peu la prise en compte du fin de course par le programme.

Les fourches optiques

Les fourches optiques ont l’immense avantage de ne pas rebondir. La prise en compte du fin de course est quasi immédiate (pas besoin d’attendre la fin des rebonds). Toutefois, tout comme pour les microrupteurs mécaniques, encore faut-il consulter l’état de la broche aussi souvent que possible.

Le câblage est très simple : il faut relier le Vcc et le GND de la fourche optique au 5 V et au GND de l’arduino. La 3ème broche de la fourche optique (souvent marquée OUT) est mise est mise au niveau HIGH si un obstacle est présent entre les fourches et au niveau LOW dans le cas contraire. C’est cette broche que l’arduino doit tester. Contrairement aux microrupteurs, la connexion entre une fourche optique et l’arduino nécessite donc trois fils au lieu de deux.

Par ailleurs, l’usage des fourches optiques ne se limite pas aux fins de course. On peut s’en servir pour compter les trous d’une barre Meccano ! Plus sérieusement, ce comptage peut servir à mesurer un déplacement en nombre de trous, et à arrêter le déplacement au bout d’un certain nombre de trous : dans le programme il suffit de programmer un compteur qui s’incrémente à chaque trou et qui arrête le déplacement quand le nombre de trous désiré est atteint. On peut aussi calculer la vitesse de rotation d’un moteur entrainant un disque percé (une roue barillet Meccano par exemple). Je vous laisse imaginer d’autres applications possibles…

La programmation du « aussi souvent possible »

Que l’on utilise des microrupteurs ou des fourches optiques, le problème est de consulter l’état des fins de course aussi souvent que possible. C’est un problème car un arduino exécute ses instructions les unes après les autres, la lecture de l’état de la broche du fin de course (microrupteur ou fourche optique) ne sera faite que quand son tour viendra. Si cette broche change d’état pendant que l’arduino exécute d’autres instructions, on risque soit de louper l’évènement (le fin de course est dépassé) soit de le consulter trop tard ! Il faut donc que la consultation du dispositif de fin de course soit très souvent répétée.

La solution courante est de consulter l’état du fin de course avec un digitalRead dans la boucle loop du programme. Il faut donc que l’exécution des autres instructions de la boucle loop (et des éventuelles fonctions qu’elle appelle) ne durent pas trop longtemps afin que la consultation de l’état du fin de course se fasse suffisamment souvent. Cette solution est acceptable, si un retard de quelques dixièmes de seconde pour l’arrêt du moteur est supportable par votre modèle ; tout dépend de la durée d’exécution de votre boucle loop ! Certaines instructions telles que delay ou Serial.print ou analogRead (100 microsecondes) ou encore des calculs longs ou compliqués, rendent l’exécution d’une boucle loop trop longue. Si vous êtes certain qu’elle ne dure pas trop, cette solution est acceptable.

Mais il existe une solution simple, élégante et fiable à ce problème : la programmation par interruptions. Peu de programmeurs débutants en arduino osent la pratiquer. Pourtant, les fins de course sont d’usage courant dans les constructions Meccano, et l’urgence de la prise en compte d’un fin de course ne surprendra personne. De plus, vous verrez que le problème des rebondissements d’un microrupteur peut être parfaitement résolu :  dans le squelette de programme proposé plus loin, seule la première transition d’état aura un effet.

La programmation par interruptions

L’instruction attachInterrupt, placée dans le setup, demande à l’arduino de surveiller de manière quasi permanente (environ toutes les quelques nanosecondes !) l’état d’une broche. Si l’état de la broche change, alors le programme principal en cours d’exécution est interrompu, l’arduino exécute la fonction associée à l’interruption, puis une fois terminée, il reprend l’exécution du programme précédemment interrompu, à l’endroit où il en était. On comprend bien qu’une interruption doit être réservée aux actions qui doivent être exécutées de toute urgence et c’est le cas des fins de course qui doivent arrêter ou inverser un moteur. Quand on a défini une interruption, il n’y a plus besoin de surveiller les fins de course dans la boucle loop : c’est la simple installation d’une interruption qui s’en charge. La programmation de la boucle loop s’en trouve fortement simplifiée. La mise en place d’une interruption se fait de manière très simple :
     attachInterrupt(n°d’interruption , fonctionAExécuterEnCasDInterruption , mode);
Elle est normalement faite dans la fonction setup du programme. On décrit ci-après les différents arguments de cette fonction .

Le numéro d’interruption :

Dans un arduino UNO il n’y a que deux numéros d’interruption possibles :

  • l’interruption 0 (surveillance de la broche digitale 2)
  • l’interruption 1 (surveillance de la broche digitale 3).

Dans un arduino MEGA il y en a quatre de plus : interruption 2 (surveillance de la broche 21), interruption 3 (surveillance de la broche 20), interruption 4 (surveillance de la broche 19) et interruption 5 (surveillance de la broche 18). Pour les autres arduinos, consulter la documentation. Si vous ne voulez pas retenir par cœur tous ces numéros et leur numéro de broche associé, la fonction digitalPinToInterrupt(pin) renvoie le numéro d’interruption associé à la broche pin ou bien -1 s’il n’y en a pas. Elle répond correctement en fonction du type d’arduino que vous utilisez.

La fonction à exécuter en cas d’interruption :

C’est le nom d’une fonction que vous aurez pris soin de définir et qui sera automatiquement appelée si l’interruption est déclenchée. Dans les documents en anglais elle est appelée ISR (Interrupt Service Routine). Nous continuerons à employer cet acronyme. Cette fonction doit être un peu spéciale : elle ne reçoit aucun argument et ne renvoie aucun résultat. Sa définition doit donc être de la forme :
     void nom_de_l_ISR(void){ variables locales et instructions }
Bien qu’elle n’ait ni argument ni résultat, cette fonction peut néanmoins lire ou modifier des variables globales qui doivent nécessairement être déclarées dans les déclarations de variables globales avec le préfixe volatile :
     volatile  type   variableUtiliséeDansUneISR ;
Comme toute variable, elle peut être initialisée dans sa déclaration.

Le mode :

Il précise l’évènement qui déclenche l’interruption :

  • LOW : la broche est au niveau bas ;
  • CHANGE : la broche change d’état (de bas à haut ou de haut à bas) ;
  • RISING : la broche passe du niveau bas à haut (utile pour les fourches optiques) ;
  • FALLING : la broche passe du niveau haut à bas (utile pour les microrupteurs montés en pullup).

Dans certains arduinos autres que le UNO, le mode HIGH est possible.

Quelques restrictions importantes :

Pendant l’exécution d’une ISR, les interruptions sont par défaut interdites (on ne veut pas qu’une ISR soit interrompue par une demande d’interruption). De plus, certaines instructions ou fonctions comme delay, millis, tone et certaines fonctions de bibliothèque comme Serial (utilisation du port série) ou Wire (utilisation du port I2C) ne fonctionnent pas car elles ont besoin que les interruptions soient autorisées. Toutefois, mais seulement si c’est indispensable, il est possible d’autoriser très localement les interruptions à l’intérieur d’une ISR, mais il faut prendre quelques précautions pour s’assurer qu’une récursivité infinie ne puisse pas se produire (une ISR interrompue par elle-même !). Ce cas sera envisagé plus loin.

En résumé :

  • La mise en place d’une interruption est donc très simple : il suffit d’une instruction attachInterrupt dans le setup et de définir la fonction d’interruption (l’ISR) contenant les instructions à exécuter quand l’interruption est déclenchée. La programmation du reste du programme n’a plus à s’en préoccuper.
  • Les fonctions d’interruption devraient être aussi courtes que possible (comme l’arrêt d’un moteur ou du relai qui le commande) pour ne pas suspendre trop longtemps le programme interrompu.
  • L’usage des interruptions doit être réservé aux actions urgentes sans lesquelles le modèle risquerait d’être endommagé (c’est le cas des fins de course) ou à des évènements extérieurs qu’on ne veut pas risquer de louper.

Un exemple : va-et-vient entre deux fins de course

On se propose de réaliser un va-et-vient : un mobile doit circuler en permanence sur des rails entre deux fins de course. Arrivé en fin de course il doit repartir en sens inverse.
Dans ce programme, on installe deux interruptions dans le setup : l’une déclenchée par le fin de course de gauche et l’autre déclenchée par le fin de course de droite. Chacune a son ISR, nommées respectivement demiTourGaucheDroite et demiTourDroiteGauche. Quant à la boucle loop, elle ne contient qu’une instruction longue qui ne fait rien ! Vous saurez sans doute la remplacer par quelque chose d’utile… Notez que dans ce squelette, certaines instructions des ISR sont à compléter : celles qui doivent relancer le moteur en sens inverse dépendent du matériel que vous utilisez pour piloter le moteur (relais ? carte ? shield ?) ; vous les complèterez vous-même selon le cas. Si vous n’avez pas de moteur sous la main, vous pouvez simuler le fonctionnement en allumant des LED.
Le squelette (largement commenté) du programme est à télécharger ici : SqueletteVaEtVient.zip

Quelques explications complémentaires facultatives sur l’effet des interruptions :

1- Utilité du préfixe volatile

Ce préfixe est indispensable pour toute variable utilisée à la fois dans le programme principal et dans une ISR. En fait, il oblige de processeur à lire valeur dans la mémoire centrale et à stocker immédiatement toute modification de sa valeur dans la mémoire centrale.
Explication : parfois, les compilateurs prennent l’initiative de stocker momentanément une copie de variable dans ses registres internes pour gagner du temps de transfert. Rassurez-vous, il met à jour la valeur en mémoire centrale quand c’est nécessaire. Puisqu’une interruption peut intervenir à tout instant, il se peut que la valeur en mémoire centrale et celle du registre interne soient différentes à cet instant car le processeur n’a pas encore mis à jour la mémoire centrale. Le préfixe volatile interdit cette optimisation : chaque lecture/modification d’une variable volatile se fait obligatoirement depuis/vers la mémoire centrale. Cela ralentit un peu (quelques nanosecondes) l’exécution du programme, mais cela garantit que la valeur en mémoire centrale d’une variable volatile est toujours à jour.

2- Accès à une variable volatile

Si le programme principal veut utiliser une variable globale volatile de plus d’un octet (une variable de type int par exemple), il est prudent d’en faire une copie locale en interdisant momentanément les interruptions pendant la copie. En effet, la copie d’une « grosse » variable (plus d’un octet) se fait en plusieurs opérations élémentaires. Si, par malchance, une ISR modifiant cette variable intervenait pendant la copie, les octets copiés avant l’interruption ne seraient pas cohérents avec ceux copiés après l’interruption ! Pour empêcher cette malchance de se produire, il faut interdire les interruptions pendant la copie de la manière suivante :

volatile long int maGrosseVariableGlobale;

// autres déclarations globales
Dans toute fonction (y compris la fonction loop) utilisant la variable volatile :
{

long int maCopieLocale; // déclaration d’une variable locale de même type
byte oldSREG = SREG; // sauvegarde du registre SREG (copie d’un octet, donc non interruptible)
// Info : le registre SREG contient, entre autres, l’autorisation ou non actuelle des interruptions
nointerrupts(); // on interdit les interruptions
maCopieLocale = maGrosseVariableGlobale ; // on copie sans risque d’interruption
SREG = oldSREG; // restauration du registre SREG

// travailler avec la copie locale
// Si la variable globale doit être mise à jour, utilisez les mêmes précautions pour faire la copie inverse.
}

Ces précautions alourdissent un peu l’écriture du programme mais ne ralentissent que très peu (quelques nanosecondes) l’exécution. Comme on n’est pas là pour écrire des programmes qui pourraient être victimes de malchance aléatoire, sans ces précautions votre programme serait mauvais !
Ces précautions ne sont évidemment à prendre que pour les variables volatiles, car ce sont les seules qui sont susceptibles d’être modifiées par une ISR.

3- Pendant une ISR les interruptions sont interdites par défaut

Beaucoup de fonctions telles que millis ou des fonctions de la bibliothèque Serial et bien d’autres encore, utilisent des interruptions pour fonctionner. Les programmeurs ne les voient pas (il faudrait regarder le résultat de la compilation, en langage dit « assembleur »), mais il est utile d’avoir conscience qu’elles existent.
Dans l’arduino UNO il y a 25 niveaux de priorité sur les interruptions. Lorsque plusieurs plusieurs types d’interruptions sont demandées, elles sont traitées par ordre de priorité, les autres sont mises en attente et ne seront traitées que lorsque leur priorité sera supérieure à celles qui restent. Il est donc important que le temps d’exécution de vos ISR soit le plus court possible (quelques instructions) pour ne pas trop faire attendre les éventuelles autres interruptions en attente. Les interruptions définies avec attachInterrupt sont celles de plus haut niveau de priorité. Les fonctions qui utilisent des interruptions de priorité moindre sont donc suspendues pendant l’exécution de votre ISR.
La durée d’installation d’une l’ISR dans le processeur est d’environ 3 microsecondes et la durée de retour au programme interrompu est d’environ 2 microsecondes. La fonction interrompue par votre ISR est donc suspendue pendant environ 5 microsecondes auxquelles il faut ajouter le temps d’exécution des instructions de votre ISR. Ces durées sont si brèves que l’on peut avoir l’impression que l’arduino fait plusieurs choses en même temps, mais il n’en est rien !

4- Que faire si j’ai besoin de fonctions qui utilisent des interruptions dans mon ISR ?

Cela peut arriver : par exemple, dans le squelette de programme proposé ci-dessus, il se peut que, pour piloter le moteur du va-et-vient, vous utilisiez une carte (ou un shield) évolué : pour piloter un moteur, il faut envoyer un message en utilisant le protocole I2C. Or la bibliothèque Wire (pour utiliser I2C) contient des fonctions qui ont besoin que les interruptions soient autorisées. C’est notamment le cas du shield pour quatre moteurs version 2 de chez Adafruit. La réponse est simple : il faut momentanément autoriser les interruptions juste avant d’envoyer le message à la carte et rétablir les autorisations telles qu’elles étaient juste après.
Cela se fait en sauvegardant l’état du registre SREG avant d’autoriser les interruptions et de le rétablir juste après.
Cette manœuvre est dangereuse ! Que va-t-il se passer si votre ISR (prioritaire) se suspend elle-même pendant le court moment où les interruptions sont autorisées ? Le rebond malencontreux d’un microrupteur pendant cette période critique pourrait provoquer cette situation ! On risque de tomber dans une récursion infinie qui aboutirait à un plantage de votre arduino (pour les connaisseurs : une saturation de la pile). Ce n’est pas si grave : il n’y a aucun dégât matériel ! Un simple appui sur le bouton reset y remédiera. Mais votre programme étant susceptible d’être victime d’une malchance aléatoire, il est donc mauvais !
Ce genre d’erreur de programmation (tout marche bien sauf de rares fois) est difficile à détecter et à comprendre. Il faut s’imaginer ce qui se passe ou bien, si vous savez le faire, suivre le code en assembleur pour comprendre. Avant d’autoriser les interruptions dans votre ISR, vous devez donc bien réfléchir à tout ce qui peut se passer !
La solution proposée dans le commentaire à la fin du squelette  de programme proposé précédement est quelque peu peu subtile : on ne peut pas empêcher l’ISR d’être d’interrompue pendant la période critique où les interruptions sont autorisées, elle a donc lieu. Mais l’ISR est programmée pour qu’elle ne fasse rien grâce au test du sens actuel du moteur. Vous comprenez l’importance de mettre à jour la variable globale volatile versLaGauche AVANT d’autoriser momentanément les interruptions. L’interruption malencontreuse ne fera que perdre une poignée de microsecondes pour son installation et sa désinstallation dans le processeur. Je vous invite à bien réfléchir à ce qui se passe effectivement dans ce cas. D’une manière générale, si vous le pouvez, évitez d’autoriser les interruptions dans une ISR pour vous éviter une surchauffe cérébrale.

Épilogue

Si vous m’avez lu jusqu’au bout, c’est que vous êtes courageux. J’espère que vous me pardonnerez d’avoir été si long. Les choses restent très simples si vous n’autorisez pas les interruptions dans une ISR ! C’est d’ailleurs ce que recommandent tous ceux qui n’ont pas le courage d’expliquer en profondeur comment on peut le faire si nécessaire. C’est probablement un reste de réflexe de vieux prof retraité qui m’a incité à ne rien laisser dans l’ombre.

J’espère vous avoir été utile.

Jean Garrigues, CAM 931.