Debounce Rotary Encoder

Hey there, I'm trying to get this little rotary encoder to not be so glitchy and I'm at a loss. I've had the "best" luck through basic wiring (only pullup resistors on A/B), but it's still pretty finicky getting it to land on a number you want.
Has anyone had any luck with them and what method did you use to get it to increase/decrease smoothly?

I read about a hardware solution with something called a Schmitt Trigger, but could only find that available in a giant 14 pin chip and I'd like to try and keep my project small. Seemed a little overkill to debug one small component.

I've tried several coding debounces I found on the internet and haven't had much luck with them, though I'll admit I felt a little in over my head, and again, the regular encoder read code was the one that gave "best" results.

I did try this circuit and, like the software solutions, it didn't seem to have much positive effect.

Here is my code and I included both the regular encoder read code(//commented out) and the debounce code. Which doesn't decrease and actually slowly increases as the encoder glitches upward.

Thank you to anyone with experience with these little guys or anyone willing to offer suggestions!

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

int Buzzer = 7;
int minutes_change=0;
int minutes=0;

int Current_Frame=1;

int buttonPin = 3;

//encoder pins and variables
int clkPin = 2;
int dtPin = 8;

int encoderCount = 0;
int clkPinLast = LOW;
int clkPinCurrent = LOW;

//////////////////
//States:       // 
//0 =reset      //
//1 =setup      //
//2 =countdown  //
///////////////////////

int State=0;
long Timestamp_Button_Pressed;

unsigned long Countdown_start;

// Declaration for SSD1306 display connected using software SPI 
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

//Images:

// 'TIME CHECK', 128x32px
const unsigned char TIMES_UP [] PROGMEM = {
0xdf, 0xff, 0xff, 0xef, 0xef, 0xef, 0xff, 0xf7, 0xf7, 0xf7, 0xf7, 0xff, 0xfb, 0xfb, 0xfb, 0xfb, 
0x8f, 0xcf, 0xcf, 0xc7, 0xc7, 0xc7, 0xe7, 0xe7, 0xe3, 0xe3, 0xe3, 0xf3, 0xf3, 0xf1, 0xf1, 0xf1, 
0xcf, 0xcf, 0xcf, 0xcf, 0xc7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe3, 0xf3, 0xf3, 0xf3, 0xf3, 0xf1, 0xf9, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1c, 0x38, 0x7e, 0x3c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1c, 0x38, 0x7c, 0x3c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1c, 0x38, 0x7c, 0x3c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1c, 0x38, 0x3c, 0x3c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x38, 0x38, 0x1c, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x38, 0x38, 0x1c, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x38, 0x38, 0x1c, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x38, 0x38, 0x1c, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x38, 0x10, 0x1c, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x38, 0x10, 0x1c, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x30, 0x10, 0x1c, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x30, 0x10, 0x1c, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x30, 0x00, 0x1c, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x30, 0x80, 0x1c, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x30, 0x82, 0x1c, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x30, 0x82, 0x1c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x30, 0xc2, 0x1c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x30, 0xc6, 0x1c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x78, 0xe7, 0x1c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0xcf, 0xcf, 0xcf, 0xcf, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe3, 0xf3, 0xf3, 0xf3, 0xf3, 0xf1, 0xf9, 
0x8f, 0xcf, 0xcf, 0xc7, 0xc7, 0xc7, 0xe7, 0xe7, 0xe3, 0xe3, 0xe3, 0xf3, 0xf3, 0xf1, 0xf1, 0xf1, 
0xdf, 0xdf, 0xef, 0xef, 0xef, 0xef, 0xef, 0xf7, 0xf7, 0xf7, 0xf7, 0xf7, 0xfb, 0xfb, 0xfb, 0xfb
};

//Functions:


////////////////////////////START BUTTON////////////////////////////
void Press_B_Button(){
  if (millis() - Timestamp_Button_Pressed>200){
    if (State==1){//add minutes mode
      Countdown_start=millis();
      State=2; //start countdown mode

    }
    else{//if state is 2(count) then go to state 1(add minute) and "minutes"=0
      State=0;//reset mode
      delay(2000);    
    }
  }
}
///////////////BUTTON DEBOUNCE///////////////
long debouncing_time = 15; //Debouncing Time in Milliseconds
volatile unsigned long last_micros;

void debounceInterruptB() {
  if((long)(micros() - last_micros) >= debouncing_time * 1000) {
    Press_B_Button();
    last_micros = micros();
  }
}

////////////////////////////RING BELL////////////////////////////
void Ring_Bell(int Frame =0){
  display.clearDisplay();
    int dimmerValue = analogRead(A6);
    if (dimmerValue<=50){
    display.clearDisplay();
    display.invertDisplay(true);
    }
    else{
    display.clearDisplay();
    display.invertDisplay(false);
    }

  if (Frame==1||2||3||4)display.drawBitmap(0,0,TIMES_UP,128,32,1);
  display.display();
  if (Frame>1){
    delay(200);
    digitalWrite(Buzzer,LOW);
  }
  else {
    digitalWrite(Buzzer,HIGH);
    delay(1000); 
  }
}

////////////////////////////CURRENT TIME////////////////////////////
String CurrentTime(unsigned long MsLeft){
  String Result;
  int M;
  int S;
  M=(long)MsLeft/60000;
  if (M<10) Result=(String)"0"+ M + ":";else Result=(String)M+":";
  S=(long)((MsLeft-M*60000)/1000);
  if (S<10) Result=(String)Result + "0"+ S ;else Result=(String)Result +S;
  return Result;
}

////////////////////////////BRIGHTNESS////////////////////////////
void setBrightness(int contr){
    int prech;
    int brigh; 
    switch (contr){
      case 000 ... 255: prech= 0; brigh= contr; break;
     //case 401 ... 411: prech=16; brigh= contr; break;
      default: prech= 16; brigh= 255; break;}
      
    display.ssd1306_command(SSD1306_SETPRECHARGE);      
    display.ssd1306_command(prech);                            
    display.ssd1306_command(SSD1306_SETCONTRAST);         
    display.ssd1306_command(brigh);                           
}

//////////////////////////////////////////////////////////
//                     SETUP                            //
//////////////////////////////////////////////////////////

void setup() {

  Timestamp_Button_Pressed=millis();


  
//Encoder pin setup:
  pinMode(clkPin, INPUT);
  pinMode(dtPin, INPUT);

//Button pin setup:
  pinMode(buttonPin, INPUT_PULLUP); //init button A pin to input
  digitalWrite(buttonPin, HIGH);
  attachInterrupt(digitalPinToInterrupt(buttonPin), debounceInterruptB,RISING);
  
//Buzzer pin setup:
  pinMode(Buzzer, OUTPUT);
  digitalWrite(Buzzer, HIGH);
  int Frame =0;
  
//OLED Pin setup:
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3c
  display.clearDisplay();
  display.display();
}


/////////////variables for debounced rotary
static uint8_t prevNextCode = 0;
static uint16_t store=0;



/////////////////////////////////////////////////////////////////////////////////
//                                  LOOP                                       //
/////////////////////////////////////////////////////////////////////////////////

void loop() {
 //Set contrast level
  int dimmerValue = analogRead(A6);
  if (dimmerValue <=600){
  setBrightness(dimmerValue);
  }


///////////////////RESET/////////////////////////
  if (State==0){//reset state
    digitalWrite(Buzzer,HIGH);
    minutes = 0;
    display.invertDisplay(false);
    display.clearDisplay();
    display.setTextSize(2);            
    display.setCursor(5,0);
    display.setTextColor(SSD1306_WHITE);
    display.println((String)minutes+" min");
    display.display();
    State=1;
  }





//////////////////////////////////    SET TIME STATE    ///////////////////////////
//vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv  DEBOUNCED Encoder  vvvvvvvvvvvvvvvvvvvvvvvvvvvv

static int8_t minutes,val;


   if( val=read_rotary() ) {
      minutes +=val;
      if(minutes<0){
        minutes=1;
      }

      Serial.print(minutes);Serial.print(" ");
      display.clearDisplay();
      display.setTextSize(2);            
      display.setCursor(5,0);
      display.setTextColor(SSD1306_WHITE);
      display.println((String)minutes+" min");
      display.display();

   }
///^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//////////////////////////////////////////////////////////////////////////////////




////////////////////////////////     SET TIME STATE      /////////////////////////////
//vvvvvvvvvvvvvvvvvvvvvvvvvvvvv *NOT* DEBOUNCED Encoder  vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/*  if (State==1){//set time mode
    digitalWrite(Buzzer,HIGH);

   clkPinCurrent = digitalRead(clkPin);
    if ((clkPinLast == LOW) && (clkPinCurrent == HIGH)) {
      if (digitalRead(dtPin) == HIGH) {
        if(minutes<=0){// Minutes-- unless minutes is 0, then don't count below 0
        minutes=0;
        }
        else {
        minutes--;
        }
      }
      else {
      minutes++;
      }
      display.clearDisplay();
      display.setTextSize(2);            
      display.setCursor(5,0);
      display.setTextColor(SSD1306_WHITE);
      display.println((String)minutes+" min");
      display.display();
    }
      clkPinLast = clkPinCurrent;
  }
  */
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//////////////////////////////////////////////////////////////////////////////////



  
///////////////////////Countdown State/////////////////////////////
  if(State==2){
    if(minutes*60000 >(millis()-Countdown_start)){
      display.clearDisplay();
      display.setCursor(5,0);
      display.setTextSize(4);            
      display.setTextColor(SSD1306_WHITE);
      display.println(CurrentTime((minutes*60000-(millis()-Countdown_start)))); 
      display.display();
    } 
    else {
     Ring_Bell(Current_Frame); 
     Current_Frame=Current_Frame+1;
     if (Current_Frame==5) Current_Frame=1;
    }
  }
}

