QuadEncoder counting question

clinker8

Well-known member
Trying to understand whether this library is suitable for what I have in mind. Simple questions, I hope.

Does quad encoder count on every edge of A and B? Or really only on a single edge of A? From reading the source files it wasn't clear to me.

If the former, (first question is affirmative) that's good for my application. The latter (second question is affirmative) means the encoder is effectively 1/4 the resolution. Or is there a different interpretation?

Thanks for any and all help.

Edit: Found a picture in 56.3 Functional Description of the Teensy Manual that answered my question. The encoder effectively counts every edge of A and B (question 1) I think the wording in the QuadEncoder readme is ambiguous.
 
Last edited:
The QuadTimer has many different counting modes. Among them is the quadrature-count mode, which is what the QuadEncoder library uses, and in this mode the timer counts all 4 edges of A and B, and can also handle Home and Index inputs. The "Quad" in QuadTimer does not stand for quadrature. It just means there are 4 timers per module. I recommend reading the QuadTimer section (54) of the reference manual, or at least skim it. It's not very long.

The author of QuadEncoder also wrote an encoder simulation library called EncSim, which is very handy for testing and learning about QuadEncoder. EncSim is not included in TeensyDuino. You have to install it manually.
 
Last edited:
The program below is one that I wrote to test both EncSim and QuadEncoder. The encoder simulation uses pins 0,1,2 (outputs), and those must be looped to pins 3,4,5 (inputs) for QuadEncoder. The simulated encoder is set to 1200 PPR by default, and sets RPM to 1000, but you can change those while it's running by entering commands via the serial monitor like this:

ppr 1024
rpm 2000
rpm -1000

Configuring EncSim is a bit tricky because it supports things like simulated bounce and a specific duration of output, such as turning a knob, but it has settings that allow simulation of an encoder on a high-speed shaft, i.e. a continuous pulse train in one direction or the other.

loop() is very simple. It just calls QuadEncoder::read() every ~1 ms and prints the delta count. You can modify loop to do other things if you want. It's been a while since I had this set up and running, but I'm sure it works as is.

Note that there does not seem to be a universal standard for whether positive count occurs with A leading B or with B leading A, so you may get the opposite sign of what you expected, but the counting should work correctly.


Code:
#include <EncSim.h>
#include <QuadEncoder.h>
#include <SerialCommand.h>

EncSim tachsim( 0, 1, 2 );    // pins A(0), B(1), M(2)

QuadEncoder Enc(3, 4, 5, 0);  // Enc 3 of 4, A(pin4), B(pin5), PullupsReq(0)

SerialCommand sCmd;        // SerialCommand object

uint32_t ppr = 1200;            // pulses per rev (*4 for quadrature)
float    rpm = 1000;            // rpm = (60*frq)/(ppr*4)
float    frq = (ppr*4)*(fabs(rpm)/60);    // keep this relationship
bool     update = true;            // update = true to trigger one print

void LED_on();
void LED_off();
void rpmCommand();
void pprCommand();
void frqCommand();
void default_fn( const char *command );

elapsedMillis ms;
                               
void setup()
{
  pinMode( LED_BUILTIN, OUTPUT );    // Configure the onboard LED for output
  digitalWrite( LED_BUILTIN, LOW );    // default to LED off

  // Setup callbacks for SerialCommand commands
  sCmd.addCommand( "on",  LED_on );    // Turns LED on
  sCmd.addCommand( "off", LED_off );    // Turns LED off
  sCmd.addCommand( "rpm", rpmCommand );    // rpm followed by new rpm value
  sCmd.addCommand( "ppr", pprCommand );    // ppr followed by new ppr value
  sCmd.addCommand( "frq", frqCommand );    // frq followed by new ppr value
  sCmd.setDefaultHandler( default_fn );    // Handler for command that isn't matched  (says "What?")

  Serial.begin( 115200 );
  delay( 1000 );
  Serial.printf( "Application TachSim0 %s %s\n", __DATE__, __TIME__ );

  Enc.setInitConfig();  //
  Enc.init();

  tachsim.begin();                // begin()
  tachsim                    // CONFIGURATION
  .setPhase( 90 )                // normal 90 deg phase shift
  .setTotalBounceDuration( 0 )            // no bouncing
  .setPeriod( ppr*4 )                // marker every ppr A/B pulses
  .setFrequency( frq );                // frequency = f(ppr,rpm)

  tachsim.setContinuousMode( true );        // don't stop at target
  tachsim.moveRelAsync( rpm >= 0.0 ? +1 : -1 );    // +/- direction

  ms = 0;
}

