Optical Encoder Speed on Arduino UNO

I am using an Arduino UNO and an optical encoder that is rated at 2000 pulses per revolution (PPR). The encoder’s phase A is on pin 4 (PCINT20) and phase B is on pin 5 (PCINT21). The PCINT’s are set up as fast as I know how to do that but the system is only accurate at about 2-3 RPM. Faster than than and it is missing pulses.

When I make a slow revolution in either direction, the result is 360 for forward and -360 for reverse, just as it should be. I can slowly go several turns forward then the same number of turn in reverse and it remains accurate coming back to 0 degrees. Any faster and it returns home at something significantly different than 0 degrees.

If my math is correct, the interrupts from the encoder are actually happening at 4 x the PPR rate even though I am only counting a pulse after 4 interrupts. That means 8000 interrupts/rev x 3 RPM = 24000 interrupts/second. That is around 40usec each.

I don’t know how long the loop and ISR routine takes to execute, but it seems like 40usec might be about right for everything that has to happen.

If everything I have said above is accurate, this reaches the max capability of the Arduino to process the pulses. If I want to go faster, I need more HP that an UNO has.

Two questions:
1: Is what I have calculated correct?
2: Is this code about as efficient as it can be?

#include <Arduino.h>

#define PENDULUM_ENCODER_A 4                                  // PCINT20
#define PENDULUM_ENCODER_B 5                                  // PCINT21

double pulses;
volatile int32_t angle;
volatile static byte old_port_d;                              // saved port d configuration

static byte PCMask;                                           // interrupt mask

void setup()
{
  Serial.begin(115200);
  pinMode(PENDULUM_ENCODER_A, INPUT_PULLUP);                  // PCINT20
  pinMode(PENDULUM_ENCODER_B, INPUT_PULLUP);                  // PCINT21

  // setup PCINT20 and PCINT21 on pins 4 & 5
  old_port_d = PIND;                                          // save the old port D configuration
  PCMSK2 |= (1 << PCINT20);                                   // enable PCINT20 pin 4
  PCMSK2 |= (1 << PCINT21);                                   // enable PCINT21 pin 5
  PCMask = PCMSK2;                                            // set the mask
  PCICR |= (1 << PCIE2);                                      // enable PCINT on portd.
}

void loop()
{
  Serial.println(angle);
}

volatile int MSB, LSB, encoded, lastEncoded, sum;
ISR(PCINT2_vect)
{
  MSB = PIND & B00010000;                                     // digitalRead(Pin 4); MSB
  LSB = PIND & B00100000;                                     // digitalRead(Pin 5); LSB
  encoded = (MSB >> 3) | (LSB >> 5);                          // convert the 2 pin value to single number
  sum  = (lastEncoded << 2) | encoded;                        // adding it to the previous encoded value
  if (sum == 0b1101)                                          // sum = 0b1101 2000/rev forward
    pulses ++;
  if (sum == 0b1110)                                          // sum = 0b1110 2000/rev reverse
    pulses --;
  lastEncoded = encoded;                                      // store this value for next time
  angle = pulses / 5.5555555556;                              // 2000 PPR / 360 = 5.5555555556
}

It's not a good idea to do floating point math in an ISR that is supposed to be fast.

Fixed point math is somewhat beyond me at present but if it would substantially improve this up to say 5 or 6 rpm, it would probably be worth the learning curve.

Hint: x/5.555555... = x360/2000 = x36/200 = x*18/100

Don't do any maths in the ISR - just count the pulses - I think this is all that needs to be in the ISR

ISR(PCINT2_vect)
{
  MSB = PIND & B00010000;                                     // digitalRead(Pin 4); MSB
  LSB = PIND & B00100000;                                     // digitalRead(Pin 5); LSB
  encoded = (MSB >> 3) | (LSB >> 5);                          // convert the 2 pin value to single number
  pulseCount ++;                                            // count irresepctive of direction
  if (encoded == 0b01)  { 
    pulses ++;
  }
  else {
    pulses --;
  }
  if (pulseCount >= 11) {     // roughly every 2 degrees - use a larger number for higher speeds
     pulseCount = 0;
     checkAngle = true;
  }
}

Then you can check the exact angle in loop() with code like this

if (checkAngle == true) {
  noInterrupts();
      copyOfPulses = pulses;
      checkAngle = false;
  interrupts();
  angle = copyOfPulses / 5.5555555556; 
}

...R

I agree with Robin2. However, "pulses" should not be a double. It should be some kind of unsigned integer.

Thanks Robin2.

I see your point but this version always counts backwards and -720/rev not 360/-360.

It appears to me that (encoded == 0b01) will be true in both directions.

Also, here are the new variable defs

volatile int32_t angle, pulses;
volatile unsigned int pulseCount;

PickyBiker:
I see your point but this version always counts backwards and -720/rev not 360/-360.

What version?

If you have a revised version of your program please post it in your next Reply so we can keep up with you.

It appears to me that (encoded == 0b01) will be true in both directions.

Maybe I don't understand the purpose of your sum = (lastEncoded << 2) | encoded; calculation.

Please post a link to the datasheet for the encoder.

It is my understanding (perhaps faulty) that in one direction the values are 01 and in the other direction 10 - but maybe that is only true if you have interrupts on both pins and that may be to much work for the Arduino with a high resolution encoder.

...R

Out to movie and dinner with the wife. Will post code tonight.

PickyBiker:
If my math is correct, the interrupts from the encoder are actually happening at 4 x the PPR rate even though I am only counting a pulse after 4 interrupts. That means 8000 interrupts/rev x 3 RPM = 24000 interrupts/second. That is around 40usec each.

1: Is what I have calculated correct?

Actually your Maths is out by a factor of 60, RPM = Revolutions Per Minute.
3 RPM = 1/20th of a revolution per second or roughly 400 interrupts per second i.e. once every 2.5ms (assuming the encoder generates a square wave offset by 90o between the two phases).

IANCrowe, you are correct. My mistake was in claiming RPM when it is actually Revs Per Second. I have a little plastic arm on the encoder and I am spinning with my finger it at about 3 times per second. Thanks for catching that typo.

I have this code working now and it is accurate as fast as I can spin it with my finger, probably about 5 RPS. The comments re: doing math in the ISR were right on.

The math is now in the loop and I have changed all the register variables to static volatile bytes. I also returned
to the original method of determining direction.

All is well now. Thank you very much for the great help.

Here is the result:

#include <Arduino.h>

#define PENDULUM_ENCODER_A 4                                  // PCINT20
#define PENDULUM_ENCODER_B 5                                  // PCINT21

boolean checkAngle = false;
volatile int32_t angle, pulses;
static volatile byte old_port_d, PCMask;                      // saved port d configuration                                          // interrupt mask
static volatile byte MSB, LSB, sum;
static volatile byte encoded, lastEncoded;

void setup()
{
  Serial.begin(115200);
  pinMode(PENDULUM_ENCODER_A, INPUT_PULLUP);                  // PCINT20
  pinMode(PENDULUM_ENCODER_B, INPUT_PULLUP);                  // PCINT21

  // setup PCINT20 and PCINT21 on pins 4 & 5
  old_port_d = PIND;                                          // save the old port D configuration
  PCMSK2 |= (1 << PCINT20);                                   // enable PCINT20 pin 4
  PCMSK2 |= (1 << PCINT21);                                   // enable PCINT21 pin 5
  PCMask = PCMSK2;                                            // set the mask
  PCICR |= (1 << PCIE2);                                      // enable PCINT on portd.
}

void loop()
{

  double copyOfPulses;
  if (checkAngle == true)
  {
    noInterrupts();
    copyOfPulses = pulses;
    checkAngle = false;
    interrupts();
    angle = pulses / 5.55555555556;
    Serial.println(angle);
  }
}
ISR(PCINT2_vect)
{
  MSB = PIND & B00010000;                                       // digitalRead(Pin 4); MSB
  LSB = PIND & B00100000;                                       // digitalRead(Pin 5); LSB
  encoded = (MSB >> 3) | (LSB >> 5);                            // convert the 2 pin value to single number
  sum  = (lastEncoded << 2) | encoded;                          // adding it to the previous encoded value
  if (sum == 0b1101)                                            // sum = 0b1101 2000/rev forward
    pulses ++;
  if (sum == 0b1110)                                            // sum = 0b1110 2000/rev reverse
    pulses --;
  lastEncoded = encoded;                                        // store this value for next time
  checkAngle = true;
}