Quad Encoder interrupt detection

bobpellegrino

Well-known member
I'm working on a project using a Teensy 4.1 and MJS513's Quad Encoder library to read a standard rotary encoder for user input (setting speed on a model train throttle). My goal is to use an interrupt-driven approach to detect every single step of the encoder, whether it's an increment or a decrement.

I've successfully used the library in a polling mode (encoder.read() in the main loop), and that works perfectly, confirming the hardware counting is functional.

However, I'm struggling to find the intended interrupt-driven method for catching both directions reliably using the library as-is. I explored using the Position Compare feature:
  1. Enable positionCompareMode.
  2. In the main loop, after processing a change indicated by QuadEncoder::compareValueFlag, I read the current position (currentPos = encoder.read()).
  3. I then set a new compare value, aiming to catch the next step: encoder.setCompareValue(currentPos + ENCODER_DIVIDER); (where ENCODER_DIVIDER is my steps-per-detent).
  4. Finally, I re-enable the compare interrupt using encoder.enableCompareInterrupt() (since the library ISR appears to disable ENC_CTRL_CMPIE_MASK upon match).

This works fine for detecting increments, as the counter eventually reaches currentPos + ENCODER_DIVIDER. However, if the encoder is turned backwards (decrementing), the counter moves away from the set compare value, and the compare interrupt never fires, so decrements are missed.

My question is: What is the intended interrupt-driven mechanism within the QuadEncoder library to reliably detect both increment and decrement steps?
  • Am I misunderstanding how the Position Compare feature (positionCompareMode, setCompareValue) is meant to be used for this scenario?
  • Is there another interrupt source handled by the library's ISR (perhaps related to Rollover/Rollunder flags ROIRQ/RUIRQ, or the hardware Direction Change flag DIRQ, even if not explicitly documented for this purpose) that should be configured or enabled to achieve this?
  • Or is polling the hardware counter (encoder.read()) the recommended approach when an interrupt on every arbitrary step change is desired?
I've reviewed the library code (QuadEncoder.cpp/.h) and can see the ISR handles CMPIRQ, XIRQ, and HIRQ, and checks ROIRQ/RUIRQ within the XIRQ block, but I don't see handling for DIRQ or an obvious mechanism for catching decrements via CMPIRQ. I feel like I might be missing a key concept or configuration detail.

C++:
/*

 * Model Train Controller
 */

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "QuadEncoder.h"

// Display Configuration
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET     -1
#define SCREEN_ADDRESS 0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Encoder Configuration
const int ENCODER_CHANNEL = 1;        // Using encoder channel 1 (of 4 available)
const int ENCODER_PIN_A = 8;          // Phase A pin (TEENSY_ENCA)
const int ENCODER_PIN_B = 7;          // Phase B pin (TEENSY_ENCB)
const int ENCODER_PULLUPS = 0;        // No pullups required (0) or required (1)
const int ENCODER_INDEX_PIN = 255;    // No index pin used (255 = disabled)
const int ENCODER_DIVIDER = 4;        // Encoder counts per detent (adjust based on encoder)
const int MAX_SPEED_VALUE = 999;      // Maximum speed value (0-999) for 0-99.9 display

// Variables to track encoder position
int newPosition;
int old_position = 0;

// Create encoder object
QuadEncoder encoder(ENCODER_CHANNEL, ENCODER_PIN_A, ENCODER_PIN_B, ENCODER_PULLUPS, ENCODER_INDEX_PIN);

// Function prototypes
void setupEncoder();
void updateDisplay();
void checkPosition();

void setup() {
  Serial.begin(115200);
  Serial.println("Train Controller Initializing...");

  setupEncoder();

 if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println("SSD1306 display initialization failed");
  } else {
    display.clearDisplay();
    display.display();
  }
  Serial.println("Initialization complete");
}

