rocketnumbernine

Andrew's Project Blog: Hardware, Software, Stuff I find Interesting

This article details a simple approach to decoding the output of a 2 bit incremental Rotary Encoder. The example uses interrupts with AVR GCC on an Arduino platform, but the same interrupt setup can be used on most AVR chips and the approach will of course work on any device.

Rotary Encoder on Arduino prototype shield

Rotary Encoders can offer high precision, long life and a simpler/digital interface over analogue devices, such as potentiometers, for reading a rotational movement on a computer. They're often fairly expensive, but I recently picked up a bargain pack on eBay and Mouser also have a selection for around a dollar.

There are a number of interface and encoding techniques used by encoders, ranging from devices that provide a digital output of the angle of the shaft - with resolutions ranging from 8 to over 8000 counts per revolution, to simple 2 bit incremental encoders which report clockwise or counterclockwise movement of the shaft, this article looks at decoding the latter.

The rotary encoder indicates movement by toggling the output of two pins from high to low or vice-versa, the direction of rotation is indicated by which line goes high (or low first) as shown in the following diagram from wikipedia:

The waveform produced from the encoder I tested with when rotating clockwise then counter clockwise is shown below:

Rotary Encoder Waveform

Decoding the Encoder

There are many ways of decoding the changes in pin logic level to direction of rotation, but I think the following is the simplest. If you look at the waveforms above, a clear pattern emerges: travel from left to right over the waveform above (simulating rotation the shaft in one direction), when the inputs are the same (both high or both low) the last input to have changed will be A, if the inputs are different then the last input to have changed will be B. If the shaft is rotated in the reverse direction (travelling from right to left over the waveform) the opposite is true. If we write these conditions in binary expressions: A is equal to B, last changed pin is A, direction is left we can create a truth table:

A==BchangedPin==Adirection==left
truetruefalse
truefalsetrue
falsetruetrue
falsefalsefalse

From this we see that the we can use a simple XOR to calculate the direction of travel (direction == left) = (A == B) ^ (changedPin == A)

Note, that the above generates 4 rotation signals on each complete quadrature rotation - with some encoders this is too high a resolution (the mechanical ones I've tested this with are 'stepped' - they click as the shaft is turned, resting with with both pins at zero). So it may be better to just record movement when both pins pins have fallen to the low state (A == 0 && B == 0) and ignore other changes.

Detecting Pin Changes

The two pins from the rotary encoder are attached to digital IO pins on the microcontroller and held to ground with a resistor until rotation causes them to be go high. Rather than polling for changes, interrupts are used to trigger execution of a handler function every time the line state changes. This allows for other operations to be done whilst waiting for input.

In this example AVR external pin change interrupts are used. These can be used to detect changes on nearly all of the devices IO pins. PCINT18 and PCINT19 are used below, these are triggered by changes to the AVR IO pins PD2 and PD3 - Arduino pins D2 and D3 respectively, see here for a mapping. Pin change interrupts are handled in 3 groups, PCINT0..7, PCINT8..14, PCINT16...23 and are controlled by the PCICR and PCMSK0-PCMS2 registers, we're using pins in the same group to simplify setup. Note that we have to use the same interrupt handler to receive events for both pins, the pin value is queried to see which has changed:

1
2
3
4
5
6
7
8
9
10
void setup()
{
  Serial.begin(19200);
  pinMode(2, INPUT); // 2 Encoder pins as inputs
  pinMode(3, INPUT);
  // enable interrupts on those two pins:
  PCICR |= (1 << PCIE2); 
  PCMSK2 |= (1 << PCINT18) | (1 << PCINT19);
  sei();
}

Debouncing

The mechanical encoder I tested with create multiple triggers on a single transition due to bouncing. The encoders datasheet claims a maximum bounce of 5ms, but I didn't see more than 1ms. The waveform below shows a very much worse case of a pin transitioning from high to low.

This is simply debounced in software by waiting for 1ms and then reading the pin values - if neither has changed from the stored value we ignore the trigger. Note, that this technique will fail if the shaft is being rotated to fast and the width of the cycle approaches 1ms or the two signals are less than 1ms apart.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
volatile uint8_t pinValues[2] = {0,0};
volatile int position = 0;

ISR(PCINT2_vect)
{
  _delay_ms(1);
  int pin0 = digitalRead(2);
  int pin1 = digitalRead(3);
  if (pin0 != pinValues[0]) {
    rotary_encoder_change(0, pin0);
  } else if (pin1 != pinValues[1]) {
    rotary_encoder_change(1, pin1);
  }
}

The previous values are stored in a two value byte array and we refer to pins A and B as 0 and 1 in the code to simplify things. The Rotary Encoder decode logic is the simple boolean expression ((pinValues[0] == pinValues[1]) ^ changedPin) which returns true or false to indicate clockwise or counter clockwise shaft rotation. This is used to increment or decrement an integer counter, which the main loop prints back to the host computer.

1
2
3
4
5
6
7
8
9
10
11
void rotary_encoder_change(uint8_t changedPin, uint8_t value)
{
  pinValues[changedPin] = value;
  position += ((pinValues[0] == pinValues[1]) ^ changedPin) ? 1 : -1;
}

void loop()
{
  Serial.println(position);
  delay(100);
}

Conclusions

This is a fairly simple technique that works well for slow rotations of the shaft, but fast rotations of the shaft get lost or be reported in the wrong direction - good for replacing potentiometers as instrumentation dials but not for recording the exact movement of a motor etc.

Full source is here.

References/Bibliography

  • Wiki Rotary Encoder Page
  • Arduino/Atmega168 pin mapping
  • Atmega 168/328 datasheet - see chapter 11 and 12 for interrupt details.
  • It looks like your browser doesn't support javascript so comments won't work

    Tags/Categories: arduino, AVR, interrupts, howto