How to use Interrupt Service Routine with a rotary encoder module

neat approach, thanks

Ok well i have a simple example that i use on an AVR with pin change interrupts. And i started from the basic sketch you started with but did it something like this.

#include <Arduino.h>

#define signalA 4
#define signalB 5

volatile int reader = 0;

void setup() {
  Serial.begin(115200);  // actually object to these long Serial messages
  pinMode(signalA, INPUT_PULLUP);
  pinMode(signalB, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(signalB), Roatar, CHANGE);
  attachInterrupt(digitalPinToInterrupt(signalA), Roatar, CHANGE);
}


void loop() {
  static int oldReader = 0;
  int readNow = reader;
  if (readNow != oldReader) {
    if (readNow < oldReader) {
      Serial.print("ClockWise  ");
      Serial.println(oldReader - readNow, DEC);
      digitalWrite(LED_BUILTIN, LOW);
    }
    else {
      Serial.print("AntiClockWise  ");
      Serial.println(readNow - oldReader, DEC);
      digitalWrite(LED_BUILTIN, HIGH);
    }
    oldReader = readNow;
  }
}

ICACHE_RAM_ATTR void Roatar() {
  uint8_t currentState = (digitalRead(signalA) << 1) | digitalRead(signalB);
           // reading the pins straightaway ideally a port read but on an ESP32 ? don't know
  if (currentState == 2) {
    reader++;
  }
  else if (currentState == 1) {
    reader--;
  }
}

Do call the same interrupt but determine what is causing the change. Now if 1 side is HIGH then it's 1 way, if the other side is HIGH it's the other, if it's both HIGH or both LOW discard. (sketch compiles, not tested though)

isn't the result of OR, '|', simply 0 or 1? how can it be 2?

Don't confuse a logical OR (||) with a bitwise OR (|).

right

so then there are 4 possible values. so doesn't ++/-- depend on the previous state, not simply the value of the current state?

i think these are the 8 possible cases

 00 10 ++ 01 --
 10 11 ++ 00 --
 11 01 ++ 10 --
 01 00 ++ 11 --

Ah you got me thinking there, and i did this late last night and i made a small error in the ISR, and looking through where i got it from the correction is

It's too early in the morning to explain still though. This is what i have and what works for me.

I want to ask again, a link of what you have a picture ?

  pinMode(signalA, INPUT_PULLUP);
  pinMode(signalB, INPUT_PULLUP);

The internal pullups are quite likely to weak and your encoder may need additional capacitors, hence the question.

the problem is not with the hardware, it's with the logic to recognize either one tic for each AB sequence or 4 possible tics and changing direction with the AB sequence

You're approaching the State Table Encoder Method that I linked back in Post #7.

@Deva_Rishi is.

I tried to explain that her code wasn't complete. i'm suggesting the simpler, lower resolution method that just recognizes a single tic when A RISES

OK, but I will extoll the virtues of the State Table method ... it's very fast (especially if you bypass Arduino's digitalRead() function) and automatically handles contact bounce. It also handles rapid rotation direction changes, even if they happen while the quadrature cycle is only part way completed.

sure. but i think the OP may just want something simpler

years ago someone had a web page that did similar and handled missed state transitions.

be more specific:
instead of if (digitalRead(signalA) != digitalRead(signalB)) ...

try:

if(digitalRead(signalA) == true && digitalRead(signalB) == false ) value++;
         else
if(digitalRead(signalA) == false && digitalRead(signalB) == true ) value--;

digitalRead() actually returns an int, not a bool
HIGH is defined as 1, LOW is defined as 0
so
if(digitalRead(signalA) == HIGH && digitalRead(signalB) == LOW ) value++;

Well i can totally understand that you may want to incorporate that sequence from #25 fully, depending on the type of encoder. (the encoder i have been using is actually just a cheap turning knob) Main thing to watch for is to keep the ISR as short as possible, and do the direction recognition outside of the ISR. Normally speaking one would even stop interrupts while reading a multi-byte ISR variable, but an ESP is a 32-bit MCU, so there is no need (int is 32 bit signed btw) if you want to keep track of a previous state then that variable should be of a local static type (uint8_t in this case)

?

what do you mean by types of encoders? mechanical, optical, magnetic, ???

quadrature encoders have to outputs, each changing a half a tic (?) apart from one another.

you can't determine direction without 2 inputs. the resolution can be 4x greater with a quadrature encoder

look at the black discs in the photo mounted to the back side of the motors. They have slots that can be monitored using optical transmissive sensors. just once sensor is needed if you only want to measure speed. would you describe them as an encoder

optical and mechanical, are of course options, but some encoders are specifically to monitor motor drives some are meant for knobs. Some mechanical come with de-bounce capacitors some don't need some do need but don't have. The ones for knobs tend to have a 'dead' zone, the ones meant for motor drives don't

Not really. I have something similar on a motor driver for a CV radiator valve controller, which contains a (ir ?) led and an photo-transistor and just monitors revolutions using a small reflective tab on one of the gears. For motor drives this actually makes sense, since the direction is already known, and position is all that is unknown.

