Debouncing Rotary Encoders

~/ ../

About

One of my embedded projects needs a way to navigate a user interface and adjust values. Using push buttons to navigate the interface would be easier implement, but a rotary encoder would provide a better user experience. Making it easier for the user to make quick adjustements to values, by feel.

This post details my experience dealing with noisy rotary encoder signals on an RP2040 based Pico board.

Raspberry Pi Pico Serial Wire Debug Setup

What is a Rotary Encoder?

Despite having a similar appearance, a rotary encoder is very different from a potentiometer.

Rotary encoders are well suited for measuring many rotations. Potentiometers are essentially variable resistors and have a finite range of rotation and resistance. It is not possible to measure continuous rotations using a potentiometer.

My project will require the ability to track small increments and many rotations. I chose to use a KY-040 incremental encoder, which has 20 detents, per 360 degrees. So each detent, is 18 degrees. Also, the tactile feedback is nice for making fine adjustments.

KY-040 Pin Out
When rotating clockwise, the state of pin A (relative to pin C) changes before pin B. The opposite occurs with counter clockwise rotation. Applying power to pin C and rotating the shaft will generate two useful signals from pins A and B.
Signals generated by rotating the encoder. Clockwise (1,2,3,4) or Counter Clockwise (4,3,2,1)

There are 2 pins, with 2 states each, making a total of 4 possible states. That makes this a “quadrature phase” encoder.

Phase/State A B
1 0 0
2 1 0
3 1 1
4 0 1

Binary states of pins AB while transitioning clockwise a single detent:

graph LR; 01==>00; 00==>10; 10==>11; 11==>01;

So, all we need to do is keep track of transitions these these four phases/states.

Sounds Easy Right?

Not exactly… The quality of the signal output by the encoder is affected by temperature, rotation speed, and variability in component quality/wear.

Coincidentally, I have a real world example of poor rotary encoder performance, in my car.

When cold, attempting to turn the temperature up, actually turns it down! Turning it very slowly helps, but still has accuracy issues.

Initial Results

After reviewing and implementing a variety of approaches. I was left with something that mostly worked.

The KY-040 has 20 detents per 360 degrees of rotation. While monitoring the count of detents through a serial debugger, I noticed that carefully rotating 20 detents did not consistently record 20 changes to the counter variable. Instead I was seeing between 17-23. So, detents were being over and under counted.

Realistically, this level of accuracy is good enough for my project, but I just couldn’t leave it alone. Maybe it had something to do with fighting the temperature knob in my car, but I felt there just had to be a better way.

The Cause - Switch Bouncing

“Bouncing is the tendency of any two metal contacts in an electronic device to generate multiple signals as the contacts close or open.” [*]

In an effort to understand what was happening, I captured the output of 10 clockwise rotations. The results are represented using the graphs below. The nodes represent the bit state of pins AB. The edge weights represent the number of transitions between those states.

Expected Transitions

(If the signal were clean.)

graph LR; 01== 200 ==>00; 00== 200 ==>10; 10== 200 ==>11; 11== 200 ==>01;

Actual Transitions

(Dotted edges represent invalid transitions.)

graph LR; 00== 2 ==>01; 01== 201 ==>00; 00== 201 ==>10; 10-. 69 .->10 10== 200 ==>11; 11== 207 ==>01; 00-. 307 .->00 01-. 183 .->01 01== 8 ==>11; 11-. 21 .->11 10== 1 ==>00;

Yep, that definitely looks like switch bounce. It turns out that this is a common problem and can be addressed through “debouncing”.

What is Debouncing?

“Debouncing is any kind of hardware device or software that ensures that only a single signal will be acted upon for a single opening or closing of a contact.” [*]

The Solution

  1. Create a dictionary of valid clockwise/counterclockwise transitions and invalid transitions.
  2. Track previous AB and new AB pin state; in 4 bits. E.g. “1110” represents transitioning from 11 to 10.
  3. Use the dictionary to look up new transitions and ignore invalid transitions.
  4. Count valid consecutive transitions for clockwise and counter clockwise separately.
  5. When 4 valid transitions occur, reset the direction specific counter and take appropriate action for that direction detent.

