Register forum user name Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to verify your details, confirm your email, resolve issues, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.
 Entire forum ➜ Electronics ➜ Microprocessors ➜ Rotary encoders and interrupts

Rotary encoders and interrupts

Postings by administrators only.

Refresh page


Posted by Nick Gammon   Australia  (23,120 posts)  Bio   Forum Administrator
Date Tue 24 May 2011 04:20 AM (UTC)

Amended on Mon 20 Oct 2014 09:43 PM (UTC) by Nick Gammon

Message
I have been reading 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:



The 0.1 uF (ceramic) capacitors shown in the schematic are not in the photo. They were suggested as a means of doing a "hardware debounce". I did not use them initially, and you may find that the encoder works without them. But if you get occasional jumps or wrong readings, the capacitors may very well help.

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:


  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:


  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):


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:


// 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)
    {
    long currentCount;
    
    // protected access to the counter
    noInterrupts ();
    currentCount = rotaryCount;
    interrupts ();
    
    Serial.print ("Count = ");  
    Serial.println (currentCount);
    fired = false;
  }  // end if fired

}  // end of loop

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,120 posts)  Bio   Forum Administrator
Date Reply #1 on Wed 25 May 2011 05:56 AM (UTC)

Amended on Fri 23 Dec 2016 11:12 PM (UTC) by Nick Gammon

Message
Following on from suggestions from jraskell on the Arduino forum, the simplified version below only requires a single interrupt pin, so you could use pin D2 (for the interrupt) and another pin (eg. D5) for the "B" side, thus freeing up an interrupt.

For simplicity I removed the code to detect the speed at which you were turning the encoder, but you could add that back in, in the main loop if you wanted it.


// 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;

const byte encoderPinA = 2;
const byte encoderPinB = 5;

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


void setup ()
{
  pinMode (encoderPinA, INPUT_PULLUP);     // enable pull-ups
  pinMode (encoderPinB, INPUT_PULLUP); 
  attachInterrupt (digitalPinToInterrupt (encoderPinA), isr, CHANGE);   // interrupt 0 is pin 2

  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

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,120 posts)  Bio   Forum Administrator
Date Reply #2 on Sat 24 Dec 2016 01:35 AM (UTC)

Amended on Sat 24 Dec 2016 01:38 AM (UTC) by Nick Gammon

Message
Reading 4 encoders using pin-change interrupts


The version below uses pin-change interrupts to detect 4 or more encoders being turned. It uses direct port manipulation, based on pin numbers (and converted to port numbers and bit patterns inside setup).


// Wiring: Connect common pin of encoder to ground.

volatile bool fired;

const byte ENCODERS = 4;

typedef struct 
  {
  int aPin;     // which Arduino pin for the "A" side
  int bPin;     // which Arduino pin for the "B" side

  // values below are calculated at run-time
  volatile byte * aPort;  // which processor port the A side is plugged into
  volatile byte * bPort;  // which processor port the B side is plugged into
  byte aBitMask;   // which bit in the A port
  byte bBitMask;   // which bit in the B port
  byte whichInterrupt;  // which pin-change interrupt port (0, 1, 2)
  byte oldValue;   // old value of A port (to see if it changed)
  int count;       // current encoder counter
  } encoder;

 volatile encoder encoders [ENCODERS] =
  {

 // A   B  pins  (eg. D4 and D8, D5 and D9 and so on)

  { 4,  8 },
  { 5,  9 },
  { 6, 10 },
  { 7, 11 },

  };  // end of encoders


void checkForPinChange (const byte which)
  {
  for (byte i = 0; i < ENCODERS; i++)
    {
    if (encoders [i].whichInterrupt == which)
      {
      byte newValue = *(encoders [i].aPort) & encoders [i].aBitMask;
      if (newValue != encoders [i].oldValue)
        {
        bool up;
        byte bPort = *(encoders [i].bPort) & encoders [i].bBitMask;
        if (newValue)
          up = bPort;
        else
          up = !bPort;
        fired = true;
        if (up)
          encoders [i].count++;
        else
          encoders [i].count--;
        encoders [i].oldValue = newValue;
        }  // end of if value has changed
      }   // end of if this is the right interrupt number
    }     // end of for each encoder
  } // end of checkForPinChange

// handle pin change interrupt for D8 to D13 here
ISR (PCINT0_vect)
 {
 checkForPinChange (PCIE0);
 }  // end of PCINT0_vect

// handle pin change interrupt for A0 to A5 here
ISR (PCINT1_vect)
 {
 checkForPinChange (PCIE1);
 }  // end of PCINT1_vect

// handle pin change interrupt for D0 to D7 here
ISR (PCINT2_vect)
 {
 checkForPinChange (PCIE2);
 }  // end of PCINT2_vect

void setup ()
{
 Serial.begin (115200);
 Serial.println ("Starting ...");

 PCIFR  |= bit (PCIF0) | bit (PCIF1) | bit (PCIF2);   // clear any outstanding interrupts

 for (byte i = 0; i < ENCODERS; i++)
    {
    pinMode (encoders [i].aPin, INPUT_PULLUP);
    pinMode (encoders [i].bPin, INPUT_PULLUP);
    // convert pin number to port, mask, etc. for efficiency

    // Input port
    encoders [i].aPort = portInputRegister (digitalPinToPort (encoders [i].aPin));
    encoders [i].bPort = portInputRegister (digitalPinToPort (encoders [i].bPin));

    // Which bit in the port to test
    encoders [i].aBitMask = digitalPinToBitMask (encoders [i].aPin);
    encoders [i].bBitMask = digitalPinToBitMask (encoders [i].bPin);

    // Which interrupt number (0, 1, 2)
    encoders [i].whichInterrupt = digitalPinToPCICRbit (encoders [i].aPin);

    // activate this pin-change interrupt bit (eg. PCMSK0, PCMSK1, PCMSK2)
    volatile byte * ICRmaskPort = digitalPinToPCMSK (encoders [i].aPin);
    *ICRmaskPort  |= bit (digitalPinToPCMSKbit (encoders [i].aPin));

    // enable this pin-change interrupt
    PCICR |= bit (digitalPinToPCICRbit (encoders [i].aPin));

    } // end of for each encoder

}  // end of setup

void loop ()
{
  if (fired)
    {
    for (byte i = 0; i < ENCODERS; i++)
      {
      char buf [10];
      sprintf (buf, "%5d", encoders [i].count);
      Serial.print (buf);
      }
    Serial.println ();
    fired = false;
    }  // end if fired

}  // end of loop

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


42,186 views.

Postings by administrators only.

Refresh page

Go to topic:           Search the forum


[Go to top] top

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.