¿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.
No es la primera vez que hablamos de encoders y su utilidad. Previamente hemos visto encoders ópticos y encoders magnéticos, así como su importancia a la hora de aplicar criterios de selección de motores y accionamientos.
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.
Sin embargo, no hay que confundir un encoder rotativo con un potenciómetro ya que tanto su electrónica como comportamiento son totalmente diferentes.
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.
Precio
Los encoders son dispositivos baratos. Podemos encontrarlos en módulos preparados para conectar de forma sencilla a Arduino por 0,45€ en vendedores internacionales de Ebay y Aliexpress.
También podemos adquirir el encoder suelto como componente, sin estar integrado en una placa. Sin embargo el precio es prácticamente el mismo por lo que, en general, es recomendable adquirirlo en un módulo.
¿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.
Para leer el encoder deberemos entender cómo leer un pulsador, como vimos en Leer un pulsador con Arduino y Leer un pulsador con Arduino con interrupciones y debounce.
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.
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.
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.
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.
Más información sobre interrupciones en Arduino en Qué son y cómo usar interrupciones en Arduino
En este caso, la conexión del encoder sería la siguiente.
Mientras que la conexión vista desde Arduino sería la siguiente.
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.
En Arduino Uno, Nano y Mini Pro los pines de interrupciones son D2 y D3. Para otros modelos de Arduino consultar el esquema patillaje correspondiente.
Ejemplos de código
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();
}
}
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.