Codeur rotatif incrémental : Différence entre versions

De Wiki LOGre
Aller à : navigation, rechercher
(Solution avec un microcontrôleur : Nouvelle section.)
 
(28 révisions intermédiaires par 4 utilisateurs non affichées)
Ligne 1 : Ligne 1 :
Je vais essayer de compiler sur cette page tout ce que je sais et qui peut être utile à propos des encodeurs.
+
[[Category:Microcontroleur]] [[Category:Materiel]]
  
=== Qu'est ce qu'un encodeur ? ===
+
Je vais essayer de compiler sur cette page tout ce que je sais et qui peut être utile à propos des codeurs rotatifs incrémentaux en quadrature.
Ici nous allons parler d'encodeur rotatif incrémentaux, par la suite je dirais simplement encodeur pour parler de ce type précis d'encodeur.<br />
+
Un encodeur rotatif sert à donner une information d'angles en mesurant la rotation autours d'un axe, un peu comme un potentiomètre rotatif.<br />
+
Ici, avec un encodeur incrémental, on incrémente un compteur à chaque fois que l'encodeur tourne d'un cran, cran qui correspond à sa résolution.<br />
+
C'est grâce à ce compteur qu'on connaît par la suite l'angle de l'encodeur.<br />
+
  
=== Comment lire l'encodeur ? ===
+
== Qu'est ce qu'un codeur ? ==
L'encodeur agit un peu comme deux switch. On regarde en permanence si il y a un changement d'état sur chacun de ces deux switchs.<br />
+
 
En analysant les changement on peut par la suite définir si l'encodeur a était tourné dans un sens ou dans l'autre.<br />
+
Ici nous allons parler de codeurs rotatifs incrémentaux, par la suite je dirais simplement codeur pour parler de ce type précis de codeur.<br />
Pour cela on a besoin de l'état précédent de chacun de deux pôle, ainsi que leur état actuel.<br />
+
Un codeur rotatif sert à donner une information d'angle en mesurant la rotation autours d'un axe, un peu comme un potentiomètre rotatif.<br />
Voici la variation de l'état des deux pôle d'un encodeur :<br />
+
Ici, avec les codeurs incrémentaux, on incrémente un compteur à chaque fois que le codeur tourne d'un cran, cran qui correspond à sa résolution.<br />
[[Fichier:2000px-Quadrature Diagram.svg.png|700px]]<br />
+
C'est grâce à ce compteur qu'on connaît par la suite l'angle du codeur.<br />
Lorsque l'encodeur tourne dans le sens des aiguilles d'une montre, l'état des deux pôles change comme sur le dessin en allant vers la droite, et dans le sens inverse il change comme en allant vers la gauche.<br />
+
 
Ici une phase de 1 à 4 correspond à un cran. Mais en réalité on peut lire le changement de chaque phase et ainsi multiplier la précision par 4.<br />
+
== Comment lire le codeur ? ==
Voici le code le plus simple possible pour analyser l'état de l'encodeur et incrémenter une position. Il faudra exécuter ce code à chaque fois qu'un changement est détecté sur un des deux pôles de l'encodeur. <br />
+
 
 +
Le codeur agit un peu comme deux switchs. On regarde en permanence si il y a un changement d'état sur chacun de ces deux switchs.<br />
 +
En analysant les changements on peut par la suite définir si le codeur a été tourné dans un sens ou dans l'autre.<br />
 +
Pour cela on a besoin de l'état précédent de chacun des deux pôle, ainsi que leur état actuel.<br />
 +
Voici la variation de l'état des deux pôle d'un codeur :
 +
 
 +
