Interrupt Timing and Maximum Encoder Rotation Speed?

I have bought a cheap quadrature encoder in order to build a homemade string potentiometer. The string pot will eventually be used to take linear velocity measurements.

I've also written/stolen some code for proof of concept and it all works fine on the bench, turning the encoder by hand. The thing that has started to concern me is that I am only using a single interrupt to halve the resolution but the output still shows 720 counts for a full rotation (which makes sense since it a 360P/R encoder).

That means that, at some rotational speed, the encoder will be pulsing faster than the interrupts can handle. I don't really have any experience counting clock cycles or timing interrupts but is there a simple way that I could check how long the ISR takes so I can calculate the highest speed this code would be capable of dealing with?

The code is attached in case anyone is interested.

/*
 * Quadrature Encoder Reading 
 * 
 * Only utilises a single interrupt so encoder resolution is halved
 * 
 * Most code and concepts taken directly from: 
 * http://makeatronics.blogspot.com.au/2013/02/efficiently-reading-quadrature-with.html
 * 
 */
//Includes

//Globals
volatile long enc_count = 0;

//Constants

//Pin Definitions
const byte CHAN_A = 2;
const byte CHAN_B = 3;


void setup() 
{
  //Both channels of the encoder only output a ground so require a pullup
  pinMode(CHAN_A, INPUT_PULLUP);
  pinMode(CHAN_B, INPUT_PULLUP);
  
  attachInterrupt(digitalPinToInterrupt(CHAN_A),encoder_isr,CHANGE);
    
  Serial.begin(9600);
}

void loop() 
{
  static long current_enc_count;

  long previous_enc_count = current_enc_count;
  
  //Disable interrupts and get the most up to date value from the encoder
  // This is necessary because enc_count is longer than a byte
  noInterrupts();
  current_enc_count = enc_count;
  interrupts();

  //Only update the output if we have a change
  if (previous_enc_count != current_enc_count)
  {
    Serial.println(current_enc_count);
  }
}

void encoder_isr() 
{
  static uint8_t enc_val = 0;
  
  //Lookup table determines whether rotation is forward or backward. 0s represent unchanged values and disallowed states
  static int8_t lookup_table[] = {0,0,0,-1,0,0,1,0,0,1,0,0,-1,0,0,0};
  
  enc_val = enc_val*4; //Shift old values up two places to make room for the new ones

  //Directly reading PIND register is the only real method of obtaining the state of both pins at once
  // This is much faster than doing digitalRead on each pin separately
  // 0b1100 bit mask gives the value of pins 2 and 3
  // Dividing by 4 shifts the pin 2 & 3 values down to the two LSbs
  // ORing with the current enc_val ensure we don't ruin the integrity of the previous values
  enc_val = enc_val | ((PIND & 0b1100)/4); 
 
  //We only want the lowest 4 bits for the lookup table (values 0-15)
  enc_count = enc_count + lookup_table[enc_val & 0b1111];
}

You can count the loop() iterations within a certain time, with the ISR enabled and disabled. Then the loop iteration difference corresponds to the ISR count difference. The attachable ISR will take longer to execute than the other interrupts.

In practice not much more than 100kHz interrupt frequency can be reached, without blocking any other activities or interference with other interrupts.

s

That means that, at some rotational speed, the encoder will be pulsing faster than the interrupts can handle.

I doubt it. I think your limitation is likely to be the mechanical and electronic limits of the encoder if it is "cheap". Most encoders have specifications on max rpm. Do you have the data sheet for the one you purchased?

Latency in and out of an ISR is around 5 microseconds, and given the spare nature of the code inside, I can't think it will go much over that. I'd certainly guess less than 10 microseconds total. If you use a pin change interrupt instead of an external interrupt it is slightly faster.

Sometime ago a wrote a quadrature output simulator program which can be helpful in working through your code questions without using the actual encoder. Here's a link to the original post. If you have a second Arduino, you can run this program on it, and jumper pins 9 and 10 to the inputs on your encoder reading Arduino.

When I bench marked code similar to yours but reading all 4 transitions I could read somewhat over 90K counts/sec. Your 2 transition ISR should be faster. You can cut the resolution further to 360 count/rev for higher speed, but I don't know what resolution you require.

One issue with how many interrupts you can process is what else your program needs to be doing.

//Quadrature signal generator two outputs offset 90 degrees
//emulate rotary encoder
//Timer 1 CTC with ICR1 TOP and 

