Rotary Encoder KY40 with Interrupt

hello everyone,
I am working on a project using KY40 rotary encoder.


I did my research around the internet, tried several examples, tested my own code, but nothing works. except for this one this code works flawlessly :

// Used for generating interrupts using CLK signal
const int PinA = 3; 

// Used for reading DT signal
const int PinB = 4;   

// Updated by the ISR (Interrupt Service Routine)
volatile int virtualPosition = 0;

void isr ()  {
  static unsigned long lastInterruptTime = 0;
  unsigned long interruptTime = millis();

  // If interrupts come faster than 5ms, assume it's a bounce and ignore
  if (interruptTime - lastInterruptTime > 5) {
    if (digitalRead(PinB) == LOW)
    {
      virtualPosition-- ;
    }
    else {
      virtualPosition++ ;
    }

    // Keep track of when we were here last (no more than every 5ms)
    lastInterruptTime = interruptTime;
  }
}


void setup() {
  // Just whilst we debug, view output on serial monitor
  Serial.begin(9600);

  // Rotary pulses are INPUTs
  pinMode(PinA, INPUT);
  pinMode(PinB, INPUT);

  // Attach the routine to service the interrupts
  attachInterrupt(digitalPinToInterrupt(PinA), isr, LOW);

  // Ready to go!
  Serial.println("Start");
}


void loop() {
  Serial.println(virtualPosition);
}

well, the problems are :

  1. the code above is using LOW level interrupt trigger so it has the possibility to be triggered multiple times if the encoder is rotating super slowly (hasn't happened so far). I think using falling / raising edge triggered interrupt is more appropriate(correct me if I'm wrong). But if I do so, it won't work.
  2. I'm only using Arduino as a prototype. After I make sure everything is working properly I'll switch to stm32 and stm32 can only do edge triggered interrupt (raising, falling, change).

any solutions? thanks.

I assume the above code works as you do debounce in software.

Most rotary encoders need 2 capacitors and 2 resistors to do debounce in hardware.
Check the datasheet for details.

to work woth CHANGE you might need to change this line

if (digitalRead(PinB) == digitalRead(PinA))

robtillaart:
I assume the above code works as you do debounce in software.

Most rotary encoders need 2 capacitors and 2 resistors to do debounce in hardware.
Check the datasheet for details.

to work woth CHANGE you might need to change this line

if (digitalRead(PinB) == digitalRead(PinA))

added 2 capacitor and resistor, changed the code as you suggest, still not working :frowning:

Do you have a link to the datasheet?

Can you post your schematic?

Abizhar:
I did my research around the internet, tried several examples, tested my own code, but nothing works.

Was the Buxtronix approach tried? Works well for me with no additional external passive components.

I use simple capacitors for debounce:

Encoder.jpg
This is my basic encoder code:

#define ClockPin 2 // Must be pin 2 
#define DataPin 3 // Must be pin 3
#define readA bitRead(PIND,2)//faster than digitalRead()  (((value) >> (bit)) & 0x01)
#define readB bitRead(PIND,3)//faster than digitalRead()  (((value) >> (bit)) & 0x01)
volatile long count = 0;
long lastCtr;
unsigned long SpareCycles;

void Encoder(bool A) {
  (readB == A)  ? count++ : count--;
}

void setup() {
  Serial.begin(115200); //115200
  pinMode(ClockPin, INPUT);
  pinMode(DataPin, INPUT);
  /* 1 Step */
  attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( true);}, RISING);
  /**/

  /*  2 step count
  attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( readB);}, CHANGE);
  */
  
  /* Full 4 Step Count
  attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( readB);}, CHANGE);
  attachInterrupt(digitalPinToInterrupt(DataPin ), [] {Encoder( !readA);}, CHANGE);
  */
}

void loop() {
  long Counter;
  SpareCycles++;
  noInterrupts ();
  Counter = count;
  interrupts ();
  if ((lastCtr != Counter) & (SpareCycles > 10)) {
    Serial.println(Counter);
    SpareCycles = 0;
  }
  lastCtr = Counter;
}

Hello zhomeslice,

I can't compile your code "Rotary Encoder KY40 with Interrupt" on Jan,05,2018

Error line is:

attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( true);}, RISING);

My Arduino version is 1.05.-r2 with UNO

Please help

glamo:
Hello zhomeslice,

I can't compile your code "Rotary Encoder KY40 with Interrupt" on Jan,05,2018

Error line is:

attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( true);}, RISING);

My Arduino version is 1.05.-r2 with UNO

Please help

this is a lambda anonymous function

[] {Encoder( true);}

it is a function all in its self inserted into the callback portion of the attached interrupt because the attach Interrupt can't take a value

attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( true);}, RISING);

This is identical for the following MyCallbackFunction function and attachinterrupt

void MyCallbackFunction(){
   Encoder( true);
}
attachInterrupt(digitalPinToInterrupt(ClockPin),MyCallbackFunction, RISING);

Hope that helps :slight_smile:
Z
attach your code and or the entire error message if I'm to determine why my original code failed to compile
This Compiles:

Sketch uses 2262 bytes (7%) of program storage space. Maximum is 32256 bytes.
Global variables use 204 bytes (9%) of dynamic memory, leaving 1844 bytes for local variables. Maximum is 2048 bytes.

code:

#define ClockPin 2 // Must be pin 2 
#define DataPin 3 // Must be pin 3
#define readA bitRead(PIND,2)//faster than digitalRead()  (((value) >> (bit)) & 0x01)
#define readB bitRead(PIND,3)//faster than digitalRead()  (((value) >> (bit)) & 0x01)
volatile long count = 0;
long lastCtr;
unsigned long SpareCycles;

void Encoder(bool A) {
  (readB == A)  ? count++ : count--;
}

void setup() {
  Serial.begin(115200); //115200
  pinMode(ClockPin, INPUT);
  pinMode(DataPin, INPUT);
  /* 1 Step */
  attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( true);}, RISING);
  /**/

  /*  2 step count
  attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( readB);}, CHANGE);
  */
  
  /* Full 4 Step Count
  attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( readB);}, CHANGE);
  attachInterrupt(digitalPinToInterrupt(DataPin ), [] {Encoder( !readA);}, CHANGE);
  */
}

void loop() {
  long Counter;
  SpareCycles++;
  noInterrupts ();
  Counter = count;
  interrupts ();
  if ((lastCtr != Counter) & (SpareCycles > 10)) {
    Serial.println(Counter);
    SpareCycles = 0;
  }
  lastCtr = Counter;
}

check this topic - Lightweight Arduino Library for Rotary Encoder

enjoyneering:
check this topic - Lightweight Arduino Library for Rotary Encoder

whats lighter weight than this?

#define ClockPin 2 // Must be pin 2 
#define DataPin 3 // Must be pin 3
#define readA bitRead(PIND,2)//faster than digitalRead()  (((value) >> (bit)) & 0x01)
#define readB bitRead(PIND,3)//faster than digitalRead()  (((value) >> (bit)) & 0x01)
volatile long count = 0;
void Encoder(bool A) {
  (readB == A)  ? count++ : count--;
}
void setup() {
  Serial.begin(115200); //115200
  pinMode(ClockPin, INPUT);
  pinMode(DataPin, INPUT);
  /* 1 Step */
  attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( true);}, RISING);
}
void loop() {
  long Counter;
  noInterrupts ();
  Counter = count;
  interrupts ();
  if ((lastCtr != Counter)) {
    Serial.println(Counter);
  }
  lastCtr = Counter;
}

Z

zhomeslice:
whats lighter weight than this?

you are doing it wrong:

  • The encoder uses the Manchester code. You must read A & B at the same time, otherwise you will have wrong counts due to noise, debounce...
  • pins and ports are hardcoded and can only be used with atmega328 / 168

The only reason you get a decent result with your code is because the atmega328 & arduino framework is so slow and cannot catch the noise yet. :slight_smile: But the encoder is aging & debounce will increase....