[[Fichier:2000px-Quadrature Diagram.svg.png|700px]]<br />''Source : [http://en.wikipedia.org/wiki/Rotary_encoder#Incremental_rotary_encoder Wikipedia]''
 +
 
 +
Lorsque le codeur tourne dans le sens des aiguilles d'une montre, l'état des deux pôles change comme sur le dessin en allant vers la droite, et dans le sens inverse il change comme en allant vers la gauche.
 +
 
 +
Ici les phases de 1 à 4 correspondent à un cran. Mais en réalité on peut lire un changement à chaque phase et ainsi multiplier la précision par 4.
 +
 
 +
Voici le code le plus simple possible pour analyser l'état du codeur et incrémenter une position. Il faudra exécuter ce code à chaque fois qu'un changement est détecté sur un des deux pôles du codeur :
  
 
<syntaxhighlight>
 
<syntaxhighlight>
// a l'état actuel du pôle 1, et pa au l'état précedent. Idem pour b et pb. pos est la valeur d'incrémentation de la position de l'encodeur.
+
// a contient l'état actuel du pôle 1, et pa contient l'état précedent
 +
// Idem pour b et pb
 +
// pos est la valeur d'incrémentation de la position du codeur
 
if(!b) {
 
if(!b) {
   if(pa) pos--;  
+
   if(pa) pos++;
   else pos++;  
+
   else pos--;
 
}
 
}
 
else {
 
else {
Ligne 28 : Ligne 38 :
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
Ici nous avons une précision de 4 fois le nombre de cran de l'encodeur. Par exemple pour un encodeur à 20 cran, on pourra détecter 80 positions différentes.<br />
 
  
===Les crans d'un encodeur rotatif ===
+
Ici nous avons une précision de 4 fois le nombre de cran du codeur. Par exemple pour un codeur à 20 crans, on pourra détecter 80 positions différentes.
Sur la pluspart des encodeurs il y a des crans. Pour certaine application il ne sont pas souhaitable, et surtout si vous voulez utiliser un précision supérieur au nombre de cran.<br />
+
 
Je ne sais pas si c'est pareil sur tout les encodeurs, mais sur ceux que j'ai acheté il suffit d'enlever un bille à l'intérieur de l'encodeur pour se passer des crans.<br />
+
Une variante pour retrouver la direction est de faire un simple XOR entre les signaux a et b, dans la routine d'interruption.
[[Fichier:Encodeur - bille.jpg|700px]]<br />
+
== Les crans d'un codeur rotatif ==
 +
 
 +
Sur la pluspart des codeurs il y a des crans. Pour certaines applications il ne sont pas souhaitables, surtout si vous voulez utiliser un précision supérieure au nombre de crans.<br />
 +
Je ne sais pas si c'est pareil sur tous les codeurs, mais sur ceux que j'utilise il suffit d'enlever une bille à l'intérieur pour se passer des crans :
 +
 
 +
[[Fichier:Encodeur - bille.jpg|700px]]
 +
 
 +
== Codeurs, Arduino, et Midi ==
 +
 
 +
[[Arduino/Midi/Controlleur|Lire un codeur avec un arduino, et envoyer sa position en midi.]]
 +
 
 +
== Mise en parallèle de 2 codeurs ==
 +
 
 +
Suite à une [https://listes.logre.eu/pipermail/grenoble-hackerspace/2015-December/000446.html discussion sur la liste], cet chapitre tente de trouver une solution pour mettre en parallèle 2 codeurs incrémentaux.
 +
 
 +
=== Solution avec portes XOR ===
 +
 
 +
Une proposition de [[Utilisateur:Guy|Guy]] est de combiner deux à deux les signaux des codeurs avec des portes XOR. On obtient alors ceci comme table de vérité :
 +
 
 +
Aout = A1 ⊕ A2
 +
Bout = B1 ⊕ B2
 +
 +
1) Si la molette 2 tourne et que la molette 1 est posée sur deux valeurs low :
 +
 +
A1 A2 Ao B1 B2 Bo
 +
0  0  0  0  0  0
 +
0  1  1  0  0  0
 +
0  1  1  0  1  1
 +
0  0  0  0  1  1
 +
 +
2) Si la molette 2 tourne et que la molette 1 est posée sur deux valeurs high :
 +
 +
A1 A2 Ao B1 B2 Bo
 +
1  0  1  1  0  1
 +
1  1  0  1  0  1
 +
1  1  0  1  1  0
 +
1  0  1  1  1  0
 +
 +
3) Si la molette 2 tourne et que la molette 1 est posée sur des valeurs low/high :
 +
 +
A1 A2 Ao B1 B2 Bo
 +
0  0  0  1  0  1
 +
0  1  1  1  0  1
 +
0  1  1  1  1  0
 +
0  0  0  1  1  0
 +
 +
4) Si la molette 2 tourne et que la molette 1 est posée sur des valeurs high/low :
 +
 +
