¿Qué es un Encoder Rotativo?

Un encoder rotativo es un dispositivo genérico que permite determinar la posición y velocidad angular de un accionamiento, y registrar la medición desde un procesador o autómata como Arduino.

Existen múltiples tipos de encoders rotativos, pero en el ámbito de Arduino y los proyectos de electrónica caseros es muy frecuente encontrar encoders rotativos electromecanicos.

Externamente estos encoders pueden ser parecidos a ciertos tipos modelos de potenciómetros, lo cual puede ser una ventaja porque hace que ciertos accesorios sean similares, e incluso sea posible sustituir uno por otro.

Este tipo de encoder rotativo es un dispositivo incremental que proporciona un pulso digital cada vez que el encoder gira un determinado ángulo. El número de pulsos por vuelta depende del encoder empleado, siendo habitual 256 pulsos/vuelta.

Frecuentemente, también incorporan un pulsador que actual al apretar la palanca del encoder.

Curiosamente, este tipo de encoders no son demasiado útiles para actuar como encoders propiamente dichos, es decir, para registrar el giro de un elemento (por ejemplo, la rueda de un robot) de un debido a la dificultad de acoplarlo al eje y la baja resolución del encoder.

Su uso principal es como mando o dispositivo de control o en sustitución de potenciómetros. Por ejemplo, pueden emplearse para regular el brillo de una pantalla LCD, el volumen de un dispositivo, o el ángulo de un motor paso a paso o servo.

¿Cómo funciona un Encoder Rotativo?

Internamente el encoder está formado por dos escobillas que deslizan sobre una pista de metálica con divisiones. Al girar el eje, una pequeña bola metálica cierra el contacto, actuando como un pulsador.

Normalmente disponen de dos salidas formando un sistema equivalente a disponer dos pulsadores (Canal A y B). Estos pulsadores están desplazados uno respecto al otro, formando lo que se denomina un encoder en cuadratura.

En un encoder en cuadratura existe un desfase entre ambos sensores de forma que la señal que producen está desplazada 90º eléctricos. Gráficamente, la señal de ambos canales respecto al ángulo girado sería la siguiente.

Señal de ambos canales respecto al ángulo girado

La ventaja de los encoder en cuadratura es que, además de detectar la posición y la velocidad, permiten determinar el sentido de giro.

Para visualizarlo, consideremos que tomamos como origen de eventos los flancos de subida o bajada del Canal A. Si giramos en el sentido CW, se producirán los eventos t0, t1, t2, t3… tn.

Si en estos eventos miramos el Canal B, vemos que la señal A es siempre inversa al Canal B.

La señal A es siempre inversa al Canal B

Si invertimos el sentido de giro, e igualmente tomamos como referencia los flancos de bajada o subida del Canal A, vemos que en los instantes (t0, t1, t2, t3… tn) la señal del Canal A y B son siempre idénticas.

La señal del Canal A y B son siempre idénticas

En realidad, que el sentido de giro sea CW o CCW dependerá de la construcción interna del sensor, de la conexión, y del canal que tomemos como referencia.

Pero, en cualquier caso, vemos que es posible diferenciar el sentido de giro simplemente comparando las señales obtenidas en el encoder en cuadratura, y asignar un significado físico CW o CCW es inmediato, simplemente probando el montaje una vez.

Respecto a la precisión, tenemos más de una opción.

  • Precisión simple: registrando un único flanco (subida o bajada) en un único canal.
  • Precisión doble: registrando ambos flancos en un único canal.
  • Precisión cuádruple: registrando ambos flancos en ambos canales.

Esquema de montaje

Para conectar el encoder a Arduino, necesitamos tres entradas digitales, dos para la detección del encoder y una adicional si queremos registrar la pulsación de la palanca.

Idealmente, deberíamos emplear interrupciones para registrar el movimiento del encoder. Lamentablemente, la mayoría de placas de Arduino sólo tienen dos pines asociados a interrupciones. En el caso de querer precisión cuádruple esto supone emplear los dos pines con interrupciones.

En este caso, la conexión del encoder sería la siguiente.

Encoder Rotativo: Esquema de montaje

Mientras que la conexión vista desde Arduino sería la siguiente.

Encoder Rotativo: Esquema de montaje, vista del Arduino

No obstante, es posible emplear el encoder sin emplear interrupciones, lo que permite emplear entradas digitales. Sin embargo, tendremos que preguntar por pool el estado de la entrada, lo que supone un peor rendimiento.

Código de ejemplo

Precisión simple o doble por pool

En este primer ejemplo realizamos la lectura del encoder por pool, sin emplear interrupciones. Para ello podemos usar dos entradas digitales cualquiera, en el ejemplo D9 y D10. La precisión puede ser doble o simple, para lo cuál tendréis que cambiar la linea comentada en el condicional (aunque no se me ocurre una razón para preferir precisión simple a doble)


