Reliably debouncing rotary encoders with Arduino and ESP32

I love those simple cheap rotary encoders as used in the KY-040 modules as a method of getting user input with Arduino and ESP32 projects. The issue of bounce with them is significant and for years I’ve been looking a reliable method of dealing with it. I thought I had it figured out by either using a couple of 100nF capacitors or by using code ignore quick changes but this was not always reliable and sometimes missed legitimate changes when rotating at speed. In forums I read people recommending to use a lookup table, but not me. I persevered with what I knew, at least until my methods didn’t work.

I’ve only recently started using ESP32s. The first time I started using one with an encoder the bouncing was terrible. Turning a single indent resulted in a large number of false increments. I assumed that this was because the ESP32 was so running much faster than the Nanos I’ve been using and triggering on even quicker bounces. I don’t know if that is a possibility, but I now believe that the encoder I was using was extremely noisy. I thought it was time to revisit debouncing. After all a reliable encoder will not necessarily stay reliable forever.

An alternative solution

In some forums I read there was sometimes a comment saying there was a more reliable way. That is to use a table to compare the previous state with the new state and using a lookup table to ignore those changes that are not valid for a legitimate change. I decided to investigate. I came across this page Rotary Encoder : How to use the Keys KY-040 Encoder on the Arduino. I recommend reading it if you want to learn more about it.

I tried the Code For Improved Table Decode and was really surprised just how well it worked without any hardware filtering. There were only two things missing that I wanted. Firstly, it uses polling and I wanted to use interrupts and secondly it has a copyright notice and I respect that. However, I want to use code that I can include in projects that I can place online to share with others. It’s still a great read and I am grateful for Best Microcontroller Projects for providing the resource.

Next I came across code by Oleg Mazurov’s pages Reading rotary encoder on Arduino and Rotary encoder interrupt service routine for AVR micros

I also found these to be very helpful. I found some others had similar code that I believe is based on the code by Oleg. Oleg’s code worked well too, but I couldn’t get it to work on the ESP32 without a couple of changes. It uses port read to read the value of the encoder pins, which I expect is a very efficient thing to do, but didn’t work with the ESP32. Also, the interrupt version uses PROGMEM that also appears to be incompatible with the ESP32, or at least as it is done in the example.

However, you can stop here and use Oleg’s code if you are using a Nano or Uno, or use the Best-Microcontroller-Projects.com version. To get Oleg’s version working on the ESP32 I made these changes.

Port read

Olig’s code reads the ports directly without using Arduino’s digitalRead. I had a go at using digitalRead, which I hoped would allow it to work on the ESP32 and perhaps other microcontrollers. To do that I:

Removed this define

define ENC_PORT PINC

And changed this:

old_AB |= ( ENC_PORT & 0x03 ); // Add current state 

to this to use digitalRead:

if (digitalRead(ENC_A)) old_AB |= 0x02; // Add current state of pin A
if (digitalRead(ENC_B)) old_AB |= 0x01; // Add current state of pin B

Progmem

I’m not sure why program memory is used instead of SRAM, only that I couldn’t get it to work on the ESP32 so I removed the reference to it making it the same as Oleg’s polling version. So from this:

static const int8_t enc_states [] PROGMEM = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};

To this:

static const int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};

Changed versions

I confess to not fully understanding the code, so I can’t be sure that there are not issues with it. I have made some other tweaks too, but those are mainly just the addition of comments. These are the two versions that I am currently using.

Polling version

/* Based on Oleg Mazurov's code for Reading rotary encoder on Arduino, here   
   https://chome.nerpa.tech/mcu/reading-rotary-encoder-on-arduino/ and here
   https://chome.nerpa.tech/mcu/rotary-encoder-interrupt-service-routine-for-avr-micros/

   This example does not use the port read method. Tested with Nano and ESP32

   Connections
   ===========
   Encoder | ESP32 |  Nano
   --------------------------
     A     |  D5   |  Nano D2
     B     |  D21  |  Nano D3
     GND   |  GND  |  GND
*/

// Define rotary encoder pins
#define ENC_A 21
#define ENC_B 5

volatile int counter = 0;

void setup() {

  // Set encoder pins
  pinMode(ENC_A, INPUT_PULLUP);
  pinMode(ENC_B, INPUT_PULLUP);

  // Start the serial monitor to show output
  Serial.begin(115200); // Change to 9600 for Nano, 115200 for ESP32
  delay(500);           // Wait for serial to start  
  Serial.println("Start");
}

void loop() {
  static int lastCounter = 0;
  
  read_encoder();

  // If count has changed print the new value to serial
  if(counter != lastCounter){
    Serial.println(counter);
    lastCounter = counter;
  }
}