A1 A2 Ao B1 B2 Bo
 +
1  0  1  0  0  0
 +
1  1  0  0  0  0
 +
1  1  0  0  1  1
 +
1  0  1  0  1  1
 +
 
 +
Le hic, c'est que le sens de rotation des cas 1 et 2 est inversé par rapport au cas 3 et 4. Donc suivant comment est arrêté le codeur 1, le codeur 2 va inverser le sens de codage.
 +
 
 +
Comment régler ce problème ?
 +
 
 +
À noter que suivant le type de ''détente'' du codeur, c'est à dire les positions stables forcées par les ''clicks'' mécaniques, on peut se retrouver dans le cas 1 uniquement (type A ci-dessous), ou dans les cas 1 et 2 seulement (type B ci-dessous). Le sens de rotation sera alors bien conservé.
 +
 
 +
[[Fichier:detente_codeur.jpg]]
 +
 
 +
=== Solution avec diodes ===
 +
 
 +
[[Utilisateur:Mbxddp|Marc]] propose le montage suivant (non testé) :
 +
 
 +
[[Fichier:HamsterMolettes.png|650px]]
 +
 
 +
C à ajuster en fonction de la fréquence du créneau de la molette, R à ajuster en fonction des résistances de pull-up du chip (commencer par R = 4 x Rchip environ).
 +
 
 +
=== Solution avec un additionneur ===
 +
 
 +
Solution proposée par [[Utilisateur:Ebonet|Edgar]]. Si on renumérote les
 +
phases de 0 à 3, on peut considérer que chaque codeur fournit dans ses
 +
sorties la valeur de sa phase représenté en code de Gray à 2 bits :
 +
 
 +
{|class="wikitable" style="text-align: center"
 +
! n !! colspan="2" | bits
 +
|-
 +
| 0 || 0 0
 +
|-
 +
| 1 || 0 1
 +
|-
 +
| 2 || 1 1
 +
|-
 +
| 3 || 1 0
 +
|}
 +
 
 +
Quand le codeur tourne, la phase est soit incrémentée, soit décrémentée
 +
(toujours modulo 4), suivant le sens de rotation.
 +
 
 +
Si on a maintenant deux codeurs, et qu'on veut envoyer l'information à
 +
un circuit qui attend un seul codeur, il suffit de combiner les signaux
 +
des deux codeurs avec un circuit qui ''additionne'' les phases modulo 4.
 +
Voici la table de Pythagore de l'addition modulo 4 :
 +
 
 +
{|class="wikitable" style="text-align: center"
 +
! + !! 0 !! 1 !! 2 !! 3
 +
|-
 +
! 0
 +
| 0 || 1 || 2 || 3
 +
|-
 +
! 1
 +
| 1 || 2 || 3 || 0
 +
|-
 +
! 2
 +
| 2 || 3 || 0 || 1
 +
|-
 +
! 3
 +
| 3 || 0 || 1 || 2
 +
|}
 +
 
 +
Donc il nous faut tout simplement un additionneur à deux bits en code de
 +
Gray. Sa table de vérité se calcule en combinant les deux tables
 +
précédentes. La voici triée par ordre binaire. Les entrées sont dans
 +