Try your code on fast Cortex MCU, for example, STM32F103xxxx or ESP8266 - you will be surprised. Ohh but you can't - pins and ports are hardcoded

enjoyneering:
you are doing it wrong:

  • The encoder uses the Manchester code. You must read A & B at the same time, otherwise you will have wrong counts due to noise, debounce...
  • pins and ports are hardcoded and can only be used with atmega328 / 168

The only reason you get a decent result with your code is because the atmega328 & arduino framework is so slow and cannot catch the noise yet. :slight_smile: But the encoder is aging & debounce will increase....

Try your code on fast Cortex MCU, for example, STM32F103xxxx or ESP8266 - you will be surprised. Ohh but you can't - pins and ports are hardcoded

So how long of a delay between the interrupt triggering (pin2) and the Reading of Pin3?
(this should be almost instantaneous.)
We know the Clock pin Just went HIGH (Pin2). the callback immediately reads the Data Pin 3 which is either HIGH or LOW. Now we know that the direction of rotation. and can make a count.
You could add some sort of debounce timer on the interrupt maybe turn off interrupts for a short period of time. the Data pin is in the middle of its pulse and shouldn't be bouncing by now.
What do you think?
Z
Z

I do not like software debounce timers - eats memory and cpu time. It is easier for me to add 0.1uf between each pin and ground.

As for the first question, this is how I did it:

enjoyneering:
I do not like software debounce timers - eats memory and cpu time. It is easier for me to add 0.1uf between each pin and ground.

As for the first question, this is how I did it:

@enjoyneering this is interesting! Thank you.
I Have a question.
how does the shift _prevValueAB =_curvalAB << 2; get clear _prevValueAB Shifting bits further over would not match the switch case: options
_prevValueAB 00000001 <<2 changes to 00000111 <<2 changes to 00011110 changes to 01111000 and so on so this would need to be managed or we will not match.
How do you do it?

My thoughts: maybe change

switch ((_prevValueAB !_currValueAB))
to 
switch ((_prevValueAB !_currValueAB) &0x0F) / keep only first 4 bytes

I'm working on a version of my code to match your suggestion :slight_smile:
Thank you
Z

this is not true:

zhomeslice:
_prevValueAB 00000001 <<2 changes to 00000111

the shifting works like this:

_prevValueAB 00000001 <<2 changes to 00000100

see official manual

this is how my code works:

prevAB=0b00001000

read A = 1 = 0b00000001

A << 1 = 0b00000010

read B = 1 = 0b00000001

currAB = A | B = 0b00000011

switch(prevAB | currAB ) = 0b00001011

prevAB = currAB << 2 = 0b00000011 << 2 = 0b00001100

enjoyneering:
this is not true:

the shifting works like this:

_prevValueAB 00000001 <<2 changes to 00000100

see official manual

ok then when you read again
_prevValueAB 00000100 <<2 changes to 00010010
then again:
_prevValueAB 00010010 <<2 changes to 01001011
How does your "switch case" handle the subsequent shifts in the uint8_t (byte)
z

this is how my code works:

prevAB=0b00001000

read A = 1 = 0b00000001

A << 1 = 0b00000010

read B = 1 = 0b00000001

currAB = A | B = 0b00000011

switch(prevAB | currAB ) = 0b00001011 //the result is not assigned to prevAB or currAB, it is a third temporary variable in the memory

prevAB = currAB << 2 = 0b00000011 << 2 = 0b00001100

enjoyneering:
this is how my code works:

prevAB=0b00001000

read A = 1 = 0b00000001

A << 1 = 0b00000010

read B = 1 = 0b00000001

currAB = A | B = 0b00000011

switch(prevAB | currAB ) = 0b00001011 //the result is not assigned to prevAB or currAB, it is a third temporary variable in the memory

prevAB = currAB << 2 = 0b00000011 << 2 = 0b00001100

