Taming a bouncing Rotary Encoder

1. Motivation

Rotary encoders are small useful mechanical devices for controlling microcontroller programs. When the knob is turned, they provide quadrature signals, but these are usually very noisy. Therefore, the bouncing caused by the rotary motion must be eliminated either electrically or programmatically, or by both methods. These methods have been described many times on the World Wide Web. In the C++ class presented here I have implemented two of them which can be used alternatively. I do not want to repeat the descriptions of the algorithms. They can be found here:

2. Parts

Rotary Encoder
parts

3. Wiring

            
          USB                                   
   .------I I------.                    
  -|3V3   ```   Vin|-                  
  -|GND         GND|----+------+----+            
  -|D15  ESP32  D13|-   | 22nF |    |        
  -|D2   DevKit D12|-  ===    ===   |         
  -|D4     V1   D14|-   |      |    |         .----------------.  
  -|RX2         D27|----|------+----|---------| CLK            |       
  -|TX2         D26|----+-----------|---------| DT    Rotary   |            
  -|D5          D25|----------------|---------| SW    Encoder  | 
  -|D18         D33|-               | 3.3V <--| +      with    | 
  -|D19         D32|-               +---------| GND  Pusbutton |     
  -|D21         D35|-                         `----------------´    
  -|RX0         D34|-             
  -|TX0          VN|-                             
  -|D22          VP|-                           
  -|D23          EN|-                   
   `---------------´                                   
            

4. Requirements

With a rotary encoder, only 2 events occur, namely one step forward or one step backward. These events must be detected unambiguously, regardless of how fast the knob is turned in whatever direction. To ensure this is the task of the debounce algorithm. If there is also a pushbutton, there are 3 more events, click, long click and double click. So in the C++ class we are designing here, we need to handle these 5 events. Because we don't know what tasks the user program should perform, we only provide methods that allow the user to register his own callback functions for the events.

5. Class RotaryEncoder

The RotaryEncoder class has 2 constructors, one for encoders without a pushbutton and one for those with a pushbutton.

In addition there are the methods to register the callbacks, the method to select the debounce algorithm and the loop() function which reacts to the events and calls the corresponding callbacks. Furthermore it also needs quite a number of variables to store the intermediate states.

			
  #ifndef _ROTARYENCODER_H_
  #define _ROTARYENCODER_H_
  #include 

  typedef void (*CallbackFunction)();

  class RotaryEncoder
  {
  public:
    // Encoders without pushbutton
    RotaryEncoder(uint8_t pinClk, uint8_t pinData) :
      _pinClk(pinClk), 
      _pinData(pinData)
      {
      pinMode(_pinClk, INPUT_PULLUP);
      pinMode(_pinData, INPUT_PULLUP);
      }

    // Encoders with axial pushbutton
    RotaryEncoder(uint8_t pinClk, uint8_t pinData, uint8_t pinButton) : 
      _pinClk(pinClk), 
      _pinData(pinData), 
      _pinButton(pinButton)
    {
      pinMode(_pinClk, INPUT_PULLUP);
      pinMode(_pinData, INPUT_PULLUP);
      pinMode(_pinButton, INPUT_PULLUP);
    }
 
    void setDebouncingRotEncByTable(bool byTable = true);  // byTable=false selects debouncing by 
                                                           // cleaning clock and data signal
    void addOnClickCB(CallbackFunction cb);
    void addOnLongClickCB(CallbackFunction cb);
    void addOnDoubleClickCB(CallbackFunction cb);
    void addOnClockwiseCB(CallbackFunction cb);
    void addOnCounterClockwiseCB(CallbackFunction cb);

    void loop();
   
  private:
    static void _nop(){};
    void _debounceRotaryByCleaning();
    void _debounceRotaryByTable();
    void _debounceButton();
    CallbackFunction _onClick = _nop;
    CallbackFunction _onLongClick = _nop;
    CallbackFunction _onDoubleClick = _nop;
    CallbackFunction _onCW = _nop;
    CallbackFunction _onCCW = _nop;
    uint8_t _clkState = HIGH;
    uint8_t _prevClkState = LOW;
    uint8_t _cleanedClkState;
    uint8_t _prevCleanedClkState = HIGH;
    uint8_t _buttonState = HIGH;
    uint8_t _prevButtonState;
    uint8_t _dataState = HIGH;
    uint8_t _prevDataState;
    uint8_t _cleanedDataState;
    uint8_t _prevCleanedDataState;
    uint8_t _pinClk;
    uint8_t _pinData;
    uint8_t _pinButton;
    uint8_t _clickCount = 0;
    unsigned long _msDebounce = 50;        // After 50ms the button should have reached a stationary state
    unsigned long _msLongClick = 300;      // Button held longer than 300ms is considered LongClick
    unsigned long _msDoubleClickGap = 250; // Two button clicks within 250ms count as DoubleClick
    unsigned long _msButtonDown;
    unsigned long _msFirstClick = 0;
    uint8_t _newTransition = 0;
    uint16_t _transitions = 0;
    const uint8_t _validTransitions[16] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0};
    bool _debouncingRotEncByTable = true;
  };
  #endif			
			

6. Example Program

In the test program, a counter is incremented or decremented depending on the direction of rotation. By setting one or the other debouncing method, their effectiveness can be compared.

The program also shows how easily the whole behavior of the application can be changed when other callbacks are registered.

			
  #include "RotaryEncoder.h"

  const uint8_t PIN_CTRLKNOB_SW  = GPIO_NUM_25;
  const uint8_t PIN_CTRLKNOB_DAT = GPIO_NUM_26;
  const uint8_t PIN_CTRLKNOB_CLK = GPIO_NUM_27;

  RotaryEncoder ctrlKnob(PIN_CTRLKNOB_CLK, PIN_CTRLKNOB_DAT, PIN_CTRLKNOB_SW);
  int counter = 0;

  /**
   * Reset the counter and select debouncing method "table lookup of valid transitions"
   */
  void onClick()
  {
    counter = 0;
    ctrlKnob.setDebouncingRotEncByTable();
    Serial.printf("Debouncing by table lookup, counter set to %d\n", counter);
  }

  /**
   * Reset counter and select debouncing method "cleaning of clock and data signals"
   */
  void onLongClick()
  {
    counter = 0;
    ctrlKnob.setDebouncingRotEncByTable(false);
    Serial.printf("Debouncing by cleaning of clock and data signals, counter set to %d\n", counter);
  }

  /**
   * Show angular position of rotary encoder
   * 1 step = 18° (20 steps per revolution)
   */
  void onDoubleClick()
  {
    Serial.printf("Position = %d°\n", (18 * counter) % 360);
  }

  /**
   * Callback which is called on every step in clockwise direction
   */
  void countUp()
  {
    counter++;
    Serial.printf("count = %4d\n", counter);
  }

  /**
   * Callback which is called on every step in counterclockwise direction
   */
  void countDown()
  {
    counter--;
    Serial.printf("count = %4d\n", counter);
  }


  void setup() 
  {
    Serial.begin(115200);

    // Add the callbacks
    ctrlKnob.addOnClickCB(onClick);
    ctrlKnob.addOnLongClickCB(onLongClick);
    ctrlKnob.addOnDoubleClickCB(onDoubleClick);
    ctrlKnob.addOnClockwiseCB(countUp);
    ctrlKnob.addOnCounterClockwiseCB(countDown);
  }

  void loop() 
  {
    ctrlKnob.loop();
  }
			

Interested? Please download the entire program code. The zip-file contains the complete PlatformIO project.

My programming environment is not the native Arduino™ IDE but PlatformIO™ on top of Microsoft's Visual Studio Code™. This combination offers many advantages and allows a much better structuring of the code into several modules especially when we adopt The Object Oriented way.