l'ordre (A<sub>1</sub>, B<sub>1</sub>, A<sub>2</sub>, B<sub>2</sub>).
 +
 
 +
{|class="wikitable" style="text-align: center"
 +
! in !! out
 +
|-
 +
| 0 0 0 0 || 0 0
 +
|-
 +
| 0 0 0 1 || 0 1
 +
|-
 +
| 0 0 1 0 || 1 0
 +
|-
 +
| 0 0 1 1 || 1 1
 +
|-
 +
| 0 1 0 0 || 0 1
 +
|-
 +
| 0 1 0 1 || 1 1
 +
|-
 +
| 0 1 1 0 || 0 0
 +
|-
 +
| 0 1 1 1 || 1 0
 +
|-
 +
| 1 0 0 0 || 1 0
 +
|-
 +
| 1 0 0 1 || 0 0
 +
|-
 +
| 1 0 1 0 || 1 1
 +
|-
 +
| 1 0 1 1 || 0 1
 +
|-
 +
| 1 1 0 0 || 1 1
 +
|-
 +
| 1 1 0 1 || 1 0
 +
|-
 +
| 1 1 1 0 || 0 1
 +
|-
 +
| 1 1 1 1 || 0 0
 +
|}
 +
 
 +
Je (Edgar) ne vois pas de moyen simple de réaliser ça avec des portes
 +
logiques discrètes. Mais ça peut s'implémenter facilement avec une PROM
 +
ayant une capacité d'au moins 16 mots de 2 bits. Nous avons dans la
 +
[[Composants|réserve de composants]] des TBP18SA030N (32 mots de 8 bits)
 +
en boîtier DIP16 qui semblent tout indiquées. Une fois programmée, la
 +
PROM se comporte comme une grosse porte logique dont les entrées sont
 +
les lignes d'adresse. Si on laisse à zéro les six bits non utilisés de
 +
chaque octet (ce qui permet de les programmer plus tard si on a envie),
 +
le contenu à programmer dans la PROM est le suivant :
 +
 
 +
  {0x00, 0x01, 0x02, 0x03, 0x01, 0x03, 0x00, 0x02,
 +
    0x02, 0x00, 0x03, 0x01, 0x03, 0x02, 0x01, 0x00}
 +
 
 +
On peut aussi répéter quatre fois chaque sortie, si on veut de la
 +
redondance en cas de problème à la programmation :
 +
 
 +
  {0x00, 0x55, 0xaa, 0xff, 0x55, 0xff, 0x00, 0xaa,
 +
    0xaa, 0x00, 0xff, 0x55, 0xff, 0xaa, 0x55, 0x00}
 +
 
 +
'''Remarque''' : il semble que que les codeurs soient de type B, ce qui
 +
devrait permettre d'avoir une solution plus simple (portes XOR ?). Mais
 +
les crans des molettes de souris ne sont pas toujours très marqués, et
 +
il ne semble pas difficile de laisser involontairement la molette à
 +
cheval entre deux crans. Avec un additionneur en code de Gray ce n'est
 +
pas un problème.
 +
 
 +
=== Solution avec un microcontrôleur ===
 +
 
 +
Solution proposée par [[Utilisateur:Ebonet|Edgar]]. Comme l'a fait
 +
remarquer Guy sur la liste, la solution la plus compacte est
 +
probablement un microcontrôleur à 8 broches, par exemple un ATtiny13A.
 +
L'approche la plus simple consiste à stocker la table de vérité dans un
 +
tableau et l'utiliser pour convertir les entrées en sorties dans une
 +
boucle infinie :
 +
 
 +
<syntaxhighlight>
 +
for (;;)
 +
    PORTB = truth_table[PINB & 0x0f];
 +
</syntaxhighlight>
 +
 
 +
Pour limiter la consommation électrique, il vaut mieux mettre le
 +
microcontrôleur en sommeil en ayant configuré les entrées pour qu'elles
 +
le réveillent à chaque transition. La boucle principale peut alors
 +
ressembler à
 +
 
 +
<syntaxhighlight>
 +
for (;;) {
 +
    sleep_cpu();  /* wait for next transition */
 +
    PORTB = truth_table[PINB & 0x0f];
 +
}
 +
