EmbDev.net

Rotary Encoders

This article is work in progress.

[edit] Why edge detection is bad

You can often see implementations of rotary encoder interfaces that are based on detecting level changes in the signals with interrupts. This has several disadvantages, especially if used with cheap mechanical encoders:

  • It reacts to the smallest signal glitches, for example caused by corroded and "bouncing" contacts.
  • It fails to detect invalid Gray code transitions and therefore miscounts clicks if the signal quality is low.
  • Timing of your program becomes nondeterministic because it depends on how fast the encoder is turned - something you generally want to avoid in any real-time application.

[edit] "Correct" implementation: example code for AVR-GCC

The following code demonstrates what is widely considered the best way to interpret the data of a rotary encoder. It works by periodically sampling the encoder pins instead of using edge detection interrupts. The code is written for AVR-GCC, but can be easily modified for other compilers or translated to a different language.

Depending on the type of encoder you have (number of signal steps per click), use one of the three provided encode_read*() routines. It gives you the number of steps the encoder was turned since the last call to the routine.

/************************************************************************/
/*                                                                      */
/*                      Reading rotary encoder                   	*/
/*                      one, two and four step encoders supported	*/
/*                                                                      */
/*              Author: Peter Dannegger                                 */
/*                                                                      */
/************************************************************************/
#include <avr/io.h>
#include <avr/interrupt.h>
 
				// target: ATmega16
//------------------------------------------------------------------------
 
#define	XTAL		8e6			// 8MHz
 
// define the two inputs that the encoder is connected to
#define PHASE_A		(PINA & 1<<PA1)
#define PHASE_B		(PINA & 1<<PA3)
 
#define LEDS_DDR	DDRC
#define	LEDS		PORTC			// LEDs against VCC
 
 
volatile int8_t enc_delta;			// -128 ... 127
static int8_t last;
 
 
void encode_init( void )
{
  int8_t new;
 
  new = 0;
  if( PHASE_A )
    new = 3;
  if( PHASE_B )
    new ^= 1;					// convert gray to binary
  last = new;					// power on state
  enc_delta = 0;
  TCCR0 = 1<<WGM01^1<<CS01^1<<CS00;		// CTC, XTAL / 64
  OCR0 = (uint8_t)(XTAL / 64.0 * 1e-3 - 0.5);	// 1ms
  TIMSK |= 1<<OCIE0;
}
 
// Timer interrupt handler. Should be executed periodically, e.g. once every 1ms (1kHz)
ISR( TIMER0_COMP_vect )
{
  int8_t new, diff;
 
  new = 0;
  if( PHASE_A )
    new = 3;
  if( PHASE_B )
    new ^= 1;					// convert gray to binary
  diff = last - new;				// difference last - new
  if( diff & 1 ){				// bit 0 = value (1)
    last = new;					// store new as next last
    enc_delta += (diff & 2) - 1;		// bit 1 = direction (+/-)
  }
}
 
int8_t encode_read1( void )			// read single step encoders
{
  int8_t val;
 
  cli();
  val = enc_delta;
  enc_delta = 0;
  sei();
  return val;					// counts since last call
}
 
 
int8_t encode_read2( void )			// read two step encoders
{
  int8_t val;
 
  cli();
  val = enc_delta;
  enc_delta = val & 1;
  sei();
  return val >> 1;
}
 
 
int8_t encode_read4( void )			// read four step encoders
{
  int8_t val;
 
  cli();
  val = enc_delta;
  enc_delta = val & 3;
  sei();
  return val >> 2;
}
 
// example main program
int main( void )
{
  int32_t val = 0;
 
  LEDS_DDR = 0xFF;
  encode_init();
  sei();
 
  for(;;){
    val += encode_read1();			// read a single step encoder
    LEDS = val;
  }
}

Questions on this topic? Ask them in the Microcontrollers & Digital Electronics Forum

webmaster@embdev.netContactAdvertising on EmbDev.net