Cómo usar un Encoder con un Arduino

Un encoder es muy parecido a un potenciómetro por fuera, pero en realidad son muy distintos. El potenciómetro nos da un valor analógico que tenemos que leer, pero el encoder nos envía información «digital» que vamos leyendo según la ruedecita gira. Un modelo común es el PEC11-4215F-S0024, que nos envía 24 «pulsos» en una vuelta completa.

El encoder tiene 3 patillas para los pulsos, y 2 patillas para el boton principal (si pulsas el eje hacia dentro actua como un pulsador).  Este sería el esquema del encoder. Los puntos rojos es donde conectamos los pines del arduino:

sche

BTN es el pin del Botón. Cuando pulsamos, se conectan los 5V con tierra, y BTN vale 0. El resto del tiempo, se mantiene alto.

Para detectar el giro, la idea es conectar una patilla común a tierra, y las otras dos a 5V (con una resistencia de pull up de por ejemplo, 10k). Según vamos girando, el encoder conecta la patilla A y la patilla B a tierra, con un ligero desfase, efectuando así un pulso, que podremos detectar en nuestro arduino.

Si la patilla A baja antes que la patilla B, vamos en el sentido del reloj, y si la patilla B baja antes, vamos hacia atrás.

Esto es una foto del osciloscopio, mostrando las patillas A y B superpuestas, cuando giramos el encoder:

osc

En «reposo», el encoder tiene las 2 patillas en 1. Al girar, se conecta la patilla A con GND, y el voltaje se pone a 0,  seguimos girando y la patilla B, más o menos a la mitad del pulso toca GND, y ambas se ponen a 0. Segun seguimos girando, la patilla A deja de estar conectada a GND, asi que vuelve a 1, y luego la patilla B, volviendo ambas a 1. La longitud del pulso depende de la velocidad a la que nosotros giramos la ruedecita.

Aqui tenemos 1 pulso del encoder :

img1

Podemos pasar esto a binario, 1 cuando el voltaje está alto, 0, cuando está bajo, tomando los valores en distintos momentos del tiempo:

img2

Esto lo podemos traducir a binario:

img3

O sea, la secuencia seria 3 2 0 1 3. Así que, ¿cómo leer esto, y tener control del encoder?

Yo voy a leer todo en la interrución del timer, a intervalos regulares, a una velocidad constante de 125khz. Lo hago así porque luego voy a comunicarme con otros dispositivos, y asi puedo aprovechar la interrupción más tarde. Asi que 125 veces por milisegundo, voy a leer el estado de las 2 patillas para ver si estamos en un pulso o no.

Pero aqui surge un problema. Si mirais la foto del osciloscopio, vemos que hay unos picos raros:

osc2

Esto es debido a que los contactos metálicos producen vibraciones, como minichispazos, cuando se separan, y es posible que durante un tiempo la carga se mantenga saltando (bouncing en inglés). Asi que no nos podemos fiar de una lectura simple. Lo que hacemos generalmente es leer varias veces el valor de las patillas, y ver que no han cambiado en el tiempo. Si tras varias lecturas, por ejemplo 125, o lo que es lo mismo, 1 milisegundo, el valor es el mismo, pues aceptamos esa lectura. Este proceso se denomina «debouncing«.

static volatile uint8_t enc_value;
static volatile uint8_t enc_tests;

# define PINS_READ_ENC PINB
# define PINS_WRITE_ENC PORTB
# define PINS_DDR_ENC DDRB
# define PINS_OFFSET_ENC_A 0
# define PINS_OFFSET_ENC_B 1

void encoder_read (void )
{
// leo las 2 patillas al mismo tiempo. Debeis de conectar las 2 patillas del encoder en el mismo puerto!
const uint8_t input = PINS_READ_ENC;
const uint8_t read = ( BitIsSet ( input , PINS_OFFSET_ENC_B ) << 1 ) |
                     ( BitIsSet ( input , PINS_OFFSET_ENC_A ) << 0 ) ;

if ( read != enc_value )
{
enc_tests = 125; // 1 ms a 125khz
enc_value = read;
return;
}

if (enc_tests > 0)
{
-- enc_tests ;
return;
}

if ( enc_tests == 0 )
{
// comprobamos si estamos en un pulso
}

}

El código es C puro, pero facilmente lo podréis pasar al arduino. Realmente q la interrupción sea de 125Khz no es relevante. 1 ms es suficiente para la mayoria de usos.

¿Cómo saber si estamos o no estamos girando, y la direccion? Aqui muchos tutoriales se lian un poco con la secuencia. Si miramos el osciloscopio, vemos que un pulso lo tenemos cuando ambos pines están a 0. Y el valor previo nos dice si estamos girando hacia un lado o hacia otro. Asi que cuando el valor de los 2 pines sea 0, incrementamos o decrementamos en función de la posición.

Técnicamente necesitaríamos salir del pulso, pero en general estos encoders se usan para mover un cursor en el menú de la pantalla y no tiene sentido conocer los pasos intermedios del giro. Si tuviesemos un encoder para medir la posición de un servo entonces usaríamos otras formas de leer los datos, y seguramente, otro encoder más caro :)

Asi que si recordamos la imagen del osciloscopio, vemos que cuando ambos pines están a 0, o bien el A o bien el B tienen que estar abajo antes. Esto es 01 ó 10 en binario: 1 ó 2 en decimal.

Para incrementar el valor de la posición actual (index), sumamos 1 al contador, pero luego comprobamos que no nos pasamos de los pulsos por vuelta (24 según este modelo de encoder). Asi que sumamos 1 y aplicamos el módulo, para que al llegar a 23+1, volvamos a 0. De este modo index se moverá de 0 a 23.

Para decrementar, en vez de restar 1, damos una vuelta entera, sumando 23 y aplciando el módulo. De esta forma, no tenemos que lidiar con valores negativos, y todo es más sencillo luego.

El bit más alto, el signo, del índice, lo reservo para indicar la dirección de giro. 0 significa al sentido del reloj, 1 al revés.

Al guardar el indice de esta forma, digamos que tenemos una representacion angular del encoder, que representa de 0 a 360 grados (con el valor de 0 a 23), con el signo para decirnos qué dirección tomamos para girar.

static volatile uint8_t enc_value;
static volatile uint8_t enc_tests;
static volatile uint8_t enc_index;
static volatile uint8_t enc_previ;

# define PINS_READ_ENC		PINB
# define PINS_WRITE_ENC		PORTB
# define PINS_DDR_ENC		DDRB
# define PINS_OFFSET_ENC_A		0
# define PINS_OFFSET_ENC_B		1

# define ENC_STEPS			24

# define ENC_INDEX_INC		(enc_index = 0< 0)
	{
		-- enc_tests ;
		return;
	}

	if ( enc_tests == 0 )
	{
		if ( ENC_READ_AB == 0)
		{
		     if ( enc_previ == 1 ) ENC_INDEX_INC ;
		else if ( enc_previ == 2 ) ENC_INDEX_DEC ;
		}
 		enc_previ = ENC_READ_AB ;

	}
}

Tambien podemos incluir aqui el valor del pulsador. Al incluir la rutina de «debounce«, nos ahorramos tambien el tema del ruido, pero eso hace que el pulso sea de al menos 1 ms, que depende de lo rapido que seamos, puede ser o no suficiente. Usamos el bit 6 del index para representar el estado actual del pulsador. El código final quedaría asi:

static volatile uint8_t enc_value;
static volatile uint16_t enc_tests;
static volatile uint8_t enc_index;
static volatile uint8_t enc_previ;

# define PINS_READ_ENC PINB
# define PINS_WRITE_ENC PORTB
# define PINS_DDR_ENC DDRB
# define PINS_OFFSET_ENC_A 0
# define PINS_OFFSET_ENC_B 1
# define PINS_OFFSET_ENC_BTN 2

# define ENC_STEPS 20

# define ENC_INDEX_INC (enc_index = 0<<7 | ( (enc_index & 0x3f) + 1 ) % ENC_STEPS)
# define ENC_INDEX_DEC (enc_index = 1<<7 | ( (enc_index & 0x3f) + (ENC_STEPS-1) ) % ENC_STEPS)

# define ENC_READ_AB (read & 0x03)
# define ENC_READ_BTN (read & 1<<2)

void encoder_read (void )
{
 // sure both reads same time
 const uint8_t input = PINS_READ_ENC;
 const uint8_t read = ( BitIsSet ( input , PINS_OFFSET_ENC_BTN ) << 2 ) | ( BitIsSet ( input , PINS_OFFSET_ENC_B ) << 1 ) | ( BitIsSet ( input , PINS_OFFSET_ENC_A ) << 0 ) ;

 if ( read != enc_value )
 {
 enc_tests = 125; // 1 milliseconds on 125khz timer (PEC11 datasheet max debounce rate)
 enc_value = read;
 return;
 }

 if (enc_tests > 0)
 {
 -- enc_tests ;
 return;
 }

 if ( enc_tests == 0 )
 {
 if ( ENC_READ_AB == 0)
 {
 if ( enc_previ == 1 ) ENC_INDEX_INC ;
 else if ( enc_previ == 2 ) ENC_INDEX_DEC ;
 }
 enc_previ = ENC_READ_AB ;

 if ( ENC_READ_BTN )
 {
 ClearBit (enc_index,6);
 }
 else
 {
 SetBit (enc_index,6);
 }
 }
}

Como ejercicio para el lector, queda poder medir la velocidad de giro, contando los «ticks» que han pasado desde que dejan de estar ambos pines a 1, hasta que vuelven a estar ambos pines a 1. Y si tienes ganas, pasarlo a arduino para que la gente pueda usarlo rápidamente.

Por ahora funciona bien, si giras muy muy rápido puede perder algun paso, sobre todo por el «debouncer», pero no es relevante, ya que personalmente voy a usar el encoder para controlar el menú de una pantalla, y es más que suficiente.

15 comentarios en “Cómo usar un Encoder con un Arduino

    • claro que se usan, es más, las máquinas suelen llevar servos con encoders. Los encoders del articulo son para menus y eso, pero para posición hay que usar encoders un poco mejores, con más pulsos, etc. Lo normal es comprar servos con encoders que detectan si ha perdido la posicion y volver, etc, o para devolverte la posicion actual y volver a su sitio. Se usan muchisimo, lo unico que cuanta más precision y fiabilidad, más caros, obiamente.

  1. como se hace la logica
    de los encoder
    por ejemplo si tengo un encoder roativo donde cada pulso equivale 0.975
    como aser la logica en este caso, ya que la mayoria de los ejemplos que existen no le dan un valor a los pulsos tanto ascendente como descendente

  2. mi encoder tiene 300 pulsos por revolucion entonces tendria un valor entre 0 y 300
    en este caso yo le doy valor a los pulsos por que tiene conectado una rueda medidora de 29.25cm
    esta programacion del conteo lo tengo que proyectar en una lcd 16×2
    al igual que tengo que utilizar interrupciones por que cambia su jiro
    en este caso es la misma programacion verdad.

  3. Este codigo se basa en una interrupcion de reloj, que se ejecuta 125.000 veces por segundo. Yo creo que a mas pulsos, es verdad que deberias de tomar mas muestras. Digamos que la idea es «cada cierto tiempo, mira como está el encoder, y actualiza la posicion».

    Es cierto que si tienes tantos pulsos, deberias de contar todos los ticks que se generan usando interrupciones, pero el codigo seria distinto. La idea seria «ha cambiado la posicion del encoder, entonces hay que actualizar la posicion».

    Tampoco creo que sea 100% exacto. La interrupcion salta si el pin cambia. Lo que no te dice es cuantas veces ha cambiado. Puede ser que el encoder esté girando muy rapido, y se generen 2 pulsos (la interrupcion salta, pero no te da tiempo de resetearla antes del segundo pulso)

  4. QUE PUEDO ASER EN ESTE CASO. DE QUE MANERA PUEDES AYUDARME YA QUE MI LOGICA NO LO CONPRENDE DEL TODO, EL ENCODER NO GIRA MUY RAPIDO

  5. BUENAS CUANDO LO PRUEBO ME MOLESTA QUE LIBRERÍA O QUE TENGO QUE HACER PARA QUE NO ME APAREZCAN ESTOS PROBLEMAS:
    sketch_may22b:78: error: ‘SetBit’ was not declared in this scope Y DEMÁS GRACIAS

    • Pon estas lineas…

      # define SetBit(adr, bit) ( adr |= _BV(bit) )
      # define ClearBit(adr, bit) ( adr &= ~_BV(bit) )
      # define ToggleBit(adr, bit) ( adr ^= _BV(bit) )
      # define BitIsSet(adr, bit) ((adr & _BV(bit) ) ? 1 : 0 )
      # define BitIsClear(adr, bit) ((adr & _BV(bit) ) ? 0 : 1 )
      # define BitAck(adr, bit) ( adr =~ _BV(bit) )

      # define MIN(a,b) ( (a)(b) ? (a) : (b) )

      # define ROUNDNEAR(num,factor) ((num) + (factor) - 1 - ((num) - 1) % (factor))

    • Prueba con estas lineas

      # define SetBit(adr, bit) ( adr |= _BV(bit) )
      # define ClearBit(adr, bit) ( adr &= ~_BV(bit) )
      # define ToggleBit(adr, bit) ( adr ^= _BV(bit) )
      # define BitIsSet(adr, bit) ((adr & _BV(bit) ) ? 1 : 0 )
      # define BitIsClear(adr, bit) ((adr & _BV(bit) ) ? 0 : 1 )
      # define BitAck(adr, bit) ( adr =~ _BV(bit) )

      # define MIN(a,b) ( (a)(b) ? (a) : (b) )

      # define ROUNDNEAR(num,factor) ((num) + (factor) – 1 – ((num) – 1) % (factor))

  6. Buenas. Necesito incluir encoders a arduino para un proyecto. Cómo asigno los pines?? Agradecería la ayuda y ya que trabajo para una empresa serías recompensado por tus servicios. Muchas gracias

Deja un comentario