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;
}
} |