Arduino , comment utiliser un rotary encoder et les attachInterrupt

10620

Aujourd’hui on va apprendre à comment utiliser un rotary encoder et les attachInterrupt .
Il se peut que dans nos programmes on souhaite contrôler le changement d’état sur un pin de façon automatique; mais cela demande de le contrôler dans le programme tout le temps donc une programmation rigoureuse pour intégrer ce contrôle tout le temps à tout moment.
Grosse galère à coder surtout que l’on peut automatiser ce contrôle sans se fatiguer et on tester tout ça aujourd’hui avec un rotaty encoder.

Rotary Encoder Circuit

En matériel il nous faut :
– 1 carte arduino
– 1 module rotary encoder ( bouton rotatif )
– 1 buzzer (option pour le tuto )

Pour le montage nous avons le rotary encoder connecté au 5v et gnd; le pin DT et CLK sont branché au pin 2 et 3; et le pin button est connecté à une résistance (l’autre coté de la résistance est sur le 5V) qui est sur le meme point connecté sur le pin 6 de l’arduino.
J’ai un buzzer qui est branché sur le pin 7 de l’arduino pour notre test ( l’autre coté du buzzer est sur le gnd).

Qu’est ce qu’un rotary encoder (bouton rotatif) ?

Un rotary encoder est un bouton rotatif qui tourne dans deux sens, sens horaire et anti-horaire.
Il a un nombre de cran pour faire un tour
Il est utilisé pour compter un nombre de tour et a un nombre de cran pour faire un tour.
Vous en avez surement utilisé sur un autoradio ou sur une imprimante 3d si vous en possédez une.

 

Comment fonctionne un rotary encoder ?

C’est un bouton rotatif avec un nombre de position et qui n’a pas de début ni de fin contrairement aux potentiomètres. Certains rotary encoder ont un bouton poussoir intégrés d’autres non.
Voyons un petit schéma pour comprendre le chevauchement des crans et la séquence que notre encodeur va effectuer.

Rotary encoder rotation

Quand on tourne le bouton, l’alignement des 2 pin de sortie de l’encodeur ne sont pas exactement au même endroit. Et c’est ce décalage qui va nous permettre de connaitre le sens de rotation.

Maintenant voyons les séquences par lesquelles passent un rotary encodeur.
J’ai déjà moi même deux encodeur différent et celui que j’ai utilisé pour le tuto est celui du graphique de gauche (celui le plus commun).

 

Sequence Rotary Encoder

On peut voir qu’il y à deux état de repos pour l’encodeur utilisé, un état ou les deux positions sont à haut et un état ou les deux sont à bas.
Pour l’exemple du graphique, un pin de l’encodeur est A et l’autre pin est B.
Voyons par quels états vont passer A et B pour définir l’algorithme du code pour détecter le sens de rotation.
On débute par l’état de repos 1 couleur jaune , A = 1 et B aussi, on tourne dans le sens anti-horaire vers couleur orange. Donc si A change d’état ( IF A != A précédent ) alors on va contrôler B, si B égal à 0 alors sens anti-horaire et on arrive sur la couleur blanche qui correspond a l’état repos 2 en bleu clair.
Ce qui nous donne en résumé : Si dernier état de A == 1 et que A change et B état Bas alors sens anti-horaire. On va voir ensuite comment simplifier les conditions pour ce cas là (jaune => orange => bleu clair).

Je me positionne à nouveau sur l’état repos 1 en jaune , A=1 (B=1), on tourne dans le sens horaire vers couleur verte  . Changement d’état de A, donc condition ( IF A != A prédécent ) , on contrôle B, si B égal a 1 alors sens horaire.On arrive sur la zone état repos 2 e couleur bleu clair.
Ce qui nous donne en résumé : Si dernier état de A == 1 et que A change et B état Haut alors sens horaire (jaune => vert => bleu clair).

Et pour un changement d’état à partir de la zone repos 2 on suit le même principe.
Si dernier état de A == 0 et que A change et B état Bas alors sens horaire ( bleu clair => orange => jaune ).
Si dernier état de A == 0 et que A change et B état Haut alors sens anti-horaire ( bleu clair => vert => jaune )

