0

I want to read a quadrature rotary encoders at full resolution with only one interrupt on Arduino Nano (ATmega328). So I found out that we can use XOR to reach a full resolution:

       

Where pin 3 is interruptable but 4 and 5 are not.

Our requirements are:

  1. Full resolution: for example, the solution offered in this answer works at a half-resolution only, not detecting all the edges.
  2. Real-time: being as fast as possible up to the level that this hardware allows and safe in terms of not missing any of the signal edges.
  3. using only one interrupt. We need the other one for other purposes.

So far, I have developed two different codes, one aiming for safety and the other for performance:

Preamble:

#include "digitalWriteFast.h"

#define pinT 3 // trigger pin comming from XOR

#define pinA 4 // channel A
#define pinB 5 // channel B

volatile long counter = 0;
long counter_ = counter;

where the digitalWriteFast library can be downloaded from here.

Performant:

volatile short int increment = 0;
volatile bool tmpA;
volatile bool tmpB;
volatile bool lastA;

ISR(trigger) {
  tmpA = digitalReadFast(pinA);
  tmpB = digitalReadFast(pinB);
  counter += (1 - 2 * tmpA) * (1 - 2 * tmpB) * (1 - 2 * (lastA != tmpA));
  lastA = tmpA;
}

void setup() {
  pinMode(pinA, INPUT_PULLUP);
  pinMode(pinB, INPUT_PULLUP);
  pinMode(pinT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinT), trigger, CHANGE);
  lastA = digitalReadFast(pinA);
  Serial.begin(9600);
}

void loop() {
  if (counter != counter_) {
    Serial.println(counter);
    counter_ = counter;
  }
}

Safe:

volatile unsigned long error = 0;
unsigned long error_ = 0;

volatile bool inputA;
volatile bool inputB;

volatile short state; // 100 110 101 111
volatile short state_;

void detectState() {
  inputA = digitalReadFast(pinA);
  inputB = digitalReadFast(pinB);

  if (inputA) {
    if (inputB) {
      state = 111;
    } else {
      state = 110;
    }
  } else {
    if (inputB) {
      state = 101;
    } else {
      state = 100;
    }
  }
}

ISR(trigger) {

  detectState();

  if (state_ != state) {
    switch (state_) {
      case 100:
        if (state == 110) {
          counter++;
        } else if (state == 101) {
          counter--;
        } else {
          error++;
        }
        break;
      case 110:
        if (state == 100) {
          counter--;
        } else if (state == 111) {
          counter++;
        } else {
          error++;
        }
        break;
      case 101:
        if (state == 100) {
          counter++;
        } else if (state == 111) {
          counter--;
        } else {
          error++;
        }
        break;
      case 111:
        if (state == 110) {
          counter--;
        } else if (state == 101) {
          counter++;
        } else {
          error++;
        }
        break;
      default:
        error++;
        break;
    }

    state_ = state;
  } else {
    error++;
  }
}

void setup() {
  pinMode(pinT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinT), trigger, CHANGE);

  pinMode(pinA, INPUT_PULLUP);
  pinMode(pinB, INPUT_PULLUP);

  detectState();
  state_ = state;

  Serial.begin(9600);
}

void loop() {
  if (counter != counter_) {
    Serial.print("counter: ");
    Serial.println(counter);
    counter_ = counter;
  }

  if (error != error_) {
    Serial.print(error);
    Serial.println(" errors");
    error_ = error;
  }
}

Now my questions are:

  1. Is there any way to improve the speed/safety of the performant/safe code? Any way to improve this code is appreciated.
  2. Is it possible to have maximum speed and safety together or some trade-off in between?
  3. What is the maximum frequency we can get out of this configuration?

Thanks for your help in advance.

Foad
  • 143
  • 9
  • 1
    Note that, instead of a XOR gate, you could use a single pin change interrupt on both channels. – Edgar Bonet Oct 24 '19 at 19:30
  • @EdgarBonet Do you mean connecting two channels to two interrupts? that we can't do. The Arduino Uno/Nano has only two interrupts and we need one for other things. Is that what you meant? – Foad Oct 24 '19 at 19:56
  • No, I mean connecting the two channels to a single pin change interrupt. The Arduino has three pin change interrupts in addition to the two external interrupts. – Edgar Bonet Oct 25 '19 at 07:51
  • I'm not sure what "pin change interrupts" is, but if you connect two channels to a single interruptable pin (i.e, 3 or 4) then it works like an AND. – Foad Oct 25 '19 at 07:55
  • No, it doesn't. Do a web search for “_Arduino pin change interrupt_”, or “_AVR pin change interrupt_”. – Edgar Bonet Oct 25 '19 at 08:34
  • @EdgarBonet OK. Reading this page, I don't think using pin/port change interrupts are a better solution. Unless I haven't understood your point and you may be kind to elaborate. – Foad Oct 25 '19 at 09:48
  • The MCU has pin change interrupts so that you don't need to add a XOR gate and use one of the external interrupt pins. You don't need a library for using them, and you probably don't want the overhead of a library anyway. Read Arduino Pin Change Interrupts. – Edgar Bonet Oct 25 '19 at 10:53
  • @EdgarBonet please consider that the rest of the pins are not free. there are reading and writing other stuff. So if I use on the 3 available blocks of PinChangeInterrupts (0-7, 8-13, A0-A5). I receive lots of unnecessary interrupts. The XOR helps to read both channels in "real-time" with just one interrupt pin and one normal one. See this image. – Foad Oct 25 '19 at 12:01
  • No, you don't receive unnecessary interrupts if you don't activate PCINT on the other pins. – Edgar Bonet Oct 25 '19 at 12:27
  • @EdgarBonet what is the correct syntax for reading pins 4 and 5 (which are not interruptable) using the pin/port change interrupt? what binary/hexadecimal values should be assigned to the PCICR and PCMSK0/PCMSK1/PCMSK2? – Foad Oct 25 '19 at 12:30

1 Answers1

1

This is just a partial, rushed answer.

You should be able to get a significant speed-up if you define your own ISR for handling the interrupt, as in:

ISR(INT1_vect) { ... }

The Arduino way of using interrupts is instead

void trigger() { ...}

void setup() {
    attachInterrupt(int_number, trigger, CHANGE);
    ...
}

but this is slow, as it involves the ISR provided by the Arduino core, which has to locate and call your interrupt handler. The way you wrote the handler is wrong: either you define a regular function, without the ISR macro, and go the Arduino way, or you go the plain AVR path and define the ISR yourself, which should be named INTERRUPT_NAME_vect, in which case you don't use attachInterrupt(). The way you did it is probably the slowest you can get, as the compiler will needlessly add an ISR-type prologue and epilogue to your handler.

As for the maximum frequency, that's hard to assess without testing, but you may get some idea by reading this answer on interrupt latency.


Edit: As I stated in my first comment to the question, a pin change interrupt can be used instead of an external logic gate. Here is how to do it.

First, one should look at the Arduino Uno pinout and see that:

  • digital 4 = PD4 = PCINT20
  • digital 5 = PD5 = PCINT21

Both pins are tied to the pin change interrupt request 2 (PCINT2_vect). This interrupt can be configured in setup() as follows:

PCMSK2 = _BV(PCINT20)   // enable interrupt on pin PCINT20 = PD4
       | _BV(PCINT21);  // enable interrupt on pin PCINT21 = PD5
PCIFR  = _BV(PCIF2);    // clear interrupt flag
PCICR  = _BV(PCIE2);    // enable pin change interrupt request 2

Now, the interrupt will fire on every change of the logic levels of inputs 4 and 5.

Then, in the ISR, the whole port D can be read in a single cycle by just evaluating the corresponding pin input register, i.e. PIND. With some shifting and masking, one can get a phase between 0 and 3 (the count is in Gray code: 0, 1, 3, 2, 0...). From the previous and the current phase, one can compute the change that needs to be applied to the counter:

// Handle pin change interrupt request 2.
ISR(PCINT2_vect) {
    static uint8_t previous_phase;
    uint8_t phase = (PIND >> 4) & 0x03;  // read input pins
    counter += encoder_delta(previous_phase, phase);
    previous_phase = phase;
}

The implementation of encoder_delta() is left as an exercise to the reader. ;-)

Edgar Bonet
  • 43,033
  • 4
  • 38
  • 76
  • Thanks. I will go through the new edits and will bother you if I had further questions. – Foad Oct 25 '19 at 13:29