Rotary encoder, messy signal?

Hello,

I'm using a rotary encoder to set a variable to a certain number. It's counting up and down depending on which way it's turning.

This work fine for the most part, except every now and then it either goes up two numbers at once, or go back one and forward one instead of just one forward. I copied the code from the arduino examples, and I've used several different sets of code to make sure the code is not the problem.

My guess is the signal coming from the A and B pins is not 100% 'digital' and it has some noise which causes the interrupt to see two changes when there really only was one.

Does that sound logical and is there a remedy?

Thanks a lot,
Harry

p.s. here is some of the code I'm using:

--
#define encoder0PinA 2 //interrupt 0 on pin 2
#define encoder0PinB 4
volatile int encoder0Pos = 0;

--
pinMode(encoder0PinA, INPUT);
pinMode(encoder0PinB, INPUT);
digitalWrite(encoder0PinA, HIGH); // turn on pullup resistor
digitalWrite(encoder0PinB, HIGH); // turn on pullup resistor
attachInterrupt(0, doEncoder0, CHANGE); // encoder interrupt 0 on pin 2

--
void doEncoder0() {
if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) {encoder0Pos--;}
else {encoder0Pos++;}

Serial.println(encoder0Pos, DEC);
}

These are classic symptoms of either contact bounce or not servicing the interrupt fast enough. Is it a contact encoder or is it optical? Have you got any capacitors on the input?

Please post the whole code (using the # button to provide right tags) as the snippets don't provide enough insight

Thanks for the quick replies!

I'm at home right now, so I'll post the code and more information about the encoder tomorrow when I get to work.

Right now there are no capacitors in the circuit. The A and B pins are connected to a 'high' input pin with the internal pull-up resistor and the common is directly connected to the ground.

I think the encoder is a contact encoder, but I'll have to check to make sure tomorrow. There's really nothing in my interrupt, except handling the encoder change and printing the new value to the serial port, which I imagine should be fine.

The contact bounce theory sounds very plausible, and after reading: http://www.elexp.com/t_bounc.htm I'm wondering if there's a way to fix it software based on the arduino? I know the delay function doesn't work within interrupts. Any common software solutions against contact bounce? Otherwise I'll have to go to town and get some capacitors. At least there's a fix! :slight_smile:

Posting more info tomorrow. Thanks again!
Harry

harryvermeulen:
I'm wondering if there's a way to fix it software based on the arduino?

Hey Harry,

I had the same problem with rotary encoder bouncing. I found a website which explains rotary encoders pretty good and has some software debouncing in the source code: Home - Technology In The Arts - Emerging Science and Technology Trends
It's not Arduino specific, but it is not very hard to port to Arduino.

Best regards,

Harald (<-- almost the same as Harry ;))

Alright, I'll go have a look at that!

This is the encoder I got: http://uk.farnell.com/alps/ec11b15242/encoder-rotary-11mm-switch/dp/1191733?Ntt=1191733

And here's my code: (some of it is commented out, since I haven't connected those electronic parts yet).

/* read a rotary encoder with interrupts
   Encoder hooked up with common to GROUND,
   encoder0PinA to pin 2, encoder0PinB to pin 4 (or pin 3 see below)
   it doesn't matter which encoder pin you use for A or B  

   uses Arduino pullups on A & B channel outputs
   turning on the pullups saves having to hook up resistors 
   to the A & B channel outputs 

*/ 

#define max_category 22
#define max_age 99

//encoder0
#define encoder0PinA 2  //interrupt 0 on pin 2
#define encoder0PinB 4

//encoder1
//#define encoder1PinA 3  //interrupt 1 on pin 3
//#define encoder1PinB 5

//rocker0
#define rocker0Pin 21   //interrupt 2 on pin 21

//keySwitch0
#define keySwitch0Pin 20 //interrupt 3 on pin 20

//button0
#define scanButtonPin 19 //interrupt 4 on pin 19
#define scanLedPin 6
#define scanTimeOut 3000

//button1
#define deleteButtonPin 18 //interrupt 5 on pin 18

//segment0
#define segment0PinA 43
#define segment0PinB 45
#define segment0PinC 27
#define segment0PinD 25
#define segment0PinE 23
#define segment0PinF 41
#define segment0PinG 39
#define segment0PinDP 29
int segment0Pins[8] = { 
                        segment0PinA,
                        segment0PinB,
                        segment0PinC,
                        segment0PinD,
                        segment0PinE,
                        segment0PinF,
                        segment0PinG,
                        segment0PinDP
                      };

