Monday, June 1, 2009

Arduino Code for Rotary Encoder

(Update: I've improved the code. Take a look here. You have the choice to do h/w deboucing or s/w debouncing or both. The s/w debouncing code is just one line of code)

Reliable use of mechanical encoders requires two components: a hardware tweak and (of course) code. Although there are many sample code for rotary encoder, I feel the following is the simplest code for rotary encoders. This version is interrupt-based.

The interrupt routine simply detects direction of motion:
void decoder()
{
if (digitalRead(4) == digitalRead(2))
{
volUp = 1; //if encoder channels are the same, direction is CW
}
else
{
volDown = 1; //if they are not the same, direction is CCW
}
}
Here we connect terminal A of the rotary encoder to Arduino's digital pin 4 and terminal B to digitalpin 2. The third terminal of the encoder is connected to GND. In this configuration the rotary encoder pulses will be pulling the pins to GND. In this example, we are controlling volume so one direction is volume up and the other direction is volume down.

We attach the interrupt with:
attachInterrupt(0, decoder, CHANGE);
and we detects CHANGE of the pulse. We need to detect rising edge and falling edge of the pulse in order to utilize the full resolution of the rotary encoder.

During set up, we want to enable the pull-up resistors for pin 2 and pin 4 with:
pinMode(VOLUPPIN, INPUT); // Pin 2
digitalWrite(VOLUPPIN, HIGH); // Enable pull-up resistor

pinMode(VOLDOWNPIN, INPUT); // Pin 4
digitalWrite(VOLDOWNPIN, HIGH); // Enable pull-up resistor
Note: it doesn't matter which channel is connected to which pin to start with. You can always switch the connections if you expect the opposite result.

Then in a loop we detect when the flags are set and do accordingly:
while(volUp==1) // CW motion in the rotary encoder
{
volUp=0; // Reset the flag
// Do something
}

while(volDown==1) // CCW motion in rotary encoder
{
volDown=0; // clear the flag
// Do something else
}

13 comments:

  1. Great work as usual glt (Lazy engineer) - I came across this LCD - too late for this project but maybe for next/others http://ie.mouser.com/Search/ProductDetail.aspx?qs=sGAEpiMZZMt7dcPGmvnkBq9RSAvEJGmRspzq0zQda%252bA%3d

    It's 146 X 62.5 X 14.5 mm - quite a bit bigger than the standard

    ReplyDelete
  2. Actually, cheaper in Digikey http://search.digikey.com/scripts/DkSearch/dksus.dll?Detail&name=NHD-0420E2Z-NSW-BBW-ND

    ReplyDelete
  3. dweeb4, That is one BIG LCD. Won't fit in my case though :-). I'm happy with the 9x large numbers. Also will have to use parallel communication unless you get a serial adapter. Thanks for your comments!

    ReplyDelete
  4. Hello,
    I am the newest bee ever.
    I have a performance problem between arduino mega and an incremental encoder (WDG 58-10-2500-ABN-r05-L3) and am trying to optimize my arduino code.

    I have a problem with this code above. I am not able to understand which interrupt number is for which pins.

    I use 3 interrupts, for encoder A (interrupt 0, pin 2), 1 for encoder B (interrupt 1, pin 3)and for Index (interrupt 5, pin 18).I found this code below and try to modify it accordingly, can you give suggestions pls?

    Thanks!!


    #define encoder0PinA 2
    #define encoder0PinB 3
    #define encoder0Index 18


    volatile unsigned int encoder0Pos = 0;
    unsigned int tmp_Pos = 1;

    boolean doPrint = false;
    int valR = 0;

    void setup() {
    //int x;
    pinMode(encoder0PinA, INPUT);
    pinMode(encoder0PinB, INPUT);
    pinMode(encoder0Index, INPUT);
    pinMode(13, OUTPUT);


    // encoder pin on interrupt 0 (pin 2)
    attachInterrupt(0, doEncoderA, CHANGE);

    // encoder pin on interrupt 1 (pin 3)
    attachInterrupt(1, doEncoderB, CHANGE);

    // encoder pin on interrupt 5 (pin 18)
    attachInterrupt(5, doEncoderIndex, CHANGE);



    Serial.begin (9600);
    }
    void loop(){

    //Check each second for change in position

    if (tmp_Pos != encoder0Pos) {
    tmp_Pos = encoder0Pos;
    Serial.println(encoder0Pos, DEC);
    }

    if (doPrint == true) {
    Serial.print("R:");
    Serial.println(encoder0Pos, DEC);
    doPrint = false;
    }


    }


    // Interrupt on A changing state
    void doEncoderA(){

    // look for a low-to-high on channel A
    if (digitalRead(encoder0PinA) == HIGH) {

    // check channel B to see which way encoder is turning
    if (digitalRead(encoder0PinB) == LOW) {
    encoder0Pos += 1; // CW
    }
    else {
    encoder0Pos = encoder0Pos - 1; // CCW
    }
    }

    else // must be a high-to-low edge on channel A
    {
    // check channel B to see which way encoder is turning
    if (digitalRead(encoder0PinB) == HIGH) {
    encoder0Pos = encoder0Pos + 1; // CW
    }
    else {
    encoder0Pos = encoder0Pos - 1; // CCW
    }
    }

    }

    void doEncoderB(){

    // look for a low-to-high on channel B
    if (digitalRead(encoder0PinB) == HIGH) {

    // check channel A to see which way encoder is turning
    if (digitalRead(encoder0PinA) == HIGH) {
    encoder0Pos = encoder0Pos + 1; // CW
    }
    else {
    encoder0Pos = encoder0Pos - 1; // CCW
    }
    }

    // Look for a high-to-low on channel B

    else {
    // check channel B to see which way encoder is turning
    if (digitalRead(encoder0PinA) == LOW) {
    encoder0Pos = encoder0Pos + 1; // CW
    }
    else {
    encoder0Pos = encoder0Pos - 1; // CCW
    }
    }

    }

    void doEncoderIndex(){
    doPrint = true;
    valR = encoder0Pos;
    }

    ReplyDelete
  5. Hello newest bee ever,

    A couple of suggestions:
    1- You need to turn on the pull-up resistors with digitalWrite
    2- You can vastly simplify the code if you are only checking for turning direction on the rotary encoder (look at the code I wrote)
    Hope this helps.
    3- You can also post your question at the Arduino forums

    ReplyDelete
  6. I have found that the original code was totally unreliable. The use of interrupts without hardware debounce only makes a bad situation worse.

    So, I started to write the code using a fixed timing for taking regular samples. I thought that the millis() function should be the way to start, but found it to be unreliable. I did finally find a library MsTimer2 that works very well. I set it to generate a 1 millisecond sampling rate. Within the function flash() is the code to sample each encoder phase 12 times (12 successive 1 millisecond samples). I submit the following code, which satisfies my need for a reliable encoder input:

    #include
    #include

    int ledPin = 13; // LED connected to digital pin 13
    int ENCODE_A = 2;
    int ENCODE_B = 3;
    boolean CW = HIGH;
    int counter = 0;
    int oldCounter = 0;

    void flash()
    {
    static uint16_t stateA = 0; // Current debounce status A
    static uint16_t stateB = 0; // Current debounce status B
    stateA = (stateA<<1) | digitalRead(ENCODE_A);
    stateB = (stateB<<1) | digitalRead(ENCODE_B);
    // High to Low transitions
    if ((stateA == 0xf000) && (stateB == 0xffff)) counter++;
    if ((stateB == 0xf000) && (stateA == 0x0000)) counter++;
    // Low to High transitions
    if ((stateA == 0x0fff) && (stateB == 0x0000)) counter++;
    if ((stateB == 0x0fff) && (stateB == 0xffff)) counter++;

    // High to Low transitions
    if ((stateB == 0xf000) && (stateA == 0xffff)) counter--;
    if ((stateA == 0xf000) && (stateB == 0x0000)) counter--;
    // Low to High transitions
    if ((stateB == 0x0fff) && (stateA == 0x0000)) counter--;
    if ((stateA == 0x0fff) && (stateB == 0xffff)) counter--;
    }

    void setup()
    {
    // sets the digital pin as output
    pinMode(ledPin, OUTPUT);

    //During set up, we want to enable the pull-up resistors
    //for pin 2 and pin 4 with:

    pinMode(ENCODE_A, INPUT); // Pin 2
    digitalWrite(ENCODE_A, HIGH); // Enable pull-up resistor

    pinMode(ENCODE_B, INPUT); // Pin 3
    digitalWrite(ENCODE_B, HIGH); // Enable pull-up resistor

    MsTimer2::set(1, flash); // 1ms period
    MsTimer2::start();
    Serial.begin(9600);
    }

    void loop()
    {
    if (counter != oldCounter)
    {
    oldCounter = counter;
    Serial.println(counter,DEC);
    }
    }

    ReplyDelete
  7. Yes, without the capacitors added to the rotary encoder, the s/w I used is totally unreliable. However, the h/w mod is very simple and the code is very simple and does nothing if you are not turning the knob

    ReplyDelete
  8. #include
    #include

    int ledPin = 13; // LED connected to digital pin 13
    int ENCODE_A = 2;
    int ENCODE_B = 3;
    boolean CW = HIGH;
    int counter = 0;
    int oldCounter = 0;

    void flash()
    {
    static uint16_t stateA = 0; // Current debounce status A
    static uint16_t stateB = 0; // Current debounce status B
    stateA = (stateA<<1) | digitalRead(ENCODE_A);
    stateB = (stateB<<1) | digitalRead(ENCODE_B);
    // High to Low transitions
    if ((stateA == 0xf000) && (stateB == 0xffff)) counter++;
    if ((stateB == 0xf000) && (stateA == 0x0000)) counter++;
    // Low to High transitions
    if ((stateA == 0x0fff) && (stateB == 0x0000)) counter++;
    if ((stateB == 0x0fff) && (stateB == 0xffff)) counter++;

    // High to Low transitions
    if ((stateB == 0xf000) && (stateA == 0xffff)) counter--;
    if ((stateA == 0xf000) && (stateB == 0x0000)) counter--;
    // Low to High transitions
    if ((stateB == 0x0fff) && (stateA == 0x0000)) counter--;
    if ((stateA == 0x0fff) && (stateB == 0xffff)) counter--;
    }

    void setup()
    {
    // sets the digital pin as output
    pinMode(ledPin, OUTPUT);

    //During set up, we want to enable the pull-up resistors
    //for pin 2 and pin 4 with:

    pinMode(ENCODE_A, INPUT); // Pin 2
    digitalWrite(ENCODE_A, HIGH); // Enable pull-up resistor

    pinMode(ENCODE_B, INPUT); // Pin 3
    digitalWrite(ENCODE_B, HIGH); // Enable pull-up resistor

    MsTimer2::set(1, flash); // 1ms period
    MsTimer2::start();
    Serial.begin(9600);
    }

    void loop()
    {
    if (counter != oldCounter)
    {
    oldCounter = counter;
    Serial.println(counter,DEC);
    }
    }

    Hi for this code , why did you leave the two prepocessor directives blank? sorry im quite new for arduino , so what are those supposed to be ?

    ReplyDelete
  9. Would please tell me what rotary encoder you are using? ...and thanks alot for posting all the info.

    Greatly appreciated...

    Jim

    ReplyDelete
  10. hi , here is the encoder that i'm using : http://www.robotshop.ca/5v-mechanical-quadrature-encoder.html

    ReplyDelete
  11. The one used in the project documented in this blog is this: $.99 panasonic: http://www.goldmine-elec-products.com/prodinfo.asp?number=G16267

    But in order to make it work with my code you need to solder some capacitors like this: http://hifiduino.blogspot.com/2009/05/blog-post_06.html

    Hope this helps.

    ReplyDelete
  12. dude... THIS IS EXACTLY what i needed for my project!!!

    works perfect, easy to implement and understand... THANK YOU!

    I too had issues with debounce code..so i used your cap. solution... worked like a charm!!

    ReplyDelete
  13. Learned Nerd:
    I am happy it worked for you. My s/w skills are just not good enough to implement a s/w debouncer. But I think a h/w-s/w combination is a more elegant solution

    ReplyDelete