Shopping Cart

Posts

Reading rotary encoder on Arduino

Rotary encoder connected to Arduino

Rotary encoder connected to Arduino


Quadrature rotary encoders, also known as rotary pulse generators, are popular input devices for embedded platforms, including Arduino. Several rotary encoder code examples are posted on Arduino site and elsewhere, however, they treat encoder as a pair of switches, adding decoding/debouncing overhead. For many years, I used an algorithm based on the fact that quadrature encoder is a Gray code generator and if treated as such, can be read reliably in 3 straight step without need for debouncing. As a result, the code I’m using is very fast and simple, works very well with cheap low-quality encoders, but is somewhat cryptic and difficult to understand. Soon after posting one of my projects where I used rotary encoder to set motor speed i started receiving e-mails asking to explain the code. This article is a summary of my replies – I’m presenting small example written for the purpose of illustrating my method. I’m also going through the code highlighting important parts.

The hardware setup can be seen on title picture. The encoder from Sparkfun is connected to a vintage Atmega168-based Arduino Pro. Common pin of the encoder is connected to ground, pins A and B are connected to pins 14 and 15, AKA Analog pins 0 and 1, configured as digital inputs. We also need a serial connection to the PC to receive power, program the Arduino, and send program output to the terminal. For this purpose, Sparkfun FTDI basic breakout is used.

Connecting encoder pins to pins 0 and 1 of 8-bit MCU port makes encoder reading code very simple. If analog pins are needed for something else, it is possible to move encoder to digital pins 8,9 or 0,1 (losing serial port) with no modification of code logic. While technically using any two consecutive port pins is possible with a bit of tweaking, using non-consecutive pins for encoder input with this method is not recommended. Lastly, it sometimes hard to determine which encoder pin is A and which is B; it is easier to connect them at random and if direction is wrong, swap the pins.

Example code is posted below. It is complete sketch – you can copy it from this page and paste into Arduino IDE window, compile, upload and run. The result of rotating the encoder can be seen in terminal window. Note that serial speed in the sketch is set to 115200, you will need to set your PC terminal to that speed as well. The explanation of the code is given after the listing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* Rotary encoder read example */
#define ENC_A 14
#define ENC_B 15
#define ENC_PORT PINC
 
void setup()
{
  /* Setup encoder pins as inputs */
  pinMode(ENC_A, INPUT);
  digitalWrite(ENC_A, HIGH);
  pinMode(ENC_B, INPUT);
  digitalWrite(ENC_B, HIGH);
  Serial.begin (115200);
  Serial.println("Start");
}
 
void loop()
{
 static uint8_t counter = 0;      //this variable will be changed by encoder input
 int8_t tmpdata;
 /**/
  tmpdata = read_encoder();
  if( tmpdata ) {
    Serial.print("Counter value: ");
    Serial.println(counter, DEC);
    counter += tmpdata;
  }
}
 
/* returns change in encoder state (-1,0,1) */
int8_t read_encoder()
{
  static int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
  static uint8_t old_AB = 0;
  /**/
  old_AB <<= 2;                   //remember previous state
  old_AB |= ( ENC_PORT & 0x03 );  //add current state
  return ( enc_states[( old_AB & 0x0f )]);
}

First three #defines set names for port pins. This way, if you want to use different pins for your encoder, you can do it here without sifting through code looking for pin names. If you’d like to move encoder pins to Arduino pins 8 and 9, ENC_PORT shall be defined as PINB, and for pins 0,1 as PIND.

In setup() function hardware is initialized. pinMode sets the pin as input so we can read it and digitalWrite turns on pull-up resistor on the pin so it won’t dangle in the air when switch is open. Finally, the serial port is initialized and message is printed as an indicator that port is working – if you can’t see “Start” on your terminal screen, check PC port settings.

Let’s go down to line 30, where read_encoder() function starts. enc_states[] array is a look-up table; it is pre-filled with encoder states, with “-1″ or “1″ being valid states and “0″ being invalid. We know that there can be only two valid combination of previous and current readings of the encoder – one for the step in a clockwise direction, another one for counterclockwise. Anything else, whether it’s encoder that didn’t move between reads or an incorrect combination due to switch bouncing, is reported as zero.