//segment1
#define segment1PinA 51
#define segment1PinB 53
#define segment1PinC 35
#define segment1PinD 33
#define segment1PinE 31
#define segment1PinF 49
#define segment1PinG 47
#define segment1PinDP 37
int segment1Pins[8] = { 
                        segment1PinA,
                        segment1PinB,
                        segment1PinC,
                        segment1PinD,
                        segment1PinE,
                        segment1PinF,
                        segment1PinG,
                        segment1PinDP
                      };

/*
//segment2
#define segment2Pin1 38
#define segment2Pin2 39
#define segment2Pin3 40
#define segment2Pin4 41
#define segment2Pin5 42
#define segment2Pin6 43
#define segment2Pin7 44
#define segment2Pin8 45

//segment3
#define segment3Pin1 46
#define segment3Pin2 47
#define segment3Pin3 48
#define segment3Pin4 49
#define segment3Pin5 50
#define segment3Pin6 51
#define segment3Pin7 52
#define segment3Pin8 53
*/

//inverse
byte seven_seg_digits[10][7] = { 
                                { 0,0,0,0,0,0,1 },  // = 0
                                { 1,0,0,1,1,1,1 },  // = 1
                                { 0,0,1,0,0,1,0 },  // = 2
                                { 0,0,0,0,1,1,0 },  // = 3
                                { 1,0,0,1,1,0,0 },  // = 4
                                { 0,1,0,0,1,0,0 },  // = 5
                                { 0,1,0,0,0,0,0 },  // = 6
                                { 0,0,0,1,1,1,1 },  // = 7
                                { 0,0,0,0,0,0,0 },  // = 8
                                { 0,0,0,1,1,0,0 }   // = 9
                               };

volatile int encoder0Pos = 0;
volatile int encoder1Pos = 0;
volatile boolean rocker0Pos;
volatile boolean keySwitch0Pos;
volatile int fixCounter = 0;

void setup() { 
  //input / output
  setupEncoders();
  setupRocker();
  setup7Segments();
  setupScanButton();
  setupKeySwitch();
  setupDeleteButton();
  
  //serial connection
  Serial.begin(9600);
} 

void setupEncoders() {
  //encoder0
  pinMode(encoder0PinA, INPUT); 
  pinMode(encoder0PinB, INPUT); 
  digitalWrite(encoder0PinA, HIGH);         // turn on pullup resistor
  digitalWrite(encoder0PinB, HIGH);         // turn on pullup resistor
  attachInterrupt(0, doEncoder0, CHANGE);   // encoder interrupt 0 on pin 2
  
  /*
  //encoder1
  pinMode(encoder1PinA, INPUT); 
  pinMode(encoder1PinB, INPUT); 
  digitalWrite(encoder1PinA, HIGH);         // turn on pullup resistor
  digitalWrite(encoder1PinB, HIGH);         // turn on pullup resistor
  attachInterrupt(1, doEncoder1, CHANGE);   // encoder interrupt 1 on pin 3
  */
}

void setupRocker() {
  //rocker0
  pinMode(rocker0Pin, INPUT);
  digitalWrite(rocker0Pin, HIGH);           // turn on pullup resistor
  attachInterrupt(2, doRocker0, CHANGE);    // rocker interrupt 2 on pin 21
  rocker0Pos = digitalRead(rocker0Pin);     // can we do this here already? need to test it.
}

void setup7Segments() {
  for(int i = 0; i < 8; i++) {
    pinMode(segment0Pins[i], OUTPUT);
  }
  for(int i = 0; i < 8; i++) {
    pinMode(segment1Pins[i], OUTPUT);
  }   

  disableDots();  // start with the "dot" off
}

void setupScanButton() {
  pinMode(scanButtonPin, INPUT);
  digitalWrite(scanButtonPin, HIGH);
  pinMode(scanLedPin, OUTPUT);
  digitalWrite(scanLedPin, HIGH);
}

void setupKeySwitch() {
  pinMode(keySwitch0Pin, INPUT);
  digitalWrite(keySwitch0Pin, HIGH);           // turn on pullup resistor
  attachInterrupt(3, doKeySwitch, CHANGE);    // rocker interrupt 2 on pin 21
  keySwitch0Pos = digitalRead(keySwitch0Pin);     // can we do this here already? need to test it.
}