uint32_t encNow, encPrev = 0;

void loop()
{
  sCmd.readSerial();
  if (update == true) {
    Serial.printf( "ppr = %4lu   rpm = %6.1f   frq = %8.1f\n", ppr, rpm, frq );
    update = false;
  }
  else if (ms >= 1000) {
    encNow = Enc.read();  
    Serial.println( encNow - encPrev );
    encPrev = encNow;
    ms -= 1000;
  }
}

void LED_on() { digitalWrite( LED_BUILTIN, HIGH ); }
void LED_off() { digitalWrite( LED_BUILTIN, LOW ); }

void rpmCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    rpm = atoi(arg);                // char* to integer
    frq = (ppr*4)*(fabs(rpm)/60);        // compute new frequency
    tachsim.moveRelAsync( rpm >= 0 ? +1 : -1);    // set direction = f(rpm)
    tachsim.setFrequency( frq );        // set frequency
  }
  update = true;
}

void pprCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    int temp = atoi(arg);        // char* to integer
    if (temp > 0) {            // if valid ppr
      ppr = temp;            //   set ppr
      frq = (ppr*4)*(fabs(rpm)/60);    //   compute new frequency
      tachsim.setPeriod( ppr*4 );    //   marker every ppr pulses
      tachsim.setFrequency( frq );    //   set frequency
    }
  }
  update = true;
}

void frqCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    frq = (float)atoi(arg);        // char* to integer
    rpm = (60*frq)/(ppr*4);        // back-calc rpm
    tachsim.setFrequency( frq );    // set frequency
  }
  update = true;
}

// This gets set as the default handler, and gets called when no other command matches.
void default_fn( const char *command )
{
  Serial.println( "What?" );
}
 
The program below is one that I wrote to test both EncSim and QuadEncoder. The encoder simulation uses pins 0,1,2 (outputs), and those must be looped to pins 3,4,5 (inputs) for QuadEncoder. The simulated encoder is set to 1200 PPR by default, and sets RPM to 1000, but you can change those while it's running by entering commands via the serial monitor like this:

ppr 1024
rpm 2000
rpm -1000

Configuring EncSim is a bit tricky because it supports things like simulated bounce and a specific duration of output, such as turning a knob, but it has settings that allow simulation of an encoder on a high-speed shaft, i.e. a continuous pulse train in one direction or the other.

loop() is very simple. It just calls QuadEncoder::read() every ~1 ms and prints the delta count. You can modify loop to do other things if you want. It's been a while since I had this set up and running, but I'm sure it works as is.

Note that there does not seem to be a universal standard for whether positive count occurs with A leading B or with B leading A, so you may get the opposite sign of what you expected, but the counting should work correctly.


Code:
#include <EncSim.h>
#include <QuadEncoder.h>
#include <SerialCommand.h>

EncSim tachsim( 0, 1, 2 );    // pins A(0), B(1), M(2)

QuadEncoder Enc(3, 4, 5, 0);  // Enc 3 of 4, A(pin4), B(pin5), PullupsReq(0)

SerialCommand sCmd;        // SerialCommand object

uint32_t ppr = 1200;            // pulses per rev (*4 for quadrature)
float    rpm = 1000;            // rpm = (60*frq)/(ppr*4)
float    frq = (ppr*4)*(fabs(rpm)/60);    // keep this relationship
bool     update = true;            // update = true to trigger one print

void LED_on();
void LED_off();
void rpmCommand();
void pprCommand();
void frqCommand();
void default_fn( const char *command );

elapsedMillis ms;
                             
void setup()
{
  pinMode( LED_BUILTIN, OUTPUT );    // Configure the onboard LED for output
  digitalWrite( LED_BUILTIN, LOW );    // default to LED off

  // Setup callbacks for SerialCommand commands
  sCmd.addCommand( "on",  LED_on );    // Turns LED on
  sCmd.addCommand( "off", LED_off );    // Turns LED off
  sCmd.addCommand( "rpm", rpmCommand );    // rpm followed by new rpm value
  sCmd.addCommand( "ppr", pprCommand );    // ppr followed by new ppr value
  sCmd.addCommand( "frq", frqCommand );    // frq followed by new ppr value
  sCmd.setDefaultHandler( default_fn );    // Handler for command that isn't matched  (says "What?")

  Serial.begin( 115200 );
  delay( 1000 );
  Serial.printf( "Application TachSim0 %s %s\n", __DATE__, __TIME__ );

  Enc.setInitConfig();  //
  Enc.init();

  tachsim.begin();                // begin()
  tachsim                    // CONFIGURATION
  .setPhase( 90 )                // normal 90 deg phase shift
  .setTotalBounceDuration( 0 )            // no bouncing
  .setPeriod( ppr*4 )                // marker every ppr A/B pulses
  .setFrequency( frq );                // frequency = f(ppr,rpm)

  tachsim.setContinuousMode( true );        // don't stop at target
  tachsim.moveRelAsync( rpm >= 0.0 ? +1 : -1 );    // +/- direction

  ms = 0;
}

