Need to improve 2x Encoder.h performance on Arduino Uno

Hi all.
I'm using the Encoder.h library from PJRC (link) to read 2 encoders on an Auduino Uno. As this board only has 2 external interrupt pins I'm limited to "Good" performance by only using 1 external interrupt pin per encoder. I was wondering if I can implement "pin change" interrupts together with Encoder.h to improve my performance by then using 2 interrupt pins (I'm missing some steps at higher speeds).
Thanks
Rob

Please post your example code that shows the performance problem.

How much interrupts per second do you want to process?

2 interrupts pin can read 2 different encoders.

Pin change interrupts are not to difficult (helped GrayNomad to optimize his lib). Problem is that you get an interrupt and then you need to check all pins to see which (multiple) pins did change. This takes time.


Another solution uses an additional XOR chip on the A and B lines of the encoders

  A    B    XOR
-----------------
  0    0     0
  0    1     1
  1    1     0
  1    0     1
  0    0     0

By putting the XOR line to the interrupt pin you get all the changes of the encoder.
In the interrupt you need to read the pinA and pinB which are connected to e.g. pin 3 and 4
Reading those 2 pins can be done fast on registerlevel, comparing them with previous value
indicates the direction

Give it a try

I would move to a Mega which has more digital pins which support interrupts
or move to a microcontroller which has hardware QEI support, see
http://www.microchip.com/maps/microcontroller.aspx

robtillaart:
Another solution uses an additional XOR chip on the A and B lines of the encoders

Nice to know! That very idea is on my experiments to-do list.

If OP wants to try this, know that you don't need an XOR IC (74XX86), you can create an XOR function with a quad-NAND.

dougp:
Nice to know! That very idea is on my experiments to-do list.

If OP wants to try this, know that you don't need an XOR IC (74XX86), you can create an XOR function with a quad-NAND.

When you do I am curious what the max nr of irq's is you can handle in practice.
Note:
The XOR solution generates 4 irq's per rotation, while connecting pinA directly to irq can be configured to give 1 irq per rotation. Question is allways does the application need it?

robtillaart:
When you do I am curious what the max nr of irq's is you can handle in practice.

I'm only planning on doing this on a manual rotary encoder, like you use for navigating menus. I don't know how to quantify the responsiveness at speed.

dougp:
I'm only planning on doing this on a manual rotary encoder, like you use for navigating menus. I don't know how to quantify the responsiveness at speed.

Put a drill on it and see at what speed "stuttering" in the output begins

robtillaart:
Put a drill on it and see at what speed "stuttering" in the output begins

Hmmm. Guess that means incorporating a tach function on the Arduino.

dougp:
Hmmm. Guess that means incorporating a tach function on the Arduino.

a counter that counts interrupts and is printed/reset once a second should be sufficient.
wrote a small sketch that could test it (unfortunatelly no rotary encoder and drill nearby)

//
//    FILE: encoderFast.ino
//  AUTHOR: Rob Tillaart
// VERSION: 0.0.1
//    DATE: 2018-01-14
// PURPOSE: fast decoder algorithm with one interrupt and an XOR
//     URL: http://forum.arduino.cc/index.php?topic=522410
//
// Encoder          Arduino
//  A --------------- 3
//      |
//      ---|
//         | XOR ---- 2  --> interrupt on change
//      ---|
//      |
//  B --------------- 4
//
//  A    B    XOR
// ---------------
//  0    0     0
//  0    1     1
//  1    1     0
//  1    0     1
//  0    0     0
//
//

uint32_t lastUpdate = 0;

volatile uint32_t irqCount;
volatile int32_t stepsSinceStart = 0;

const int encoderPinA = 3;
const int encoderPinB = 4;

void setup()
{
  Serial.begin(230400);
  Serial.println(__FILE__);

  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(encoderPinB, INPUT_PULLUP);
  attachInterrupt(0, readEncoder, CHANGE);
}

void loop()
{
  uint32_t now = millis();
  if (now - lastUpdate >= 1000)
  {
    lastUpdate = now;
    cli();
    uint32_t cnt = irqCount;
    irqCount = 0;
    uint32_t pos = stepsSinceStart;
    sei();
    Serial.print("CNT: ");
    Serial.print(cnt);
    Serial.print("\t\tPOS: ");
    Serial.println(pos);
  }
}

void readEncoder()
{
  static uint8_t val = 0;
  irqCount++;
  val <<= 2;
  val |= digitalRead(encoderPinA) << 1;   // direct register read is even faster
  val |= digitalRead(encoderPinB);

  // val = bitpattern [x x x x prevA prevB curA curB ]
  //       x = ignore
  // 8 transitions possible, 4 CW and 4 CCW
  switch (val & 0x0F)
  {
    case B0001:
    case B0111:
    case B1110:
    case B1000:
      stepsSinceStart++;
      break;
    case B0010:
    case B1011:
    case B1101:
    case B0100:
      stepsSinceStart--;
      break;
  }
}

// --- END OF FILE ---

robtillaart:
a counter that counts interrupts and is printed/reset once a second should be sufficient.
wrote a small sketch that could test it (unfortunatelly no rotary encoder and drill nearby)

Thanks for the sketch! I wasn't looking forward to slogging through past posts to find one to hack at or cobbling one together from scratch.

I've modified your sketch slightly to have encA and encB drive interrupts 0 & 1 - which is the way my encoder is currently hooked up. Highest reading so far, turning by hand is 621. I'll chuck up a drill tomorrow and see what happens with a more consistent speed.

I was disappointed to see your wiring diagram. My vision was that using the XOR would not only save an interrupt pin but would still only require two pins overall. Looking at the wiring and the truth table I see that can't happen. Oh, well, it still saves an interrupt. :grinning:

I've placed an order for a few ICs, etc., but don't yet have a delivery date so the XOR portion of this will have to wait a bit.

Do you think this experiment should continue here or, would it be better to open a new thread?

The XOR solution only works with attachInterrupt(f(), CHANGE); not with RISING or FALLING.
If you use RISING (FALLING) you could connect pinA to an IRQ pin. When the intterupt comes you already know it is HIGH (LOW) so you do not need to read it. So you only need to read pinB to know the direction of rotation in the interrupt routine.

Do you think this experiment should continue here or, would it be better to open a new thread?

As the experiment tries to answer the question of the title I propose to keep it here.

Well, there are 3 different banks of pin change interrupts. If you use INT0 and INT1 for one encoder, you could use one pin in PortB and one in PortC and you wouldn't have to try to figure out which pin changed.

I have had good luck with pin change interrupts and encoders.

Well I've decided to take the advice from horace and move to the Mega board which allows me additional interrupt pins. The Encoder.h library has worked great for me and would rather keep the electronics and program intact. This will also solve my memory size issue - my current program is using 90% the Uno available flash and I still have some functionality to add.
Unfortunately this poses a new problem as I have a specific motor library (ClearPath s/d motors) which will need to be modified. Every solution poses a new problem (challenge) - and I'll be posting separate thread on how best to deal with this library (think it's the use of direct port access in the library)
Thanks for the replies - and good luck with the XOR direction.