void setupDeleteButton() {
  pinMode(deleteButtonPin, INPUT);
  digitalWrite(deleteButtonPin, HIGH);
  attachInterrupt(5, doDeleteButton, FALLING);
}

void loop(){
  if(digitalRead(scanButtonPin) == LOW) {
    Serial.println("scanButtonPushed!");
    digitalWrite(scanLedPin, LOW);
    delay(scanTimeOut);
    digitalWrite(scanLedPin, HIGH);
  }
  
  delay(10);
  if(fixCounter < 100) { fixCounter++; }
  
}

//category
void doEncoder0() {
  if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) {encoder0Pos--;} 
  else {encoder0Pos++;}
  
  if(encoder0Pos < 0) {encoder0Pos = max_category;}
  if(encoder0Pos > max_category) {encoder0Pos = 0;}

  Serial.println(encoder0Pos, DEC);
  displayCategory(encoder0Pos);
}

//age
/*
void doEncoder1() { 
  if (digitalRead(encoder1PinA) == digitalRead(encoder1PinB)) {encoder1Pos++;} 
  else {encoder1Pos--;}
  
  if(encoder1Pos < 1) {encoder1Pos = max_age;}
  if(encoder1Pos > max_age) {encoder1Pos = 1;}
  
  Serial.println(encoder1Pos, DEC);
  displayAge(encoder1Pos);
}
*/

//gender
void doRocker0() {
  rocker0Pos = digitalRead(rocker0Pin);
  Serial.println(rocker0Pos, DEC);
}

//delete lock
void doKeySwitch() {
  keySwitch0Pos = digitalRead(keySwitch0Pin);
}

//delete
void doDeleteButton() {
  if(!keySwitch0Pos) {
    Serial.println("delete");
  }
}

void disableDots() {
  digitalWrite(segment0PinDP, HIGH);
  digitalWrite(segment1PinDP, HIGH);
  //digitalWrite(segment2PinDP, LOW);
  //digitalWrite(segment3PinDP, LOW);
}

void displayCategory(int category) {
  int firstNumber = floor(category / 10);
  int secondNumber = category % 10;
  sevenSegWrite(0, firstNumber);
  sevenSegWrite(1, secondNumber);
}

/*
void displayAge(int age) {
  int firstNumber = floor(age / 10);
  int secondNumber = age % 10;
  sevenSegWrite(2, firstNumber);
  sevenSegWrite(3, secondNumber);  
}
*/
  
void sevenSegWrite(int segment, byte digit) {
  switch(segment) {
    case 0:
      for(byte segCount = 0; segCount < 7; ++segCount) {
        digitalWrite(segment0Pins[segCount], seven_seg_digits[digit][segCount]);
      }
      break;
    case 1:
      for(byte segCount = 0; segCount < 7; ++segCount) {
        digitalWrite(segment1Pins[segCount], seven_seg_digits[digit][segCount]);
      }
      break;
  }
}

That's the code in the previously mentioned example about the encoder de-bouncing, but to be honest I have no clue what's going on there, hehe.

#include "sysdef.h"
#include "sys.h"
#include "quadenc_prv.h"

// ==========================================================================
// init

void quadenc_init(void)
{
	// init locals
	quadenc_channel0SearchPattern = 0;
	quadenc_channel1SearchPattern = 0;
	quadenc_channel0Status = 0xFF;
	quadenc_channel1Status = 0xFF;
	quadenc_buttonStatus = 0xFF;
	quadenc_state = QUADENC_STATE_IDLE;
	quadenc_transitionEventIndex = 0;
	quadenc_changeCounts = 0;
}
// ==========================================================================
// get last number of counts

void quadenc_getLastChangeCount(int_8 *counts)
{
	// disable interrupts
	// sys_disableInterrupts();

	// get how many counts changed since last poll
	*counts += quadenc_changeCounts;

	// zero it out
	quadenc_changeCounts = 0;

	// enable interrupts
	// sys_enableInterrupts();
}
// ==========================================================================
// ISR

