A simple Arduino library for implementing a rotary encoder on an ESP32 using interrupts and callbacks.
This library makes it easy to add one or more rotary encoders to your ESP32 project. It uses interrupts to instantly detect when the knob is turned or the pushbutton is pressed and fire a custom callback to handle those events.
It works with assembled modules that include their own pull-up resistors as well as raw units without any other external components (thanks to the ESP32 having software controlled pull-up resistors built in). You can also specify a GPIO pin to supply the Vcc reference instead of tying the encoder to a 3v3 source.
You can specify the boundaries of the encoder (minimum and maximum values), and whether turning past those limits should circle back around to the other side.
There are already many rotary encoder libraries for Arduino, but I had trouble finding one that met my requirements. I did find a couple that were beautifully crafted, but wouldn't work on the ESP32. Others I tried were either bulky or clumsy, and I found myself feeling like it would be simpler to just setup the interrupts and handle the callbacks directly in my own code instead of through a library.
Of the many resources I used to educate myself on the best ways to handle the input from a rotary encoder was, the most notable one was a blog post by @garrysblog. In his article, he cited another article, Rotary Encoder: Immediately Tame your Noisy Encoder!, which basically asserted that when turning the knob right or left, the pulses from the A and B pins can only happen in a specific order between detents, and any other pulses outside of that prescribed order could be ignored as noise.
Thus, by running the pulses received on the A and B inputs through a lookup table, it doesn't just de-bounce the inputs -- it actually guarantees that every click of the rotary encoder increments or decrements as the user would expect, regardless of how fast or slow or "iffy" the movement is.
Garry wrote some functions that incorporated the use of a lookup table, which is what I used myself initially -- and it worked beautifully. In fact, it worked so well that I decided to turn it into a library to make it even simpler to use. And that worked so well that I decided I should package it up and share it with others.
There are a few ways, choose whichever you prefer (pick one, don't do all three!):
-
Search the Library Registry for
MaffooClock/ESP32RotaryEncoder
and install it automatically. -
Edit your platformio.ini file and add
MaffooClock/ESP32RotaryEncoder@^1.1.0
to yourlib_deps
stanza. -
Use the command line interface:
cd MyProject pio pkg install --library "MaffooClock/ESP32RotaryEncoder@^1.1.0"
There are two ways (pick one, don't do both!):
-
Search the Library Manager for
ESP32RotaryEncoder
-
Manual install: download the latest release, then see the documentation on Importing a .zip Library.
Just add include <ESP32RotaryEncoder.h>
to the top of your source file.
Adding a rotary encoder instance is easy:
-
Include the library:
#include <ESP32RotaryEncoder.h>
-
Define which pins to use, if you prefer to do it this way -- you could also just set the pins in the constructor (step 3):
// Change these to the actual pin numbers that you've connected your rotary encoder to const int8_t DO_ENCODER_VCC = D2; // Only needed if you're using a GPIO pin to supply the 3.3v reference const int8_t DI_ENCODER_SW = D3; // Pushbutton, if your rotary encoder has it const uint8_t DI_ENCODER_A = D5; // Might be labeled CLK const uint8_t DI_ENCODER_B = D4; // Might be labeled DT
-
Instantiate a
RotaryEncoder
object:a) This uses a GPIO pin to provide the 3.3v reference:
RotaryEncoder rotaryEncoder( DI_ENCODER_A, DI_ENCODER_B, DI_ENCODER_SW, DO_ENCODER_VCC );
b) ...or you can free up the GPIO pin and tie Vcc to 3V3, then just omit that argument:
RotaryEncoder rotaryEncoder( DI_ENCODER_A, DI_ENCODER_B, DI_ENCODER_SW );
c) ...or maybe your rotary encoder doesn't have a pushbutton?
RotaryEncoder rotaryEncoder( DI_ENCODER_A, DI_ENCODER_B );
d) ...or you want to use a different library with the pushbutton, but still use a GPIO to provide the 3.3v reference:
RotaryEncoder rotaryEncoder( DI_ENCODER_A, DI_ENCODER_B, -1, DO_ENCODER_VCC );
-
Add callbacks:
void knobCallback( long value ) { // This gets executed every time the knob is turned Serial.printf( "Value: %i\n", value ); } void buttonCallback( unsigned long duration ) { // This gets executed every time the pushbutton is pressed Serial.printf( "boop! button was down for %u ms\n", duration ); }
-
Configure and initialize the
RotaryEncoder
object:void setup() { Serial.begin( 115200 ); // This tells the library that the encoder has its own pull-up resistors rotaryEncoder.setEncoderType( EncoderType::HAS_PULLUP ); // Range of values to be returned by the encoder: minimum is 1, maximum is 10 // The third argument specifies whether turning past the minimum/maximum will // wrap around to the other side: // - true = turn past 10, wrap to 1; turn past 1, wrap to 10 // - false = turn past 10, stay on 10; turn past 1, stay on 1 rotaryEncoder.setBoundaries( 1, 10, true ); // The function specified here will be called every time the knob is turned // and the current value will be passed to it rotaryEncoder.onTurned( &knobCallback ); // The function specified here will be called every time the button is pushed and // the duration (in milliseconds) that the button was down will be passed to it rotaryEncoder.onPressed( &buttonCallback ); // This is where the inputs are configured and the interrupts get attached rotaryEncoder.begin(); }
-
Done! The library doesn't require you to do anything in
loop()
:void loop() { // Your stuff here }
There are other options and methods you can call, but this is just the most basic implementation.
Important
Keep the onTurned()
and onPressed()
callbacks lightweight, and definitely do not use any calls to delay()
here. If you need to do some heavy lifting or use delays, it's better to set a flag here, then check for that flag in your loop()
and run the appropriate functions from there.
This library makes use of the ESP32-IDF native logging to output some helpful debugging messages to the serial console. To see it, you may have to add a build flag to set the logging level. For PlatformIO, add -DCORE_DEBUG_LEVEL=4
to the build_flags
option in platformio.ini.
After debugging, you can either remove the build flag (if you had to add it), or just reduce the level from debug (4) to info (3), warning (2), or error (1). You can also use verbose (5) to get a few more messages beyond debug, but the overall output from sources other than this library might be noisy.
See esp32-hal-log.h for more details.
So far, this has only been tested on an Arduino Nano ESP32. This should work on any ESP32 in Arduino IDE and PlatformIO as long as your framework packages are current.
This library more than likely won't work at all on non-ESP32 devices -- it uses features from the ESP32 IDF, such as esp_timer.h, along with FunctionalInterrupt.h from the Arduino API. So, to try and use this on a non-ESP32 might require some serious overhauling.
Check the examples folder.