Update: With the encoder wired as described in post #9 the count reading maxes out at a little over sixteen hundred. I believe it could go higher but the drill speed is topped out. It generally varies plus or minus twenty to forty counts at all speeds, no doubt partly due tolerances in the drill speed controller - and me keeping my finger steady!

I've been looking over the interrupt section in the 328P datasheet. I was hoping for some way to get the state of the pin which caused the interrupt. Nothing jumps out. Could it just be as easy a doing a digital read of the port/pin?

@dougp that is the standard way of doing it, and works most of the time. The problem is that there is a very small but real possibility that the state of that pin will have changed between when it fired the interrupt and when you check it in the ISR. Due to bounce. Just keep that in mind and handle the possibility.

So, I could mitigate the bounce by adding a .1 uf cap on each encoder lead, no?

What I’m after is to reproduce encB’s signal with code and thus avoid using encB’s physical input.

My reasoning is: if encA’s state and the interrupt pin state ( encA hardware XOR’ed with encB ) are known, encB can be reproduced by ( encA software XOR’ed with interrupt pin ). The reconstructed encB can then be plugged into the encoder direction code.

It would be more clock cycles but probably wouldn’t matter for a manually operated encoder.

I have some results from the hardware-XOR-to-an-encoder-interrupt experiment.

I modified @robtillaart’s sketch to use direct port reads and built an XOR out of NAND gates - image attached if you want to see it. And yes, there is a .1uf cap on the NAND.

With the drill at maximum (1000 rated RPM, unverified) the reading is ~1380 to ~1410. I suspect a lot of the instability is due to the encoder not having been designed for this purpose – the mechanical contacts are doubtless bouncing madly at these speeds. But, it’s pretty jumpy too, anywhere from 30 to 40, even at the lowest setting I could manage on the drill. Too bad I don’t have a nice optical encoder to test it with, I’m sure it would be more steady. At human hand speeds though, it looks more than adequate.

Although this approach seems OK for a manually operated encoder, there might be trouble driving this with something like a D.C. motor. At very high speeds I suspect the non-90° phase difference between encA and INT1, introduced by the approximately 27ns propagation delay for INT1 through three NAND gates, could be enough to give erroneous readings. A true XOR gate, 74HC86, would bring this down to ~11ns.

In any case, it is possible to use an XOR gate to generate encoder interrupts and get a useable output using only two I/O pins
.

//
//    FILE: encoderFast.ino
//  AUTHOR: Rob Tillaart
// VERSION: 0.0.1
//    DATE: 2018-01-14
// PURPOSE: fast decoder algorithm with one interrupt and an XOR
//     URL: http://forum.arduino.cc/index.php?topic=522410
//
/* dougp modifications: utilizes only one general purpose
   I/O pin, for encoder channel A, and one interrupt (INT1).
   The encoder A and B signals are hardware XOR'ed to INT1
   to generate an interrupt transition each time either
   encoder channel makes a transition.  Direct port reads of
   these signals are used to enhance performance.

   The encoder channel B signal is recovered in software to
   drive the encoder direction/transition logic.
*/
//  Tillaart's starting point
//
// Encoder          Arduino
//  A --------------- 3
//      |
//      ---|
//         | XOR ---- 2  --> interrupt on change
//      ---|
//      |
//  B --------------- 4
//
//  Hardware setup demonstrated in this sketch
//
// Encoder         Arduino
//  A --+------------ 9
//      |
//      +--+
//         | XOR ---- 3  --> interrupt on change
//      +--+
//      |
//  B --+
//
//  A    B    XOR
// ---------------
//  0    0     0
//  0    1     1
//  1    1     0
//  1    0     1
//  0    0     0
//
//

uint32_t lastUpdate = 0;
volatile uint32_t irqCount;
volatile int32_t stepsSinceStart = 0;
const int encoderPinA = 9;
const int encoderPinB = 3;

void setup()
{
  Serial.begin(230400);
  Serial.println(__FILE__);
  pinMode(encoderPinA, INPUT); // pullup applied externally
  pinMode(encoderPinB, INPUT); // no pullup, driven by logic gate
  // attachInterrupt(0, readEncoder, CHANGE);
  attachInterrupt(1, readEncoder, CHANGE);
}

void loop()
{
  uint32_t now = millis();
  if (now - lastUpdate >= 1000)
  {
    lastUpdate = now;
    cli();
    uint32_t cnt = irqCount;
    irqCount = 0;
    uint32_t pos = stepsSinceStart;
    sei();
    Serial.print("CNT: ");
    Serial.print(cnt);
    Serial.print("\t\tPOS: ");
    Serial.println(pos);
  }
}
void readEncoder()  // the ISR
{
  byte IRQ1, encA, recoveredB;
  /*
     Note: the bit operations used here tie the code to fixed
     I/O pins and fixed masks and shift values. Modification
     will be needed if general purpose code is desired.
  */
  static uint8_t val = 0;
  irqCount++; // register the interrupt
  IRQ1 = PIND; // capture the INT1 bit (PD3)

  /*
      Capture current encA and shift it left two places to line
      up PB/1 with PD/3 in preparation for XOR.
  */
  encA = (PINB << 2) ;

  /*
     XOR'ing encA and the interrupt pin recovers the state
     of encB
  */
  recoveredB = (IRQ1 ^ encA) & 0x8; // mask extraneous bits
  /*
     Logically OR encA and the recovered encB into bits three
     and two, respectively, of 'val' for processing in the
     state machine
  */
  val >>= 2; // LSR current bits to previous bits
  val &= 0x3; // mask off any set upper bits
  val |= (encA | recoveredB >> 1); // OR encoder values into current bits (2,3)

  // val = bitpattern [x x x x curA curB prevA prevB ]
  //       x = ignore
  // 8 transitions possible, 4 CW and 4 CCW

  switch (val & 0x0F)
  {
    case B0001:
    case B0111:
    case B1110:
    case B1000:
      stepsSinceStart++;
      break;
    case B0010:
    case B1011:
    case B1101:
    case B0100:
      stepsSinceStart--;
      break;
  }
}
// --- END OF FILE ---

Update to the update

It occurred to me hours after posting above that the serial monitor only displayed even numbers. Also, the first detent turned after startup only registers two counts and then four thereafter - the ISR logic having no history for a compare the initial time through.

Anyhow, I retested and found I apparently wasn't holding the drill trigger down hard enough, I got even higher readings the second time:

CNT: 1724	        POS: 4771
CNT: 1177	        POS: 5606
CNT: 296	        POS: 5800
CNT: 37                 POS: 5825
CNT: 1755	        POS: 6976     \
CNT: 1721	        POS: 8205     |
CNT: 1649	        POS: 9442     |
CNT: 1683	        POS: 10678    |
CNT: 1722	        POS: 11915    |
CNT: 1748	        POS: 13151    |
CNT: 1732	        POS: 14386    |   Drill at max. speed here
CNT: 1714	        POS: 15618    |
CNT: 1133	        POS: 16448    |
CNT: 314	        POS: 16663    /
CNT: 1                  POS: 16662
CNT: 0                  POS: 16662
CNT: 0                  POS: 16662

I did the math on the 1748 count and arrived at this:

1748 interrupts/sec X 60sec = 104880 interrupts/min

104880 interrupts / 4 interrupts per detent = 26220 detents/min

26220 detents / 20 detents/revolution of encoder = 1311RPM

Noticeably higher that the 1000RPM on the drill nameplate but not bad.

Very well tested, and results are positive too!

You made my day :wink:

NOte: at 1748 interrupts per second the time between interrupts is 500 uSec,
so the propagation time of the NANDs should not be noticable.

robtillaart:
Very well tested, and results are positive too!

You made my day :wink:

Thanks! It's a somewhat narrow application but at least we know it works.