I tried a variety of debouncing approaches, without much success. Eventually it occurred to me that the debouncing approaches I had researched rely on a single transition occuring the correct number of times. In reality, this cannot be counted on, as the process can start and end in any of the 4 states and sometimes flips back and forth between two states.

Making the changes (steps 4 and 5) to wait for 4 consecutive valid transitions, per direction, accounts for the back and forth seen in my encoder.

This approach is significantly more accurate. 20 clockwise rotations yields a detent counter of 400. 20 counter clockwise rotations reduces it back to 0. Much better!

Twisting the knob wildy seems to cause misses, but that is far outside my use case and probably beyond what the KY-040 is capable of.

Bonus Points: Debouncing Rotatary Switch

Since I want to use this knob for user input, I needed to debounce the switch output as well. Since there are only two possibles states, this can be accomplished by keeping track of the previous state of the switch and only taking action when the new state differs.

This will avoid accidental double entry on the user interface. Also, keeping track of the button down and up states enables an additional input mode based on an extended press. For example “Hold for 2 seconds to save changes.”

Demonstration

Compile, flash, and monitor through serial wire debugger.

Logging Format Explained

Clockwise

CW 2 // 4 valid clockwise transitions detected, detent count incremented to 2.
  10-- invalid -->10; // invalid transition from 10 to 10
  10-->11; // valid transition from 10 to 11
  11-- invalid -->11;
  11-->01;
  01-- invalid -->01;
  01-->00;
  00-- invalid -->00;
  00-- invalid -->00;
  00-- invalid -->00;
  00-- invalid -->00;
  00-- invalid -->00;
  00-->10;
CW 3 // 4 valid clockwise transitions detected, detent count incremented to 3.
  10-- invalid -->10;
  10-- invalid -->10;
  10-- invalid -->10;
  10-->11;

Counter Clockwise

  00-- invalid -->00; // invalid transition from 00 to 00
  00-->10; // valid transition from 00 to 10
  10-->11;
  11-->01;
CW 21 // 4 valid clockwise transitions detected, detent count incremented to 21.
  01-- invalid -->01;
  01-- invalid -->01;
  01-->11;
CCW 20 // 4 valid counter clockwise transitions detected, detent count decremented to 20.
  11-->10;
  10-->11;
  11-->10;
  10-- invalid -->10;
  10-- invalid -->10;
  10-- invalid -->10;
  10-->11;

Knob Press (Switch)

Button Down
Button Up
Button Down
Button Up

C Code

