Rotary encoder

Behaviour during clockwise rotation (CW)

00
10, INT0
00, 10, 00, ... 10 (1 ms)
11, INT1
10, 11, 10, ... 11 (1 ms)
01, INT0
11, 01, 11, ... 01 (1 ms)
00, INT1
01, 00, 01, ... 00 (1 ms)

Behaviour during counter clockwise rotation (CCW)

00
01, INT1
00, 01, 00, ... 01 (1 ms)
11, INT0
01, 11, 01, ... 11 (1 ms)
10, INT1
11, 10, 11, ... 10 (1 ms)
00, INT0
10, 00, 10, ... 00 (1 ms)

States

prev (P) = previous stable state [00,01,10,11]
next (N) = next stable state [00,01,10,11]

On interrupt (INT0 or INT1)

Get next state 2 ms after last interrupt.

Get rotation from P and N.

P  N
=========
00 10 CW
10 11 CW
11 01 CW
01 00 CW
00 01 CCW
01 11 CCW
11 10 CCW
10 00 CCW
00 11 N/A
00 00 N/A
01 10 N/A
01 01 N/A
10 01 N/A
10 10 N/A
11 00 N/A
11 11 N/A

Using only previous and interrupt.

Delay 1 ms in interrupt routine so that interupts is not triggered by bounces. Interrupts are automatically disabled in the interrupt routine.

Next state is also clearly defined from interupt pin. You don't need to read pins after delay.

P  IRQ         P'
=================
00 INT0 -> CW  10  
00 INT1 -> CCW 01
01 INT0 -> CCW 11
01 INT1 -> CW  00
10 INT0 -> CCW 00
10 INT1 -> CW  11
11 INT0 -> CW  01
11 INT1 -> CCW 10

Example code for NodeMCU 0.9 and ArduinoIDE

const int BLUE_LED = D10;
const int A_PIN = D1;
const int B_PIN = D2;
const int PUSH_PIN = D4;

void ICACHE_RAM_ATTR pushISR(); // ICACHE_RAM_ATTR force routine to IRAM
void ICACHE_RAM_ATTR ARotISR();
void ICACHE_RAM_ATTR BRotISR();

volatile byte pushInt = 0;      // volatile is needed in ISR
volatile byte ARotInt = 0;
volatile byte BRotInt = 0;

int pushCount = 0;
byte rotState = 0; // CW: 0, 2, 3, 1, ... CCW: 0, 1, 3, 2, ...
int rotation = 0;

byte state_a_change[] = {2, 3, 0, 1}; // new state after a change
byte state_b_change[] = {1, 0, 3, 2}; // b change

// #  Pr Ne
//    AB AB
// 
// 0  00 00 -
// 1  00 01 CCW
// 2  00 10 CW
// 3  00 11 -
//    
// 4  01 00 CW
// 5  01 01 -
// 6  01 10 -
// 7  01 11 CCW
//    
// 8  10 00 CCW
// 9  10 01 -
// 10 10 10 -
// 11 10 11 CW
//    
// 12 11 00 -
// 13 11 01 CW
// 14 11 10 CCW
// 15 11 11 -

const int CW = 1;     // clockwise rotation
const int CCW = -1;   // couter clockwise
const int N = 0;      // no rotation

int rot_inc[] = {N,CCW,CW,N, CW,N,N,CCW, CCW,N,N,CW, N,CW,CCW,N};

void setup() {
  pinMode(BLUE_LED, OUTPUT);
  pinMode(A_PIN, INPUT_PULLUP);
  pinMode(B_PIN, INPUT_PULLUP);
  pinMode(PUSH_PIN, INPUT_PULLUP);
  Serial.begin(115200);
  attachInterrupt(digitalPinToInterrupt(PUSH_PIN), pushISR, RISING);
  attachInterrupt(digitalPinToInterrupt(A_PIN), ARotISR, CHANGE);
  attachInterrupt(digitalPinToInterrupt(B_PIN), BRotISR, CHANGE);
  Serial.print("Setup done ...");
}

void pushISR() {
  pushInt++;
}

void ARotISR() {
  ARotInt++;
}

void BRotISR() {
  BRotInt++;
}

void blink() {
  digitalWrite(BLUE_LED, LOW);
  delay(50);
  digitalWrite(BLUE_LED, HIGH);
}