</syntaxhighlight>
 +
 
 +
Le problème est que si une transition se produit après la lecture des
 +
entrées et avant la prochaine phase de sommeil (typiquement un rebond),
 +
elle ne réveillera pas le microcontrôleur. La solution consiste à lire
 +
les entrées avec les interruptions désactivées, et à entrer en sommeil
 +
juste après avoir réactivé les interruptions. Comme l'instruction
 +
suivant immédiatement l'activation des interruptions s'exécute
 +
obligatoirement avant de prendre en compte la prochaine interruption, on
 +
a la garantie que le microcontrôleur entre en sommeil et est
 +
immédiatement réveillé par l'interruption en attente. Le programme
 +
ci-dessous implémente cette stratégie :
 +
 
 +
<syntaxhighlight>
 +
/*
 +
* encoder-sum.c: Combine two incremental rotary encoders into a single
 +
* virtual encoder. This is done by adding the two phases modulo 4.
 +
*
 +
* For ATtiny13/13A/13V:
 +
*  inputs:  PB[3:0] = [A1, B1, A2, B2] as switches to ground
 +
*  outputs: PB[5:4] = [Aout, Bout] totem pole outputs
 +
* The RSTDISBL fuse should be programmed in order for PB5 to be
 +
* available as output.
 +
*/
 +
 
 +
#include <avr/io.h>
 +
#include <avr/interrupt.h>
 +
#include <avr/sleep.h>
 +
 
 +
/* For testing on an Arduino Uno or similar. */
 +
#ifdef __AVR_ATmega328P__
 +
#  define PCMSK PCMSK0
 +
#  define GIMSK PCICR
 +
#  define PCIE  PCIE0
 +
#endif
 +
 
 +
/*
 +
* The high nibble is the output, the low nibble enables the pull-up
 +
* resistors on the input lines.
 +
*/
 +
const __flash uint8_t truth_table[16] = {
 +
    0x0f, 0x1f, 0x2f, 0x3f, 0x1f, 0x3f, 0x0f, 0x2f,
 +
    0x2f, 0x0f, 0x3f, 0x1f, 0x3f, 0x2f, 0x1f, 0x0f
 +
};
 +
 
 +
/* The pin change interrupt is only used as a wake up source. */
 +
EMPTY_INTERRUPT(PCINT0_vect);
 +
 
 +
