December 19, 2009

Using a quadrature encoder (rotary switch) with Arduino

I've found several sites and posts explaining how to use a quadrature encoder with an Arduino, but wasn't completely satisfied with any of the methods used. Perhaps part of the problem is with the encoder I'm using: it's part #COM-09117 at Sparkfun.com.

This encoder has twelve detents per rotation, and each detent covers one complete cycle of Gray code. This means that the most common method of reading the knob counts four steps per detent, which isn't what I wanted.

So, backing up a bit... Here's how one of these things works. Instead of separate pins for each position as the knob turns, there are two pins (A and B) that produce offset pulses. The direction of the offset indicates the direction of the rotation.
As you can see from the diagram, as you rotate the knob clockwise, the pulses from A lead the pulses from B. Rotating counterclockwise, the pulses from B come first.

The detent spacing is marked on the diagram as "one notch", and if you just keep track of changes on  A and B there are four steps between detent positions. There is an event that happens only once per detent, though: that is a falling (or rising) edge on either A or B. So what I did is run an interrupt on the falling edge of A. Clockwise, B is high when A has its falling edge (at point 1) and counterclockwise B is low when A has its falling edge (at point 2). The interrupt routine just reads B, and adjusts the recorded position of the knob accordingly.

I connected one quadrature pin to Arduino pin 2 and the other to Arduino pin 3. Pin 2 is interrupt 0 for the Arduino. Here's my code:

/* knobtest.pde
Test of optical encoder (Gray scale)
Eric Ayars
*/

byte Blinker = 13;
int Delay = 250;
byte A = 2;        // One quadrature pin
byte B = 3;        // the other quadrature pin
volatile int Rotor = 0;

void setup() {

    // set DIO pins
    pinMode(Blinker, OUTPUT);
    pinMode(A, INPUT);
    pinMode(B, INPUT);

    // Turn on pullup resistors
    digitalWrite(A, HIGH);
    digitalWrite(B, HIGH);

    // Attach interrupt to pin A
    attachInterrupt(0, UpdateRotation, FALLING);

    // Use serial port to keep user informed of rotation
    Serial.begin(9600);
}

void loop() {
    // Basic blink program here: interrupt comes in as needed.
    digitalWrite(Blinker, HIGH);
    delay(Delay);
    digitalWrite(Blinker, LOW);
    delay(Delay);
}

void UpdateRotation() {
    // This is the subroutine that runs every time pin 2
    // switches from high to low.

    if (digitalRead(B)) {
        Rotor++;
    } else {
        Rotor--;
    }
    Serial.println(Rotor, DEC);
}


The program blinks the LED on pin 13 at a frequency of roughly 2 Hz, but any time the knob is turned it sends the new position out the serial line and then goes back to blinking.

I wouldn't recommend this method if the exact position of the knob is critical. It works great as a user input device, though.

4 comments:

  1. Thanks! I was just going through this problem myself, the interrupt seems like a good solution for a small project.

    ReplyDelete
  2. So does this detect rotation in both directions, using just one interrupt? I've seen stuff about needing two interrupts for some reason...

    ReplyDelete
  3. It uses two digital lines, one of which is an interrupt. The UpdateRotation() function is triggered when line A goes low, and it checks the status of line B to see which way the step went.

    ReplyDelete
  4. Thanks very much for this Eric! For those who are interested, I have designed a breakout board for this encoder: http://www.inmojo.com/store/disassemble-reassemble/item/rotary-encoder-breakout-pcb-only/

    ReplyDelete