Note that old_AB is declared static. This means that it’s value will be retained between function calls therefore previous encoder reading will be preserved. When function is called, old_AB gets shifted left two times (line 36) saving previous reading and setting two lower bits to “0″ so the current reading can be correctly ORed here. Then ENC_PORT & 0×03 reads the port to which encoder is connected and sets all but two lower bits to zero so when you OR it with old_AB bits 2-7 would stay intact. Then it gets ORed with old_AB (line 37, old_AB |= ( ENC_PORT & 0×03 )). At this point, we have previous reading of encoder pins in bits 2,3 of old_AB, current readings in bits 0,1, and together they form index of (AKA pointer to) enc_states[] array element containing current state – either increment, decrement, or no change. All that is left to do is return this state to the calling function, and line 38 does just that. Upper half of old_AB gets zeroed by old_AB & 0x0f – if we don’t do this, we will be reading memory past enc_states[] array.

The above paragraph was pretty long explanation for mere 5 lines of code (counting two declarations); if you are reading this, 98% of work is done. The rest of the code is very easy. We are now moving up to line 17, where loop() function is residing. counter variable is the one which is modified by encoder and tmpdata is used to hold the reading. On line 22, encoder is read. If encoder state has changed, i.e., read_encoder() returned non-zero, current value of counter variable is printed and then counter is updated with tmpdata. This is done within conditional statement on lines 22-27. The loop then ends and starts again.

Now let’s briefly talk about real-life applications of this method. If you’re using Sparkfun encoder, you’ll notice that it gives 4 increments per click and it’s impossible to make it hold position between clicks. Therefore, to count clicks you will need to divide counter variable by 4. Also, if counter larger than 255 is necessary, the declaration for it would have to be changed to uint16_t or longer. Another nice modification, left out to keep code simple, would be to move enc_states[] array to program memory using PROGMEM type saving several bytes of precious RAM.

Rotary encoder singing out of tune

Rotary encoder singing out of tune


In order for this method to work well, read_encoder() function needs to be called fairly often . To see what happens when loop is slow, lower serial port speed – go to line 13, change “115200″ to “9600″, recompile the sketch and run (don’t forget to reconfigure the terminal on PC side). Picture on the left shows the result. Note that encoder goes down from 237 to 229, then jumps up to 230, then continues going down. Sometimes it counts correctly but gives 2 or 3 states per click instead of 4. Instabilities like this are good indication of slow loop. If encoder is used to things like setting motor speed or sound volume, this behavior won’t do much harm because encoder will never skip back more than one state and overall counting would still be in the right direction. However, if encoder is used for interfacing with display, glitches like that are very annoying. Therefore, for user interface applications, loop time should be kept short and in some cases it may be even necessary to convert encoder reading function into interrupt service routine. Given the size and speed of encoder read function, such conversion can be done without much difficulty.

To summarize: I presented a method to convert quadrature rotary encoder output into direction information using look-up table. The method makes debouncing unnecessary, and as a result, conversion function implemented with this method is very short, fast, and works very well on small 8-bit MCU platforms, such as Arduino.

Oleg.
[EDIT] I posted a new article describing reading an encoder from an ISR.

Related posts:

  1. Vigorius stirring redefined. Part 2 – electronics

114 comments to Reading rotary encoder on Arduino

  • I’m confused by the use of ENC_PORT, and new to Arduino. I also have an UNO so that may not be helping. I assume that ENC_PORT looks at the state of the pins without the use of digitalRead(). would the following be about correct for line 37?

    old_AB |= (digitalRead(ENC_A) * 2) + dititalRead(ENC_B);

    p.s. my issue is that I may want to use 2 pins that are not next to each other, or actually I don’t like to update 3 variables if I change something when I can get away with only changing 2 :)

    If I can get this working I can get rid of 3 switches and 3 resistors as well as solving my debounce and long hold issues :D

    Thanks

  • Ok, I can’t get this working and I’m a lot stuck. My understanding is that the pins should cycle… for example…

    00, 01, 11, 10 –> and repeat going one way

    and

    00, 10, 11, 01 –> and repeat going the other way.

    I understand about digitalRead being bad for for testing I’d expect it to be ok. I have the same physical setup as you and this sketch.

    #define LEDPIN 13
    #define ROTPIN_A 14
    #define ROTPIN_B 15
    
    void setup()
    {
      pinMode(ROTPIN_A, INPUT);
      digitalWrite(ROTPIN_A, HIGH);
    
      pinMode(ROTPIN_B, INPUT);
      digitalWrite(ROTPIN_B, HIGH);
    
      pinMode(LEDPIN, OUTPUT);
    
      Serial.begin (9600);
      Serial.println("Start");
    }
    
    void loop()
    {
      Serial.print("pin state: ");
      Serial.print(digitalRead(ROTPIN_A));
      Serial.print(digitalRead(ROTPIN_B));
      Serial.print("\n");
    
      digitalWrite(LEDPIN, HIGH);
      delay(10);
      digitalWrite(LEDPIN, LOW);
      delay(10);
    
      delay(100); // Make it visible
    }
    

    The issue is that it output 11 a whole lot. If I rotate the spindle one click, most of the time I get 10, and then back to 11. If I go the other way I get 01 briefly, then 11 again. Sometimes if I go a bit nuts I have managed to get 00. This behaviour is the same if I print PINC & 0×03 with 1, 2 and 3s being output.

    Is there anything else I can look at? Is my Encoder busted? Is my brain busted? :)

    Thanks.

    • Many encoders go through 4 states between clicks. Some of them read 00 at detent, others (like yours) read 11. Move your encoder between clicks slowly and you shall see all four states.

      BTW, this is the purpose of initialization on line 34 of the example – for your encoder you need to set old_AB to 0×03 if you want it to work correctly at power-up.

  • Hello, Me again :) I found the exact same encoder as you, the sparkfun one, and its much better.. Other encoders don’t seem to have the same workings. Another thing I found – 1 “click” on the rotor increments the counter by 4 because the code detects each change in both pulses, so I use a long counter (for negative values) and then round(counter/4.0) in the output. Now awesome… now to work out the interrupts and I’m done :)

    One other question I have – why do you not have any resistors on the pins? Are they not needed? There is nothing in the datasheet that mentions internal loads.

    Thanks
    Nigel

  • Kevin Karagitz

    So I’m having some issues.. not sure if it’s hardware or an issue with the code. I’m using the following encoder.
    http://www.sparkfun.com/products/9117

    When I use the code provided above I’m always getting 0,1 0,1 in the output through serial. This happens regardless of the direction I turn the encoder.

    Kevin

  • Kevin Karagitz

    Thanks for the reply.. Really appreciate it.

  • Paul Marvell

    - Oleg

    Hi, I am currently trying to get two sparkfun rotary encoders to work side by side on the analogue pins. But I’m not entirely sure how to go about it. Im relatively new to Arduino programing (but have used processing to make a clock, timer and a simple space invaders game) as struggle with pins etc.
    I would prefer to use A0,A1,A4&A5 if possible.
    Thanks

    • The pins look good. I’d start with making first one work, this would give you good understanding of the algorithm. Then make a variant with second set of pins. Then combine both into one program.

      • Paul Marvell

        Thanks for the quick reply.
        I was able to get A0 and A1 working correctly. But have got the problem where changing just the following (to work on A4 & A5), outputs only dec 0 and dec 1.
        2// #define ENC_A 14 >> #define ENC_A 18
        3// #define ENC_B 15 >> #define ENC_A 19

        So I changed the end of the code as well to;
        37// old_AB |= ( ENC_PORT & 0×03 ); (binary 11000000 L2R)
        >> old_AB |= ( ENC_PORT & 0×30 ); (binary 00001100 L2R)
        38// return ( enc_states[( old_AB & 0x0f )]); (binary 11110000 L2R)
        >> return ( enc_states[( old_AB & 0xf0 )]); (binary 00001111 L2R)

        but the serial monitor just spits out random numbers.
        Am I changing too much or too little.
        (I am still just trying to get 1 encoder to work)
        Thanks

        • You move encoder 4 bits up and you move the mask only two bits up. I can’t understand the reason behind second modification. Additionally, since now your table index is contained in 4 high bits of old_AB, you need to shift it 4 bits down.

          • Paul Marvell

            I only half understand what you mean.
            Im moving the encoder from bits 0 & 1 to 4 & 5
            I thought I was moving the mask from bits 0 & 1 (0×03) to 4 & 5 (0×30) or am I wrong?
            Do I not need the 2nd modification (line 38) ?
            Where do I need to shift the table?

            Can you show me the changes I need to make

            Thanks

          • You’re correct, I overlooked it. Don’t number bits from left to right, do it like if you were expanding hex, i.e, 0×41 = (0100)(0001).

            old_AB for the first encoder uses two lower bits 0000 00xx, when you shift it to produce the index, you use next two bits, i.e. 0000 xxxx. The same is true for the second encoder, but at this time you’re using upper nibble – 00xx 0000, xxxx 0000 – this is too large for an index unless you want to expand the lookup table to 256 members and only use first and last 16. This is what you’re trying to do in line 38, however, since your lookup table has only 16 members, you’re reading past it picking up some random bytes from the memory.

            Instead, if after shifting it to the left you shift it again to the right 4 times, the index will be small again and work exactly like one from the article. BTW, encoders must use separate old_ABs.

  • Paul Marvell

    I noticed my mistake in the original post. I wrote it out how I was looking at the arduino board (hence the L2R).
    so the code will be
    old_AB <>= 4

    or can I write it like this
    old_AB >>= 2 <<= 4

    • You need to preserve old_AB for the next reading, so something like this is necessary:

      old_AB <<= 2;
      old_AB |= ( ENC_PORT & 0x30 );  //add current state
      uint8_t new_AB = old_AB >> 4;
      return ( enc_states[( new_AB & 0x0f )]);
  • Paul Marvell

    -Oleg,
    Thanks for your help. Moving it right 4 times worked a treat, and now have both encoders working along side each other.

  • KenH

    Hello Oleg – that is nice code you’ve got there and over my head, BUT it does run nicely for me. My encoder counts up and down just as it should. I get one increment per click – not sure what encoder I’ve got, just one in box.

    I desire to use the encoder to enter diameter in a project I’m working on. I would like to enter from 0.1 to 10 (this will be inches of diameter to be used in a calculation. My encoder with your code reads from 0 to 255, then starts over. Is there any way I could get this to read from 1 to 100 only? I could divide that by 10 to have my .1 to 10 value for calculation. I would like to do this without using interrupts – I have a Atmega168 and I understand it only has 2 interrupts? Those I’m hoping to use to read an IR sensor for a Tachometer.

    Thank you for any help,
    Ken H>

  • Rich

    KenH – To have it count from 1 to 100 you would initialize counter to 1 in line 19. Then every time through the loop as counter is updated in line 26, check the value to see if it is > 100. If so, then set it to 1 again, etc. as shown below.

    19 static uint8_t counter = 1; //this variable will be changed by encoder input

    26 counter += tmpdata;
    if( counter > 100 ){
    counter = 1;
    }
    else if( counter < 0 ){
    counter += 100;
    }

  • KenH

    Thank you Rich – I’ll try that- looks good to me, but I’m very much a novice at this stuff.

    Thank you again,

    Ken H>

  • Mehos

    Just yesterday I had to solve this problem and my method is Finite State Machine. We have a set of states and transitions between them.

    Square – set of input data, circle – states, double circle – recognized state.

    My code:

    #include

    int sv1,sv2,q = 0;

    LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

    void setup()
    {
    lcd.begin(16, 2);
    pinMode(9, INPUT);
    pinMode(8, INPUT);
    }

    void doL() {
    lcd.scrollDisplayLeft();
    }
    void doR() {
    lcd.scrollDisplayRight();
    }

    void loop()
    {

    // read the value from the sensor:
    sv1 = digitalRead(9);
    sv2 = digitalRead(8);

    if ( q == 0 ) {
    if( sv1 == 0 & sv2 == 0 ) q = 1;
    if( sv1 == 1 & sv2 == 1 ) q = 7;
    }

    if ( q == 1 & sv1 == 0 & sv2 == 0 ) { q = 1; }
    else if ( q == 1 & sv1 == 0 & sv2 == 1 ) { q = 2; }
    else if ( q == 1 & sv1 == 1 & sv2 == 0 ) { q = 3; }
    else if ( q == 2 & sv1 == 0 & sv2 == 0 ) { q = 1; }
    else if ( q == 2 & sv1 == 0 & sv2 == 1 ) { q = 2; }
    else if ( q == 2 & sv1 == 1 & sv2 == 1 ) { q = 4; }
    else if ( q == 3 & sv1 == 0 & sv2 == 0 ) { q = 1; }
    else if ( q == 3 & sv1 == 1 & sv2 == 0 ) { q = 3; }
    else if ( q == 3 & sv1 == 1 & sv2 == 1 ) { q = 5; }
    else if ( q == 6 & sv1 == 1 & sv2 == 1 ) { q = 6; }
    else if ( q == 6 & sv1 == 1 & sv2 == 0 ) { q = 7; }
    else if ( q == 6 & sv1 == 0 & sv2 == 1 ) { q = 8; }
    else if ( q == 7 & sv1 == 0 & sv2 == 0 ) { q = 9; }
    else if ( q == 7 & sv1 == 1 & sv2 == 0 ) { q = 7; }
    else if ( q == 7 & sv1 == 1 & sv2 == 1 ) { q = 6; }
    else if ( q == 8 & sv1 == 0 & sv2 == 0 ) { q = 10; }
    else if ( q == 8 & sv1 == 0 & sv2 == 1 ) { q = 8; }
    else if ( q == 8 & sv1 == 1 & sv2 == 1 ) { q = 6; }

    if ( q == 4) { doR(); q = 7; }
    else if ( q == 5) { doL(); q = 7; }
    else if ( q == 9) { doR(); q = 1; }
    else if ( q == 10) { doL(); q = 1; }
    lcd.setCursor(0, 0);
    lcd.print("X");
    }

    It more longer but take 2902 bytes of memory, uses fewer variables, not need to remember the previous states.

    PS sorry for my English:)

  • owar

    Hi, I have question. But first I need to thank you for posting this helpful code!

    I have setup of Arduino Mega with sample code from Sparkfun(http://dlnmh9ip6v2uc.cloudfront.net/datasheets/Components/Switches/Rotary_Encoder_LED_Ring_Example.pde) and rotary encoder with LED ring.

    On Mega it seems that only A0&A1 on port F pins working for recieve signal from rotrary.Why ?
    And second question is how can I connect two encoders to it?

  • Kayla and Isaac

    Will the program work for an encoder plugged into pins 15 and 16 (analog pins 2 and 3)? We tried plugging it into these pins and it wasn’t working.

    Our ultimate project requires us to use 3 encoders. At first, we are trying to make it work for 2 encoders. This is the program we wrote up. I assume the first problem is that the pins 15 and 16 aren’t working properly with the program. Is there anything else that we must fix?

    Here’s our program for 2 encoders:

    #define ENC1_A 14
    #define ENC1_B 15
    #define ENC2_A 16
    #define ENC2_B 17
    #define ENC_PORT PINC

    void setup()
    {

    pinMode(ENC1_A, INPUT);
    digitalWrite(ENC1_A, HIGH);
    pinMode(ENC1_B, INPUT);
    digitalWrite(ENC1_B, HIGH);

    pinMode(ENC2_A, INPUT);
    digitalWrite(ENC2_A, HIGH);
    pinMode(ENC2_B, INPUT);
    digitalWrite(ENC2_B, HIGH);

    Serial.begin (9600);
    Serial.println(“Start”);
    }

    void loop()
    {
    static unsigned int counter1 = 0;
    int tmpdata1 = read_encoder1();
    if( tmpdata1 ) {
    Serial.print(“Counter value of Encoder 1: “);
    Serial.println(counter1, DEC);
    counter1 += tmpdata1;
    }

    static unsigned int counter2 = 0;
    int tmpdata2 = read_encoder2();
    if( tmpdata2 ) {
    Serial.print(“Counter value of Encoder 2: “);
    Serial.println(counter2, DEC);
    counter2 += tmpdata2;
    }
    }

    const int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};

    /* returns change in encoder state (-1,0,1) */
    int read_encoder1()
    {
    static uint8_t old_AB = 0;
    old_AB <<= 2; //remember previous state
    old_AB |= ( ENC_PORT & 0×03 ); //add current state
    return ( enc_states[( old_AB & 0x0f )]);
    }

    /* returns change in encoder state (-1,0,1) */
    int read_encoder2()
    {
    static uint8_t old_AB = 0;
    old_AB <<= 2; //remember previous state
    old_AB |= ( ENC_PORT & 0×03 ); //add current state
    return ( enc_states[( old_AB & 0x0f )]);
    }

  • Great Code. I am very new to both the arduino and the rotary encoder. Your code is working great for me, but I would like to set an alarm if the encoder stops turning. I set pinMode on 13 for output and I wrote this if statement:

    if ( old_AB == uint8_t )
    delay(5000);
    digitalWrite(13, HIGH);

    This is not working for me. Could you explain what I am doing wrong? Any help would be greatly appreciated. Thank you.

Leave a Reply

  

  

  

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">