int main(void)
 +
{
 +
    PCMSK = 0x0f;      /* sense pin change on inputs */
 +
    GIMSK = _BV(PCIE);  /* enable pin change interrupt */
 +
    DDRB = 0x30;        /* enable outputs */
 +
 
 +
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
 +
    sleep_enable();
 +
 
 +
    for (;;) {
 +
 
 +
        /*
 +
        * To avoid race conditions, PINB should be read with
 +
        * interrupts disabled, and sleep_cpu() should be the
 +
        * very first instruction after sei().
 +
        */
 +
        cli();
 +
        PORTB = truth_table[PINB & 0x0f];
 +
        sei();
 +
        sleep_cpu();
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 
 +
Ce programme n'a pas été testé. Il nécessite 6 broches d'entrée/sortie
 +
et 2 broches d'alimentation. Avec une puce à 8 broches, on ne peut plus
 +
avoir de broche RESET, et il faut donc programmer le fusible RSTDISBL
 +
pour recycler la broche RESET en broche d'entrée/sortie. La
 +
reprogrammation de la puce devient alors compliquée. Je suggère donc de
 +
tester le programme sur un Arduino Uno (ou compatible), qui ne nécessite
 +
pas de reprogrammer des fusibles, avant de le flasher sur l'ATtiny. La
 +
partie protégée par <code>#ifdef __AVR_ATmega328P__</code> permet au
 +
code d'être compilable pour l'Arduino.

Version actuelle en date du 8 décembre 2015 à 18:02


Je vais essayer de compiler sur cette page tout ce que je sais et qui peut être utile à propos des codeurs rotatifs incrémentaux en quadrature.

Qu'est ce qu'un codeur ?

Ici nous allons parler de codeurs rotatifs incrémentaux, par la suite je dirais simplement codeur pour parler de ce type précis de codeur.
Un codeur rotatif sert à donner une information d'angle en mesurant la rotation autours d'un axe, un peu comme un potentiomètre rotatif.
Ici, avec les codeurs incrémentaux, on incrémente un compteur à chaque fois que le codeur tourne d'un cran, cran qui correspond à sa résolution.
C'est grâce à ce compteur qu'on connaît par la suite l'angle du codeur.

Comment lire le codeur ?

Le codeur agit un peu comme deux switchs. On regarde en permanence si il y a un changement d'état sur chacun de ces deux switchs.
En analysant les changements on peut par la suite définir si le codeur a été tourné dans un sens ou dans l'autre.
Pour cela on a besoin de l'état précédent de chacun des deux pôle, ainsi que leur état actuel.
Voici la variation de l'état des deux pôle d'un codeur :

2000px-Quadrature Diagram.svg.png
Source : Wikipedia

Lorsque le codeur tourne dans le sens des aiguilles d'une montre, l'état des deux pôles change comme sur le dessin en allant vers la droite, et dans le sens inverse il change comme en allant vers la gauche.

Ici les phases de 1 à 4 correspondent à un cran. Mais en réalité on peut lire un changement à chaque phase et ainsi multiplier la précision par 4.

Voici le code le plus simple possible pour analyser l'état du codeur et incrémenter une position. Il faudra exécuter ce code à chaque fois qu'un changement est détecté sur un des deux pôles du codeur :

// a contient l'état actuel du pôle 1, et pa contient l'état précedent
// Idem pour b et pb
// pos est la valeur d'incrémentation de la position du codeur
if(!b) {
   if(pa) pos++;
   else pos--;
}
else {
   if(!pa) pos++; 
   else pos--; 
}

Ici nous avons une précision de 4 fois le nombre de cran du codeur. Par exemple pour un codeur à 20 crans, on pourra détecter 80 positions différentes.

Une variante pour retrouver la direction est de faire un simple XOR entre les signaux a et b, dans la routine d'interruption.

Les crans d'un codeur rotatif

Sur la pluspart des codeurs il y a des crans. Pour certaines applications il ne sont pas souhaitables, surtout si vous voulez utiliser un précision supérieure au nombre de crans.
Je ne sais pas si c'est pareil sur tous les codeurs, mais sur ceux que j'utilise il suffit d'enlever une bille à l'intérieur pour se passer des crans :

Encodeur - bille.jpg

Codeurs, Arduino, et Midi

Lire un codeur avec un arduino, et envoyer sa position en midi.

Mise en parallèle de 2 codeurs

Suite à une discussion sur la liste, cet chapitre tente de trouver une solution pour mettre en parallèle 2 codeurs incrémentaux.

Solution avec portes XOR

Une proposition de Guy est de combiner deux à deux les signaux des codeurs avec des portes XOR. On obtient alors ceci comme table de vérité :

Aout = A1 ⊕ A2
Bout = B1 ⊕ B2

1) Si la molette 2 tourne et que la molette 1 est posée sur deux valeurs low :

A1 A2 Ao B1 B2 Bo
0  0  0  0  0  0
0  1  1  0  0  0
0  1  1  0  1  1
0  0  0  0  1  1

2) Si la molette 2 tourne et que la molette 1 est posée sur deux valeurs high :

A1 A2 Ao B1 B2 Bo
1  0  1  1  0  1
1  1  0  1  0  1
1  1  0  1  1  0
1  0  1  1  1  0

3) Si la molette 2 tourne et que la molette 1 est posée sur des valeurs low/high :

A1 A2 Ao B1 B2 Bo
0  0  0  1  0  1
0  1  1  1  0  1
0  1  1  1  1  0
0  0  0  1  1  0

4) Si la molette 2 tourne et que la molette 1 est posée sur des valeurs high/low :

A1 A2 Ao B1 B2 Bo
1  0  1  0  0  0
1  1  0  0  0  0
1  1  0  0  1  1
1  0  1  0  1  1

Le hic, c'est que le sens de rotation des cas 1 et 2 est inversé par rapport au cas 3 et 4. Donc suivant comment est arrêté le codeur 1, le codeur 2 va inverser le sens de codage.

Comment régler ce problème ?

À noter que suivant le type de détente du codeur, c'est à dire les positions stables forcées par les clicks mécaniques, on peut se retrouver dans le cas 1 uniquement (type A ci-dessous), ou dans les cas 1 et 2 seulement (type B ci-dessous). Le sens de rotation sera alors bien conservé.

Detente codeur.jpg

Solution avec diodes

Marc propose le montage suivant (non testé) :

HamsterMolettes.png

C à ajuster en fonction de la fréquence du créneau de la molette, R à ajuster en fonction des résistances de pull-up du chip (commencer par R = 4 x Rchip environ).

Solution avec un additionneur

Solution proposée par Edgar. Si on renumérote les phases de 0 à 3, on peut considérer que chaque codeur fournit dans ses sorties la valeur de sa phase représenté en code de Gray à 2 bits :

n bits
0 0 0
1 0 1
2 1 1
3 1 0

Quand le codeur tourne, la phase est soit incrémentée, soit décrémentée (toujours modulo 4), suivant le sens de rotation.

Si on a maintenant deux codeurs, et qu'on veut envoyer l'information à un circuit qui attend un seul codeur, il suffit de combiner les signaux des deux codeurs avec un circuit qui additionne les phases modulo 4. Voici la table de Pythagore de l'addition modulo 4 :

+ 0 1 2 3
0 0 1 2 3
1 1 2 3 0
2 2 3 0 1
3 3 0 1 2

Donc il nous faut tout simplement un additionneur à deux bits en code de Gray. Sa table de vérité se calcule en combinant les deux tables précédentes. La voici triée par ordre binaire. Les entrées sont dans l'ordre (A1, B1, A2, B2).

in out
0 0 0 0 0 0
0 0 0 1 0 1
0 0 1 0 1 0
0 0 1 1 1 1
0 1 0 0 0 1
0 1 0 1 1 1
0 1 1 0 0 0
0 1 1 1 1 0
1 0 0 0 1 0
1 0 0 1 0 0
1 0 1 0 1 1
1 0 1 1 0 1
1 1 0 0 1 1
1 1 0 1 1 0
1 1 1 0 0 1
1 1 1 1 0 0

Je (Edgar) ne vois pas de moyen simple de réaliser ça avec des portes logiques discrètes. Mais ça peut s'implémenter facilement avec une PROM ayant une capacité d'au moins 16 mots de 2 bits. Nous avons dans la réserve de composants des TBP18SA030N (32 mots de 8 bits) en boîtier DIP16 qui semblent tout indiquées. Une fois programmée, la PROM se comporte comme une grosse porte logique dont les entrées sont les lignes d'adresse. Si on laisse à zéro les six bits non utilisés de chaque octet (ce qui permet de les programmer plus tard si on a envie), le contenu à programmer dans la PROM est le suivant :

  {0x00, 0x01, 0x02, 0x03, 0x01, 0x03, 0x00, 0x02,
   0x02, 0x00, 0x03, 0x01, 0x03, 0x02, 0x01, 0x00}

On peut aussi répéter quatre fois chaque sortie, si on veut de la redondance en cas de problème à la programmation :

  {0x00, 0x55, 0xaa, 0xff, 0x55, 0xff, 0x00, 0xaa,
   0xaa, 0x00, 0xff, 0x55, 0xff, 0xaa, 0x55, 0x00}