void quadenc_isr(void)
{
	//
	// Debounce channel 0
	//

	// shift channel0 value into status. Save only N bits history
	quadenc_channel0Status = ((quadenc_channel0Status << 1) | RB0) & 0x7;

	// do lower 3 bits match search pattern?
	if (quadenc_channel0Status == quadenc_channel0SearchPattern)
	{
		// change search pattern
		quadenc_channel0SearchPattern = ((~quadenc_channel0SearchPattern) & 0x7);

		// declare debounced transition
		quadenc_transitionEvent = quadenc_channel0Status;
	}

	//
	// Debounce channel 1
	//

	// shift channel0 value into status. Save only N bits history
	quadenc_channel1Status = ((quadenc_channel1Status << 1) | RB1) & 0x7;

	// do lower 3 bits match search pattern?
	if (quadenc_channel1Status == quadenc_channel1SearchPattern)
	{
		// change search pattern
		quadenc_channel1SearchPattern = ((~quadenc_channel1SearchPattern) & 0x7);

		// declare debounced transition
		quadenc_transitionEvent = (quadenc_channel1Status | (1 << 3));
	}

	//
	// Check if transition forms an acceptable movement
	//

	// check if a transition occured
	if (quadenc_transitionEvent != QUADENC_NO_TRANSITION_EVENT)
	{
		// 
		// Idle state
		// 
		if (quadenc_state == QUADENC_STATE_IDLE)
		{
			// first value of CW transition?
			if (quadenc_transitionEvent == quadenc_cwTransitions[0])
			{
				// yup - enter state
				quadenc_state = QUADENC_STATE_DETECTING_CW;

				// next transition
				quadenc_transitionEventIndex++;
			}
			// first value of CCW transition?
			else if (quadenc_transitionEvent == quadenc_ccwTransitions[0])
			{
				// yup - enter state
				quadenc_state = QUADENC_STATE_DETECTING_CCW;

				// next transition
				quadenc_transitionEventIndex++;
			}
		}
		//
		// CW state
		// 
		else if (quadenc_state == QUADENC_STATE_DETECTING_CW)
		{
			// found next expected transition?
			if (quadenc_transitionEvent == quadenc_cwTransitions[quadenc_transitionEventIndex])
			{
				// increment it
				quadenc_transitionEventIndex++;

				// are we done?
				if (quadenc_transitionEventIndex == 4)
				{
					// decrement count
					quadenc_changeCounts++;

					// back to idle
					quadenc_state = QUADENC_STATE_IDLE;
					quadenc_transitionEventIndex = 0;
				}
			}
			else
			{
				// incorrect transition, back to idle
				quadenc_state = QUADENC_STATE_IDLE;
				quadenc_transitionEventIndex = 0;
			}
		}
		//
		// CCW state
		// 
		else if (quadenc_state == QUADENC_STATE_DETECTING_CCW)
		{
			// found next expected transition?
			if (quadenc_transitionEvent == quadenc_ccwTransitions[quadenc_transitionEventIndex])
			{
				// increment it
				quadenc_transitionEventIndex++;

				// are we done?
				if (quadenc_transitionEventIndex == 4)
				{
					// increment count
					quadenc_changeCounts--;

					// back to idle
					quadenc_state = QUADENC_STATE_IDLE;
					quadenc_transitionEventIndex = 0;
				}
			}
			else
			{
				// incorrect transition, back to idle
				quadenc_state = QUADENC_STATE_IDLE;
				quadenc_transitionEventIndex = 0;
			}
		}

		// zero out transition
		quadenc_transitionEvent = QUADENC_NO_TRANSITION_EVENT;
	}
}

I can see he's checking for a 'search pattern', but I don't understand what that is or how he's forming one. (after reading the tutorial I do understand what it is, but I'm still unsure how to form and use one in the arduino).

// change search pattern
quadenc_channel0SearchPattern = ((~quadenc_channel0SearchPattern) & 0x7);

There's also a de-bounce example on the arduino website:

/* 
 Debounce
 
 Each time the input pin goes from LOW to HIGH (e.g. because of a push-button
 press), the output pin is toggled from LOW to HIGH or HIGH to LOW.  There's
 a minimum delay between toggles to debounce the circuit (i.e. to ignore
 noise).  
 
 The circuit:
 * LED attached from pin 13 to ground
 * pushbutton attached from pin 2 to +5V
 * 10K resistor attached from pin 2 to ground
 
 * Note: On most Arduino boards, there is already an LED on the board
 connected to pin 13, so you don't need any extra components for this example.
 
 
 created 21 November 2006
 by David A. Mellis
 modified 3 Jul 2009
 by Limor Fried
 
This example code is in the public domain.
 
 http://www.arduino.cc/en/Tutorial/Debounce
 */

