Rotary encoder with interrupts counts wrong.

Hi,
I am trying to make a rotary encoder work with my Ardino 101. Right now I am working with a rotary knob but later I will also read a motor encoder. So I will need something faster than digitalRead(). Sadly, the great encoder.h library doesn’t work with my 101.
To avoid port register manipulation, I am just counting the interrupts. Also I couldn't find out if the port registers work the same way on the 101 as on the avr chips.
So the approach bellow works, however every 10 turns, or so, it counts 3 or 5 interrupts instead of 4 and then logically starts counting backwards. Can this be solved with programming or perhaps with external pullups or capacitors? I am not so fit hardwarewise.

const uint8_t encoderButtonPinA = 12;
const uint8_t encoderButtonPinB = 13;

volatile boolean encoderButtonPastA = false;
volatile boolean encoderButtonPastB = false;

volatile int encoderButtonPosition = 0;
volatile int allenc = 0;

void setup() {
  pinMode(encoderButtonPinA, INPUT_PULLUP); 
  pinMode(encoderButtonPinB, INPUT_PULLUP);
  
  encoderButtonPastA = digitalRead(encoderButtonPinA);
  encoderButtonPastB = digitalRead(encoderButtonPinB);
  
  attachInterrupt(encoderButtonPinA, doEncoderButtonA, CHANGE);
  attachInterrupt(encoderButtonPinB, doEncoderButtonB, CHANGE);
  Serial.begin(9600);
  while(!Serial);
}

void loop() {
  Serial.println(encoderButtonPosition / 4); //Division by 4 because the knob increments in 4 steps.
  Serial.println(encoderButtonPastA);
  Serial.println(encoderButtonPastB);
  Serial.println(allenc);
  Serial.println("------");  
  delay(3000);
}

void doEncoderButtonA(){
  static unsigned int lastInterruptTime = 0;//Debounce
  unsigned int interruptTime = micros();
  if (interruptTime - lastInterruptTime > 1000){
    if(encoderButtonPastA == encoderButtonPastB){
      encoderButtonPosition++;  
    } else{
      encoderButtonPosition--;
    }
    encoderButtonPastA = !encoderButtonPastA; 
    allenc++;  
  }
  lastInterruptTime = interruptTime;
}

void doEncoderButtonB(){
  static unsigned int lastInterruptTime = 0;//Debounce
  unsigned int interruptTime = micros();
  if (interruptTime - lastInterruptTime > 1000){
    if(encoderButtonPastA != encoderButtonPastB){
      encoderButtonPosition++;  
    } else{
      encoderButtonPosition--;
    }
    encoderButtonPastB = !encoderButtonPastB;
    allenc++;
  }
  lastInterruptTime = interruptTime;  
}

Am I perhaps doing something wrong with debouncing? It doesn’t seem to get any better than with one millisecond debouncing time.

  attachInterrupt(encoderButtonPinA, doEncoderButtonA, CHANGE);

For most Arduinos, the 1st argument is the interrupt number, not the pin number.

How fast are the interrupts actually happening? Is the problem that the encoder is bouncing? Or is it something else?

Hi PaulS,
Thanks for the quick reply.

PaulS:
For most Arduinos, the 1st argument is the interrupt number, not the pin number.

On the 101 interrupt number equals the pin number.

PaulS:
How fast are the interrupts actually happening?

How can I test that? The interrupt function itself takes about 3 micro seconds. but I can't think of a way right now to measure the time between the electric signal and the beginning of the interrupt function.

PaulS:
Is the problem that the encoder is bouncing? Or is it something else?

I don't know. The results get worse if I go above or below 1 millisecond of debouncing time. It is a really nice 3€ encoder made in Japan. One would expect good results from it.

How can I test that? The interrupt function itself takes about 3 micro seconds. but I can't think of a way right now to measure the time between the electric signal and the beginning of the interrupt function.

What I want to know is how often, when you rotate the encoder, the interrupt happens. While Serial.print() statements in ISRs are generally now a good thing, sometimes you just have to take the risk. Use Serial.print() to print the value of micros() each time the interrupt happens. Show the output.

Does this show any difference:

/*
   Rev 1.00:  July 25, 2015, econjack
*/

#include <rotary.h>

#define ROTARYSWITCHPIN   7                         // Used by switch for rotary encoder

#define ELEMENTCOUNT(x) (sizeof(x) / sizeof(x[0]))  // A macro that calculates the number
// of elements in an array.

