Monday, July 14, 2014

Interfacing A Rotary Incremental Encoder to an Arduino _Part 1

I had seen some examples for rotary encoders but found the logic hard to understand. So I did my own implementation based on a table. I have seen other people using a similar method, but I think this method is easier to understand.

PrevNext CodeDirectionArray positonCodeDescription
110113CW00Invalid
01004CW1-1CCW
00102CW21CW
101111CW30Invalid
111014CCW41CW
10008CCW50Invalid
00011CCW60Invalid
01117CCW7-1CCW
8-1CCW
90Invalid
100Invalid
111CW
120Invalid
131CW
14-1CCW
150Invalid
In the two tables above you can see the combinations. The green table is the sequence that follows when turning clockwise or counter clockwise. What I did is combine the two and put it in a table.  Each combination is unique. By looking at the code you can determine if the data read is valid or the direction the encoder is going.

There are three main sections.

First is the setup, which defines the pins to use, enable interrupts and initialize  the previous pin state. Next is the interrupt service routine, and then the actual code that will check for the rotation and increment counters if applicable.

Below is an extract from the data sheet for the ATMEGA 1280, 2560 chip:

PCICR – Pin Change Interrupt Control Register

• Bit 1 – PCIE1: Pin Change Interrupt Enable 1
When the PCIE1 bit is set (one) and the I-bit in the Status Register (SREG) is set (one), pin
change interrupt 1 is enabled. Any change on any enabled PCINT15:8 pin will cause an interrupt.
The corresponding interrupt of Pin Change Interrupt Request is executed from the PCI1
Interrupt Vector. PCINT15:8 pins are enabled individually by the PCMSK1 Register.

This is the code that enables interrupts on those pins:

void InitialiseInterrupt(){
  // switch interrupts off while messing with their settings 
  cli();   
  PCICR =0x01;          // Enable PCIE0: interrupt
  PCMSK0 = 0b00110000;  // Only want pins 10 and 11
  sei();  // turn interrupts back on
}

The service routine reads the state of the encoder pins and combines with the previous state to have an index to the array that contained the valid combinations.

Finally the code in the main loop that checks to see if a rotation occurred. Only a valid rotation will cause it to do something. I also have a counter, because in reality each detent positions generates 4 pulses, which can cause the menu to change to rapidly and hard to select.

Below is the full code:

// Code by Manuel Negri
// July 2014
// Released to the public domain! Enjoy!
#define encoder0PinA 10 // INT4  2
#define encoder0PinB 11 //INT5  3

boolean aa=true;
int prevRot =0;
int curRot =0;
int positionIndex =0;
int rotDirection=0;
int menuInc =0;
int menuDec=0;
int menuItem=0;

// keep track of the position, 
//1 is cw , -1 is ccw and 0 is invalid
int positionTrack[16]= {  
  0,-1, 1, 0,1,  0, 0, -1, -1, 0, 0,1,0,  0, -1, 1};

void setup() {

  pinMode(encoder0PinA, INPUT); 
  pinMode(encoder0PinB, INPUT); 
  pinMode(13,OUTPUT);
  
  // we just have to set, cause they are clear to start
  InitialiseInterrupt();

  if (digitalRead(encoder0PinA) == HIGH) { 
    bitSet(prevRot,1);
  }
  if (digitalRead(encoder0PinB) == HIGH) { 
    bitSet(prevRot,0);
  }

  Serial.begin (19200);
  Serial.println ("start");
}
void InitialiseInterrupt(){
  // switch interrupts off while messing with their settings 
  cli();   
  PCICR =0x01;          // Enable PCIE0: interrupt
  PCMSK0 = 0b00110000;  // Only want pins 10 and 11
  sei();  // turn interrupts back on
}

ISR(PCINT0_vect) {   
  // Interrupt service routine. 
  // Every single PCINT0..7 (=ADC0..5) change
  // will generate an interrupt. This will 
  // always call this method
  
  curRot=0;
  if (digitalRead(encoder0PinA) == HIGH) { 
    bitSet(curRot,1);
  }
  if (digitalRead(encoder0PinB) == HIGH) { 
    bitSet(curRot,0);
  }
  // he we combine  the bits for the previous
  // position with the current
  
  // move the prevRot to the upper nibble
  positionIndex = prevRot<<2 | curRot;

  // and depending on the combination , the rotDirection
  // variable will contain which way the encoder turned
  // or if it was an invalid code
  // with invalid combination, we do nothing.
  rotDirection =positionTrack[positionIndex];

  // The current postion now becomes the previous position
  prevRot = curRot;

}
void loop(){ //Do stuff here }
  // my main code actually checks every 20 msec, so that 
  // is why i have this delay
  delay(20);

  if (rotDirection != 0 ) { // only check if a valid direction
    if (rotDirection == 1 ){
      rotDirection=0;  // reset the direction
      menuInc++;
      // menuInc counts how many pulses where generated 
      // in this direction. Basically it is so we dont go
      // to fast when turning the knob.
      // 
      if (menuInc >=4){ 
        if ( menuItem==15) { // I have 16 menu items
          menuItem=0;        // this is to catch the overflow
        }
        else {
          menuItem++;        // no overflow, so increment
        }
        Serial.print  (menuItem);
        Serial.println ("  menu inc ");
        menuInc= 0;          // since we moved a position
        menuDec = 0;         // reset the two counters
      }
    } 
    else {
      rotDirection=0; // reset the direction
      menuDec--;
      // menuDec counts how many pulses where generated 
      // in this direction. Basically it is so we dont go
      // to fast when turning the knob.
      
      if (menuDec <=-4){
        if ( menuItem==0) { // prevent underflow
          menuItem=15;   
        }
        else {
          menuItem--;
        }
        Serial.print  (menuItem);
        Serial.println ("  menu dec ");
        menuInc= 0;          // since we moved a position
        menuDec = 0;         // reset the two counters
      }
    }
  }
}

No comments:

Post a Comment