// constants won't change. They're used here to 
// set pin numbers:
const int buttonPin = 2;     // the number of the pushbutton pin
const int ledPin =  13;      // the number of the LED pin

// Variables will change:
int ledState = HIGH;         // the current state of the output pin
int buttonState;             // the current reading from the input pin
int lastButtonState = LOW;   // the previous reading from the input pin

// the following variables are long's because the time, measured in miliseconds,
// will quickly become a bigger number than can be stored in an int.
long lastDebounceTime = 0;  // the last time the output pin was toggled
long debounceDelay = 50;    // the debounce time; increase if the output flickers

void setup() {
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // read the state of the switch into a local variable:
  int reading = digitalRead(buttonPin);

  // check to see if you just pressed the button 
  // (i.e. the input went from LOW to HIGH),  and you've waited 
  // long enough since the last press to ignore any noise:  

  // If the switch changed, due to noise or pressing:
  if (reading != lastButtonState) {
    // reset the debouncing timer
    lastDebounceTime = millis();
  } 
  
  if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer
    // than the debounce delay, so take it as the actual current state:
    buttonState = reading;
  }
  
  // set the LED using the state of the button:
  digitalWrite(ledPin, buttonState);

  // save the reading.  Next time through the loop,
  // it'll be the lastButtonState:
  lastButtonState = reading;
}

But that's running in the loop() and can use millis() to keep track of time, which I think doesn't work in interrupts either.

Forget a software solution, that only masks the problem and reduces the maximum speed. This is the way to do it:-

You can miss out the 74LS74 if you want to use the software to sort things out, but the capacitors are a must and the schimitt buffers help as well.

Notwithstanding the sledgehammer-to-crack-a-walnut approach above, have a look at the code here

http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino

It is a beautifully simple, elegant and very fast way of reading an encoder that will catch errors that you describe.

With respect to your code, it's considered good practise to make your interrupt routines as small and fast as possible - this means you should just update the position counter and get out. The other stuff (roll-over), serial.printing and displayAge (which is itself not a fast function) is best done outside the interrupt routine.

void doEncoder1() { 
  if (digitalRead(encoder1PinA) == digitalRead(encoder1PinB)) {encoder1Pos++;} 
  else {encoder1Pos--;}
  
  if(encoder1Pos < 1) {encoder1Pos = max_age;}
  if(encoder1Pos > max_age) {encoder1Pos = 1;}
  
  Serial.println(encoder1Pos, DEC);
  displayAge(encoder1Pos);
}

Great! I got it working nicely using the software solution. That's quite a nifty way of checking for bounces :slight_smile:

For some reason it's counting two numbers instead of one per click, but it's constant and reliable, not random like before. I'm just dividing the number by 2 and then it all seems to work.

Thanks a lot for the help! I really appreciate it.

Harry

For some reason it's counting two numbers instead of one per click, but it's constant and reliable,

That is what it is supposed to do, look at the signal translations for a click, you count when it goes high and again when it is low.

I solved the problem using a 2 ms delay between the interrupt and the moment I digitalRead the pins.
Since you cannot use a delay() in an interrupt service routine i did it simply by addressing a new function:

In setup:
attachInterrupt(0, onderbreking, FALLING); // Interrupt 0 on pin 2

In the main loop:

void onderbreking() {
encode();
}

void encode() {
delay(2);
if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB))
encoder = 2; // clockwise
else
encoder = 1; // counterclockwise
}

Did you try that code on a real Arduino? I doubt if it works because the delay is still in the interrupt routine.

LOL, nice try Arthur, but calling delay() from a function called by an ISR is no different than using delay() in the ISR directly. The code path is the same (except for a little additional overhead for setting up before and tearing down after the jump.) :wink:

I know some people prefer software debounce, but personally I agree it's a band-aid to the real problem. Grumpy's solution is solid engineering and is going directly into my stash, but it is a lot of parts for a simple project input. OP, have you tried the caps-n-resistors approach?

With a quadrature signal so long as only the changing signal has bounce (not both at once), the
accounting can be exact and no debouncing is needed - the counter will oscillate between two
neighbouring readings, but you just arrange to ignore 1 bit changes in the rest of the code.