Remarque : il semble que que les codeurs soient de type B, ce qui devrait permettre d'avoir une solution plus simple (portes XOR ?). Mais les crans des molettes de souris ne sont pas toujours très marqués, et il ne semble pas difficile de laisser involontairement la molette à cheval entre deux crans. Avec un additionneur en code de Gray ce n'est pas un problème.

Solution avec un microcontrôleur

Solution proposée par Edgar. Comme l'a fait remarquer Guy sur la liste, la solution la plus compacte est probablement un microcontrôleur à 8 broches, par exemple un ATtiny13A. L'approche la plus simple consiste à stocker la table de vérité dans un tableau et l'utiliser pour convertir les entrées en sorties dans une boucle infinie :

for (;;)
    PORTB = truth_table[PINB & 0x0f];

Pour limiter la consommation électrique, il vaut mieux mettre le microcontrôleur en sommeil en ayant configuré les entrées pour qu'elles le réveillent à chaque transition. La boucle principale peut alors ressembler à

for (;;) {
    sleep_cpu();  /* wait for next transition */
    PORTB = truth_table[PINB & 0x0f];
}

Le problème est que si une transition se produit après la lecture des entrées et avant la prochaine phase de sommeil (typiquement un rebond), elle ne réveillera pas le microcontrôleur. La solution consiste à lire les entrées avec les interruptions désactivées, et à entrer en sommeil juste après avoir réactivé les interruptions. Comme l'instruction suivant immédiatement l'activation des interruptions s'exécute obligatoirement avant de prendre en compte la prochaine interruption, on a la garantie que le microcontrôleur entre en sommeil et est immédiatement réveillé par l'interruption en attente. Le programme ci-dessous implémente cette stratégie :

/*
 * encoder-sum.c: Combine two incremental rotary encoders into a single
 * virtual encoder. This is done by adding the two phases modulo 4.
 *
 * For ATtiny13/13A/13V:
 *   inputs:  PB[3:0] = [A1, B1, A2, B2] as switches to ground
 *   outputs: PB[5:4] = [Aout, Bout] totem pole outputs
 * The RSTDISBL fuse should be programmed in order for PB5 to be
 * available as output.
 */
 
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
 
/* For testing on an Arduino Uno or similar. */
#ifdef __AVR_ATmega328P__
#  define PCMSK PCMSK0
#  define GIMSK PCICR
#  define PCIE  PCIE0
#endif
 
/*
 * The high nibble is the output, the low nibble enables the pull-up
 * resistors on the input lines.
 */
const __flash uint8_t truth_table[16] = {
    0x0f, 0x1f, 0x2f, 0x3f, 0x1f, 0x3f, 0x0f, 0x2f,
    0x2f, 0x0f, 0x3f, 0x1f, 0x3f, 0x2f, 0x1f, 0x0f
};
 
/* The pin change interrupt is only used as a wake up source. */
EMPTY_INTERRUPT(PCINT0_vect);
 
int main(void)
{
    PCMSK = 0x0f;       /* sense pin change on inputs */
    GIMSK = _BV(PCIE);  /* enable pin change interrupt */
    DDRB = 0x30;        /* enable outputs */
 
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_enable();
 
    for (;;) {
 
        /*
         * To avoid race conditions, PINB should be read with
         * interrupts disabled, and sleep_cpu() should be the
         * very first instruction after sei().
         */
        cli();
        PORTB = truth_table[PINB & 0x0f];
        sei();
        sleep_cpu();
    }
}

Ce programme n'a pas été testé. Il nécessite 6 broches d'entrée/sortie et 2 broches d'alimentation. Avec une puce à 8 broches, on ne peut plus avoir de broche RESET, et il faut donc programmer le fusible RSTDISBL pour recycler la broche RESET en broche d'entrée/sortie. La reprogrammation de la puce devient alors compliquée. Je suggère donc de tester le programme sur un Arduino Uno (ou compatible), qui ne nécessite pas de reprogrammer des fusibles, avant de le flasher sur l'ATtiny. La partie protégée par #ifdef __AVR_ATmega328P__ permet au code d'être compilable pour l'Arduino.