Encoder Library not as efficient as XOR method

Status
Not open for further replies.

Iwilsonp

Member
I decided to take it on myself to test out the Encoder library performance vs. the XOR method posted on Arduino playground. Here's the setup:

IMG_4454.jpg

Encoder Emulator

I built an encoder emulator with a Teensy 3.5 optimize faster (left side of image). The DIP switches are to input the desired speed, the two wires on the top to the T 3.2 are the emulated encoder output. It works great: I checked with an oscilloscope: the waveform's fine and right on the desired frequency.

I also had it output the last 3 bits of the accumulated position on the 3 LEDs on the left at 1 Hz and signal the T 3.2 via the white wire (hidden under the orange wire) when it did so.
Here's the code:
Code:
//pins that output encoder signals
static const byte pinA = 33;
static const byte pinB = 34;

//pins that read the dip switches used to control speed
static const byte dipSwitchPins[8] = {0, 1, 2, 3, 4, 5, 6, 7}; //from least significant to most significant

#define uS_PER_SEC 1e6
//the timer that updates the encoder waveform
IntervalTimer encoderEmulator;
volatile byte encoderWaveformPosition = 0;
volatile unsigned long position = 0;

unsigned int encoderSpeedKHz = 0; //cycles/sec
unsigned int oldEncoderSpeedKHz = 0;
#define ONES_PIN 30
#define TWOS_PIN 29
#define FOURS_PIN 28
#define TRIGGER_PIN 32
#define ZERO_POS_PIN 12

void setup(){
  
  pinMode(pinA, OUTPUT);
  pinMode(pinB, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWriteFast(LED_BUILTIN, HIGH);
  
  pinMode(ONES_PIN, OUTPUT);
  pinMode(TWOS_PIN, OUTPUT);
  pinMode(FOURS_PIN, OUTPUT);
  pinMode(TRIGGER_PIN, OUTPUT);
  
  pinMode(ZERO_POS_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ZERO_POS_PIN), zeroPosition, RISING);
  
  for(byte listIterator = 0; listIterator < 8; ++listIterator){
    pinMode(dipSwitchPins[listIterator], INPUT_PULLUP);
  }
  
  //for some reason, the interrupt won't start until one of the DIP switches is flipped.
  encoderSpeedKHz = 1;
  encoderEmulator.begin(updateEncoderPos, uS_PER_SEC/(4*1000*encoderSpeedKHz));
  
}

void loop(){
  //use each pin as one bit so that we can input the desired speed (in kilocycles/sec) into the Teensy without using Serial, the not is so that the ONs on the DIP switches make the number increase, the +1 so that the numbering on the DIP switches lines up (1 is 2^1, 2 is 2^2, etc)
  encoderSpeedKHz = (!digitalReadFast(dipSwitchPins[0]) << 1) + (!digitalReadFast(dipSwitchPins[1]) << 2) + (!digitalReadFast(dipSwitchPins[2]) << 3) + (!digitalReadFast(dipSwitchPins[3]) << 4) + (!digitalReadFast(dipSwitchPins[4]) << 5) + (!digitalReadFast(dipSwitchPins[5]) << 6) + (!digitalReadFast(dipSwitchPins[6]) << 7) + (!digitalReadFast(dipSwitchPins[7]) << 8);
  
  
  if(encoderSpeedKHz != oldEncoderSpeedKHz){   //if the requested speed has changed, then change the encoder speed
    if(encoderSpeedKHz == 0){  //at speed 0, just end the timer to stop the encoder
      encoderEmulator.end();
    }
    else{  //need to restart the encoder at a different speed
      if(oldEncoderSpeedKHz !=0){   //if oldEncoderSpeedKHz == 0, the timer was already stopped. Else, it needs to be stopped
        encoderEmulator.end();
      }
      encoderEmulator.begin(updateEncoderPos, uS_PER_SEC/(4*1000*encoderSpeedKHz));  //restart the encoder at the new speed
    }
    
    oldEncoderSpeedKHz = encoderSpeedKHz;  //update the current encoder speed
  }
  
  outputPosition();
  delay(1000);
}
//every time called, advances the encoder's position by 1. Called 4 times per cycle
void updateEncoderPos(void){
  digitalWriteFast(pinB, encoderWaveformPosition >> 1);  //writes HIGH when encoderWaveformPosition == 2 or 3
  ++encoderWaveformPosition;
  ++position;
  encoderWaveformPosition = encoderWaveformPosition&3;  //do %4 fast
  digitalWriteFast(pinA, encoderWaveformPosition >> 1);
}

