Arduino, fins de course et interruptions

Tout ce qu'il faut savoir pour radio-commander vos modèles de voitures, robots, tractopelle, etc..
Avatar du membre
JeanGar
Nouveau Membre
Nouveau Membre
Messages : 9
Enregistré le : 13 Fév 2021, 10:56
Localisation : Marseille

Arduino, fins de course et interruptions

Message par JeanGar »

Salut à tous les arduinophiles, arduinomanes et arduinophones. :001:

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. On utilise aussi des fourches optiques. Sur la photo ci-dessous (merci à Bernard Garrigues) : en haut : deux vues d'une fourche optique, en bas différents microrupteurs.
 
Détecteurs de présence.jpg

Les fourches optiques 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 d’une broche lorsque un objet opaque est entre les fourches. Le câblage est très simple : une borne Vcc (5V) et une borne GND (pour le fonctionnement de la LED infrarouge et de l’électronique de la fourche), plus une broche de sortie qui est mise au niveau HIGH si un obstacle est présent et LOW dans le cas contraire. Les fourches nécessitent donc 3 fils de connexion au lieu de 2 pour les microrupteurs.
A vrai dire, il est facile 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 page 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. A près avoir lance le moteur, l’arduino peut donc faire autre chose sans se soucier des fins de course car il n’a pas à consulter l’état des fins de course. Cette manière de procéder simplifie la programmation. En revanche il vous faudra souder quelques diodes dans le bon sens, 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 de l’état du fin de course doit se faire aussi souvent que possible pour arrêter le moteur à temps ! Nous reviendrons plus loin sur les 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 ce montage, on utilise la résistance pullup interne de l’arduino en déclarant dans le setup le mode d’utilisation de la broche du microrupteur avec l’instruction 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 ce problème en soudant un condensateur de quelques nF (nanoFarad) aux bornes du microrupteur. Cette solution est simple mais incertaine et 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 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 (ces nombres, décidés par le programmeur, dépendent de la qualité des microrupteurs) avant de prendre en compte l’état final de la broche. Cette méthode, tout aussi incertaine, 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, encore faut-il consulter l’état de la broche aussi souvent que possible, tout comme pour les microrupteurs mécaniques.
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 fabriquer 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 »
Qu’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 microrupteur ne sera faite que quand sont tour viendra. Si la broche du fin de course 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 fin de course se produise très souvent.
 
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 plus fiable à ce problème : la programmation par interruptions. Peu de programmeurs novices en arduino la pratiquent. 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.
 
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.
 
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 la fonction digitalPinToInterrupt(pin) renvoie le numéro d’interruption associé à la broche pin ou -1 s’il n’y en a pas.
 
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) . Cette fonction doit être un peu spéciale : elle ne peut recevoir aucun argument ni renvoyer de 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, elle 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 ;
 
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 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 dans 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) avec 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 ISR devraient être aussi courtes que possible (comme l’arrêt d’un moteur ou du relai qui le commande).
  • 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 emplacer par quelque chose d’utile. Notez que les instructions des ISR sont incomplètes : elles doivent relancer le moteur dans le bon sens et ces instructions dépendent de la manière dont vous pilotez le moteur (relais ? carte ? shield ?) ; vous les complèterez 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, le compilateur prend 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 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 interromptible)
// 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, 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 des autres. 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.
 
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é avec qui il faut communiquer en utilisant le protocole I2C. Or la bibliothèque Wire (pour utiliser I2C) contient des fonctions qui utilisent les interruptions. C’est notamment le cas du shield pour 4 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 ? Un rebond malencontreux du fin de course 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, 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 programme ci-dessus est un 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 ne rien faire 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. Cela vous épargnera un bonne séance de creusage de cervelle pour analyser tout ce qui peut se passer.
 
Epilogue
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 simples si vous n’autorisez pas les interruptions dans une ISR ! C’est d’ailleurs ce que recommandent tous ceux qui n’ont pas l’envie d’expliquer pourquoi. C’est probablement un reste de rélexe 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.
Vous n’avez pas les permissions nécessaires pour voir les fichiers joints à ce message.

Philippe
Membre Actif
Membre Actif
Messages : 53
Enregistré le : 10 Mars 2021, 05:38

Re: Arduino, fins de course et interruptions

Message par Philippe »

Merci pour cet article très détaillé et complet.
Je l'ai lu avec plaisir et je le garde dans mes archives.

Avatar du membre
marc80
Modérateur
Modérateur
Messages : 353
Enregistré le : 27 Juil 2020, 14:22
Localisation : 80500 Montdidier

Re: Arduino, fins de course et interruptions

Message par marc80 »

Bravo Jean, Tu y mets le paquet !
Bon Meccano à tous ! :020:  Marc
 

Répondre