Skip to content

Instantly share code, notes, and snippets.

@mnesarco
Created June 30, 2022 22:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mnesarco/9f138cef1308904ba3050324feb0d9e6 to your computer and use it in GitHub Desktop.
Save mnesarco/9f138cef1308904ba3050324feb0d9e6 to your computer and use it in GitHub Desktop.
Arduino Rotary Encoder with Automatic Debounce
/* Rotary encoder handler for arduino. v2.0
*
* Original author of version 1.1: Ben Buxton <bb@cactii.net>
* https://github.com/buxtronix/arduino/tree/master/libraries/Rotary
*
* Author of version 2.0 based on 1.1: Frank Martinez <https://twitter.com/mnesarco>
*
* Copyright 2011 Ben Buxton. Licensed under the GNU GPL Version 3.
* Contact: bb@cactii.net
*
* Copyright 2022 Frank Martinez.
* Contact: <https://twitter.com/mnesarco>
*
* +------------------------------------------------------+
* | Changes introduced in version 2.0: |
* | |
* | 1. Make it a Header only library. |
* | 2. Removed macros, replaced by c++ templates. |
* | 3. Added begin() method because pinMode should not |
* | be called before setup(). |
* +------------------------------------------------------+
* | Usage: |
* | |
* | Rotary<HalfStepEncoder, INPUT_PULLUP> half(2,3); |
* | Rotary<FullStepEncoder, INPUT_PULLUP> full(4,5); |
* | |
* | void setup() |
* | { |
* | half.begin(); |
* | full.begin(); |
* | } |
* | |
* | loop() |
* | { |
* | unsigned char v1 = half.process(); |
* | unsigned char v2 = full.process(); |
* | } |
* | |
* | Rest is the same as of 1.1 |
* +------------------------------------------------------+
*
* A typical mechanical rotary encoder emits a two bit gray code
* on 3 output pins. Every step in the output (often accompanied
* by a physical 'click') generates a specific sequence of output
* codes on the pins.
*
* There are 3 pins used for the rotary encoding - one common and
* two 'bit' pins.
*
* The following is the typical sequence of code on the output when
* moving from one step to the next:
*
* Position Bit1 Bit2
* ----------------------
* Step1 0 0
* 1/4 1 0
* 1/2 1 1
* 3/4 0 1
* Step2 0 0
*
* From this table, we can see that when moving from one 'click' to
* the next, there are 4 changes in the output code.
*
* - From an initial 0 - 0, Bit1 goes high, Bit0 stays low.
* - Then both bits are high, halfway through the step.
* - Then Bit1 goes low, but Bit2 stays high.
* - Finally at the end of the step, both bits return to 0.
*
* Detecting the direction is easy - the table simply goes in the other
* direction (read up instead of down).
*
* To decode this, we use a simple state machine. Every time the output
* code changes, it follows state, until finally a full steps worth of
* code is received (in the correct order). At the final 0-0, it returns
* a value indicating a step in one direction or the other.
*
* It's also possible to use 'half-step' mode. This just emits an event
* at both the 0-0 and 1-1 positions. This might be useful for some
* encoders where you want to detect all positions.
*
* If an invalid state happens (for example we go from '0-1' straight
* to '1-0'), the state machine resets to the start until 0-0 and the
* next valid codes occur.
*
* The biggest advantage of using a state machine over other algorithms
* is that this has inherent debounce built in. Other algorithms emit spurious
* output with switch bounce, but this one will simply flip between
* sub-states until the bounce settles, then continue along the state
* machine.
* A side effect of debounce is that fast rotations can cause steps to
* be skipped. By not requiring debounce, fast rotations can be accurately
* measured.
* Another advantage is the ability to properly handle bad state, such
* as due to EMI, etc.
* It is also a lot simpler than others - a static state table and less
* than 10 lines of logic.
*/
#ifndef RotaryEncoder_h
#define RotaryEncoder_h
#include <Arduino.h>
struct HalfStepEncoder {};
struct FullStepEncoder {};
// Values returned by 'process'
// No complete step yet.
static constexpr unsigned char DIR_NONE = 0x0;
// Clockwise step.
static constexpr unsigned char DIR_CW = 0x10;
// Anti-clockwise step.
static constexpr unsigned char DIR_CCW = 0x20;
/*
* The below state table has, for each state (row), the new state
* to set based on the next encoder output. From left to right in,
* the table, the encoder outputs are 00, 01, 10, 11, and the value
* in that position is the new state to set.
*/
static constexpr unsigned char R_START = 0x0;
// Full Step States
static constexpr unsigned char RF_CW_FINAL = 0x1;
static constexpr unsigned char RF_CW_BEGIN = 0x2;
static constexpr unsigned char RF_CW_NEXT = 0x3;
static constexpr unsigned char RF_CCW_BEGIN = 0x4;
static constexpr unsigned char RF_CCW_FINAL = 0x5;
static constexpr unsigned char RF_CCW_NEXT = 0x6;
template<typename EncoderType = FullStepEncoder>
constexpr unsigned char ttable[7][4] = {
// R_START
{R_START, RF_CW_BEGIN, RF_CCW_BEGIN, R_START},
// RF_CW_FINAL
{RF_CW_NEXT, R_START, RF_CW_FINAL, R_START | DIR_CW},
// RF_CW_BEGIN
{RF_CW_NEXT, RF_CW_BEGIN, R_START, R_START},
// RF_CW_NEXT
{RF_CW_NEXT, RF_CW_BEGIN, RF_CW_FINAL, R_START},
// RF_CCW_BEGIN
{RF_CCW_NEXT, R_START, RF_CCW_BEGIN, R_START},
// RF_CCW_FINAL
{RF_CCW_NEXT, RF_CCW_FINAL, R_START, R_START | DIR_CCW},
// RF_CCW_NEXT
{RF_CCW_NEXT, RF_CCW_FINAL, RF_CCW_BEGIN, R_START},
};
// Half Step States
static constexpr unsigned char RH_CCW_BEGIN = 0x1;
static constexpr unsigned char RH_CW_BEGIN = 0x2;
static constexpr unsigned char RH_START_M = 0x3;
static constexpr unsigned char RH_CW_BEGIN_M = 0x4;
static constexpr unsigned char RH_CCW_BEGIN_M = 0x5;
template<>
constexpr unsigned char ttable<HalfStepEncoder>[6][4] = {
// R_START (00)
{RH_START_M, RH_CW_BEGIN, RH_CCW_BEGIN, R_START},
// RH_CCW_BEGIN
{RH_START_M | DIR_CCW, R_START, RH_CCW_BEGIN, R_START},
// RH_CW_BEGIN
{RH_START_M | DIR_CW, RH_CW_BEGIN, R_START, R_START},
// RH_START_M (11)
{RH_START_M, RH_CCW_BEGIN_M, RH_CW_BEGIN_M, R_START},
// RH_CW_BEGIN_M
{RH_START_M, RH_START_M, RH_CW_BEGIN_M, R_START | DIR_CW},
// RH_CCW_BEGIN_M
{RH_START_M, RH_CCW_BEGIN_M, RH_START_M, R_START | DIR_CCW},
};
template<typename EncoderType = FullStepEncoder, uint8_t PinMode = INPUT_PULLUP>
class Rotary
{
public:
Rotary(char _pin1, char _pin2) {
pin1 = _pin1;
pin2 = _pin2;
state = R_START;
}
void begin()
{
pinMode(pin1, PinMode);
pinMode(pin2, PinMode);
}
unsigned char process() {
// Grab state of input pins.
unsigned char pinstate = (digitalRead(pin2) << 1) | digitalRead(pin1);
// Determine new state from the pins and state table.
state = ttable<EncoderType>[state & 0xf][pinstate];
// Return emit bits, ie the generated event.
return state & 0x30;
}
private:
unsigned char state;
unsigned char pin1;
unsigned char pin2;
};
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment