Pages: [1] 2   Go Down
Author Topic: Rotary encoders and interrupts  (Read 9308 times)
0 Members and 1 Guest are viewing this topic.
Global Moderator
Offline Offline
Brattain Member
*****
Karma: 480
Posts: 18732
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

I have been reading on the forum about rotary encoders recently, and just assumed that they looked like this:



After all, that is a rotary-dial phone, and presumably as you dial the numbers are encoded.

However I now realize that people are probably talking about these things:



These are rotary switches, which (unlike potentiometers) are not analogue, but digital. As you turn the knob pulses are generated by switching the center (C) pin to either of the outer pins (A and B) in such a way that you can tell which way it is being turned.

To test this, I wired up the switch like this:



It was very simple, here is a photo of it being connected to an Arduino Uno:



In the photo a couple of diodes are visible. This was part of a scheme to try to use only one interrupt, but it didn't work perfectly.

Now the center (common, or C) pin is grounded. The outer two pins are connected to pins 2 and 3 of the Arduino, which are then pulled high by setting pull-up resistors on them. In the code that is done like this:

Code:
 digitalWrite (2, HIGH);
  digitalWrite (3, HIGH);

To detect when the knob is being turned we set up an interrupt handler which fires whenever either pin changes:

Code:
 attachInterrupt (0, isr, CHANGE);   // pin 2
  attachInterrupt (1, isr, CHANGE);   // pin 3

A bit of investigation shows that whenever you turn the encoder by one click you get one of these state changes (where H is high and L is low):

Code:
Forward direction: LH then HH, or HL then LL
Reverse direction: HL then HH, or LH then LL

So we can see that both pins the same (HH or LL) is the "resting" position. So we need to remember what preceded them. Thus we remember the previous switch state, and then when we get HH or LL, we look at what we had last time to see which way the switch must have been turned.

An extra test was needed to cater for switch bounce (sometimes we got HH followed by HH). I also added code to alter the "increment" amount. The idea was that if a human is turning the knob, if he or she turns it faster, then we increment by more than one. That is so if you want a big change, you turn the knob quickly, and if you want a small change, you turn it slowly.

The finished test program looks like this:

Code:
// Rotary encoder example.
// Author: Nick Gammon
// Date:   24th May 2011

// Wiring: Connect common pin of encoder to ground.
// Connect pins A and B (the outer ones) to pins 2 and 3 (which can generate interrupts)

volatile boolean fired = false;
volatile long rotaryCount = 0;

// Interrupt Service Routine
void isr ()
{
  
static boolean ready;
static unsigned long lastFiredTime;
static byte pinA, pinB;  

// wait for main program to process it
  if (fired)
    return;
    
  byte newPinA = digitalRead (2);
  byte newPinB = digitalRead (3);
  
  // Forward is: LH/HH or HL/LL
  // Reverse is: HL/HH or LH/LL
  
  // so we only record a turn on both the same (HH or LL)
  
  if (newPinA == newPinB)
    {
    if (ready)
      {
      long increment = 1;
        
      // if they turn the encoder faster, make the count go up more
      // (use for humans, not for measuring ticks on a machine)
      unsigned long now = millis ();
      unsigned long interval = now - lastFiredTime;
      lastFiredTime = now;
      
      if (interval < 10)
        increment = 5;
      else if (interval < 20)
        increment = 3;
      else if (interval < 50)
        increment = 2;
        
      if (newPinA == HIGH)  // must be HH now
        {
        if (pinA == LOW)
          rotaryCount += increment;
        else
          rotaryCount -= increment;
        }
      else
        {                  // must be LL now
        if (pinA == LOW)  
          rotaryCount -= increment;
        else
          rotaryCount += increment;        
        }
      fired = true;
      ready = false;
      }  // end of being ready
    }  // end of completed click
  else
    ready = true;
    
  pinA = newPinA;
  pinB = newPinB;
}  // end of isr


void setup ()
{
  digitalWrite (2, HIGH);   // activate pull-up resistors
  digitalWrite (3, HIGH);
  
  attachInterrupt (0, isr, CHANGE);   // pin 2
  attachInterrupt (1, isr, CHANGE);   // pin 3

  Serial.begin (115200);
}  // end of setup