Attention cette logique peut être simplifiée, ici on à une première condition qui regarde si A a changé d’état, ensuite on regarde quel était l’état de A avant le changement donc deuxieme condition, puis une troisième condition pour vérifier l’état de B et son “else”. Dans la deuxième condition dans le else on aurait la quatrième condition de B  et son “else”.
if(A != Aprécédent ){
if(Aprécédent == 1){
if(B == 1){
// horaire
}
else{
// anti-horaire
}
}
else{
if(B == 1){
// anti-horaire
}
else{
// horaire
}
}
// mémorisation de Aprécédent avec la nouvelle valeur
}

En améliorant le logique les conditions deviennent, si A change d’état et que B est différent que l’état précédent de A alors sens anti-horaire, sinon c’est le sens horaire.

if( A != Aprécédent ){
// Si B different de l’ancien état de A alors
if(B != Aprécédent){
// anti-horaire
}
else{
// horaire
}
}
// memorisation de l’état de A
Aprécédent = A ;
}

Qu’est ce qu’un interrupt sur arduino ?

Ici au niveau du code nous allons voir une fonctionnalité extrêmement utile en arduino pour gérer un événement récurent, les interrupts ( attachInterrupt() ) .
Un interrupt nous permet quand on a une tache très importante, d’interrompre le programme ou il était et d’effectuer une action.

Imaginon que je veux calculer les rotations d’un rotary encoder tout en executant un programme.
A chaque fois que l’état du rotary encoder va changer, on va exécuter une fonction dans le code; peut importe ou l’on se situe dans notre boucle de code, l’arduino va effectuer une action.

Il faut savoir que les pin utilisables pour les interrupts sont limités aux modèles de carte.

Uno, Nano, Mini, autres 328-base2, 3
Uno WiFi Rev.2tous les pins digitaux
Mega, Mega2560, MegaADK2, 3, 18, 19, 20, 21
Micro, Leonardo, autres 32u4-based0, 1, 2, 3, 7
Zerotous les pins digitaux sauf 4
Duetous les pins digitaux

 

Pour affecter un pin à un changement d’état on utilise :
attachInterrupt(digitalPinToInterrupt(pin), mafonction, mode)
Le code est a placer dans la partie setup.
On remplacera pin par la valeur du pin que l’on souhaite associer ( ici 2 ou 3 )

Puis la fonction à appeler ici je l’ai appelé mafonction
void mafonction(){
//mon code a executer

}

Et enfin le mode.
On a le choix entre plusieurs déclencheurs (LOW,CHANGE,RISING,FALLING ):
– LOW, déclenche l’interrupt quand le pin est a l’état bas
– CHANGE, déclenche l’interrupt quand le pin change d’état ( de bas à haut , ou de haut à bas )
– RISING, sur front montant, il se déclenche seulement quand on va passer d’un état bas à haut
– FALLING, sur front descendant, il se déclenche seulement quand on va passer d’un état haut à bas
– et un dernier état mais que l’on ne va pas voir car il ne fonctionne que sur les cartes Due et Zero, c’est l’état HIGH

Pour plus de détails https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

On peut désactiver un interrupt avec detachInterrupt(digitalPinToInterrupt(pin)); et on oublie pas le pin associé. On pourra activer à nouveau un interrupt avec une nouvelle fonction partout dans le code avec attachInterrupt(digitalPinToInterrupt(pin), mafonction, mode) ce qui peut être utile dans quelques cas ( on peut commenter avec un interrupt sur front montant et appeler la fonction 1 , le détacher une fois effectuer et en attacher un nouveau sur front descendant et appeler la fonction 2 ).

Dernier point, il se peut que l’on ai une partie du code critique et que l’on ne souhaite pas activer un interrupt dans cette partie la. Alors on peut désactiver les interrupts sans lé détacher avec noInterrupts(); et les activer à nouveau avec interrupts();

Je pense avoir vu l’essentiel des interrupts donc passons au rotary encoder.

Maintenant que nous avons vu quel était la logique du rotary encoder et de l’interrupt sur arduino nous allons passer au code sans interrupt et avec interrupt.

Sans interrupt

 
// rotary encoder
#define pinA 2 
#define pinB 3

#define pinButton 6
#define pinBuzzer 7

int compteur = 0 ;
bool etatA ;
bool dernierEtatA ;

long unsigned tempsA ;


void setup() {
  pinMode(pinA,INPUT);
  pinMode(pinB,INPUT);

  Serial.begin (115200);

  // état de A au setup
  dernierEtatA = digitalRead(pinA);
  // memorisation du temps pour eviter des erreurs de changements d'etat
  tempsA = millis();
  
  pinMode(pinButton,INPUT);  
  pinMode(pinBuzzer, OUTPUT);
}

void loop() {

  // on mesure A
  etatA = digitalRead(pinA);

  // A a t'il changé ?
  if( etatA != dernierEtatA ){ 
    // controle du temps pour eviter des erreurs 
    if( abs(millis() - tempsA) > 50 ){
      // Si B different de l'ancien état de A alors
      if(digitalRead(pinB) != dernierEtatA){
        compteur--;
      }
      else{
        compteur++;
      }
      // memorisation du temps pour A
      tempsA = millis();
    } 
    // memorisation de l'état de A
    dernierEtatA = etatA ;
    
    //affichage du compteur
    Serial.print("Compteur :");
    Serial.println(compteur);
  }// fin du if( etatA != dernierEtatA )

  if(compteur == 20 ){
    digitalWrite(pinBuzzer,HIGH);
  }
  else{
    digitalWrite(pinBuzzer,LOW);
  }


  if(digitalRead(pinButton) == LOW){
    Serial.println("Bouton");
  }
  // test avec delay pour le tuto
  //delay(10000);
}

Avec Interrupt

 
// rotary encoder avec interrupt
#define pinA 2 
#define pinB 3

#define pinButton 6
#define pinBuzzer 7

int compteur = 0 ;
bool etatA ;
bool dernierEtatA ;

long unsigned tempsA ;


void setup() {
  pinMode(pinA,INPUT);
  pinMode(pinB,INPUT);

  Serial.begin (115200);

  // état de A au setup
  dernierEtatA = digitalRead(pinA);
  // memorisation du temps pour eviter des erreurs de changements d'etat
  tempsA = millis();
  
  pinMode(pinButton,INPUT);  
  pinMode(pinBuzzer, OUTPUT);
}

void loop() {

attachInterrupt(digitalPinToInterrupt(pinA), changementA, CHANGE);

  if(compteur == 20 ){
    digitalWrite(pinBuzzer,HIGH);
  }
  else{
    digitalWrite(pinBuzzer,LOW);
  }


  if(digitalRead(pinButton) == LOW){
    Serial.println("Bouton");
  }
  // test avec delay pour le tuto
  //delay(10000);
}

void changementA(){
  // on mesure A
  etatA = digitalRead(pinA);
   
    // controle du temps pour eviter des erreurs 
    if( abs(millis() - tempsA) > 50 ){
      // Si B different de l'ancien état de A alors
      if(digitalRead(pinB) != dernierEtatA){
        compteur--;
      }
      else{
        compteur++;
      }
      // memorisation du temps pour A
      tempsA = millis();
    } 
    // memorisation de l'état de A
    dernierEtatA = etatA ;
    
    //affichage du compteur
    Serial.print("Compteur :");
    Serial.println(compteur);

}


Le code

En première partie on définit les pin utilisés pour l’encodeur, pin 2,3 et 6 pour le bouton.
Puis le pin 7 pour le buzzer pour notre test.

Ensuite nous avons une variable compteur en int ( de -32768 à 32767) qui nous permet de “connaitre” la position de notre encodeur.
Un booléen etatA pour enregistrer la valeur du pinA, un autre booléen dernierEtatA qui lui va nous permettre de détecter un changement de A.
Et pour terminer un long unsigned tempsA qui nous permet d’enregistrer le temps en millisecondes au moment ou A a changé pour éviter les faux positifs.

Dans le setup, on choisit le mode INPUT de nos pinA et pinB avec pinMode.
Ensuite un Serial.begin pour connaitre visuellement la valeur de mon compteur dans la console.
Je mémorise la valeur du pinA et j’enregistre la valeur dans dernierEtatA.
Je mémorise aussi la valeurA temps en millis pour éviter les erreurs de changement d’état.
Et je termine avec le mode INPUT pour le bouton dans l’encoder et en OUTPUT pour le pinBuzzer de test.

Passons à la boucle loop
Premièrement on récupère l’état de pinA avec digital read et on stocke dans etatA.
On vérifie avec un “if( etatA != dernierEtatA )” si A a changé d’état par rapport à sont état précédent.
Si A à changé alors on contrôle le temps pour éviter un faux positif avec if( abs(millis() – tempsA) > 50 ).
Pour détailler j’ai utilisé abs pour retourner la valeur absolue de la soustraction du temps actuel – tempsA.
Pourquoi j’ai utilisé abs ?
Le temps en millis retourne a zéro après une très longue période et dans mon calcul si le temps A était a 9999 et que millis est retourné au début et est égal à 100 mon calcul va être (100 – 9999) ce qui me donne un résultat négatif -9899 et moi ce que je souhaite c’est que le compteur se déclenche après au minimum 50ms.
La fonction abs va ici me retourné la valeur positive 9899 et donc la valeur est supérieur a 50 donc je peux déclencher la suite.
C’est une bonne pratique pour les programmes qui vont être allumés non stop et qui ont besoin du temps en milli secondes.
Donc j’ai dépassé les 50ms, nouvelle condition, est ce que la valeur de pinB est différente du dernierEtatA ?
Si oui je décrémente le compter avec compteur–, sinon j’incrémente compteur++ .
Je n’oublie pas dans la condition if du temps de rajouter “tempsA = millis();” pour mémoriser le nouveau temps et éviter les faux positifs.
Puis dans le “if( etatA != dernierEtatA )” de mettre à jour dernierEtatA par etatA.
Et deux Serial.print avec “Serial.print(“Compteur :”); Serial.println(compteur);” pour afficher la valeur du compteur à chaque changement.

Pour le test j’ai une condition si mon compteur est égal à 20 alors j’active le buzzer, sinon je le coupe “if(compteur == 20 ) … “.
Et une condition pour le bouton de l’encodeur juste pour l’exemple mais il faudrait contrôler son état précédent pour l’activer ou non comme dans mes autres tuto.
J’ai mis un “delay(10000);” en commentaire pour montrer la différence avec gérer un encoder avec le loop et gérer l’encodeur avec un interrupt.

J’en ai fini avec le code sans utiliser interrupt et on peut voir qu’avec un delay l’arduino ne compte pas le nombre de tour pendant le temps de pause !

Voyons maintenant le code avec un interrupt !
Le changement commence dans le setup ou l’on va rajouter un interrupt “attachInterrupt(digitalPinToInterrupt(pinA), changementA, CHANGE);”
On attache le pinA, qui va appeler la fonction changementA, qui se déclenchera sur l’événement CHANGE.
Dans la partie loop j’ai déplacé et modifié le if de “if( etatA != dernierEtatA )” car ici pas de detection de changement d’état de pinA car c’est notre interrupt qui va le faire.
Donc il va rester le if pour activer le buzzer et le if du bouton dans la partie loop.
Tout le reste est allé dans la fonction “void changementA(){”  et a partir de la fonction j’ai le reste de code que j’avais dans “if( etatA != dernierEtatA )”.

Comme on peut le voir dans un interrupt, si l’événement se déclenche, l’arduino arrête tout ce qu’il était entrain de faire pour exécuter la fonction de l’interrupt.
Bien pratique pour effectuer une pause, ou déclencher une procédure d’urgence !

J’en ai terminé sur l’explication des codes en montrant comment gérer un rotary encoder ( bouton rotatif ) avec et sans interrupt fonction super intéressante sur arduino.
Pour télécharger les codes :
Rotary encoder sans interrupt
Rotary encoder avec interrupt

Participe à l’évolution de RetroEtGeek en seulement 30 secondes sur Utip.
Soit en donnant un pourboire, soit en regardant une (ou plusieurs publicité).
Et si tu crée ton compte je peux savoir qui participe 🙂
Le but c’est de financer des projets et de vous faire gagner des produits que je crée.
C’est juste ici qu’il faut cliquer

Utip

N’hésitez pas à poser vos questions sur les réseaux sociaux de la chaîne instagramtwitter , facebook ,youtube ; si vous ne comprenez pas certaines parties du tutoriel n’hésitez pas , me dire ce que vous aimeriez que je crée pour en faire des vidéos tutoriel  et à partager les projets que vous aimeriez créer etc…

Comme toujours allez sur la page de C’est quoi Retro et Geek pour connaître tout ce que je recherche à faire sur la chaîne.

Merci les RetroGeeker et RetroGeekeuse