void loop() {
  // Enable roll over/under interrupts to detect direction changes
  encoder.EncConfig.positionROIE = ENABLE;
  encoder.EncConfig.positionRUIE = ENABLE;

  // Check if position compare flag is set by the hardware
  if (QuadEncoder::compareValueFlag) {
    QuadEncoder::compareValueFlag = 0;

   // Check encoder position and update display if needed
    checkPosition();
  }
}

void setupEncoder() {
  encoder.setInitConfig();

  // Configure filtering for mechanical encoders
  encoder.EncConfig.filterCount = 3;         // Range 0-7, higher values = more filtering
  encoder.EncConfig.filterSamplePeriod = 20; // Range 0-255, higher values = slower sample rate

   // Enable monitoring of rollover/rollunder for direction detection
  encoder.EncConfig.revolutionCountCondition = ENABLE;

  // Initialize encoder hardware
  encoder.init();

  // Set initial position
  encoder.write(0);

  // Enable position compare mode
  encoder.EncConfig.positionCompareMode = ENABLE;

  // Set initial compare value - will trigger on any change
  encoder.setCompareValue(ENCODER_DIVIDER);

  // Enable compare interrupt
  encoder.enableCompareInterrupt();

  Serial.println("Encoder initialized");

}

void checkPosition() {

  newPosition = encoder.read() / ENCODER_DIVIDER;

   // Apply constraints to keep within valid range
  if (newPosition < 0) {
    newPosition = 0;
    encoder.write(0);
  }

  if (newPosition > MAX_SPEED_VALUE) {
    newPosition = MAX_SPEED_VALUE;
    encoder.write(MAX_SPEED_VALUE * ENCODER_DIVIDER);
  }

  // Update display if position changed
  if (newPosition != old_position) {
    Serial.print("Position: ");
    Serial.println(newPosition);
    old_position = newPosition;
    updateDisplay();
  }

  // Set a new compare value that will trigger on either increment or decrement
  int currentPos = encoder.read();
  // This value is an absolute position to compare against, not a relative offset
  encoder.setCompareValue(currentPos + ENCODER_DIVIDER);

  // Re-enable the compare interrupt
  encoder.enableCompareInterrupt();
}

void updateDisplay() {
  display.clearDisplay();
  display.setTextWrap(false);
  display.setTextSize(5);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0,0);
  display.print((float)newPosition/10, 1);
  display.display();
}
 
I'm working on a project using a Teensy 4.1 and MJS513's Quad Encoder library to read a standard rotary encoder for user input (setting speed on a model train throttle). My goal is to use an interrupt-driven approach to detect every single step of the encoder, whether it's an increment or a decrement.

I have never tried to do what you're doing, but I think of the whole point of using QuadEncoder is NOT to have an interrupt per A/B input edge as you would with Encoder. I see the compare function you're trying to use, but I don't know if it's possible to use it to interrupt on every edge. Could you just use Encoder instead?
 
I have never tried to do what you're doing, but I think of the whole point of using QuadEncoder is NOT to have an interrupt per A/B input edge as you would with Encoder. I see the compare function you're trying to use, but I don't know if it's possible to use it to interrupt on every edge. Could you just use Encoder instead?
I use QuadEncoder because it does not need interrupts as it runs on dedicated hardware pins,
 
@bobpellegrino

As @MorryStu said the Teensy 4.1 Quadencoder is all done in hardware and automatically counts pulses (rising and falling) so every 4 pulses = 1 increment on the encoder. Take a look at this https://www.pjrc.com/teensy/td_libs_Encoder.html#optimize to understand quad encoders. You definitely do not need to track you own interrupts on each pulse/direction - the lib does that for you. You can

Now the positionCompareMode you are talking about it an interrupt when you want to compare a set value say 20 counts to the counts from the encoder. So when the counter reaches the compare value of 20 it will trigger every 20 counts unless you change the compare value.

If you just want to get direction use the function
Code:
getHoldDifference()
. When called it will give you a +1 or -1 indicating the direction of the count.
Code:
Left = 0, Right = 3291
Position differential value1: 1