void loop ()
{

  if (fired)
    {
    Serial.print ("Count = ");  
    Serial.println (rotaryCount);
    fired = false;
  }  // end if fired

}  // end of loop

(edit) See below for improved version that only requires a single interrupt and is shorter.
« Last Edit: May 24, 2011, 11:52:13 pm by Nick Gammon » Logged


North Yorkshire, UK
Offline Offline
Faraday Member
**
Karma: 104
Posts: 5531
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
I have been reading on the forum about rotary encoders recently, and just assumed that they looked like this:
Haha made me smile smiley
Logged

New Hampshire
Offline Offline
God Member
*****
Karma: 17
Posts: 781
There are 10 kinds of people, those who know binary, and those who don't.
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Since you are effectively only using half the resolution of your quadrature encoder, you can significantly simplify your code.

1.  You only need to setup your interrupt on one of the pins, eg pinA.

Then HH = increment count.
HL = decrement count.
LH = increment count.
LL = decrement count.

No need to remember previous state information.  You have the same resolution, simpler code.
Logged


Värmland, Sweden
Offline Offline
Sr. Member
****
Karma: 9
Posts: 262
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Since you are effectively only using half the resolution of your quadrature encoder, you can significantly simplify your code.

1.  You only need to setup your interrupt on one of the pins, eg pinA.

Then HH = increment count.
HL = decrement count.
LH = increment count.
LL = decrement count.

No need to remember previous state information.  You have the same resolution, simpler code.

I agree that the code is much simpler when using only one interrupt but what kind of trigger do you use for your interrupt when you use those rules?
When I've used rotary encoders with only one interrupt I've triggered on CHANGE on either of the pins and the rules have been
HH or LL = one direction
HL or LH = the other direction

so something like this has worked fine for me when attaching an interrupt to pinA, change:
Code:
if (pinA != pinB)
{
      //clockwise
}
else
{
      //counterclockwise
}
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 480
Posts: 18732
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Since you are effectively only using half the resolution of your quadrature encoder, you can significantly simplify your code.

I'm not totally sure what you mean by using half the resolution. The two state changes (eg. LH then HH) represent a single "click" of the knob against the detent. It isn't possible to leave the knob between detent positions. Thus I only "recorded" a change after two state changes. Otherwise (say if it was a thermostat) you could only set it to 20, 22, 24 etc. and not the positions inbetween.

When I've used rotary encoders with only one interrupt I've triggered on CHANGE on either of the pins and the rules have been
HH or LL = one direction
HL or LH = the other direction

Yes, well I initially had one interrupt, and I agree that in the cases you quote you probably don't need both pins. Except, and this is a big except, if you change directions. My testing showed, and you can see why, if you change directions the first move in the opposite direction is incorrectly recorded. Here, say we look at Pin A:

Code:
H -> L -> H -> L  (forwards)
H -> H -> L -> L  (reverse)

Now consider what happens if we reverse in the middle:

Code:
(forwards) H -> L -> H -> L  (reverse) H -> H -> L -> L

The first transition in the reverse direction is still L -> H which looks like a forwards movement. So we add one to the counter, when we should subtract one. And if you are tweaking your thermostat, you might dial up (say) 24 degrees, decide that is too high, and turn it one click to the left, but it shows 25! Not good enough. That's why I said above:

Quote
This was part of a scheme to try to use only one interrupt, but it didn't work perfectly.

(edit) I agree however that if the encoder is specified to move in one direction only, then the simplified code is much better, it uses less pins, less interrupts, and is cleaner.
Logged


New Hampshire
Offline Offline
God Member
*****
Karma: 17
Posts: 781
There are 10 kinds of people, those who know binary, and those who don't.
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Trigger the interrupt on PinA change only.

In the interrupt, read both pinA and pinB.


if pinA is High, you know you've just had a Low to High transition on pinA.
If pinB is also High, you know this transition was in the positive direction.
If pinB is Low, you know this transition was in the negative direction.

If pinA is Low, you know you've just had a High to Low transition on pinA.
If pinB is also Low, you again know this transition was in the positive direction.
If pinB is High, you know this transition was in the negative direction.

