Rotary Encoders

Jump to: navigation, search

This article is work in progress.

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.

"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.

<c> /************************************************************************/ /* */ /* Reading rotary encoder */ /* one, two and four step encoders supported */ /* */ /* Author: Peter Dannegger */ /* */ /************************************************************************/

  1. include <avr/io.h>
  2. include <avr/interrupt.h>

// target: ATmega16 //------------------------------------------------------------------------

  1. define XTAL 8e6 // 8MHz

// define the two inputs that the encoder is connected to

  1. define PHASE_A (PINA & 1<<PA1)
  2. define PHASE_B (PINA & 1<<PA3)
  1. define LEDS_DDR DDRC
  2. 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;
 }

} </c>

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