Left = 0, Right = 3292
Position differential value1: 1

Left = 0, Right = 3293
Position differential value1: 1

Left = 0, Right = 3292
Position differential value1: -1

The other interrupts you mentioned are overflow interrupts and you don't have to use them at all.

Hope this helps
 
@Rezo
If you want to just get every count just use something like in the simple_encoder example sketch, i.e.,
C++:
long positionEnc = -999;

void loop() {
  long newEnc;
  newEnc = myEnc.read();
  if (newEnc != positionEnc) {
    Serial.print("Counts = ");
    Serial.print(newEnc);
    Serial.println();
    positionEnc = newEnc;
  }
  if (Serial.available()) {
    Serial.read();
    Serial.println("Reset Counts to zero");
    myEnc.write(0);
  }

}

which will give you every count, divide by 4 to get encoder increments.

Sampe data:

Code:
Reset Counts to zero
Counts = 1
Counts = 2
Counts = 3
Counts = 4
Counts = 5
Counts = 6
Counts = 7
Counts = 8
Counts = 9
Counts = 10
 
Heres a whole sketch for what I was talking about
C++:
#include "QuadEncoder.h"
QuadEncoder myEnc(2, 2, 3);
void setup() {
  while(!Serial && millis() < 5000){}
  Serial.begin(115200);
  Serial.println("Encoder Test:");
  /* Initialize Encoder/knobLeft. */
  myEnc.setInitConfig();
  myEnc.init();
}
long positionEnc = -999;
int icount = 0;
void loop() {
  long newEnc;
  newEnc = myEnc.read();
  if (newEnc != positionEnc) {
    if(newEnc % 4 == 0) {
      icount += 1;
      Serial.print("Counts/Increments = ");
      Serial.print(newEnc);
      Serial.print(", ");
      Serial.print(icount);
      Serial.println();
      positionEnc = newEnc;
    }
  }
  if (Serial.available()) {
    Serial.read();
    Serial.println("Reset Counts to zero");
    myEnc.write(0);
    icount = 0;
  }
}
 
If you use compareValue

C++:
#include <Arduino.h>
#include "TeensyThreads.h"
#include "QuadEncoder.h"

QuadEncoder myEnc(2, 2, 3);

void setup() {
  while(!Serial && millis() < 5000){}
  Serial.begin(115200);
  Serial.println("Encoder Test:");
  /* Initialize Encoder/knobLeft. */
  myEnc.setInitConfig();

  myEnc.EncConfig.revolutionCountCondition = ENABLE;
  myEnc.EncConfig.enableModuloCountMode = ENABLE;
  myEnc.EncConfig.positionModulusValue = 4;

  myEnc.init();
}

long positionEnc = -999;

void loop() {
  long newEnc;
  newEnc = myEnc.read();

  if(myEnc.compareValueFlag == 1) {
    //myEnc2.init();
    //resets counter to positionInitialValue so compare
    //will hit every positionCompareValue
    myEnc.write(myEnc.EncConfig.positionInitialValue);
    Serial.print("Compare Value Hit for Encoder 2:  ");
    Serial.println(myEnc.compareValueFlag);
    Serial.println();
    myEnc.compareValueFlag = 0;
    // re-enable the Compare Interrupt
    myEnc.enableCompareInterrupt();

  }

  if (newEnc != positionEnc) {
    Serial.print("Counts = ");
    Serial.print(newEnc);
    Serial.println();
    positionEnc = newEnc;
  }
 
  if (Serial.available()) {
    Serial.read();
    Serial.println("Reset Counts to zero");
    myEnc.write(0);
  }

}

It resets after every 4 counts or 1 increment:

Code:
Encoder Test:
Counts = 0
Counts = 1
Counts = 2
Counts = 3
Counts = 4
Counts = 0
Counts = 1
Counts = 2
Counts = 3
Counts = 4
Counts = 0
Counts = 1
Counts = 2
Counts = 3
Counts = 4
Counts = 0
 
Back
Top