void read_encoder() {
  // Encoder routine. Updates counter if they are valid
  // and if rotated a full indent
 
  static uint8_t old_AB = 3;  // Lookup table index
  static int8_t encval = 0;   // Encoder value  
  static const int8_t enc_states[]  = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; // Lookup table

  old_AB <<=2;  // Remember previous state  

  if (digitalRead(ENC_A)) old_AB |= 0x02; // Add current state of pin A
  if (digitalRead(ENC_B)) old_AB |= 0x01; // Add current state of pin B
  
  encval += enc_states[( old_AB & 0x0f )];

  // Update counter if encoder has rotated a full indent, that is at least 4 steps
  if( encval > 3 ) {        // Four steps forward
    counter++;              // Increase counter
    encval = 0;
  }
  else if( encval < -3 ) {  // Four steps backwards
   counter--;               // Decrease counter
   encval = 0;
  }
}

Interrupt version

/* Based on Oleg Mazurov's code for rotary encoder interrupt service routines for AVR micros
   here https://chome.nerpa.tech/mcu/reading-rotary-encoder-on-arduino/
   and using interrupts https://chome.nerpa.tech/mcu/rotary-encoder-interrupt-service-routine-for-avr-micros/


   This example does not use the port read method. Tested with Nano and ESP32
   both encoder A and B pins must be connected to interrupt enabled pins
   

   Connections
   ===========
   Encoder | ESP32 |  Nano
   --------------------------
     A     |  D5   |  Nano D2
     B     |  D21  |  Nano D3
     GND   |  GND  |  GND
*/

// Define rotary encoder pins
#define ENC_A 21
#define ENC_B 5

volatile int counter = 0;

void setup() {

  // Set encoder pins and attach interrupts
  pinMode(ENC_A, INPUT_PULLUP);
  pinMode(ENC_B, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ENC_A), read_encoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_B), read_encoder, CHANGE);

  // Start the serial monitor to show output
  Serial.begin(115200); // Change to 9600 for Nano, 115200 for ESP32
  delay(500);           // Wait for serial to start  
  Serial.println("Start");
}

void loop() {
  static int lastCounter = 0;

  // If count has changed print the new value to serial
  if(counter != lastCounter){
    Serial.println(counter);
    lastCounter = counter;
  }
}

void read_encoder() {
  // Encoder interrupt routine for both pins. Updates counter
  // if they are valid and have rotated a full indent
 
  static uint8_t old_AB = 3;  // Lookup table index
  static int8_t encval = 0;   // Encoder value  
  static const int8_t enc_states[]  = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; // Lookup table

  old_AB <<=2;  // Remember previous state

  if (digitalRead(ENC_A)) old_AB |= 0x02; // Add current state of pin A
  if (digitalRead(ENC_B)) old_AB |= 0x01; // Add current state of pin B
  
  encval += enc_states[( old_AB & 0x0f )];

  // Update counter if encoder has rotated a full indent, that is at least 4 steps
  if( encval > 3 ) {        // Four steps forward
    counter++;              // Increase counter
    encval = 0;
  }
  else if( encval < -3 ) {  // Four steps backwards
   counter--;               // Decrease counter
   encval = 0;
  }
}

Downsides of this method

While it is working well in tests, I have not used it in a permanent project yet. The only downside I can find so far is that if using interrupts instead of polling it requires two interrupt pins. Other methods I’ve used only need one. I don’t expect this will be an issue of the ESP32 but it may be with the Nano.

Update: 10 June 2022

I thought it was worth adding an update. I notice this post has now had over 1900 views which makes it my second most popular post which encourages me to keep it updated.

The examples above are missing the ability to change the output, in these cases update the counter at a greater amount per step if the encoder is rotated at a greater rate. A commenter asked about this and I posted a reply in the comments but at the time I had not used it in a project. I now have and it is working successfully. I’m using it in a couple timer projects to set the countdown time. In those projects I only use two steps. For steps slower than 40ms the count increased by 1. If quicker the count increases by 3.

Rather than use a formula to give a fully variable change I’ve opted in the examples below to use 3 steps. In my kitchen timer I only had two speeds and that worked really well.

The interrupt method probably has more code in the interrupt routine than is good practice. It has some magic numbers in there too, but there should be enough info to tweak it. I’m not convinced that there is an ideal formula or numbers for this. In testing the size of the knob seemed to play a factor.

In the examples below the time after the four steps of each click of the counter is recorded. I’m not sure if that is the best approach but seems reasonable. I’ve only included the read_encoder routine below. The rest of the sketch is the same as above so just swap the routine above with this. It is the same for polling an interrupt versions.

void read_encoder() {
  // Encoder interrupt routine for both pins. Updates counter
  // if they are valid and have rotated a full indent
 
  static uint8_t old_AB = 3;  // Lookup table index
  static int8_t encval = 0;   // Encoder value  
  static const int8_t enc_states[]  = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; // Lookup table
  static unsigned long lastInterruptTime = 0;
  unsigned long interruptTime = millis();
  
  old_AB <<=2;  // Remember previous state
  
  if (digitalRead(ENC_A)) old_AB |= 0x02; // Add current state of pin A
  if (digitalRead(ENC_B)) old_AB |= 0x01; // Add current state of pin B
  
  encval += enc_states[( old_AB & 0x0f )];

  // Update counter if encoder has rotated a full indent, that is at least 4 steps
  if( encval > 3 ) {                                    // Four steps forward
    if (interruptTime - lastInterruptTime > 40) {       // Greater than 40 milliseconds
      counter ++;                                       // Increase by 1
    } else if (interruptTime - lastInterruptTime > 20){ // Greater than 20 milliseconds
      counter += 3;                                     // Increase by 3
    } else {                                            // Faster than 20 milliseconds
      counter += 10;                                    // Increase by 10
    }
    encval = 0;
    lastInterruptTime = millis();                       // Remember time
  }
  else if( encval < -3 ) {                              // Four steps backwards
    if (interruptTime - lastInterruptTime > 40) {       // Greater than 40 milliseconds
      counter --;                                       // Increase by 1
    } else if (interruptTime - lastInterruptTime > 20){ // Greater than 20 milliseconds
      counter -= 3;                                     // Increase by 3
    } else {                                            // Faster than 20 milliseconds
      counter -= 10;                                    // Increase by 10
    }
    encval = 0;
    lastInterruptTime = millis();                       // Remember time
  }
}

7 thoughts on “Reliably debouncing rotary encoders with Arduino and ESP32

Add yours

      1. Hi, acceleration sounds like a good improvement. I’ve not done that with this code before, but I did a simple acceleration addition in an old project that used hardware denouncing so I have a basic idea of how to do it. I’ll take a look, but it may be a while before I get a chance to do it.

        Like

      2. Hi stategrid, Here is a some code to try that adds acceleration. It is not full acceleration, rather there are 3 different speeds. Changes faster than 20 milliseconds increase the counter by 10. Between 20 and 40 milliseconds increase by 3 and slower than 40 milliseconds increase by 1.

        There is perhaps too much going on in the interrupt routine, but hopefully is ok. Keep in mind I am not a programmer. This is not the full sketch. Use the interrupt example above and completely replace the interrupt routing read_encoder().

        Hopefully you should be able to tweak it meet your needs. I only tested with a nano as I already had one set up with an encoder but I don’t see why it shouldn’t work with a ESP32. Let me know how it goes.

        
        void read_encoder() {
          // Encoder interrupt routine for both pins. Updates counter
          // if they are valid and have rotated a full indent
         
          static uint8_t old_AB = 3;  // Lookup table index
          static int8_t encval = 0;   // Encoder value  
          static const int8_t enc_states[]  = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; // Lookup table
          static unsigned long lastInterruptTime = 0;
          unsigned long interruptTime = millis();
          
          if (digitalRead(ENC_A)) old_AB |= 0x02; // Add current state of pin A
          if (digitalRead(ENC_B)) old_AB |= 0x01; // Add current state of pin B
          
          encval += enc_states[( old_AB & 0x0f )];
        
          // Update counter if encoder has rotated a full indent, that is at least 4 steps
          if( encval > 3 ) {                                    // Four steps forward
            if (interruptTime - lastInterruptTime > 40) {       // Greater than 40 milliseconds
              counter ++;                                       // Increase by 1
            } else if (interruptTime - lastInterruptTime > 20){ // Greater than 20 milliseconds
              counter += 3;                                     // Increase by 3
            } else {                                            // Faster than 20 milliseconds
              counter += 10;                                    // Increase by 10
            }
            encval = 0;
            lastInterruptTime = millis();                       // Update time for next time
          }
          else if( encval  40) {       // Greater than 40 milliseconds
              counter --;                                       // Increase by 1
            } else if (interruptTime - lastInterruptTime > 20){ // Greater than 20 milliseconds
              counter -= 3;                                     // Increase by 3
            } else {                                            // Faster than 20 milliseconds
              counter -= 10;                                    // Increase by 10
            }
            encval = 0;
            lastInterruptTime = millis();                       // Update time for next time
          }
        }
        

        Like

Leave a Reply to Steven Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Website Powered by WordPress.com.

Up ↑

%d bloggers like this: