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.