uint32_t encNow, encPrev = 0;

void loop()
{
  sCmd.readSerial();
  if (update == true) {
    Serial.printf( "ppr = %4lu   rpm = %6.1f   frq = %8.1f\n", ppr, rpm, frq );
    update = false;
  }
  else if (ms >= 1000) {
    encNow = Enc.read();
    Serial.println( encNow - encPrev );
    encPrev = encNow;
    ms -= 1000;
  }
}

void LED_on() { digitalWrite( LED_BUILTIN, HIGH ); }
void LED_off() { digitalWrite( LED_BUILTIN, LOW ); }

void rpmCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    rpm = atoi(arg);                // char* to integer
    frq = (ppr*4)*(fabs(rpm)/60);        // compute new frequency
    tachsim.moveRelAsync( rpm >= 0 ? +1 : -1);    // set direction = f(rpm)
    tachsim.setFrequency( frq );        // set frequency
  }
  update = true;
}

void pprCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    int temp = atoi(arg);        // char* to integer
    if (temp > 0) {            // if valid ppr
      ppr = temp;            //   set ppr
      frq = (ppr*4)*(fabs(rpm)/60);    //   compute new frequency
      tachsim.setPeriod( ppr*4 );    //   marker every ppr pulses
      tachsim.setFrequency( frq );    //   set frequency
    }
  }
  update = true;
}

void frqCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    frq = (float)atoi(arg);        // char* to integer
    rpm = (60*frq)/(ppr*4);        // back-calc rpm
    tachsim.setFrequency( frq );    // set frequency
  }
  update = true;
}

// This gets set as the default handler, and gets called when no other command matches.
void default_fn( const char *command )
{
  Serial.println( "What?" );
}
I need to return to this. I can try this code on a spare Teensy4.1 with a screw terminal test board. I'm trying to rule out whether EncoderTool on my ELS project is slipping counts. It's going to be a bear to shoehorn in. Not sure if the application will run after integration.

One more question on Quad Encoder, if I specify the Index pin, does the count reset at index time, or continue to count? Can I use the index time to check my SW encoder is at some constant offset? (I'm not sure if I can sync them exactly, but I could check if the count offset between HW and SW accumulates.)

If I understand your test code, I can issue an RPM and PPR command to change how it operates? What is the the command string?
cmd <space> value <carriage return> ?

Sorry to be asking a question like that. Honestly, I'm not a good C/C++ programmer, only learned by the seat of my pants, and not very well at that.

Edit: Which SerialCommand are you using? I just installed the one from Hugo Arganda, V2.0.1, and I get a lot of errors compiling your code. No matching function for call.
 
Last edited:
One more question on Quad Encoder, if I specify the Index pin, does the count reset at index time, or continue to count? Can I use the index time to check my SW encoder is at some constant offset? (I'm not sure if I can sync them exactly, but I could check if the count offset between HW and SW accumulates.)

I don't think that Index causes count to reset, but that may be an option. You'll be able to tell as soon as you have the code running.

If I understand your test code, I can issue an RPM and PPR command to change how it operates? What is the the command string?
cmd <space> value <carriage return> ?

Yes, just like I show in my previous post, and below, where I've added <enter>. I use Arduino 1.8.19, where the Serial Monitor has a "Send" button. I don't know whether Arduino 2.x is the same, but it must have a similar capability.

ppr 1024 <enter>
rpm 1000 <enter>

Edit: Which SerialCommand are you using? I just installed the one from Hugo Arganda, V2.0.1, and I get a lot of errors compiling your code. No matching function for call.

I'm using the one below. I just visited the site, and the library was last updated 7 years ago, so it should be exactly the same as what I'm using. Try to get my program running with no changes before you do anything else.

 
I don't think that Index causes count to reset, but that may be an option. You'll be able to tell as soon as you have the code running.



Yes, just like I show in my previous post, and below, where I've added <enter>. I use Arduino 1.8.19, where the Serial Monitor has a "Send" button. I don't know whether Arduino 2.x is the same, but it must have a similar capability.

ppr 1024 <enter>
rpm 1000 <enter>



I'm using the one below. I just visited the site, and the library was last updated 7 years ago, so it should be exactly the same as what I'm using. Try to get my program running with no changes before you do anything else.

Thanks for the URL, that ought to help. Hugo's library came up first, so I used that.
I was trying to compile as is, just to see how things worked.
 
I don't think that Index causes count to reset, but that may be an option. You'll be able to tell as soon as you have the code running.



Yes, just like I show in my previous post, and below, where I've added <enter>. I use Arduino 1.8.19, where the Serial Monitor has a "Send" button. I don't know whether Arduino 2.x is the same, but it must have a similar capability.

ppr 1024 <enter>
rpm 1000 <enter>



I'm using the one below. I just visited the site, and the library was last updated 7 years ago, so it should be exactly the same as what I'm using. Try to get my program running with no changes before you do anything else.

FYI that project you linked was archived last year. There will not be any updates. I will install it to try it out though.
 
EncSim has a typo? in one of it's whatevers. Methods?
tachsim.setContinuousMode( true ); <=== won't compile. Compiler suggests setContinousMode
tachsim.setContinousMode( true ); will compile
 
Why does the count value vary? Because the millisecond timer isn't 1ms exactly?
Code:
Application TachSim0 May 12 2025 14:19:55
ppr = 1200   rpm = 1000.0   frq =  80000.0
4294967229
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
4294967228
4294967228
4294967232
If my code had different counts for a constant time interval, it would end up with big errors. So is what I observe because the millisecond counter isn't exact, or it's being interrupted? Or?
 
Yes, EncSim has a type on the word "continuous". Yes, the use of millis() or elapsedMillis yields only an approximate time, so you will get slight differences in the counts. Your delta count values look like negative numbers, so try entering a negative RPM value to see if you get positive delta count values.
 
OK. Here's the output with -400 RPM, 1024 PPR
Code:
ppr = 1024   rpm = -400.0   frq =  27306.7
4294967276
0
1
0
0
0
0
0
0
0
4294967295
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
4294967295
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
4294967295
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
4294967295
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
4294967295
0
0
0
0
0
0
0
The error doesn't seem to accumulate, that's good. 1, 7 zeros, -1, 7 zeros,... So this is the fact that millis isn't the same all the time?
 
I haven't run the program in a long time, but it seems like if the frequency is 27 kHz, and the count is being read at 1 kHz, then the delta should be about 27. I'll try to set it up on my bench tomorrow.
 
Okay, pilot error on my part. The wiring should be pins 0,1,2 (EncSim outputs) to pins 4,5,6 (QuadEncoder inputs). The comments in the program are correct about that. I always get confused about the first argument in the QuadEncoder constructor. It's not a pin number, but rather a value from 1-4 to identify which of 4 possible QuadEncoder timers to use. For the default settings of the program, you should get 80,000 counts per second. Note that the program as is does not use the Index pin, but we can add that later.
 
Would it be useful for you if I modified this program to call Enc.read() from a timer interrupt rather than from loop()?
Yes. Would be instructive to see how that's done.

Down in the shop, all the timing indications show "things are fine", but the syncing is most definitely erratic. Sync every once in a while is not good enough. So I'd like somehow to shoehorn quad encoder into it, so I can determine if the SW encoders are leading me astray.
 
Here is a modified version with an IntervalTimer configured for 10 kHz. The timer handler reads the encoder, computes the delta since the previous read, and updates a running sum. The number of interrupts and the running sum of encoder counts are printed from loop() every 1 second. The timer priority is set to 0 (absolute highest priority), so it takes precedence over the 1-ms tick and everything else. This still won't help you if print to USB serial because that sometimes disables interrupts altogether.

I know this isn't necessarily how you would use QuadEncoder, but I hope it's a useful demo. It seems to work well up to +/- 6000 rpm, and after that the encoder simulation can't keep up, so the counts are lower than expected.

Code:
#include <EncSim.h>
#include <QuadEncoder.h>
#include <SerialCommand.h>
#include <IntervalTimer.h>

EncSim tachsim( 0, 1, 2 );    // pins A(0), B(1), M(2)

QuadEncoder Enc(3, 4, 5, 0);  // Enc 3 of 4, A(pin4), B(pin5), PullupsReq(0)

SerialCommand sCmd;        // SerialCommand object

IntervalTimer timer;  // periodic timer

uint32_t ppr = 1200;            // pulses per rev (*4 for quadrature)
float    rpm = 1000;            // rpm = (60*frq)/(ppr*4)
float    frq = (ppr*4)*(fabs(rpm)/60);    // keep this relationship
bool     update = true;            // update = true to trigger one print

void LED_on();
void LED_off();
void rpmCommand();
void pprCommand();
void frqCommand();
void default_fn( const char *command );

elapsedMillis ms;
                                
void setup()
{
  pinMode( LED_BUILTIN, OUTPUT );    // Configure the onboard LED for output
  digitalWrite( LED_BUILTIN, LOW );    // default to LED off

  // Setup callbacks for SerialCommand commands
  sCmd.addCommand( "on",  LED_on );    // Turns LED on
  sCmd.addCommand( "off", LED_off );    // Turns LED off
  sCmd.addCommand( "rpm", rpmCommand );    // rpm followed by new rpm value
  sCmd.addCommand( "ppr", pprCommand );    // ppr followed by new ppr value
  sCmd.addCommand( "frq", frqCommand );    // frq followed by new ppr value
  sCmd.setDefaultHandler( default_fn );    // Handler for command that isn't matched  (says "What?")

  Serial.begin( 115200 );
  delay( 1000 );
  Serial.printf( "Application TachSim0 %s %s\n", __DATE__, __TIME__ );

  Enc.setInitConfig();  //
  Enc.init();

  tachsim.begin();                // begin()
  tachsim                    // CONFIGURATION
  .setPhase( 90 )                // normal 90 deg phase shift
  .setTotalBounceDuration( 0 )            // no bouncing
  .setPeriod( ppr*4 )                // marker every ppr A/B pulses
  .setFrequency( frq );                // frequency = f(ppr,rpm)

  tachsim.setContinuousMode( true );        // don't stop at target
  tachsim.moveRelAsync( rpm >= 0.0 ? +1 : -1 );    // +/- direction

  ms = 0;

  timer.begin( timerHandler, 100.0 ); // 10 kHz -> period = 100 us
  timer.priority( 0 );                // set to highest priority
}

//---------------------------------------------------------------------
// IntervalTimer handler and variables to compute encoder delta/sum
//---------------------------------------------------------------------
uint32_t timerCount;             // counts are always unsigned
uint32_t encNow;                 // unsigned encoder count
uint32_t encPrev;                // unsigned encoder count
int32_t  encDelta;               // signed delta
int32_t  encSum;                 // signed sum

void timerHandler( void ) {
  timerCount++;                  // count interrupts
  encNow = Enc.read();           // read running encoder count
  encDelta = encNow - encPrev;   // compute delta
  encSum = encSum + encDelta;    // update running sum
  encPrev = encNow;              // save for next time
}

void loop()
{
  sCmd.readSerial();
  if (update == true) {
    Serial.printf( "ppr = %4lu   rpm = %6.1f   frq = %8.1f\n", ppr, rpm, frq );
    update = false;
  }
  else if (ms >= 1000) {
    // print running sum of encoder counts and interrupt counts, then zero   
    Serial.printf( "encoder counts = %8d  IRQ count = %8lu\n", encSum, timerCount );
    encSum = 0;
    timerCount = 0;
    ms -= 1000;
  }
}

void LED_on() { digitalWrite( LED_BUILTIN, HIGH ); }
void LED_off() { digitalWrite( LED_BUILTIN, LOW ); }

void rpmCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    rpm = atoi(arg);                // char* to integer
    frq = (ppr*4)*(fabs(rpm)/60);        // compute new frequency
    tachsim.moveRelAsync( rpm >= 0 ? +1 : -1);    // set direction = f(rpm)
    tachsim.setFrequency( frq );        // set frequency
  }
  update = true;
}

void pprCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    int temp = atoi(arg);        // char* to integer
    if (temp > 0) {            // if valid ppr
      ppr = temp;            //   set ppr
      frq = (ppr*4)*(fabs(rpm)/60);    //   compute new frequency
      tachsim.setPeriod( ppr*4 );    //   marker every ppr pulses
      tachsim.setFrequency( frq );    //   set frequency
    }
  }
  update = true;
}

void frqCommand()
{
  char *arg = sCmd.next();
  if (arg == NULL) {
    Serial.println( "No arguments" );
  }
  else {
    frq = (float)atoi(arg);        // char* to integer
    rpm = (60*frq)/(ppr*4);        // back-calc rpm
    tachsim.setFrequency( frq );    // set frequency
  }
  update = true;
}

// This gets set as the default handler, and gets called when no other command matches.
void default_fn( const char *command )
{
  Serial.println( "What?" );
}
 
Back
Top