// ISR variables:
volatile int rotationDirection;                     // + is CW, - is CCW

int menuSelection;                                  // Which menu is active
int menuIndex;                                      // Which menu option is active

#define MAXMENUS 3                                  // We have the following menus

static char *filters[] = {"500Hz", "1kHz", "1.5kHz", "2kHz", "3kHz"};
static char* incrementStrings[] = {"10", "20", "100", ".5", "1", "2.5", "5", "10", "100"};
static  long favorites[] = {7030, 7195, 14250, 21300, 28500};


Rotary r = Rotary(2, 3);       // sets the pins the rotary encoder uses.  Must be interrupt pins.

void setup() {
  Serial.begin(115200);       // Activate the Serial object. Make sure you set the Monitor to the same rate

  pinMode(ROTARYSWITCHPIN, INPUT_PULLUP); // Use the pullup resistors on the switch pin

  Splash();

  PCICR |= (1 << PCIE2);              // Pin Change Interrupt Enable 2 to set the Pin Change Interrupt Control Register
  PCMSK2 |= (1 << PCINT18) | (1 << PCINT19); // Set up pins 2 and 3 for the control mask
  sei();                              // Set interrupts flag
  rotationDirection = 0;

  unsigned int radChanValue = (9 & 0x001f) >> 0;
  lcd.setCursor(0, 0);
  lcd.print("CHN:");
  lcd.setCursor(4, 0);
  lcd.print(radChanValue);
}


void loop() {
  static int switchState = 1;


  if (Serial.available() > 0) {                           // Anything waiting in the Serial buffer??
    menuSelection = Serial.read() - '0';
    if (menuSelection < 1 || menuSelection > MAXMENUS) {
      menuSelection = 1;                                  // If they can't follow directions, use menu 1.
    }
    menuIndex = 0;
    MenuHeader();
    ShowCurrentSelection();
  }


  switchState = digitalRead(ROTARYSWITCHPIN);
  if (switchState == LOW) {                     // Switch pressed?
    delay(200);                                 // Yep...
    MenuHeader();
    menuIndex = 0;
    menuSelection++;
    if (menuSelection > MAXMENUS)
      menuSelection = 0;
    ShowCurrentSelection();
  }

  if (rotationDirection != 0) {                 // Encoder rotated??
    menuIndex += rotationDirection;             // Yep...
    ShowCurrentSelection();
    rotationDirection = 0;
  }
}

/*****
   This routine displays a simple menu header

   Parameter List:
      void

   Return value:
      void
 *****/
void MenuHeader()
{
  Serial.println();
  Serial.println("====== New Menu Selection =====");
  Serial.println();

}

/*****
   This routine displays a simple menu selection sequence on the Serial monitor

   Parameter List:
      void

   Return value:
      void
 *****/
void Splash()
{
  Serial.println();
  Serial.println("Rotary Encoder Routines");
  Serial.println("Menu Selections: ");
  Serial.println("   1. Favorite Frequencies");
  Serial.println("   2. Filters");
  Serial.println("   3. Frequency Increments");
  Serial.println("");
  Serial.println("Enter 1, 2, or 3 above");
}

/*****
   This routine displays a menu item from the current menu selection

   Parameter List:
      void

   Return value:
      void

   CAUTION: This function assume menuIndex and menuSelection are set
            prior to the call.
 *****/
void ShowCurrentSelection()
{

  if (menuIndex < 0) {   // Should not be negative
    menuIndex = 0;
  }

  switch (menuSelection) {
    case 1:
      if (menuIndex >= ELEMENTCOUNT(favorites)) {  // Rotated too far??
        menuIndex = 0;
      }
      Serial.println(favorites[menuIndex]);
      break;

    case 2:
      if (menuIndex >= ELEMENTCOUNT(filters))
        menuIndex = 0;
      Serial.println(filters[menuIndex]);
      break;

    case 3:
      if (menuIndex >= ELEMENTCOUNT(incrementStrings))
        menuIndex = 0;
      Serial.println(incrementStrings[menuIndex]);
      break;

    default:
      menuIndex = 0;      // Start over with choice
      menuSelection = 1;
      Splash();
      break;
  }
}

/*****
   This is the Interrupt Service Routine for the rotary encoder

   Parameter List:
      PCINT2_vec      Pin change interrupt request #2, acts on pins D0 to D7)

   Return value:
      void
 *****/
ISR(PCINT2_vect) {
  unsigned char result = r.process();

  switch (result) {
    case 0:                                                 // Nothing done...
      return;

    case DIR_CW:                                            // Turning Clockwise, going to higher frequencies
      rotationDirection = 1;
      break;

    case DIR_CCW:                                    // Turning Counter-Clockwise, going to lower frequencies
      rotationDirection = -1;
      break;

    default:                                             // Should never be here
      break;
  }
}

It's a program for a rotary encoder demo for some ham radio guys. Ignore the menus; just see if the ISR performs any differently for you. Also, I had 0.1uF caps solder to the clock and data lines to ground to help tame down the pulse chain. Also, the encoder has a built-in switch which I tied to pin 7.

Edit: I also used a 2x16 LCD display for the demo.

Ok so I changed to digitalRead(). My code above works in theory, but only when no misreadings occur ever. However, even with digitalRead() the results are very wonky.

34669841
34678305 8464
34721306 43001
34729770 8464
34738222 8452
34746682 8460
138270286 
138279596 9310
138288905 9309
138298213 9308
138307507 9294
161863518
161872829 9311
161888652 15823
161897962 9310
161907258 9296
186700623
186709935 9312
186719248 9313
186728546 9298
210360924
210370236 9312
210390949 20713
210400260 9311
210409557 9297
233407515
233416829 9314
233426144 9315
233435443 9299
233444755 9312

---------  
77362028
77370492 8464
77378943 8451
77387408 8465
77395859 8451
91493090
91501559 8469
91561063 59504
91569531 8468
91577985 8454
104714806
104724118 9312
104762807 38689
104772118 9311
104781415 9297
118801035
118810346 9311
118848838 38492
118860023 11185
118869333 9310
135001453
135010764 9311
135020074 9310
135029383 9309
135038678 9295
146047397
146056708 9311
146086063 29355
146095374 9311
146114612 19238
146123923 9311
146133221 9298

@PaulS: Here are the timings. On the top: Outside the debounce function. Bellow: Inside the debounce function. On the right: The difference.
It appears that my debouncing was just a placebo, but the timings don't make sense to me. The bonces should be shorter than the normal contacts. The Serial.write() adds about 6000 micro seconds to the function.
@econjack: Thanks I will test your code. :slight_smile:

@econjack: I can't find a function where you are actually reading the encoder. I am guessing it is in the rotary.h library. Do you have a link? And does it rely on port registry manipulation? I can't use it if it does because the Arduino 101 has a very different chip.

The ISR is just setting rotationDirection, which is either CW or CCW. The menu example simply responds to the direction of rotation. If the encoder is not being moved, the ISR returns 0 and loop() ignores it:

  if (rotationDirection != 0) {                 // Encoder rotated??
    menuIndex += rotationDirection;             // Yep...
    ShowCurrentSelection();
    rotationDirection = 0;
  }

Because I defined CW to be 1 and CCW to be -1, the direction provides a way to scroll through the menus.

So I found this library: https://github.com/mathertel/RotaryEncoder
I have extracted this code from it.

const uint8_t encoderKnobPinA = 12;
const uint8_t encoderKnobPinB = 13;
const uint8_t LATCHSTATE = 3;
int8_t oldState = 3;
const int8_t KNOBDIR[] = {
  0, -1,  1,  0,
  1,  0,  0, -1,
 -1,  0,  0,  1,
  0,  1, -1,  0};
  
int positionInternal = 0;
int positionExternal = 0;
int oldPositionExternal = 0;

void setup() {
  Serial.begin(9800);
  pinMode(encoderKnobPinA, INPUT);
  pinMode(encoderKnobPinA, INPUT);
  attachInterrupt(encoderKnobPinA, tick, CHANGE);
  attachInterrupt(encoderKnobPinB, tick, CHANGE);
}

void loop() {
  if(oldPositionExternal != positionExternal){
    Serial.println(positionExternal);
  }
  oldPositionExternal = positionExternal;
}

void tick(){
  int sigA = digitalRead(encoderKnobPinA);
  int sigB = digitalRead(encoderKnobPinB);
  int8_t thisState = sigA | (sigB << 1);
  if (oldState != thisState) {
    positionInternal += KNOBDIR[thisState | (oldState<<2)];
    if (thisState == LATCHSTATE)
      positionExternal = positionInternal >> 2;   
    oldState = thisState;
  }
}

Now it works great! :slight_smile: This method of debouncing should be on the playground. I wonder if it is possible to trigger the interrupt from the common-pin of the encoder to free up one interrupt pin.