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?