//outputs the last 3 bits of the position on LEDs
void outputPosition(void){
  noInterrupts();
  digitalWriteFast(TRIGGER_PIN, HIGH);
  digitalWriteFast(ONES_PIN, position&1);
  digitalWriteFast(TWOS_PIN, (position>>1)&1);
  digitalWriteFast(FOURS_PIN, (position>>2)&1);
  digitalWriteFast(TRIGGER_PIN, LOW);
  interrupts();
}

void zeroPosition(void){
  position = 0;
}

SpeedTester

A Teensy 3.2, 96MHz optimized overclock, optimize faster. It outputs the last 3 bits of the position to the LEDs when told to by the T 3.5. Here's the code (based on the SpeedTest sketch, with ifdefs to switch between the library and XOR methods):
Code:
/* Encoder Library - SpeedTest - for measuring maximum Encoder speed
 * http://www.pjrc.com/teensy/td_libs_Encoder.html
 *
 * This example code is in the public domain.
 */


// This SpeedTest example provides a simple way to verify how much
// CPU time Encoder is consuming.  Connect a DC voltmeter to the
// output pin and measure the voltage while the encoder is stopped
// or running at a very slow speed.  Even though the pin is rapidly
// pulsing, a DC voltmeter will show the average voltage.  Due to
// software timing, it will read a number much less than a steady
// logic high, but this number will give you a baseline reading
// for output with minimal interrupt overhead.  Then increase the
// encoder speed.  The voltage will decrease as the processor spends
// more time in Encoder's interrupt routines counting the pulses
// and less time pulsing the output pin.  When the voltage is
// close to zero and will not decrease any farther, you have reached
// the absolute speed limit.  Or, if using a mechanical system where
// you reach a speed limit imposed by your motors or other hardware,
// the amount this voltage has decreased, compared to the baseline,
// should give you a good approximation of the portion of available
// CPU time Encoder is consuming at your maximum speed.

// Encoder requires low latency interrupt response.  Available CPU
// time does NOT necessarily prove or guarantee correct performance.
// If another library, like NewSoftSerial, is disabling interrupts
// for lengthy periods of time, Encoder can be prevented from
// properly counting the intput signals while interrupt are disabled.

/* DATA FROM TESTS (T 3.2, 96MHz overclock, Optimize Faster) (0.10V output on pin, speed given in encoder cycles):
Encoder Library: 208 KHz (but LED_BUILTIN goes dim and flickers about ~128 KHz)
Encoder library with optimized interrupts: 208 KHz (LED_BUILTIN stays bright)

XOR method: 320 KHz

MAX USABLE SPEED (counts stay in sync with the emulator), (0Hz is 1.36V):
Encoder Library: 104 KHz (532 mV)
XOR method: 104 KHz (890 mV)
*/

//two encoder pins
#define encoder0PinA 10
#define encoder0PinB 11

//LEDs outputtting the last 3 bits of the position:
#define ONES_PIN 17
#define TWOS_PIN 16
#define FOURS_PIN 15

//wire to encoder emulator that tells when to output position to sync the two outputs
#define TRIGGER_PIN 14

//Zeros the position
#define ZERO_POS_PIN 21

//chooses which one to test
#define TEST_XOR

//to check how much CPU its using
//2-stage RC filter on this pin to get a steady voltage
const int outputPin = 12;

#ifdef TEST_LIBRARY

// This optional setting causes Encoder to use more optimized code,
// but the downside is a conflict if any other part of your sketch
// or any other library you're using requires attachInterrupt().
// It must be defined before Encoder.h is included.
//#define ENCODER_OPTIMIZE_INTERRUPTS

#include <Encoder.h>
#include "pins_arduino.h"

// Change these two numbers to the pins connected to your encoder
// or shift register circuit which emulates a quadrature encoder
//  case 1: both pins are interrupts
//  case 2: only first pin used as interrupt
Encoder myEnc(encoder0PinA, encoder0PinB);


void setup() {
  pinMode(outputPin, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
  
  //to output the last 3 bits of position
  pinMode(ONES_PIN, OUTPUT);
  pinMode(TWOS_PIN, OUTPUT);
  pinMode(FOURS_PIN, OUTPUT);
  pinMode(TRIGGER_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(TRIGGER_PIN), outputPosition, RISING);
  attachInterrupt(digitalPinToInterrupt(ZERO_POS_PIN), zeroPosition, RISING);
}

#if defined(__AVR__) || defined(TEENSYDUINO)
#define REGTYPE unsigned char
#else
#define REGTYPE unsigned long
#endif

void loop() {
  volatile int count = 0;
  volatile REGTYPE *reg = portOutputRegister(digitalPinToPort(outputPin));
  REGTYPE mask = digitalPinToBitMask(outputPin);

  while (1) {
    digitalWriteFast(LED_BUILTIN, myEnc.read()&1);	// Read the encoder while interrupts are enabled and do something minimal with it
    noInterrupts();
    *reg |= mask;	// Pulse the pin high, while interrupts are disabled.
    count = count + 1;
    *reg &= ~mask;
    interrupts();
  }
}

//runs when triggered by wire from other Teensy, about 1Hz
void outputPosition(void){
  digitalWriteFast(ONES_PIN, myEnc.read()&1);
  digitalWriteFast(TWOS_PIN, (myEnc.read()>>1)&1);
  digitalWriteFast(FOURS_PIN, (myEnc.read()>>2)&1);
}

//runs when external switch pressed
void zeroPosition(void){
  myEnc.write(0);
}
#endif

#ifdef TEST_XOR

volatile long encoder0Pos = 0;
byte Aold = 0;
byte Bnew = 0;
void setup() {
 
  pinMode(encoder0PinA, INPUT); 
  pinMode(encoder0PinB, INPUT);
// encoder pin on interrupt 0 (pin 2)
  attachInterrupt(digitalPinToInterrupt(encoder0PinA), doEncoderA, CHANGE);
// encoder pin on interrupt 1 (pin 3)
  attachInterrupt(digitalPinToInterrupt(encoder0PinB), doEncoderB, CHANGE);

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWriteFast(LED_BUILTIN, HIGH);
  
  pinMode(outputPin, OUTPUT);
  
  //to output last 3 bits of position
  pinMode(ONES_PIN, OUTPUT);
  pinMode(TWOS_PIN, OUTPUT);
  pinMode(FOURS_PIN, OUTPUT);
  
  pinMode(TRIGGER_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(TRIGGER_PIN), outputPosition, RISING);
  attachInterrupt(digitalPinToInterrupt(ZERO_POS_PIN), zeroPosition, RISING);
}

#if defined(__AVR__) || defined(TEENSYDUINO)
#define REGTYPE unsigned char
#else
#define REGTYPE unsigned long
#endif

void loop() {
  volatile int count = 0;
  volatile REGTYPE *reg = portOutputRegister(digitalPinToPort(outputPin));
  REGTYPE mask = digitalPinToBitMask(outputPin);

  while (1) {
    digitalWriteFast(LED_BUILTIN, encoder0Pos&1);	// Read the encoder while interrupts are enabled and do something minimal with it
    noInterrupts();
    *reg |= mask;	// Pulse the pin high, while interrupts are disabled.
    count = count + 1;
    *reg &= ~mask;
    interrupts();
  }
}
// Interrupt on A changing state
void doEncoderA(){
  Bnew^Aold ? encoder0Pos++:encoder0Pos--;
  Aold=digitalReadFast(encoder0PinA);
}
// Interrupt on B changing state
void doEncoderB(){
  Bnew=digitalReadFast(encoder0PinB);
  Bnew^Aold ? encoder0Pos++:encoder0Pos--;
}

void outputPosition(void){
  digitalWriteFast(ONES_PIN, encoder0Pos&1);
  digitalWriteFast(TWOS_PIN, (encoder0Pos>>1)&1);
  digitalWriteFast(FOURS_PIN, (encoder0Pos>>2)&1);
}

void zeroPosition(void){
  noInterrupts();
  encoder0Pos = 0;
  interrupts();
}
#endif

Here are the results:

Neither could go above 104kHz without losing tracking (as shown by the LEDs, they would typically be off by a multiple of 4), but the XOR was significantly faster: at 104KHz it was outputting 890mV while the library routines were outputting 532 mV.

EDIT:
I'm now thinking that it's very suspicious that BOTH lost tracking right at 104 KHz: maybe it has more to do with the OTHER parts of my code, like the routines to output the position to the LEDs. However, I have also done testing without any of the LED output code, just the sketch straight from SpeedTest. The results from that was that the Encoder library was down to 0.1V at 208 KHz with or without ENCODER_OPTIMIZE_INTERRUPTS and the XOR method was down to 0.1V at 320 KHz.

Perhaps an update of the library routines is in order?
 
Last edited:
I'm not sure I completely follow the code but on the XOR code it looks like volatile variable encoder0Pos is read in digitalWriteFast with interrupts enabled.

Code:
void loop() {
  volatile int count = 0;
  volatile REGTYPE *reg = portOutputRegister(digitalPinToPort(outputPin));
  REGTYPE mask = digitalPinToBitMask(outputPin);

  while (1) {
    digitalWriteFast(LED_BUILTIN, encoder0Pos&1);	// Read the encoder while interrupts are enabled and do something minimal with it
    noInterrupts();
    *reg |= mask;	// Pulse the pin high, while interrupts are disabled.
    count = count + 1;
    *reg &= ~mask;
    interrupts();
  }
}
 
Status
Not open for further replies.
Back
Top