Encoder only works with onboard pullups

I’ve been experimenting with driving a Buxton encoder* with pin change interrupts on an UNO R3 - the goal being to be able to use multiple encoders with this library – and getting odd results.  Working example sketch:

#include <Arduino.h>
#include <Rotary.h>  // encoder handler by Buxton
#include <avr/io.h>
#include <avr/portpins.h>

//#include <ArduinoShrink.h>  // ArduinoShrink must be last library included

/*  PIN-CHANGE-IRQ-ENCODER

  Program to drive multiple Buxton encoders* with pin change interrupts
  rather than the usual external interrupts INT0 & INT1.

        Program Demonstrates:
        Pin change interrupts to encoder

        Assumes a manually operated encoder with integral pushbutton

    In this version the interrupting pins are setup and initialized
  by directly writing to the pin change control registers on port B.

  * http://www.buxtronix.net/2011/10/rotary-encoders-done-properly.html

*/

//#define ENCODER_PB PINB5  // to enable the timer DIO5

#define detentFlashLED LED_BUILTIN

const bool on = HIGH;

// Encoder number one variables

unsigned char encoderTurned;
int8_t IRQActivity = 0;          //  IRQ activity flag
volatile int8_t IRQcounter = 0;  //  Encoder direction value, set by 'rotate' ISR
volatile uint8_t irqActive;
byte irqNowCounts;

bool encoderDirection;  // clockwise/counterclockwise indicator
bool isEncoderPBPressed;

unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers

// detent flash timer variables
unsigned long timer1;
unsigned long T1preset = 50;


// Encoder number two variables

unsigned char encoderTurned2;
int8_t IRQActivity2 = 0;          //  IRQ activity flag
volatile int8_t IRQcounter2 = 0;  //  Encoder direction value, set by 'rotate' ISR
volatile byte irqActive2;
byte irqNowCounts2;

bool encoderDirection2;  // clockwise/counterclockwise indicator
bool isEncoderPBPressed2;

unsigned long lastDebounceTime2 = 0;  // the last time the output pin was toggled
unsigned long debounceDelay2 = 50;    // the debounce time; increase if the output flickers

// detent flash timer variables
unsigned long timer2;
unsigned long T2preset = 50;

// Encoder I/O pin nnections

const uint8_t enc1A = 8;
const uint8_t enc1B = 9;
const uint8_t enc2A = 10;
const uint8_t enc2B = 11;

// Encoder pin status I/O
const uint8_t encoderStatusOut = 6;

// note: Encoder library auto assigns input pullup resistors,
// no need for us to duplicate.
Rotary rotary = Rotary(enc1A, enc1B);  // Physical encoder pins connected here.  Interrupts are
                               // used on both pins 23 & 24. Library is set to enable
                               // pullups automagically so we don't need to do it here.
//

Rotary rotary2 = Rotary(enc2A, enc2B);  // 2nd encoder

// === S E T U P ===

void setup() {
  Serial.begin(115200);

  delay(300);

  /*
     Assign encoder chA, encoder chB, encoder PB inputs
  */
  //pinMode(ENCODER_PB, INPUT_PULLUP);

  // This procedure replaces 'attachInterrupt'.
  PCMSK0 |= (1 << PCINT0);  // enable Arduino pin #8 as PCINT
  PCMSK0 |= (1 << PCINT1);  // enable Arduino pin #9 as PCINT
                            //
                            // Set 2nd encoder pins as enabled
  PCMSK0 |= (1 << PCINT2);  // enable Arduino pin #10 as PCINT
  PCMSK0 |= (1 << PCINT3);  // enable Arduino pin #11 as PCINT

  PCIFR |= (1 << PCIF0);  // clear any port B outstanding interrupts
  PCICR |= (1 << PCIE0);  // enable port B pin change interrupts

  // assign LEDs - use onboard pin 13 for dial status indicator

  pinMode(detentFlashLED, OUTPUT);

  // Manually invoke input pullups
  // FOR TEST: PERFBOARD ENCODER
  pinMode(enc1A, INPUT_PULLUP);
  pinMode(enc1B, INPUT_PULLUP);

  // FOR TESTING: BREAKOUT ENCODER
  pinMode(enc2A, INPUT_PULLUP);
  pinMode(enc2B, INPUT_PULLUP);

  pinMode(4, INPUT_PULLUP);
  pinMode(encoderStatusOut, OUTPUT);

  interrupts();
  printHeaderInfo();
}  // end of setup()