////////////////////////DEBOUNCED READ ROTARY FUNCTION/////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
// A vald CW or  CCW move returns 1, invalid returns 0.
int8_t read_rotary() {
  static int8_t rot_enc_table[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0};

  prevNextCode <<= 2;
  if (digitalRead(dtPin)) prevNextCode |= 0x02;
  if (digitalRead(clkPin)) prevNextCode |= 0x01;
  prevNextCode &= 0x0f;

   // If valid then store as 16 bit data.
   if  (rot_enc_table[prevNextCode] ) {
      store <<= 4;
      store |= prevNextCode;
      if ((store&0xff)==0x2b) return -1;
      if ((store&0xff)==0x17) return 1;
   }
   return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

Hello @jugglebug00

You don't need to debounce a rotary encoder: Reading rotary encoders as a state machine

1 Like

If you can dedicate two interrupts to be used by the rotary-encoder you can use the NewEncoder-library from user gfvalvo

My experience so far: 100% reliable even with the bounciest mechanical switches rotary-encoders.

here is a democode

#include "NewEncoder.h"

const byte EncChA_Pin = 2;
const byte EncChB_Pin = 3;
const int minVal = -20;
const int maxVal =  20;
const int startVal = 0;

// Pins 2 and 3 should work for many processors, including Uno. See README for meaning of constructor arguments.
// Use FULL_PULSE for encoders that produce one complete quadrature pulse per detnet, such as: https://www.adafruit.com/product/377
// Use HALF_PULSE for endoders that produce one complete quadrature pulse for every two detents, such as: https://www.mouser.com/ProductDetail/alps/ec11e15244g1/?qs=YMSFtX0bdJDiV4LBO61anw==&countrycode=US&currencycode=USD

NewEncoder myEncoderObject(EncChA_Pin, EncChB_Pin, minVal, maxVal, startVal, FULL_PULSE);

int16_t currentValue;
int16_t prevEncoderValue;

void setup() {
  // myEncState is a variable of type EncoderState
  // EncoderState is a structured variable that has two "simple" variables
  // .currentValue which is type int16_t
  // (16 bit signed integer valuerange -36767 to 36767)
  // currentValue counts up / down with each pulse created through rotating the encoder
  // and
  // .currentClick which is of type "EncoderClick"
  // the variable type "EncoderClick" can have just 3 values
  // NoClick, DownClick, UpClick where "click" means a "pulse" created through rotating the encoder
  NewEncoder::EncoderState myEncState;

  Serial.begin(115200);
  delay(2000);
  Serial.println("Starting");

  if (!myEncoderObject.begin()) {
    Serial.println("Encoder Failed to Start. Check pin assignments and available interrupts. Aborting.");
    while (1) {
      yield();
    }
  } else {
    // store values of currentValue and EncoderClick into variable myEncState
    myEncoderObject.getState(myEncState);
    Serial.print("Encoder Successfully Started at value = ");
    prevEncoderValue = myEncState.currentValue;
    Serial.println(prevEncoderValue);
  }
}

void loop() {
  NewEncoder::EncoderState myCurrentEncoderState;

  // store actual values into variable myCurrentEncoderState
  if (myEncoderObject.getState(myCurrentEncoderState)) {
    Serial.print("Encoder: ");
    currentValue = myCurrentEncoderState.currentValue;

    // if currentValue has REALLY changed print new currentValue
    if (currentValue != prevEncoderValue) {
      Serial.println(currentValue);
      prevEncoderValue = currentValue;


      // if currentValue stayed the same because the number is at upper/lower limit
      // check if encoder was rotated by using the UpClick / DownClick-values
    } else
      switch (myCurrentEncoderState.currentClick) {
        case NewEncoder::UpClick:
          Serial.println("at upper limit.");
          break;

        case NewEncoder::DownClick:
          Serial.println("at lower limit.");
          break;

        default:
          break;
      }
  }
}

best regards Stefan

Thank you, very enlightening. That run time discrepancy may explain why I would get better results when I isolate the encoder code and it glitches again when I try and insert the same code back in my project code.

I did try your code and wasn't able to get it working:
(RIGHT TURN)-2, -1, 0, -1, 0, -1, 0, -1, -2, -3, -2, -3, -2, -1, 0, 1, 0, 1, 0, -1, 0, -1, 0, -1, 0, 1, 0, 1, 0, -1, 0, -1, 0, 1, 2, 1, 2, 3, 2, 1, 0, 1, 2, 1, 0,(LEFT TURN) 1, 2, 3, 4, 3, 4, 3, 4, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 11, 10, 9, 10, 9, 8, 7, 8, 7, 8, 9

I'm thinking, between my apparent poor choice in hardware and my lack of deep understanding of the coding, I may be getting out of my depth and may just try a more tried and true encoder unit or I may try a hardware filter solution if I want something less finicky.

I tried your demo code and library on this encoder and I got the same terrible results:
-1, 0, -1, -2, -3, -4, -3, -2, -3, -2, -1, 0, -1, 0, -1, 0, -1, -2, -1, -2, -3, -4, -3, -4, -3, -4, -5, -6, -5, -6, -5, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -12, -11, -12, -13, -14, -13, -14, -13, -14, -15, -14, -13, -14, -15, -14, -15, -14, -15, -16, -17, -16, -15, -14, -13, -12, -13, -12, -11, -10, -11, -10, -11, -10, -9, -8, -9, -8, -9, -10, -11, -10, -11, -12, -13, -14, -15, -16, -17, -16, -15, -16, -15, -14, -13, -12, -13, -14, -15, -14, -15, -16, -17, -16, -15, -14, -15, -16, -17, -18, -19, -20, -21, -22, -21, -22, -23, -22, -23, -22, -23, -22, -23, -24, -25, -26, -27, -26, -27, -28, -29, -28, -29, -28, -27, -26, -27, -26, -25, -26, -27, -26, -27, -28, -27, -28, -27, -26, -27, -26, -27, -26, -25, -24, -23, -22, -21, -22, -23, -24, -25, -26, -25, -24, -23, -22, -23, -22, -23, -22, -21, -20, -21, -22, -21, -22, -23, -22, -23, -24, -25, -24

I get "better" results with the basic encoder code. I'm beginning to think these units are the problem. I've tried so many coding solutions and the best results are still finicky. Any suggestions for a very small quality encoder?

It would be very interesting to know which exact type of microcontroller you are using.
Me personally those cheap KY-40 RE work pretty good with the "standard"encoder.h-library if the code is running on Arduino Unos. But very unreliable on faster microcontrollers like the ESP8266, ESP32 or teensy

I'm using the newEncoder-library very successfully with this cheapest KY-040 rotary encoders.

best regards Stefan

I have pretty good luck with just putting 0.1uF ceramic caps from the A, B and SW terminals to ground to do hardware debounce. Wire the commons to ground and the A,B and SW terminals to inputs set to pinMode INPUT_PULLUP.

I found that using the Raspberry Pi Pico, than a simple 0.1uF (ceramic) to ground on all three signal wires was enough to get them to work reliably. But using an interrupt is a must.
The KY-40 includes pull up resistors so no need to add any extra ones. It looks like @jugglebug00 system is the same because of the 5V input. The only reason for that is to act as a source of voltage for the internal pull up resistors. So remove R1 to R5 from that circuit you posted.

I would recommend the Rotary Library
from Encoder Library, for Measuring Quadarature Encoded Position or Rotation Signals

Do keep in mind that the purpose of the tutorial was to illustrate that you don't need to debounce a rotary encoder. The code illustrates what I wanted to show but it is imperfect. The issue is you can get close to1kHz from a rotary encoder without much difficulty if you turn it fast enough. I think the serial prints in the demonstration code are enough to disrupt the reading as you found. Is my code reliable if you turn the encoder slowly?

@StefanL38 's advice to use a library is probably best. If you still have problems after that then I suspect you have something else wrong.

It's possible your encoders are so bad that they won't work well with anything. The state table method generally works very well, even with encoders that bounce a lot. But if the switches in the encoder continue to bounce long after they should have settled down, there may be no way to produce good results. I've had good results with the KY-40 modules.

Below is a long-winded alternate version of the lookup table method. It enables only one interrupt at a time, so most bounces don't trigger interrupts at all. Anyway, you might give it a try to see if it makes any difference. It sends the current count to the serial monitor along with the number of interrupts which have occurred since the last detent, which ideally should be 4 or 2 depending on the type of encoder you have.

/*
Alternate lookup table method - one external interrupt enabled at a time.

This application demonstrates an external interrupt servicing routine
for a rotary encoder.  This code specifically applies to Arduinos using the
Atmega328P microcontroller, including the Uno, Nano and Pro Mini, which have
external interrupts only on pins D2 and D3.

An external interrupt is enabled on only one pin at a time.  When an interrupt
is triggered on that pin, we immediately switch the interrupt to the other pin.
So any bouncing which may occur on the first pin will not trigger any interrupts.
That can reduce the number of interrupts that must be serviced.

However, this causes a problem when the direction changes because we are watching
the wrong pin.  The solution is to include a fifth bit in the lookup table which is
the indentity of the interrupting pin.  With the fifth bit, the lookup table is
expanded to 32 bytes.

The code also replaces the value of the interrupting pin read from the port with
the value we know it must have had when generating the interrupt.  This eliminates
false readings caused by bouncing during the delay which always exists between
triggering the interrupt and reading the port.

With the appropriate "encoderType" definition, this method will work for encoders with
the same number of pulses as detents per revolution (Type 0), as well as for those
with half as many pulses as detents (Type 1).  In either case, the code produces
one tick per detent.  For Type 0, that is only when both switches are open.
For Type 1 encoders, switches can be either both open or both closed at a detent.

The code displays the cumulative value of the encoder on the serial monitor at each
detent, followed by the number of interrupts serviced since the last detent, which
ideally would be "4" for a Type 0 encoder, and "2" for a Type 1 - one interrupt for
each transition, with no interrupts generated for bounces.

In the ISR, direct register access is used to read the port and manage the interrupt enables.
*/

const int aPIN = 2;                     // encoder pins
const int bPIN = 3;

/*  This section defines the port being used for the encoder pins, along with the external interrupt 
    registers.  Port D contains the external interrupt pins D2 and D3.
*/

#define PORT PIND                       // port input register (port D includes D2 and D3)
#define enableREG EIMSK                 // enable external interrupts
#define edgeREG EICRA                   // edge register
#define flagsREG EIFR                   // flags register

const byte encoderType = 0;             // encoder with equal # of detents & pulses per rev
//const byte encoderType = 1;             // encoder with  pulses = detents/2. pick one, commment out the other

const int THRESH =(4-(2*encoderType));  // transitions needed to recognize a tick - type 0 = 4, type 1 = 2

const byte CWPIN = bit(aPIN);           // bit value for switch that leads on CW rotation (D2 = 0b100)
const byte CCWPIN = bit(bPIN);          // bit value for switch that leads on CCW rotation (D3 = 0b1000)
const byte PINS = CWPIN + CCWPIN;       // sum of bit values of the encoder pins = 0b1100
const byte ZEERO = 0x80;                // "byte" data type doesn't do negative, so make 128 = zero

byte EDGE;                              // the edge direction of the next interrupt
byte ENABLE;                            // the pin with interrupt enabled 
byte CURRENT;                           // the current state of the switches
byte TOTAL = ZEERO;                     // accumulated transitions since last tick (0x80 = none)
byte INDEX = 0;                         // Index into lookup state table
byte NUMINTS = 0;                       // # interrupts since last detent
int Setting = 0;                        // current accumulated value set by rotary encoder

volatile byte tickArray[256];           // circular buffer of ticks and interrupts per tick/detent
volatile byte beginTICK = 0;            // pointers to beginning and ending of circular buffer
volatile byte endTICK = 0;

// The table is now 32 bytes long so as to include the identity of the pin currently interrupting
// The +2 and -2 entries are for a change of direction.

int ENCTABLE[]  = {0,1,0,-2,-1,0,2,0,0,2,0,-1,-2,0,1,0,0,0,-1,2,0,0,-2,1,1,-2,0,0,2,-1,0,0};

void setup() {

  Serial.begin(115200);
  pinMode(aPIN, INPUT);                 // set up encoder pins as INPUT.  Assumes external 10K pullups
  pinMode(bPIN, INPUT);

// Enable external interrupts on D2 and D3, both on RISING edge, both using the same ISR

  noInterrupts();
  flagsREG = 3;                         // clear any flags on both pins
  attachInterrupt(digitalPinToInterrupt(aPIN),RotaryISR, RISING);
  attachInterrupt(digitalPinToInterrupt(bPIN),RotaryISR, RISING);
  flagsREG = 3;                         // clear any flags on both pins
  interrupts();

  EDGE = PINS;                          // identifies current interrupt as falling or rising
                                        //    assume current state is low, so any change will be rising
  if(PORT & PINS) {                     // but if actual current state is already high,
    EDGE = 0;                           //    make EDGE low
    edgeREG = 0b1010;                   //    and change register to both FALLING
    INDEX = 3;                          //    and make "current" bits of INDEX match the current high state
  }

  ENABLE = CWPIN;                       // our copy of enabled pin - as pin value
  enableREG = 1;                        // enable only CWPIN interrupt in mask register - bit zero
}

void loop() {

  if(beginTICK != endTICK) {            // if anything in circular buffer

    beginTICK++;
    if(tickArray[beginTICK] == 1) Setting ++; //  print Setting
    else Setting--;

    Serial.print(Setting); Serial.print(" ");
    beginTICK++;
    Serial.println(tickArray[beginTICK]); // print number of interrupts since last detent
  }
}

void RotaryISR() {                      // external interrupts service routine. interrupts
                                        //     automatically disabled during execution

  CURRENT = PORT & PINS;                // read the entire port, mask out all but our pins

/* Time passes between the interrupt and the reading of the port.  So if there is bouncing,
the read value of the interrupting pin may be wrong.  But we know it must be the EDGE state.
So in CURRENT, we clear the bit that just caused the interrupt, and replace it with the EDGE
value. The non-interrupting pin is assumed to be stable, with a valid read. */

  CURRENT &= ~ENABLE;                   // clear the bit that just caused the interrupt
  CURRENT |= (EDGE & ENABLE);           // OR the EDGE value back in

  INDEX     = INDEX << 2;               // Shift previous state left 2 bits (0 in)
  if(CURRENT & CWPIN) bitSet(INDEX,0);  // If CW is high, set INDEX bit 0
  if(CURRENT & CCWPIN) bitSet(INDEX,1); // If CCW is high, set INDEX bit 1
  INDEX &= 15;                          // Mask out all but prev and current.  bit 4 now zero
  if(ENABLE & CCWPIN) bitSet(INDEX,4);  // if CCWPIN is the current enabled interrupt, set bit 4

// INDEX is now a five-bit index into the 32-byte ENCTABLE state table.

  TOTAL += ENCTABLE[INDEX];             //Accumulate transitions

  NUMINTS++;                            // number of interrupts - just for this demo - not normally needed

  if((CURRENT == PINS) || ((CURRENT == 0) && encoderType)) {  //A valid tick can occur only at a detent

// If we have a valid number of TOTAL transitions at a detent, add the tick direction and number
// of interrupts to the print buffer.  The MAIN loop will update the Setting total and print everything.

    if(TOTAL == (ZEERO + THRESH)) {
      endTICK++;
      tickArray[endTICK] = 1;

      endTICK++;
      tickArray[endTICK] = NUMINTS;
      NUMINTS = 0;
    }

    else if(TOTAL == (ZEERO - THRESH)) {
      endTICK++;
      tickArray[endTICK] = 0xFF;

      endTICK++;
      tickArray[endTICK] = NUMINTS;
      NUMINTS = 0;
    }

    TOTAL = ZEERO;                      // Always reset TOTAL to 0x80 at detent
  }

  if(CURRENT == EDGE) {
    EDGE ^= PINS;                       // having reached EDGE state, now switch EDGE to opposite
    edgeREG ^= 5;                       // flip in register too - bits 0 and 2
  }
  ENABLE ^= PINS;                       // switch interrupt to other pin - our copy
  enableREG ^= 3;                       // switch register too - bits 0 and 1
  flagsREG |= 3;                        // clear flags if any on both pins by writing a 1
}                                       // end of ISR - interrupts automatically re-enabled

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.