Changing direction isn't a problem.  Let's say you receive a Low to High interrupt on pinA.  pinA is in a High state.  You read pinB, it's also in a high state, so you know you are moving in a positive direction.  Now you reverse direction and the next interrupt is a High to Low transition.  Now pinA is in a Low state when you read it (which means you had a High to Low transition), but pinB is still in a High state.  As per the rules I listed above, this indicates you are moving in the negative direction.  No problem changing direction.
Logged


Global Moderator
Offline Offline
Brattain Member
*****
Karma: 480
Posts: 18732
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

With a bit of tweaking of my design, I got your suggestions to work perfectly, and it is indeed shorter and simpler. Thanks for that!

Code:
// Rotary encoder example.
// Author: Nick Gammon
// Date:   25th May 2011

// Thanks for jraskell for helpful suggestions.

// Wiring: Connect common pin of encoder to ground.
// Connect pin A (one of the outer ones) to a pin that can generate interrupts (eg. D2)
// Connect pin B (the other outer one) to another free pin (eg. D5)

volatile boolean fired;
volatile boolean up;

#define PINA 2
#define PINB 3
#define INTERRUPT 0  // that is, pin 2

// Interrupt Service Routine for a change to encoder pin A
void isr ()
{
  if (digitalRead (PINA))
    up = digitalRead (PINB);
  else
    up = !digitalRead (PINB);
  fired = true;
}  // end of isr


void setup ()
{
  digitalWrite (PINA, HIGH);     // enable pull-ups
  digitalWrite (PINB, HIGH);
  attachInterrupt (INTERRUPT, isr, CHANGE);   // interrupt 0 is pin 2, interrupt 1 is pin 3

  Serial.begin (115200);
}  // end of setup

void loop ()
{
static long rotaryCount = 0;

  if (fired)
    {
    if (up)
      rotaryCount++;
    else
      rotaryCount--;
    fired = false;
        
    Serial.print ("Count = ");  
    Serial.println (rotaryCount);
    }  // end if fired

}  // end of loop

This version only requires one interrupt, which frees up the other one for some other use (eg. a second rotary encoder). The pins are now defines so you can easily change where you connect the encoder.
« Last Edit: May 24, 2011, 11:54:14 pm by Nick Gammon » Logged


New Hampshire
Offline Offline
God Member
*****
Karma: 17
Posts: 781
There are 10 kinds of people, those who know binary, and those who don't.
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Nicely done Nick.  Glad I could help out.
Logged


Offline Offline
Edison Member
*
Karma: 3
Posts: 1001
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Nice tutorial!

One issue with "cheap" rotary encoders is contact bounce and in this case the approach suggested above may have issues. Thanks to the protocol (Manchester encoding) however it is possible to effectively eliminate all noise.

A rotary encoder has two signal pins (A and B) and so we have four possible states (0 0, 0 1, 1 0, and 1 1). In addition to this we must track previous state (additional two bits required) and so we get four bits in total. These four bits represent all of 16 possible transitions that are either invalid (due to contact bounce) or represent a valid increment/decrement. If we account for all 16 states in software, contact bounce will no longer be an issue. Fortunately this is also quite easy to implement in software as we can combine previous state and current state (four bits total) into a value in the 0 to 15 range. This value can then be used as an index into a table holding values 1 for increment, -1 for decrement and zero for invalid state changes.

This approach will also account for intermediate steps (half-way points) and work equally well with smooth or notched encoders.

An implementation of this approach is available from the following link:

http://www.circuitsathome.com/mcu/programming/reading-rotary-encoder-on-arduino

This implementation is polled, but can easily be modified to use a single pin-change interrupt if so desired.
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 480
Posts: 18732
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Ah, a state machine. Very nice.

I think I'll leave my sketch above as "one possible way" for now. The state machine sounds like it is somewhat more resistant to errors, however for the switch I had to hand the improved sketch worked pretty well.

The initial state machine sketch had the drawback that it was polled, which may or may not be suitable for every application. It linked to another sketch which uses interrupts, however then that requires two interrupts, like my earlier sketch (although it would be more resistant to "bad states").

However I have picked up on the idea of the hardware debouncing, and amended the schematic above accordingly (you may need to refresh the page to see it).

It seems to me that a combination of one interrupt, hardware debouncing, and simplicity, may well meet the needs of at least some users.

I should also point out here that if you require multiple encoders (and thus multiple interrupts) a port expander like the MCP23017 (link below to example) could fit the bill.

http://www.gammon.com.au/forum/?id=10945

Another interesting aspect is that some (more expensive) rotary coders are optical. I presume they don't suffer from switch bounce.
Logged


Global Moderator
Offline Offline
Brattain Member
*****
Karma: 480
Posts: 18732
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

This implementation is polled, but can easily be modified to use a single pin-change interrupt if so desired.

Actually I don't see how that can be modified to use one interrupt, because you will lose some transitions if you do that. The links I followed showed two interrupts to preserve the state machine information.

But thanks very much for the ideas, it is great to merge ideas together like that, we will end up with some great results this way.
Logged


Offline Offline
Edison Member
*
Karma: 3
Posts: 1001
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

The pin-change interrupt works at the port level and so if you connect A and B to pins on the same 8-bit port (such as digital pin 5 and 6) a single interrupt will suffice. You could even support a second encoder from the same pin-change interrupt (4 pins) and two regular switches (if the encoders include a push switch).

Here's my implementation to serve two encoders with push buttons from a single ISR.

Code:
ISR(PCINT2_vect)
{
  static const int8_t rot_states[] =   {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
  static uint8_t AB[2] = {0x03, 0x03};
  static uint8_t btn = _BV(PIND4) | _BV(PIND7);
  uint8_t t = PIND;  // read port status

  // check for rotary state change button1
  AB[0] <<= 2;                  // save previous state
  AB[0] |= (t >> 2) & 0x03;     // add current state
  rot[0] += rot_states[AB[0] & 0x0f];

  // check for rotary state change button2
  AB[1] <<= 2;                  // save previous state
  AB[1] |= (t >> 5) & 0x03;     // add current state
  rot[1] += rot_states[AB[1] & 0x0f];

  // check if buttons are pushed
  t &= _BV(PIND4) | _BV(PIND7);
  if (t != btn) {  // we're only interested in high to low transitions
    if (!t_rotary) { 
      push[0] |= !(t & _BV(PIND4));
      push[1] |= !(t & _BV(PIND7));
    }
    btn = t;
    t_rotary = DEBOUNCE_TIME;  // start/restart bounce guard timer (covers make and break)
 }
}

And here is the low level code to set up the interrupt:
Code:
  // enable pullup for encoder pins
  PORTD |= _BV(PORTD7) | _BV(PORTD6) | _BV(PORTD5) | _BV(PORTD4) | _BV(PORTD3) | _BV(PORTD2);

  // enable button pin change interrupt
  PCMSK2 = _BV(PCINT18) | _BV(PCINT19) | _BV(PCINT20) | _BV(PCINT21) | _BV(PCINT22) | _BV(PCINT23);
  PCICR = _BV(PCIE2);  // D-port interrupt enable
Logged

Offline Offline
Newbie
*
Karma: 0
Posts: 20
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Did anyone perhaps tried an optical mouse to read a shaft? I.e. just mount the sensor part of the mouse above the shaft, and as it turn, use the output to determine its direction and speed? Can one actually make the Arduino to read a mouse? (I'm toying with the idea to read the shaft of my telescope, which is about 20mm diameter.)
Logged

Swannanoa, New Zealand
Offline Offline
Full Member
***
Karma: 1
Posts: 202
New To Arduino (and C)
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Nick
Is that an English phone, since the 9 is at the bottom?

Bergie
There is a sketch for reading PS2 mice.
Some of them include a chip that does the quadrature encoding with 12ms debounce included.
The ones I looked at had 3 quadrature inputs plus three buttons.

I rescued them from the big garbage desposal, to use in a better home and reduce the pin count.

Mark
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 480
Posts: 18732
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Nick
Is that an English phone, since the 9 is at the bottom?

I bought it in Australia, but it probably an English design. I wasn't sure what you meant by "9 at the bottom" but some checking of Google Images seems to show that some phones have the numbers going further around the dial. Is that what you meant?
Logged


Pages: [1] 2   Go Up
Jump to: