How to use a rotary encoder

I am making my own DIY circuit board that uses two rotary controller. I cant seem to read the values of the rotary encoder correctly. The values that I do read only work in one direction when rotating the encoder. And even these values are only sometimes work.

I have wired the two rotary encoders as in the image attached. With the four resistors as 10k

I am using the encoders WITHOUT breakout boards. So i had to add my own resistors

I have the code that I wrote below. I have only shown the code for one encoder. However, its obvious and easy how to duplicate the code for the 'right' encoder

Im not sure if my problem is the way that I have wired the rotary encoders. Or if it is a coding problem

Any help is greatly appreciated

// rotary encoders
int LeftOutApin = 43;
int LeftOutBpin = 44;
int LeftCounter = 0;
int LeftState;
int LeftLastState;


void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  
  pinMode(LeftOutApin, INPUT);
  pinMode(LeftOutBpin, INPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
LeftRotaryEncoder();

Serial.print("Left: ");
Serial.println(LeftCounter); 
}


void LeftRotaryEncoder() {
  LeftState = digitalRead(LeftOutApin);
  if (LeftState != LeftLastState) {
    if (digitalRead(LeftOutBpin) != LeftState) {
      LeftCounter ++;
    } else {
      LeftCounter --;
    }
  }
  LeftLastState = LeftState;
}

some rotary-encoders with mechanical switches are unreliable.
if you change rotation-direction. Because sometimes the switch is already opened sometimes not. This leads to wrong readings.

some time ago user argmue posted a rotery-encoder-sketch that takes care of all these special situations

/*some rotary-encoders with mechanical switches are unreliable 
 * if you change rotation-direction. 
 * Because sometimes the switch is already opened sometimes not. 
 * This leads to wrong readings. some time ago user argmue 
 * posted a rotery-encoder-sketch that takes care of all 
 * these special situations
*/

// rotary-encoder with debounce written by user argmue

const byte ENCODER_A_PIN = 3;
const byte ENCODER_B_PIN = 2;
const byte SWITCH_PIN = 4;

void setup() {
  Serial.begin(115200);
  Serial.println("\nStart");
  pinMode(ENCODER_A_PIN, INPUT);
  pinMode(ENCODER_B_PIN, INPUT);
  pinMode(SWITCH_PIN, INPUT);
}

void loop() {
  int8_t state = 0;
  if (rotaryEncoder(state)) {
    Serial.println("- SWITCH -");
  }
  if (state == -1) {
    Serial.println("<-- ");
  }
  if (state == 1) {
    Serial.println(" -->");
  }
}