/*
MIT License

Copyright (c) 2022 Dylan Nash <nashimus@dismail.de>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include <stdio.h>
#include <stdlib.h>
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include "pico/stdlib.h"

#ifndef UINT32_MAX
#define UINT32_MAX (0xffffffff)
#endif

#define ENC_A 16    // GPIO - CLK - Encoder Pin A
#define ENC_B 17    // GPIO - DT - Encoder Pin B
#define ENC_SW 18   // GPIO - SW - Normally Open (NO) Pushbutton Switch

int val = 0;

void rot_enc_switch_handler ( uint gpio, uint32_t events )
{
  static uint32_t prev_state = UINT32_MAX;
  uint32_t sw_gpio_state = ( gpio_get_all() >> ENC_SW ) & 0b0001;
  if ( prev_state != sw_gpio_state )
    {
      prev_state = sw_gpio_state;
      printf ( "Button " );
      if ( sw_gpio_state == 1 )
        {
          printf ( "Up\n" );
        }
      else if ( sw_gpio_state == 0 )
        {
          printf ( "Down\n" );
        }
    }
}

void rot_enc_rotate_handler ( uint gpio, uint32_t events )
{
  const int8_t encoder_transitions[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
  static uint8_t current_transition = 0b11;
  static bool enc_a_gpio_state = 0;
  static bool enc_b_gpio_state = 0;
  enc_a_gpio_state = gpio_get ( ENC_A );
  enc_b_gpio_state = gpio_get ( ENC_B );

  // track of consecutive valid transitions in both directions
  static uint8_t cw_phase_cnt = 0;
  static uint8_t ccw_phase_cnt = 0;

  uint8_t new_status = ( enc_a_gpio_state << 1 ) | enc_b_gpio_state;

  current_transition <<= 2;                     //remember previous state
  current_transition |= ( new_status & 0x03 );  //add current state
  current_transition = current_transition & 0x0f; // truncate to 4 bits
  printf ( "  " );
  switch ( encoder_transitions[current_transition] )
    {
    case 1:
      // valid clockwise transition
      for ( int i = 3; i >= 0; i-- )
        {
          if ( i == 1 )
            {
              printf ( "-->" );
            }
          printf ( "%c", ( current_transition & ( 1 << i ) ) ? '1' : '0' );
        }
      printf ( ";\n" );
      cw_phase_cnt++;
      if ( cw_phase_cnt == 4 )
        {
          cw_phase_cnt = 0;
          val++;
          printf ( "CW  %d\n", val );
        }
      break;
    case -1:
      // valid counter clockwise transition
      for ( int i = 3; i >= 0; i-- )
        {
          if ( i == 1 )
            {
              printf ( "-->" );
            }
          printf ( "%c", ( current_transition & ( 1 << i ) ) ? '1' : '0' );
        }
      printf ( ";\n" );
      ccw_phase_cnt++;
      if ( ccw_phase_cnt == 4 )
        {
          ccw_phase_cnt = 0;
          val--;
          printf ( "CCW %d\n", val );
        }
      break;
    case 0:
      // invalid transition
      for ( int i = 3; i >= 0; i-- )
        {
          if ( i == 1 )
            {
              printf ( "-- invalid -->" );
            }
          printf ( "%c", ( current_transition & ( 1 << i ) ) ? '1' : '0' );
        }
      printf ( ";\n" );
      break;
    default:
      printf ( "Unknown Value: %d\n", encoder_transitions[current_transition] );
      break;
    }
}

void interrupt_handler ( uint gpio, uint32_t events )
{
  /*
   * Events is a bitmask of the following:
   *
   * bit | interrupt
   * ----|----------
   *   0 | Low level
   *   1 | High level
   *   2 | Edge low
   *   3 | Edge high
   */

  if ( gpio == ENC_SW )
    {
      rot_enc_switch_handler ( gpio, events );
    }

  if ( ( gpio == ENC_A || gpio == ENC_B ) )
    {
      rot_enc_rotate_handler ( gpio, events );
    }
}

int main()
{
  sleep_ms ( 500 );
  stdio_init_all();
  sleep_ms ( 500 );
  printf ( "Ready! \r\n" );

  // GPIO Setup for Encoder
  gpio_init ( ENC_SW );
  gpio_set_dir ( ENC_SW, GPIO_IN );
  gpio_disable_pulls ( ENC_SW );

  gpio_init ( ENC_A );
  gpio_set_dir ( ENC_A, GPIO_IN );
  gpio_disable_pulls ( ENC_A );

  gpio_init ( ENC_B );
  gpio_set_dir ( ENC_B, GPIO_IN );
  gpio_disable_pulls ( ENC_B );

  // Setup GPIO Interrupts
  uint32_t events = GPIO_IRQ_EDGE_FALL | GPIO_IRQ_EDGE_RISE;
  gpio_set_irq_enabled_with_callback ( ENC_SW, events, true, &interrupt_handler );
  gpio_set_irq_enabled ( ENC_A, events, true );
  gpio_set_irq_enabled ( ENC_B, events, true );

  while ( 1 )
    {
      sleep_ms ( 1000 );
    }
}
Created: