Un encodeur rotatif : à quel point cela peut-il être difficile ?

Comme vous l’avez peut-être remarqué, j’ai travaillé avec un processeur STM32 ARM utilisant Mbed. Il fut un temps où Mbed était assez simple, mais beaucoup de choses ont changé depuis qu’il s’est transformé en Mbed OS. Malheureusement, cela signifie que de nombreuses bibliothèques et exemples que vous pouvez trouver ne fonctionnent pas avec le nouveau système.

J’avais besoin d’un encodeur rotatif – j’en ai sorti un bon marché de l’un de ces kits « 49 cartes pour Arduino » que vous voyez autour. Pas le meilleur encodeur du pays, j’en suis sûr, mais il devrait faire l’affaire. Malheureusement, Mbed OS n’a pas de pilote pour un encodeur et les premières bibliothèques tierces que j’ai trouvées fonctionnaient via l’interrogation ou ne se compilaient pas avec le dernier Mbed. Bien sûr, la lecture d’un encodeur n’est pas un processus mystérieux. À quel point peut-il être difficile d’écrire le code vous-même ? Comme c’est dur, en effet. J’ai pensé que je partagerais mon code et le processus de comment j’y suis arrivé.

Il existe de nombreuses façons de lire un encodeur rotatif. Certains sont probablement meilleurs que ma méthode. De plus, ces encodeurs mécaniques bon marché sont terribles. Si vous essayez de faire un travail de précision, vous devriez probablement envisager une technologie différente comme un encodeur optique. Je mentionne cela parce qu’il est presque impossible de lire l’un d’entre eux parfaitement.

Mon objectif était donc simple : je voulais quelque chose d’interrompu. La plupart de ce que j’ai trouvé vous obligeait à appeler périodiquement une fonction ou à configurer une interruption de minuterie. Ensuite, ils ont construit une machine à états pour suivre l’encodeur. C’est bien, mais cela signifie que vous consommez beaucoup de processeur juste pour vérifier l’encodeur même s’il ne bouge pas. Le processeur STM32 peut facilement s’interrompre avec un changement de broche, c’est donc ce que je voulais.

La prise

Le problème est, bien sûr, que les interrupteurs mécaniques rebondissent. Vous devez donc filtrer ce rebond soit dans le matériel, soit dans le logiciel. Je ne voulais vraiment pas mettre de matériel supplémentaire plus qu’un condensateur, donc le logiciel devrait le gérer.

Je ne voulais pas non plus utiliser plus d’interruptions que nécessaire. Le système Mbed facilite la gestion des interruptions, mais il y a un peu de latence. En fait, après tout, j’ai mesuré la latence et ce n’est pas si mal — j’en reparlerai un peu plus tard. Quoi qu’il en soit, j’avais décidé d’essayer de n’utiliser qu’une paire d’interruptions.

En théorie

En théorie, lire un encodeur est un jeu d’enfant. Il y a deux sorties, nous les appellerons A et B. Lorsque vous tournez le bouton, ces sorties envoient des impulsions. La disposition mécanique à l’intérieur est telle que lorsque le bouton tourne dans un sens, les impulsions de A sont à 90 degrés en avance sur les impulsions de B. Si vous tournez dans l’autre sens, la phase est inversée.

Les gens pensent généralement que les impulsions deviennent positives, mais la plupart des vrais encodeurs auront un contact à la terre et une résistance de rappel, donc en fait, les sorties sont souvent élevées lorsque rien ne se passe et les impulsions sont très faibles. Vous pouvez voir que dans le diagramme, lorsque personne ne tourne le bouton, il y a une longue période de signal élevé.

Notez sur le côté gauche du diagramme que le signal B chute avant le signal A à chaque fois. Si vous échantillonnez B sur le front descendant de A, vous obtiendrez toujours un 0 dans ce cas. La largeur des impulsions dépend de la vitesse à laquelle vous tournez, bien sûr. Lorsque vous tournez dans l’autre sens, vous obtenez le cas sur le côté droit du schéma. Ici, le signal A passe au niveau bas en premier. Si vous échantillonnez au même point, B vaut maintenant 1.