bool rotaryEncoder(int8_t &delta) {
  delta = 0;
  enum {STATE_LOCKED, STATE_TURN_RIGHT_START, STATE_TURN_RIGHT_MIDDLE, STATE_TURN_RIGHT_END, STATE_TURN_LEFT_START, STATE_TURN_LEFT_MIDDLE, STATE_TURN_LEFT_END, STATE_UNDECIDED};
  static uint8_t encoderState = STATE_LOCKED;
  bool a = !digitalRead(ENCODER_A_PIN);
  bool b = !digitalRead(ENCODER_B_PIN);
  bool s = !digitalRead(SWITCH_PIN);
  static bool switchState = s;
  switch (encoderState) {
    case STATE_LOCKED:
      if (a && b) {
        encoderState = STATE_UNDECIDED;
      }
      else if (!a && b) {
        encoderState = STATE_TURN_LEFT_START;
      }
      else if (a && !b) {
        encoderState = STATE_TURN_RIGHT_START;
      }
      else {
        encoderState = STATE_LOCKED;
      };
      break;
    case STATE_TURN_RIGHT_START:
      if (a && b) {
        encoderState = STATE_TURN_RIGHT_MIDDLE;
      }
      else if (!a && b) {
        encoderState = STATE_TURN_RIGHT_END;
      }
      else if (a && !b) {
        encoderState = STATE_TURN_RIGHT_START;
      }
      else {
        encoderState = STATE_LOCKED;
      };
      break;
    case STATE_TURN_RIGHT_MIDDLE:
    case STATE_TURN_RIGHT_END:
      if (a && b) {
        encoderState = STATE_TURN_RIGHT_MIDDLE;
      }
      else if (!a && b) {
        encoderState = STATE_TURN_RIGHT_END;
      }
      else if (a && !b) {
        encoderState = STATE_TURN_RIGHT_START;
      }
      else {
        encoderState = STATE_LOCKED;
        delta = -1;
      };
      break;
    case STATE_TURN_LEFT_START:
      if (a && b) {
        encoderState = STATE_TURN_LEFT_MIDDLE;
      }
      else if (!a && b) {
        encoderState = STATE_TURN_LEFT_START;
      }
      else if (a && !b) {
        encoderState = STATE_TURN_LEFT_END;
      }
      else {
        encoderState = STATE_LOCKED;
      };
      break;
    case STATE_TURN_LEFT_MIDDLE:
    case STATE_TURN_LEFT_END:
      if (a && b) {
        encoderState = STATE_TURN_LEFT_MIDDLE;
      }
      else if (!a && b) {
        encoderState = STATE_TURN_LEFT_START;
      }
      else if (a && !b) {
        encoderState = STATE_TURN_LEFT_END;
      }
      else {
        encoderState = STATE_LOCKED;
        delta = 1;
      };
      break;
    case STATE_UNDECIDED:
      if (a && b) {
        encoderState = STATE_UNDECIDED;
      }
      else if (!a && b) {
        encoderState = STATE_TURN_RIGHT_END;
      }
      else if (a && !b) {
        encoderState = STATE_TURN_LEFT_END;
      }
      else {
        encoderState = STATE_LOCKED;
      };
      break;
  }

  uint32_t current_time = millis();
  static uint32_t switch_time = 0;
  const uint32_t bounce_time = 30;
  bool back = false;
  if (current_time - switch_time >= bounce_time) {
    if (switchState != s) {
      switch_time = current_time;
      back = s;
      switchState = s;
    }
  }
  return back;
}

best regards Stefan

serial prints cause delays. i found that I need to use interrupts even when turning the shaft with my hand.

#include <Arduino.h>

#include "encoder.h"
#include "pins.h"

// -----------------------------------------------------------------------------
//  table containing action indexed by inputs BA and state

typedef enum { ___, For, Rev, Skp, ActSize } Act_t;

const char* actStr [] = { "___", "For", "Rev", "Skp", "ActSize" };

Act_t  encAct [ActSize][ActSize] = {
// state  00   01   10   11      inputs
       { ___, Rev, For, Skp },  // 00
       { For, ___, Skp, Rev },  // 01
       { Rev, Skp, ___, For },  // 10
       { Skp, For, Rev, ___ },  // 11
};

int encApos = 0;
int encBpos = 0;

int encAst  = 0;
int encBst  = 0;

// ------------------------------------------------
// read brake
inline void readEncoder (
    byte  dt,
    byte  clk,
    int & brkPos,
    int & encSt )
{
    byte  val = digitalRead (dt) << 1 | digitalRead (clk);

    switch (encAct [val][encSt])  {
    case For:
        brkPos++;
        break;

    case Rev:
        brkPos--;
        break;

    default:
        break;
    }

    encSt = val;

 // digitalWrite (LED, ! digitalRead (LED));
}

// -------------------------------------
void IRAM_ATTR isrEncA (void)
{
    readEncoder (Enc_A_Dt, Enc_A_Clk, encApos, encAst);
}

// -------------------------------------
void IRAM_ATTR isrEncB (void)
{
    readEncoder (Enc_B_Dt, Enc_B_Clk, encBpos, encBst);
}

// -------------------------------------
void
encoderInit (void)
{
    // encoders
    pinMode (Enc_A_Clk, INPUT_PULLUP);
    pinMode (Enc_A_Dt,  INPUT_PULLUP);

    attachInterrupt (Enc_A_Clk, isrEncA, CHANGE);
    attachInterrupt (Enc_A_Dt,  isrEncA, CHANGE);

    pinMode (Enc_B_Clk, INPUT_PULLUP);
    pinMode (Enc_B_Dt,  INPUT_PULLUP);

    attachInterrupt (Enc_B_Clk, isrEncB, CHANGE);
    attachInterrupt (Enc_B_Dt,  isrEncB, CHANGE);
}

With standard encoder-code I encountered more errors in counting with ISR than when using polling.

The testcodes always included serial output of a counting up/down variable.

best regards Stefan

gcjr:
serial prints cause delays. i found that I need to use interrupts even when turning the shaft with my hand.

This is key. You're printing on every iteration of loop which will soon fill the outgoing buffer and your code will block so you miss pulses. You should probably increase the baud rate substantially, but that won't be enough by itself.

Add some logic so that you only print once a second or whenever the encoder value changes - perhaps both.

StefanL38:
With standard encoder-code I encountered more errors in counting with ISR than when using polling.

The state table approach to using encoders described HERE is, by far, the best I've come across. It handles contact bounce and rotation reversals flawlessly.

I've created an Interrupt-Driven Implementaion of this technique. Works great.

1 Like

StefanL38:
With standard encoder-code I encountered more errors in counting with ISR than when using polling.

how is that possible? an ISR is the only viable approach when driven by a motor at high speeds and the means to avoid being delayed by prints

Good job on your first post with using the code tags.

Add some logic so that you only print once a second or whenever the encoder value changes - perhaps both.

Yes, this is good advice.

Are you using Keyes 040 encoders? Are there detents?

Because of the switch states where the detents lie, and the lack of mechanical precision in how these encoders are made, the checking for state change of only one pin, (the algorithm you use) is not be a good way to read these encoders.

Does your code work any better if you switch the A and B pins?

gcjr:
how is that possible? an ISR is the only viable approach when driven by a motor at high speeds and the means to avoid being delayed by prints

I guess maybe bad written encoder-code, maybe bouncing of the mechanical switch
or that thing
let's assume encoder is rotated CW which means A ahead of B
with stopping the encoder while switch A is still closed and turn into the other direction
which creates first switch A turns to low before B turns low which looks like still rotating CW while in reality it is CCW - for one pulse - then the normal pattern B ahead A occurs for CCW
The ISR itself did only the detection. Serial-output was done in loop
best regards Stefan

StefanL38:
let's assume encoder is rotated CW which means A ahead of B
with stopping the encoder while switch A is still closed and turn into the other direction
which creates first switch A turns to low before B turns low which looks like still rotating CW while in reality it is CCW - for one pulse - then the normal pattern B ahead A occurs for CCW

The algorithm and code I linked in Reply #5 will handle this case properly.

Hi gfvalvo,

the code works flawlessly.
Thank you for giving a headsup to this library.

But there are some other flaws that are a big hurdle for newbees.

Your code is separated into multiple parts of header and *.cpp-files that must be stored in the right place to make it compile.

Of course organising code in this way makes it easier to maintain. So for itself a good thing.
It can be a source of errors. If a depending file has changes in its definitions the file which wants to include it has to be maintained too. in the right way.

But without a detailed manual how to store these files on your harddisk at the right place newbees are lost

I looked up if this libraries are found in the library-manager. I did not find the utiliies.
By the way utility what a common and therefore non-specific name.

same thing with "newencoder" not found in the library-manager.

So what do you think about storing it at GitHub in a way that the library-manager can find it?

best regards Stefan

StefanL38:
Your code is separated into multiple parts of header and *.cpp-files that must be stored in the right place to make it compile.

Of course organising code in this way makes it easier to maintain.

It's also the proper way to develop modular, reusable code.

StefanL38:
So what do you think about storing it at GitHub in a way that the library-manager can find it?

I imagine there's more to making code available in library manager than just how it's stored in GitHub. I think (hope) there's a vetting process by the Arduino development team to decide what libraries are picked up by the manager. Otherwise, any schmuck could create garbage code that people end up trying to use.

The easiest way to install libraries without the manager is to download the code as a .zip from GitHub. Then, use the Arudino IDE: Sketch --> Include Library --> Add .ZIP Library...
This will automatically unzip the files and install them in the proper directory structure in the right location.