I see my error in reading the code I interpreted. this line as prevAB |= currAB << 2 = 0b00000011 << 2 = 0b00001100
I don't know what I was thinking lol

Z

enjoyneering:
you are doing it wrong:

  • The encoder uses the Manchester code. You must read A & B at the same time, otherwise you will have wrong counts due to noise, debounce...
  • pins and ports are hardcoded and can only be used with atmega328 / 168

The only reason you get a decent result with your code is because the atmega328 & arduino framework is so slow and cannot catch the noise yet. :slight_smile: But the encoder is aging & debounce will increase....

Try your code on fast Cortex MCU, for example, STM32F103xxxx or ESP8266 - you will be surprised. Ohh but you can't - pins and ports are hardcoded

So Heres an example using both ways. I believe the additional 2 Step and 4 Step will work also but I haven't tested them with your method yet. I'm currently working on another project.
I also switched to digitalRead to provide flexibility for the faster ESP8266.
My thoughts are this.
when using interrupts with RISING we already know the prior state of the pin being interrupted LOW and it just transitioned to HIGH. The other pin Is not transitioning and so it is stable no bounce If they are == then we are moving forward if they are != then we are moving backward.

I'm still trying to understand how your code differs from my code in final results.
Here is the test program I made. Will this meet the needs to prove/disprove my thery.

#define ClockPin 2 // Must be pin 2 
#define DataPin 3 // Must be pin 3
//#define readA bitRead(PIND,2)//faster than digitalRead()  (((value) >> (bit)) & 0x01)
//#define readB bitRead(PIND,3)//faster than digitalRead()  (((value) >> (bit)) & 0x01)

#define readA digitalRead(ClockPin)// Not Hard Coded
#define readB digitalRead(DataPin)// Not Hard Coded

volatile long count1 = 0;
volatile long count2 = 0;
volatile byte PreviousAB;

// Note digitalPinToInterrupt(PIN#) Should always be High (RISING) with the interrupt for 1 Setp
// We will read both pins
void Encoder(bool A,bool B) { 
  // @enjoyneering Manchester code way
  byte CurrentAB;
  CurrentAB = A << 1 | B;
  switch ((PreviousAB | CurrentAB))
  {
    case 0b0001: case 0b1110:
      count1++;
      break;
    case 0b0100: case 0b1011:
      count1--;
      break;
  }
  PreviousAB = CurrentAB << 2;
  // @ZHomeslice Other Way.
  (A == B)  ? count2++ : count2--; 
}

void setup() {
  Serial.begin(115200); //115200
  pinMode(ClockPin, INPUT);
  pinMode(DataPin, INPUT);
  PreviousAB = (readB << 1 | readB) << 2; // initialize PreviousAB state
  /* 1 Step */
  attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( readA,readB);}, RISING);
  /**/

  /*  2 step count
    attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( readA,readB);}, CHANGE);
  */

  /* Full 4 Step Count
    attachInterrupt(digitalPinToInterrupt(ClockPin), [] {Encoder( readA,readB);}, CHANGE);
    attachInterrupt(digitalPinToInterrupt(DataPin ), [] {Encoder( !readA,readB);}, CHANGE);
  */
}

void loop() {
  static long lastCtr1;
  static long lastCtr2;
  long Counter1;
  long Counter2;
  noInterrupts ();
  Counter1 = count1;
  Counter2 = count2;
  interrupts ();
  if ((lastCtr1 != Counter1) || (lastCtr2 != Counter2)) {
    Serial.print("Counter 1 =");
    Serial.print(Counter1);
    Serial.print("Counter 2 =");
    Serial.println(Counter2);
    SpareCycles = 0;
    lastCtr1 = Counter1;
    lastCtr2 = Counter2;
    
  }
  
}

Z

did you read attachInterrupt()?

  • the ISR function must take no parameters & return nothing

when using interrupts with RISING

There are two common methods to read the encoder - internal (attached to the pin) interrupt or interrupt by the timer. With your assumptions you won't be able to use second method (see ticker example for esp8266 and TimerOne for AVR).