But for the knob encoders i have this was not required and only threw out extra counts.

Look i am so sorry, the confusion is all my fault.
With the 1/4 resolution only 1 of the pulses calls the interrupt

void setup() {
  Serial.begin(115200);  // actually object to these long Serial messages
  pinMode(signalA, INPUT_PULLUP);
  pinMode(signalB, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(signalB), Roatar, FALLING);
  //attachInterrupt(digitalPinToInterrupt(signalA), Roatar, CHANGE);  // there is no need or want for this
}

and in fact if the interrupt is set to FALLING, the ISR gets even more slick

ICACHE_RAM_ATTR void Roatar() {
  if (digitalRead(signalA)) {
    reader++;
  }
  else  {
    reader--;
  }
}

I really am sorry, but that tends to happen when doing things late at night or early in the morning.

for full resolution i guess the ISR should be triggered by both pins and keep track of the old state

void setup() {
  Serial.begin(115200);  // actually object to these long Serial messages
  pinMode(signalA, INPUT_PULLUP);
  pinMode(signalB, INPUT_PULLUP);
  //attachInterrupt(digitalPinToInterrupt(signalB), Roatar, FALLING);
  attachInterrupt(digitalPinToInterrupt(signalB), Roatar, CHANGE);
  attachInterrupt(digitalPinToInterrupt(signalA), Roatar, CHANGE);
}

ICACHE_RAM_ATTR void Roatar() {
  static uint8_t oldState = 3;  //  init both HIGH
  uint8_t currentState = (digitalRead(signalA) << 1) | digitalRead(signalB);
  uint8_t checkState = currentState | oldState << 2;
  // Quickest way to contain all options and use a single switch - case
  switch (checkState) {
    case 0b0001:
    case 0b1000:
    case 0b1110:
    case 0b0111:
      reader--;
      break;
    case 0b0010:
    case 0b1011:
    case 0b1101:
    case 0b0100:
      reader++;
      break;
  }
  oldState = currentState;
}

n.b as an excuse i would like to add that the code i had was using direct port & register manipulation, which is part of the reason the errors came in.

also as another note

  Serial.begin(115200);  // actually object to these long Serial messages

the reason i object is that this is an interrupt driven process and while the processor is in an interrupt no interrupt can be fired. On an ESP i guess the speed is such that it won't matter much though.

1 Like

yes!. i think using the switch is an easy way to handle all the combinations.

not sure what this is saying. unless reader is just a byte, it really should be copied with interrupts disabled.

Just for completeness for those who might be interested:

The state table method works great at processing bounces correctly, but still has to service an interrupt on each bounce. An alternate method eliminates bouncing interrupts by enabling the interrupt on only one line at a time. When a line generates the first interrupt, further interrupts on that line are disabled within the ISR, and interrupts are enabled on the other line, which presumably transitioned some time ago and has stopped bouncing.

However, things get complicated when there's a change of direction. This can be dealt with by expanding the lookup table to 32 bytes (5 bits).

The Github repo below includes a PDF writeup on this method, and provides sketches using the standard state table method as well as two versions of the alternate method - one using pin change interrupts, and the other using the "external" interrupts on D2 and D3. In all cases though, the sketches report the total number of interrupts which have been serviced between the previous detent position and the current one (ideally that would be 4). That information might be surprising depending on the quality of the encoder.

https://github.com/gbhug5a/Rotary-Encoder-Servicing-Routines/tree/master/Arduino

I'm partial to the simpler, lower resolution clock-on-falling(rising) method.

Relying on the higher resolution of sub-quadrature-cycle counting isn't warranted. Even the reputable encoder manufacturers can have the phase difference be 45-135° rather than the assumed 90°. They spec the encoders in plain pulses-per-revolution and can control the repeatability of the same edge fairly well. Clocking on both edges with both sensors has lots of alignment problems that all detract from accuracy.

Not on a 32-bit processor. The 32-bit variable is fetched in a with a single instruction across a 32-bit bus. There is no possibility of it being modified by the ISR during this process. On an 8-bit AVR processor this is of course not the case. Then a 32-bit variable is fetched by transferring 4 separate bytes across an 8-bit bus, requiring 4 separate instructions. The value of the variable could be modified inside the ISR if it is fired, during this process, therefore interrupts should be turned off during this process. Is that clear enough now ?

My objection to Serial is that interrupts get turned of for it's process temporarily, while for accuracy you would not want then turned off at all at any time, or you may miss an interrupt. If the interrupt gets triggered twice before it gets executed, it's callback will only be called once. Mind you with the ESP32 FIFO size being quite large, and the processing speed high, the chance of this happening is significantly reduced.

So am i, but more because i use the knob encoders to enter values, and each dead zone is a full cycle from both unconnected to both unconnected. Using PinChangeInterrupts i can run multiple encoders on an AVR at quite a low power consumption. For motor drives the simple counters like in reply #34 make more sense, since direction is known if you turn the motor on programmatically to begin with, you know in which direction it is going to go.

1 Like