Notez qu’il n’y a rien de magique dans A, B ou les étiquettes dans le sens des aiguilles d’une montre et dans le sens inverse des aiguilles d’une montre. Tout ce que cela signifie vraiment, c’est « dans un sens » et « dans l’autre sens ». Si vous n’aimez pas la façon dont l’encodeur se déplace, vous pouvez simplement échanger A et B ou l’échanger dans le logiciel. J’ai juste choisi ces directions arbitrairement. Habituellement, le canal A est censé « mener » dans le sens des aiguilles d’une montre, mais cela dépend aussi du bord que vous mesurez et de la façon dont vous connectez le tout. Dans les logiciels, vous ajoutez généralement un à un décompte pour une direction et soustrayez pour l’autre direction pour avoir une idée de l’endroit où vous vous trouvez au fil du temps.

Il existe plusieurs façons de lire ce type d’entrée. Si vous l’échantillonnez, il est assez facile de construire une machine d’état à partir des deux bits et de la traiter de cette façon. La sortie forme un code gris afin que vous puissiez jeter les mauvais états et les mauvaises transitions d’état. Cependant, si vous êtes sûr de votre signal d’entrée, cela peut être beaucoup plus facile que cela. Il suffit de lire B sur un bord de A (ou vice versa). Vous pourriez vérifier l’autre bord si vous vouliez un peu plus de robustesse.

En pratique

Malheureusement, les vrais encodeurs mécaniques ne ressemblent pas au schéma ci-dessus. Ils ressemblent plus à ça :

Cela conduit à un problème. Si vous interrompez sur les deux fronts de l’entrée A (la trace supérieure sur l’oscilloscope), vous obtiendrez une série d’impulsions sur les deux fronts. Notez que B est dans des états différents à chaque bord de A, donc si vous obtenez un nombre pair d’impulsions au total, votre nombre total sera égal à zéro. Si vous êtes chanceux, vous pourriez obtenir un nombre impair dans la bonne direction. Ou vous pourriez vous tromper de direction. Quel bordel.

Mais sur le bord d’échantillonnage de A, B est solide comme un roc. La trace inférieure sur l’oscilloscope ressemble à une ligne droite car toutes les transitions B sont hors de l’écran à cette échelle. C’est le secret pour anti-rebondir facilement un encodeur. Lorsque A change, B est stable et vice versa. Puisqu’il s’agit d’un code gris, cela a du sens, mais c’est la perspicacité qui rend possible un décodeur simple.

Le plan

Donc, le plan est de remarquer quand A passe de haut en bas, puis de lire B. Ensuite, ignorez A jusqu’à ce que B change. Si vous voulez surveiller B, bien sûr, il a le même problème donc vous devez le verrouiller sur A qui est stable au changement. Dans mon cas, je ne voulais pas utiliser deux autres interruptions donc je suis cette logique :

  1. Lorsque A tombe, enregistrez l’état de B et mettez à jour le décompte. Ensuite, définissez un drapeau de verrouillage
  2. Si A retombe, si le drapeau de verrouillage est activé ou si B n’a pas changé, ne faites rien.
  3. Lorsque A monte, si B a changé, enregistrez l’état de B et effacez le drapeau de verrouillage.

Cela signifie que dans la trace de portée ci-dessus, le premier creux dans la trace du haut nous fait lire B. Après cela, aucune des transitions à l’écran n’aura d’effet car B n’a pas changé. Le front montant hors de l’écran qui se produit après que B a eu une transition bruyante de haut en bas sera celui qui déverrouille l’algorithme.

Le problème

Il y a un problème, cependant. L’ensemble du schéma repose sur l’idée que B sera différent sur un vrai front montant pour A par rapport à un front descendant. Il y a un cas où B ne change pas mais nous voulons toujours accepter le bord A. C’est alors que vous changez de direction. Si vous surveilliez B, ce serait facile à résoudre, mais c’est plus de code et deux autres interruptions. Au lieu de cela, j’ai décidé que pour une personne qui tourne un bouton, si vous tournez sauvagement dans différentes directions très rapidement, vous ne remarquerez même pas qu’un ou deux clics de l’encodeur sont allés dans le mauvais sens. Ce que vous remarquerez, c’est si vous effectuez un réglage fin, puis tournez délibérément dans l’autre sens.

Lorsque vous pensez connaître l’état précédent de B et que rien n’a changé depuis un certain temps (comme quelques centaines de millisecondes), le code réinitialisera son idée de B sur inconnu afin que le prochain signal B soit considéré comme valide quoi qu’il arrive.

j’ai utilisé le Kernel::Clock::now fonctionnalité de Mbed. Il n’est pas clair si vous êtes censé appeler cela à partir d’une routine de service d’interruption (ISR), mais je le suis et cela semble fonctionner sans problème.

Le seul autre problème est de s’assurer que le décompte ne change pas au milieu de la lecture. J’ai désactivé les interruptions autour de la lecture juste pour être sûr.

Le code

Vous pouvez trouver le code sur GitHub. Si vous avez suivi toutes les explications, vous ne devriez avoir aucun problème à suivre.


void Encoder::isrRisingA()
{
   int b=BPin; // read B
   if (lock && lastB==b) return; // not time to unlock
// if lock=0 and _lastB==b these two lines do nothing
// but if lock is 1 and/or _lastB!=b then one of them does something
   lock=0;
   lastB=b;
   locktime=Kernel::Clock::now()+locktime0; // even if not locked, timeout the lastB
}

// The falling edge is where we do the count
// Note that if you pause a bit, the lock will expire because otherwise
// we have to monitor B also to know if a change in direction occurred
// It is tempting to try to mutually lock/unlock the ISRs, but in real life
// the edges are followed by a bunch of bounce edges while B is stable
// B will change while A is stable
// So unless you want to also watch B against A, you have to make some
// compromise and this works well enough in practice
void Encoder::isrFallingA()
{
   int b;
   // clear lock if timedout and in either case forget lastB if we haven't seen an edge in a long time
   if (locktime<Kernel::Clock::now())
     {
     lock=0;
     lastB=2; // impossible value so we must read this event
     }
   if (lock) return; // we are locked so done
   b=BPin; // read B
   if (b==lastB) return; // no change in B
   lock=1; // don't read the upcoming bounces
   locktime=Kernel::Clock::now()+locktime0; // set up timeout for lock
   lastB=b; // remember where B is now
   accum+=(b?-1:1); // finally, do the count!
}


La configuration de l’interruption est facile grâce à la InterruptIn classe. C’est comme un DigitalIn objet mais a un moyen d’attacher une fonction au front montant ou descendant. Dans ce cas, nous utilisons les deux.

Latence

Je me demandais combien de temps il fallait pour traiter une interruption sur cette configuration, afin que le code soit disponible si vous définissez #define TEST_LATENCY 1. Vous pouvez voir une vidéo de mes résultats, mais TLDR : Il n’a pas fallu plus de 10 microsecondes pour obtenir une interruption et souvent environ la moitié.

Obtenir le bon encodeur était un peu plus difficile que je ne le pensais, mais surtout parce que je ne voulais pas traiter plus d’interruptions. Il serait assez simple de modifier le code pour observer la broche B par rapport à la broche A et avoir une véritable compréhension de l’état correct de B. Si vous essayez cette modification, voici une autre idée : en mesurant le temps entre les interruptions, vous pourriez également avoir une idée de la vitesse de rotation de l’encodeur, ce qui peut être utile pour certaines applications.

Si vous voulez un rappel sur le code gris et certains de ses aspects utiles, nous en avons déjà parlé. Si tout cela semble étrangement familier, j’ai utilisé un encodeur sur une ancienne version de Mbed en 2017. Dans ce cas, j’ai utilisé une bibliothèque prédéfinie qui interrogeait périodiquement les entrées sur une interruption de minuterie. Mais comme je l’ai dit, il y a toujours plus d’une façon de faire en sorte que ce genre de choses se produise.

[Headline image: “Rotary Encoder” by SparkFunElectronics, CC BY 2.0.  Awesome.]