const int channelPinA = 9;
const int channelPinB = 10;
 
unsigned char stateChannelA;
unsigned char stateChannelB;
unsigned char prevStateChannelA = 0;
 
const int maxSteps = 255;
int prevValue;
int value;
 
const int timeThreshold = 5; 
unsigned long currentTime;
unsigned long loopTime;
 
bool IsCW = true;
 
void setup() {
   Serial.begin(9600);
   pinMode(channelPinA, INPUT);
   pinMode(channelPinB, INPUT);
   currentTime = millis();
   loopTime = currentTime;
   value = 0;
   prevValue = 0;
}
 
void loop() {
   currentTime = millis();
   if (currentTime >= (loopTime + timeThreshold))
   {
      stateChannelA = digitalRead(channelPinA);
      stateChannelB = digitalRead(channelPinB);
      if (stateChannelA != prevStateChannelA)  // Para precision simple if((!stateChannelA) && (prevStateChannelA))
      {
         if (stateChannelB) // B es HIGH, es CW
         {
            bool IsCW = true;
            if (value + 1 <= maxSteps) value++; // Asegurar que no sobrepasamos maxSteps
         }
         else  // B es LOW, es CWW
         {
            bool IsCW = false;
            if (value - 1 >= 0) value = value--; // Asegurar que no tenemos negativos
         }
 
      }
      prevStateChannelA = stateChannelA;   // Guardar valores para siguiente
 
      // Si ha cambiado el valor, mostrarlo
      if (prevValue != value)
      {
         prevValue = value;
         Serial.print(value);
 
      }
 
      loopTime = currentTime;  // Actualizar tiempo
   }
   
   // Otras tareas
}


Precisión doble con una interrupción

En este ejemplo cambiamos una de las entradas digitales por una interrupción, registrando flancos de subida y bajada, por lo que tenemos precisión doble.


const int channelPinA = 2;
const int channelPinB = 10;
 
const int timeThreshold = 5;
long timeCounter = 0;
 
const int maxSteps = 255;
volatile int ISRCounter = 0;
int counter = 0;
 
bool IsCW = true;
 
void setup()
{
   pinMode(channelPinA, INPUT_PULLUP);
   Serial.begin(9600);
   attachInterrupt(digitalPinToInterrupt(channelPinA), doEncode, CHANGE);
}
 
void loop()
{
   if (counter != ISRCounter)
   {
      counter = ISRCounter;
      Serial.println(counter);
   }
   delay(100);
}
 
void doEncode()
{
   if (millis() > timeCounter + timeThreshold)
   {
      if (digitalRead(channelPinA) == digitalRead(channelPinB))
      {
         IsCW = true;
         if (ISRCounter + 1 <= maxSteps) ISRCounter++;
      }
      else
      {
         IsCW = false;
         if (ISRCounter - 1 > 0) ISRCounter--;
      }
      timeCounter = millis();
   }
}


Precisión cuádruple con dos interrupciones

En este último ejemplo, empleamos interrupciones para ambos canales, y en ambos flancos. Obtenemos precisión cuádruple, pero a cambio dejamos sin más pines con interrupciones a la mayoría de modelos de Arduino.


const int channelPinA = 2;
const int channelPinB = 3;
 
const int timeThreshold = 5;
long timeCounter = 0;
 
const int maxSteps = 255;
volatile int ISRCounter = 0;
int counter = 0;
 
bool IsCW = true;
 
void setup()
{
   pinMode(channelPinA, INPUT_PULLUP);
   pinMode(channelPinB, INPUT_PULLUP);
   Serial.begin(9600);
   attachInterrupt(digitalPinToInterrupt(channelPinA), doEncodeA, CHANGE);
   attachInterrupt(digitalPinToInterrupt(channelPinB), doEncodeB, CHANGE);
}
 
void loop()
{
   if (counter != ISRCounter)
   {
      counter = ISRCounter;
      Serial.println(counter);
   }
   delay(100);
}
 
void doEncodeA()
{
   if (millis() > timeCounter + timeThreshold)
   {
      if (digitalRead(channelPinA) == digitalRead(channelPinB))
      {
         IsCW = true;
         if (ISRCounter + 1 <= maxSteps) ISRCounter++;
      }
      else
      {
         IsCW = false;
         if (ISRCounter - 1 > 0) ISRCounter--;
      }
      timeCounter = millis();
   }
}
 
void doEncodeB()
{
   if (millis() > timeCounter + timeThreshold)
   {
      if (digitalRead(channelPinA) != digitalRead(channelPinB))
      {
         IsCW = true;
         if (ISRCounter + 1 <= maxSteps) ISRCounter++;
      }
      else
      {
         IsCW = false;
         if (ISRCounter - 1 > 0) ISRCounter--;
      }
      timeCounter = millis();
   }
}