//////////////////////////////////////////////

// === L O O P ===
void loop() {

  dialMoved();  // check for dial rotation and direction

  if (encoderTurned or encoderTurned2) {
    Serial.print(IRQActivity);  // +- encoder counts
    Serial.print("\t");
    Serial.print(IRQActivity2);
    Serial.println();
  }

  // Flash an LED when encoder has crossed a detent
  if (millis() - timer1 < T1preset) {
    digitalWrite(detentFlashLED, on);
  }

  else {
    digitalWrite(detentFlashLED, !on);
  }

// Echo the encoder pin to a visible output.
  digitalWrite(encoderStatusOut, digitalRead(enc2B));
}  // end of loop()

//---------------------------------------------------------
ISR(PCINT0_vect) {  // All IRQs from port B come here

  /* Interrupt Service Routine for rotary encoder:
     An interrupt is generated any time either of
     the rotary inputs change state.  If a valid detent
     is registered IRQcounter will be inc/decremented,
     unchanged otherwise.
  */
  byte result = rotary.process();  // call the encoder handler function
  //                                 upon IRQ receipt.
  if (result == DIR_CW) {
    IRQcounter++;
  } else if (result == DIR_CCW) {
    IRQcounter--;
  }
  irqActive += 1;  // Register the interrupt

  // 2nd encoder handler
  result = rotary2.process();  // call the encoder handler function
  //                                 upon IRQ receipt.
  if (result == DIR_CW) {
    IRQcounter2++;
  } else if (result == DIR_CCW) {
    IRQcounter2--;
  }
  irqActive2 += 1;  // Register the interrupt
}

//-----------------------------------------------------
void dialMoved() {

  /*   After clearing any encoderTurned flag, check to
       see if a valid encoder logic sequence has been
       recognized. If it has, 'encoderTurned' will be set
       true for one scan only and 'encoder direction'
       will denote the direction of rotation.
  */
  encoderTurned = false;  // Reset previous turned value.
  /*
     detect encoder activity
  */
  if (IRQActivity != IRQcounter) {
    encoderTurned = true;
    if (IRQcounter > IRQActivity) {
      encoderDirection = true;
    } else {
      encoderDirection = false;
    }
    IRQActivity = IRQcounter;  // reset trigger value to be ready for next IRQ

    timer1 = millis();  // reset detent flash timer
  }

  // 2nd encoder

  encoderTurned2 = false;  // Reset previous turned value.
  /*
     detect encoder activity
  */
  if (IRQActivity2 != IRQcounter2) {
    encoderTurned2 = true;
    if (IRQcounter2 > IRQActivity2) {
      encoderDirection2 = true;
    } else {
      encoderDirection2 = false;
    }
    IRQActivity2 = IRQcounter2;  // reset trigger value to be ready for next IRQ
  }
  timer2 = millis();  // reset detent flash timer
}

// end of dialMoved()
//

void printHeaderInfo() {
  Serial.print("PCINT0 = ");
  Serial.println(PCINT0);

  Serial.print("PCINT1 = ");
  Serial.println(PCINT1);

  Serial.print("PCMSK0 = ");
  Serial.println(PCMSK0);

  Serial.print("PCIFR = ");
  Serial.println(PCIFR);

  Serial.print("PCICFR = ");
  Serial.println(PCICR);
  Serial.println();
}

There are two KY-040 type encoders connected, one with built-in 10K pullups - Elegoo kit, the other without – just a bare encoder mounted on a piece of perfboard.  The one with built-in pullups is the problem – the only time it works is when the on-board pullups are enabled (by tying encoder V+ to Vcc). Processor pullups – pinMode(X, INPUT_PULLUP) appear irrelevant to this encoder.

I added a program-controlled external LED to indicate the state of some digital input, currently driven by D10 (enc2B) so the LED flashes to verify the knob has turned.  D11 works identically.  Both of these correctly show the input state with and without the encoder pullup enabled.  Even so, as stated above, the code will not respond to this encoder unless its on-board pullups are on.  Just for giggles I swapped the two encoders' CLK/DT pins between 8,9 and 10,11.  No change in behavior.

As an experiment I added .1 µF caps. to ground on each encoder channel and the encoder now responds as expected - counts up/down/fast without the on-board pullups (pinMode pullups are active).

To get my head around this I made up a spreadsheet with all the pullup permutations and the results seen for each.  I'll post it if anyone needs to see it.

What is going on?

* Buxtronix: Rotary encoders, done properly

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.