void setup() {

 pinMode(9, OUTPUT); //output A
 pinMode(10, OUTPUT); //output B

 TCCR1A = 0; //clear timer registers
 TCCR1B = 0;
 TCNT1 = 0;
 GTCCR |= 1 << PSRASY; //reset prescaler

 //ICR1 and Prescaler sets frequency
 //no prescaler .0625 us per count @ 16mhz
 //prescaler 8 .5 us per count

 TCCR1B |=  _BV(CS11); // prescaler 8
 //TCCR1B |= _BV(CS10); //no prescaler

 //counts are zero indexed 2edges per ICR1 period
 //numerical values for prescaler 8.
 //e.g. 10k period give 20k encoder counts

 ICR1 = 199;//10k ICR1 period  20k encoder counts
 //ICR1 = 99; //20k ICR1 period 40k encoder counts
 //ICR1 = 49; //40K ICR1 period 80k encoder counts 
 //ICR1 = 46; //42.5K ICR1 period 85k encoder counts 
 //ICR1 = 41; //47.5k ICR1 period 95K encoder counts 
 //ICR1 = 39; //50k ICR1 period 100k encoder counts
 //ICR1 = 29; //66.6K ICR1 period 133k encoder counts
 //ICR1 = 19; //100k ICR1 period 200k encoder counts
 
 OCR1A = ICR1 - 1; //two different pulse widths almost 100% duty cycle
 OCR1B = OCR1A / 2; //offset by half period

 TCCR1B |= _BV(WGM13) | _BV(WGM12); //CTC mode with ICR1
 TCCR1A = _BV(COM1A0) | _BV(COM1B0); //Toggle OC1A/OC1B on compare match
}
void loop () {}

Here are the images from a scope showing the two offset square waves. One shot at 80khz and another at 500khz. With 4 edges per cycle, there should be 320k and 2m counts/second from the quadrature. It's somewhat confusing in that the timer cycle is 2x the square wave frequency because of the output pin toggle on match, and the quadrature counts per second are 4x the square wave frequency (i.e. 2x the timer cycle).

74ae338da2ad15e77a6451335865e7c175a5ccc5.jpg1bb38e358a6a34b5204b5b2fd180a2b25ae9736a.jpg

Thanks very much to both of you for the helpful answers. It's an eBay purchase so no real datasheet but the ad suggests a max mechanical RPM of 5000.

If my numbers are right, 90-100k counts/sec works out to be about 7500-8333 RPM so you were right that the mechanical max is the limiting factor. After considering this it occurs also to me that, for the linear velocities I will be measuring, it is unlikely that the rotation speeds will be even close to this.

@cattledog, I'm still interested in how you would decrease the resolution to 360 though? I considered triggering the interrupt on rising edges only but I didn't think I would be able to get forward and reverse measurements that way?

Maximum Encoder Rotation Speed?

I suspect the practical maximum will be determined by how much of the Arduino's time is needed to do something with the data it gets from the encoder. That may take a great deal longer than the time required to detect a pulse.

At 90k pulses per second there is only 11µsecs between pulses.

...R

Agreed but hopefully that shouldn't be a problem because the majority of the data processing will only occur once the rotation slows or stops meaning the interrupts won't be firing as quickly or at all.

I considered triggering the interrupt on rising edges only but I didn't think I would be able to get forward and reverse measurements that way?

No, you still get direction from the state of the two pins.

attachInterrupt(digitalPinToInterrupt(CHAN_A), readEncoder1x , RISING);

With CHAN A , B on Pin 2 and 3

void readEnc1X() {

  encState = (PIND >> 2) & 3; //read pins 3 and 2 (B, A)

  if (encState == B11) //B High when A goes High
    encoderCount -= 1;

  if (encState == B01) //B low when A goes High
    encoderCount += 1;

  if (encState == B10 || encState == B00)
    errorCount++;
}

That works perfectly! Thanks so much for your help :slight_smile:

I suspect a problem, when the encoder stands almost still and jitters around a state change of the interrupt pin. Then every rising (interrupting) change will affect the counter, while the falling change is disregarded. If you ever have an application that shall move to some position, both edges have to be monitored. This can be accomplished by a pin change interrupt.

Your (abbreviated) algorithm is working fine for monitoring the continuous speed of a motor.

Thanks DrDiettrich that's a good point to remember as the project progresses