You must handle CHANGE interrupts on at least one of the signals to see enough transitions to
prevent misaccounting due to bounce. For full resolution you'd handle CHANGE on both signals
and the ISR would give full resolution (4 steps per quadrature cycle).

With a mechanical switch it is possible for both sets of contacts to be dirty and glitch at the same
time, in which case quadrature assumption is broken and the hardware filtering method will probably
get better results by removing the high frequency noise preferentially. If also means the ISR won't
be called dozens of times on each transition which can only help.

SirNickity:
OP, have you tried the caps-n-resistors approach?

I have not myself.

I believe it is the most proper solution to the bouncing problem, however also the most complex. To do it properly, you would have to measure the bounce-time and calculate the required components to de-bounce the signal. Or play around with different values until it "seems to work".

Seeing as my time was limited (it being a work thing) I went with the software solution found on this website: http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino

/* Rotary encoder read example */
#define ENC_A 14
#define ENC_B 15
#define ENC_PORT PINC
 
void setup()
{
  /* Setup encoder pins as inputs */
  pinMode(ENC_A, INPUT);
  digitalWrite(ENC_A, HIGH);
  pinMode(ENC_B, INPUT);
  digitalWrite(ENC_B, HIGH);
  Serial.begin (115200);
  Serial.println("Start");
}
 
void loop()
{
 static uint8_t counter = 0;      //this variable will be changed by encoder input
 int8_t tmpdata;
 /**/
  tmpdata = read_encoder();
  if( tmpdata ) {
    Serial.print("Counter value: ");
    Serial.println(counter, DEC);
    counter += tmpdata;
  }
}
 
/* returns change in encoder state (-1,0,1) */
int8_t read_encoder()
{
  static int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
  static uint8_t old_AB = 0;
  /**/
  old_AB <<= 2;                   //remember previous state
  old_AB |= ( ENC_PORT & 0x03 );  //add current state
  return ( enc_states[( old_AB & 0x0f )]);
}

Note that it does not actually de-bounce the signal, meaning you still get a function call for every bounce. It simply filters out the calls to the function that have an incorrect pattern, which would lead to a faulty "count" so to speak.

If I were to print the function calls to the console, turning the dial up one click would give me something like the following:

+1
-1
+1
-1
0
+1
-1
0
+1

Which would lead to the number going up by one value, which is what I wanted in the end.

In conclusion: The software "de-bounce" works for a simple counter, but not for any case where the amount of calls to the function is important.

harryvermeulen:

SirNickity:
OP, have you tried the caps-n-resistors approach?

I have not myself.

I believe it is the most proper solution to the bouncing problem, however also the most complex. To do it properly, you would have to measure the bounce-time and calculate the required components to de-bounce the signal. Or play around with different values until it "seems to work".

Even if you did all that it wouldn't achieve anything which can't be done more easily in software.

The real fault lies with the library being used. Using interrupts to read switches is hardly ever a good idea. To read a switch, all you need to do is define a minimum allowed time between transitions. Ignore any transitions which occur in that time frame after the first transition is detected. 1 or 2 milliseconds usually works.

A rotary encoder doesn't even need that. The use of Gray code automatically debounces the switches for you.

harryvermeulen:
Seeing as my time was limited (it being a work thing) I went with the software solution found on this website: http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino

Still not correct, the author doesn't really understand what Gray code is for.

You should wait for both pins to go high, then both low, then look at which one went high first.

The use of Gray code automatically debounces the switches for you.

But and this is a big but

Only, if as MarkT said:-

With a mechanical switch it is possible for both sets of contacts to be dirty and glitch at the same
time, in which case quadrature assumption is broken and the hardware filtering method will probably
get better results by removing the high frequency noise preferentially.

You seem to be discounting the actual practicalities of electronics and concentrating on the theory of what should be. Sure in theory you can just use the proprieties of the Gray code to have the software ignore contact bounces. But I have found in practice that this is not always enough.

Mechanical rotary encoders should be read by polling them about every 2ms, not using interrupts. Interrupts should be used if polling is too slow and the encoder is bounce-free (for example, an optical encoder attached to a motor). The code I use to read rotary encoders is at GitHub - dc42/arduino: Reusable modules, drivers and patches for the Arduino platform. If you insist on using interrupts, then I think you will need hardware debouncing.