// the loop function runs over and over again forever
void loop() {
  if (pushInt > 0) {
    pushInt--;
    pushCount++;
    Serial.print("push count: ");
    Serial.println(pushCount);
    // blink();
  }
  if (ARotInt > 0) {
    noInterrupts();
    ARotInt--;
    byte newState = state_a_change[rotState];
    byte i = (rotState << 2) | newState;
    rotation += rot_inc[i];
    rotState = newState;
    Serial.print("A change, rotation: ");
    Serial.println(rotation);
    // blink();
    interrupts();
  }
  if (BRotInt > 0) {
    noInterrupts();
    BRotInt--;
    byte newState = state_b_change[rotState];
    byte i = (rotState << 2) | newState;
    rotation += rot_inc[i];
    rotState = newState;
    Serial.print("B change, rotation: ");
    Serial.println(rotation);
    // blink();
    interrupts();
  }
}

The version above does not work! The CHANGEDinterrupt detection generate too many interrupts and missed interrupts corrupts the counting.

An improved variant is shown below which only detect RISING changes on rotation and require two steps (pre-CW before CW and pre-CCW before CCW) to commit a count.

const int BLUE_LED = D10;
const int A_PIN = D1;
const int B_PIN = D2;
const int PUSH_PIN = D4;

void ICACHE_RAM_ATTR pushISR();   // ICACHE_RAM_ATTR force routine to IRAM
void ICACHE_RAM_ATTR ARotISR();
void ICACHE_RAM_ATTR BRotISR();
byte ICACHE_RAM_ATTR readRotPorts();

// Rotation states
const byte NONE = 0;
const byte PCW = 1;
const byte PCCW = 2;  // pre counter clockwise turn 
const byte CW = 3;
const byte CCW = 4;

volatile byte rotState = NONE; 

// Read rotation state
const byte A = 2;   // only A HIGH
const byte B = 1;
const byte AB = 3;  // A and B HIGH

volatile int pushCount = 0;   
volatile int rotation = 0;

volatile byte pushChanged = 0; 
volatile byte rotChanged = 0;

void setup() {
  pinMode(BLUE_LED, OUTPUT);
  pinMode(A_PIN, INPUT_PULLUP);
  pinMode(B_PIN, INPUT_PULLUP);
  pinMode(PUSH_PIN, INPUT_PULLUP);
  Serial.begin(115200);
  attachInterrupt(digitalPinToInterrupt(PUSH_PIN), pushISR, RISING);
  attachInterrupt(digitalPinToInterrupt(A_PIN), ARotISR, RISING);
  attachInterrupt(digitalPinToInterrupt(B_PIN), BRotISR, RISING);
  Serial.print("Setup done ...");
}

byte readRotPorts() {
  if (digitalRead(A_PIN) == HIGH) {
    if (digitalRead(B_PIN) == HIGH) {
      return AB;
    } else {
      return A;
    }
  } else {
    if (digitalRead(B_PIN) == HIGH) {
      return B;
    } else {
      return NONE;
    }
  }
}

void pushISR() {
  noInterrupts();
  pushCount++;
  pushChanged++;
  interrupts();
}

void ARotISR() {
  noInterrupts();
  byte rot = readRotPorts();
  if (rot == A) {
    rotState = PCCW;
  } else if (rotState == PCW && rot == AB) {
    rotation++;
    rotChanged++;
    rotState = NONE;
  }
  interrupts();
}

void BRotISR() {
  noInterrupts();
  byte rot = readRotPorts();
  if (rot == B) {
    rotState = PCW;
  } else if (rotState == PCCW && rot == AB) {
    rotation--;
    rotChanged++;
    rotState = NONE;
  }
  interrupts();
}

void blink() {
  digitalWrite(BLUE_LED, LOW);
  delay(10);
  digitalWrite(BLUE_LED, HIGH);
}

void loop() {
  if (pushChanged > 0) {
    pushChanged = 0;
    Serial.print("push count: ");
    Serial.println(pushCount);
    blink();
  }
  if (rotChanged > 0) {
    rotChanged = 0;
    Serial.print("rotation: ");
    Serial.println(rotation);
    blink();
  }
}

State diagram implemented in the program above can be visualized as follows.

NONE PCCW PCW CW rot++ CCW rot-- A rise, A read B rise, B read B rise, B read A rise, A read A rise, AB read B rise, AB read

References