Rotary Encoder Debouncing, GFValvo's Library, and Using it with ESP32

Hi all,

I'm currently working on a project that uses a rotary encoder. I got to googling, and found this great page from John Main:

His code at the bottom of the page ("Code for Improved Table Decode") works perfectly, but now I'm trying to understand what it is doing. I'll post the code with some annotated questions -- I think a lot of what is confusing me is syntax, or maybe his more advanced coding for brevity.

I looked up Gray codes, and understand that by only changing one byte, it removes the ambiguity of the changing states of the encoder. I guess I don't understand what part of the code is actually checking this.

// Robust Rotary encoder reading
//
// Copyright John Main - best-microcontroller-projects.com
//
#define CLK 5
#define DATA 4

void setup() {
  pinMode(CLK, INPUT);
  pinMode(CLK, INPUT_PULLUP);
  pinMode(DATA, INPUT);
  pinMode(DATA, INPUT_PULLUP);
  Serial.begin (9600);
  Serial.println("KY-040 Start:");
}

static uint8_t prevNextCode = 0;    //Q1: If static is used to create variables visible to one function why is it being used here
static uint16_t store = 0;          //to create variables for everything? Why is static the correct choice here?

void loop() {
  static int8_t c, val;

  if ( val = read_rotary() ) {
    c += val;
    Serial.print(c); Serial.print(" ");

    if ( prevNextCode == 0x0b) {
      Serial.print("eleven ");
      Serial.println(store, HEX);
    }

    if ( prevNextCode == 0x07) {
      Serial.print("seven ");
      Serial.println(store, HEX);
    }
  }
}

// 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}; //Q2: Why the usage of int8_t throughout?

  prevNextCode <<= 2;
  if (digitalRead(DATA)) prevNextCode |= 0x02;         //Q3: What's the syntax here? The if DATA ? prevNextCode OR if... decimal 2? 
  if (digitalRead(CLK)) prevNextCode |= 0x01;
  prevNextCode &= 0x0f;

  // If valid then store as 16 bit data.
  if  (rot_enc_table[prevNextCode] ) {                //Q4: What's the argument for this if statement? I see it's checking to see
    store <<= 4;                                      // the value of the array at the index [prevNextCode] but then what happens?
    store |= prevNextCode;
    //if (store==0xd42b) return 1;
    //if (store==0xe817) return -1;
    if ((store & 0xff) == 0x2b) return -1;
    if ((store & 0xff) == 0x17) return 1;
  }
  return 0;
}

Q1 (Line 17): If static is used to create variables visible to one function, why is it being used to create variables for everything? Why is static the correct choice here?

Q2 (Line 42): Why the usages of int8_t or int16_t. I understand uint8_t is an unsigned char, or one byte. Is this just a different way to declare things? Would using "byte" be the same?

Q3 (Line 45, 46:) What's up with the syntax of these if statements? I'm confused as to what they're doing.

Q4 (Line 51): What's the argument for this if statement? I see it's checking to see the value of the array at the index [prevNextCode] but then what happens?

Thanks for any input or responses,

-TBB

001_robust.ino (1.79 KB)

trappedbybicycles:
Q1 (Line 17): If static is used to create variables visible to one function, why is it being used to create variables for everything? Why is static the correct choice here?

DuckDuckGo is your friend. There are vast resources on the web covering many, many aspects of C++. Search 'static C++'.

trappedbybicycles:
Q3 (Line 45, 46:) What's up with the syntax of these if statements? I'm confused as to what they're doing.

Q4 (Line 51): What's the argument for this if statement? I see it's checking to see the value of the array at the index [prevNextCode] but then what happens?

The code inside the parentheses of an if statement returns only one of two values; true or false. Even if that code calls a function, which in turn may call another, in the end it all only evaluates to true or false. What values does digitalRead return? Again, searching the web for something like 'false C++' will lead to numerous pages discussing exactly what constitutes *true *and *false *in the language. See the Arduino Reference page for a starting point.

Not deliberately hiding info, I just want you to be able to find things on your own.

trappedbybicycles:
Q1 (Line 17): If static is used to create variables visible to one function, why is it being used to create variables for everything? Why is static the correct choice here?

The 'static' keyword, when used in the context of a global variable or a function, is a method of controlling its scope. In particular, it restricts its scope to the file in which it appears. This allows you to share functions and global variables within a file but prohibit access to them from outside the file.

Q2 (Line 42): Why the usages of int8_t or int16_t. I understand uint8_t is an unsigned char, or one byte. Is this just a different way to declare things? Would using "byte" be the same?

It's a better way. It allows you to explicitly specify both the size and signed or unsigned status of the variable.

Q3 (Line 45, 46:) What's up with the syntax of these if statements? I'm confused as to what they're doing.

The same as any "if" statement. If the condition within the '(...)' pair is true, the following statement is executed. In this case, what's executed (or not) is a single statement. Many times, it's a compound statement surrounded by '{' and '}'.

Q4 (Line 51): What's the argument for this if statement? I see it's checking to see the value of the array at the index [prevNextCode] but then what happens?

If the value of 'rot_enc_table[prevNextCode]' is "true" (i.e. non-zero) all the statements between the following '{...}' pair are executed.

Thanks for all the replies. I think what was tripping me up was that I had never seen if statements that did not explicitly say an argument, i.e. if(something == TRUE).

This is also the first time I've tried to understand bitwise operators. This wiki page, and this youtube channel have been very useful for explanations.

Trying to walk through the program now:

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;                           //bitwise shift assignment 2 left
  if (digitalRead(DATA)) prevNextCode |= 0x02;  //bitwise OR when DATA is high. 
  if (digitalRead(CLK)) prevNextCode |= 0x01;   //bitwise OR when CLK is high.
  prevNextCode &= 0x0f;                         //0x0f is 15 or 1111

  // If valid then store as 16 bit data.
  • The if statement in the loop calls read_rotary()
  • Read rotary takes prevNextCode and applies a bitwise shift operator
  • If the DATA pin is high, it applies a bitwise OR with 0x02 (aka 0010)
  • If the CLK pin is high, it applies a bitwise OR with 0x01 (aka 0001)
  • Finally it applies a bitwise AND with 0x0f (aka 1111)

That pumps out a value for the index of rot_enc_table[], and if that value is non-zero, it is considered a valid movement of the encoder.

I'm curious about the bitwise AND operation (step 5). Isn't that just multiplying the prevNextCode by 1111? What use does that have?

I'm trying to incorporate this debounce method into an interrupt and running into some issues, but I'll make a separate post for that after I understand this better.

trappedbybicycles:
I'm curious about the bitwise AND operation (step 5). Isn't that just multiplying the prevNextCode by 1111? What use does that have?

Try a simple numeric example:
x = 3 (0b11)
y = 3 (0b11)

x * y = 9 (0b1001)
x & y = 3 (0b0011)

Not the same.

I think a lot of what is confusing me is syntax, or maybe his more advanced coding for brevity.

i think this is a very interesting code.

when i initially looked at this code, i questioned how it could repeatedly read the encoder inputs without checking for a change.

the 2 encoder bits are concatenated together defining a 2-bit state and then sequential states are concatenated together to represent a transition between states. (prevNextCode is shifted left, making room for 2 more bits, the new 2-bit state is bitwise OR'd with prevNextCode and then prevNextCode bitwise ANDed with 0xF to limit it to 4-bits (prevNextCode %=16).

there are 16 possible transitions: no change, cw, ccw and invalid. It's the index to the table (rot_enc_table) that represents to transition and the table value indicates that it is a valid change in state (cw or ccw).

by handling a repetition of state, there's no need to check for a change in the encoder outputs. The 2 2-bit values in the comment (below) represent the index, the previous and current states. When they are the same, the inputs haven't changed and are ignored.

it seems the invalid transitions are assumed to be bounces and simply ignored.

  static int8_t rot_enc_table[] = {
    0, // 00 00  no change
    1, // 00 01  cw
    1, // 00 10  ccw
    0, // 00 11  invalid

    1, // 01 00  cw
    0, // 01 01  no change
    0, // 01 10  invalid
    1, // 01 11  cw

    1, // 10 00  cw
    0, // 10 01  invalid
    0, // 10 10  no change
    1, // 10 11  ccw

    0, // 11 00  invalid
    1, // 11 01  ccw
    1, // 11 10  cw
    0  // 11 11  no change
  };

not sure how compatible this approach is with an interrupt, although an interrupt would save MIPs because it requires monitoring changes of both outputs. I trigger an interrupt on the rising edge of one encoder output and read the other output to determine direction.

glad i read this post.

gcjr:
not sure how compatible this approach is with an interrupt ...

I haven't looked at this code in detail. But, it sounds similar to the State Table approach presented here: Buxtronix: Rotary encoders, done properly

And, here's a library I wrote that implements it with interrupts: GitHub - gfvalvo/NewEncoder: Rotary Encoder Library

gfvalvo:
Try a simple numeric example:
x = 3 (0b11)
y = 3 (0b11)

x * y = 9 (0b1001)
x & y = 3 (0b0011)

Not the same.

After reading your post and googling, I did not realize that bitwise representations (3 = 0b11) were a thing. I was trying to understand the code thinking between decimal, hexadecimal, and binary. It's still a bit over my head.

gfvalvo:
And, here's a library I wrote that implements it with interrupts: GitHub - gfvalvo/NewEncoder: Rotary Encoder Library

Thanks! Going to play around with your library a bit. Just a side note -- after installing your library through the Arduino compiler (sketch>include library>add .zip library) I was getting a compiler error saying that a file was not found.

In the file newEncoder.h, I changed some backslashes to slashes and it fixed the issue.

#include <Arduino.h>
#include "utility\interrupt_pins.h"
#include "utility\direct_pin_read.h"

to

#include <Arduino.h>
#include "utility/interrupt_pins.h"
#include "utility/direct_pin_read.h"

I'm on a mac, not sure if that is what created the issue.

trappedbybicycles:
In the file newEncoder.h, I changed some backslashes to slashes and it fixed the issue.

#include <Arduino.h>

#include "utility\interrupt_pins.h"
#include "utility\direct_pin_read.h"




to 



#include <Arduino.h>
#include "utility/interrupt_pins.h"
#include "utility/direct_pin_read.h"




I'm on a mac, not sure if that is what created the issue.

Thanks for the note. On my Win 10 machine it works with either "" or "/". Then I checked the PJRC Encoder library (credited in mine) where I borrowed the technique. It, in fact, uses "/". So, perhaps that's more universal. I'll update my GitHub version when I get a chance.

gfvalvo:
Thanks for the note. On my Win 10 machine it works with either "" or "/". Then I checked the PJRC Encoder library (credited in mine) where I borrowed the technique. It, in fact, uses "/". So, perhaps that's more universal. I'll update my GitHub version when I get a chance.

Good to know, thx!

I got the project working well on an Arduino Uno, and I'm currently trying to swap it all to an ESP32 to add some Wifi capabilities.

After the first compiling attempt, I was returned your error message:

#if !defined(CORE_NUM_INTERRUPT)
#error "Interrupts are unknown for this board, please add to this code"
#endif

In interrupt_pins.h, I added a new definition for the ESP32, modelled after your ESP8266 list. I'm attempting to use GPIO Pins 17 and 18 as interrupts:

// ESP8266 (https://github.com/esp8266/Arduino/)
#elif defined(ESP8266)
  #define CORE_NUM_INTERRUPT EXTERNAL_NUM_INTERRUPTS
  #define CORE_INT0_PIN 0
  #define CORE_INT1_PIN 1
  #define CORE_INT2_PIN 2
  #define CORE_INT3_PIN 3
  #define CORE_INT4_PIN 4
  #define CORE_INT5_PIN 5
  // GPIO6-GPIO11 are typically used to interface with the flash memory IC on 
  // most esp8266 modules, so we should avoid adding interrupts to these pins.
  #define CORE_INT12_PIN 12
  #define CORE_INT13_PIN 13
  #define CORE_INT14_PIN 14
  #define CORE_INT15_PIN 15

//ESP32 (Attempt)
#elif defined(ESP32)
  #define CORE_NUM_INTERRUPT EXTERNAL_NUM_INTERRUPTS
  #define CORE_INT17_PIN 17
  #define CORE_INT18_PIN 18

This is obviously a shot in the dark, as they're two seperate boards, but it got me a little further in compiling. Now I'm receiving error messages saying:

In file included from /Users/trappedbybicycles/Documents/Arduino/AH Water Meter/WM004_Esp001/WM004_Esp001.ino:17:0:
/Users/omar/Documents/Arduino/libraries/NewEncoder-master/NewEncoder.h:57:11: error: 'IO_REG_TYPE' does not name a type
volatile IO_REG_TYPE * _aPin_register;

^
/Users/trappedbybicycles/Documents/Arduino/libraries/NewEncoder-master/NewEncoder.h:58:11: error: 'IO_REG_TYPE' does not name a type
volatile IO_REG_TYPE * _bPin_register;

^
/Users/trappedbybicycles/Documents/Arduino/libraries/NewEncoder-master/NewEncoder.h:59:11: error: 'IO_REG_TYPE' does not name a type
volatile IO_REG_TYPE _aPin_bitmask;

^
/Users/trappedbybicycles/Documents/Arduino/libraries/NewEncoder-master/NewEncoder.h:60:11: error: 'IO_REG_TYPE' does not name a type
volatile IO_REG_TYPE _bPin_bitmask;

^
exit status 1
Error compiling for board ESP32 Dev Module.

I then went into direct_pin_read.h and attempted to add a definition for the ESP32, again modelled after the 8266:

/* ESP8266 v2.0.0 Arduino workaround for bug https://github.com/esp8266/Arduino/issues/1110 */
#elif defined(ESP8266)

#define IO_REG_TYPE uint32_t
#define PIN_TO_BASEREG(pin)             ((volatile uint32_t *)(0x60000000+(0x318)))
#define PIN_TO_BITMASK(pin)             (digitalPinToBitMask(pin))
#define DIRECT_PIN_READ(base, mask)     (((*(base)) & (mask)) ? 1 : 0)

/* ESP32 (Attempt) */
#elif defined(ESP32)

#define IO_REG_TYPE uint32_t
#define PIN_TO_BASEREG(pin)             ((volatile uint32_t *)(0x60000000+(0x318)))
#define PIN_TO_BITMASK(pin)             (digitalPinToBitMask(pin))
#define DIRECT_PIN_READ(base, mask)     (((*(base)) & (mask)) ? 1 : 0)

This winds up compiling, but the serial monitor returns:

12:44:24.982 -> Encoder Failed to Start. Check pin assignments and available interrupts. Aborting.

I'll keep tinkering with it, but I'm definitely in over my head. Still trying to trace through your library and see how it works (:

According to this page, any of the GPIO pins on the ESP32 can be configured to be used as interrupts. I'm avoiding the ADC 2 pins because they tend to mess with WIFI projects.

Replying as I think something has worked itself out here:

I added this definition to the direct.pin.read.h file:

#elif defined(ESP32)

#define IO_REG_TYPE			uint32_t
#define PIN_TO_BASEREG(pin)             (portInputRegister(digitalPinToPort(pin)))
#define PIN_TO_BITMASK(pin)             (digitalPinToBitMask(pin))
#define DIRECT_PIN_READ(base, mask)     (((*(base)) & (mask)) ? 1 : 0)

Which I found here.

Without adding any definition to the interrupt_pins.h file, this seems to have allowed me to use pins 2 & 4 as interrupts. I'll continue playing with it, as those pins are ADC2, but slight bits of progress.

As stated at the bottom of the library's README, I "borrowed" those two files directly from the PRJC Encoder Library --- giving credit, of course. Apparently it didn't support ESP32 at the time. I see you've found that it now does. So, you might just try copying direct_pin_read.h and interrupt_pins.h directly.

I haven't gotten around to testing on ESP32. Let me know know how it goes. If it works, you can do a Pull Request on my GitHub.

Thanks! For now, it seems like the wifi is not having any issues, and the interrupts are working well on 2&4, so I'll let it stay as is for a bit. Again, appreciate all the help (: