Determining motor RPM by using a quadrature rotary encoder of unknown PPR

So this has been driving me crazy the last couple of days.

I have a no-name micro gearbox 6V motor, and it came with an rotary quadratic encoder. Encoder has 5v, Gnd, output A and output B connections.

The only information about the encoder on seller’s page was:

  • “Includes quadrature encoder with digital outputs for measuring motor position or speed. 7-clicks per revolution @15000RPM.” - direct quote.
  • Snapshot of an oscilloscope reading.

I’m not sure how much I can trust this information.

I wanted to establish the relationship between motor RPM, Arduino PWM that I set, and the encoder readings.

I don’t have the oscilloscope myself. What I did instead was to first establish the actual RPM across a range of PWM values (I used high speed camera for accuracy). RPM at 6V is fairly close to the rated RPM.

But then I tried counting encoder’s PPR and comparing their incremental increase with the elapsed time and the results I’m getting are all over the place. I’m using Arduino’s serial monitor timestamps for this, but I tried the same with millis() and I’m getting the same gibberish. There is no resemblance of any correlation.

I would appreciate it if anyone could point me to how to fix this problem.

The code I’m using is adapted from one of the comments at this page.

Thank you.

#define outputA 6
#define outputB 7

int counter = 0;
int aState;
int aLastState;
byte encoderSt = 0;

const int motorPWM = 3;

void setup() {

  pinMode(motorPWM, OUTPUT);
  pinMode(outputA, INPUT);
  pinMode(outputB, INPUT);

  Serial.begin (9600);

  analogWrite(motorPWM, 60);
}

void loop()
{
  byte a = digitalRead( outputA );
  byte b = digitalRead( outputB );
  byte st = (b << 1) | a;
  if ( encoderSt != st )
  {
    if ( (encoderSt == 0 && st == 2) || (encoderSt == 1 && st == 0) || (encoderSt == 2 && st == 3) || (encoderSt == 3 && st == 1) ) counter-- ;
    else counter++;
    Serial.println( counter );
    encoderSt = st;
  }
}
pinMode(outputA, INPUT);
  pinMode(outputB, INPUT);

Typically the encoder outputs are open collector, and require a pullup resistor. Use the internal pullup enabled with INPUT_PULLUP pinMode, or if that doesn’t stablilize the readings try 4.7k or 2.2k external pullups.

What is the gear ratio on the motor, and the 6v rpm?

Thank you for replying cattledog,

I will go with INTERNAL_PULLUP. Do you suspect the read methods I use cannot catch up with encoder's output?

Motor data sheet/specification information is limited to:

Reduction Ratio: 150:1; speed: 100RPM; no-load current: 40mA; torque: 8 oz⋅in (0.55 kg⋅cm); stall current: 550mA

I am indeed getting pretty close to 100RPM at 6V.

Hi krlesxe,

The code you've got looks to be written for a rotary encoder, but hes using it while turning a handheld dial or a stepper motor (so moving very slowly). Your loop will probably be missing most of the inputs.

I suspect you need to look into the interrupt function - I've never used it but I'm sure with a bit of googling you can find some relevant examples. The link will tell you which pins are available for interrupt on your board.

Hi trojanhawrs,

I'm actually trying to avoid interrupts as they require pins 2 and 3 on my board. I'm driving two motors, both with encoders, and pin 3 is also PWMA pin. When I set pin 3 as an interrupt pin, my motor is always on. It's just silly that pin 3 is reserved for both things at the same time...

You don't have other PWM pins available?

You can also use PCINT, they are available on any pin on a '328P board.

I believe the encoder is reading the motor shaft at 15000 rpm. This looks like the type of encoder which is documented at Pololu at 12 counts per revolution with both channels on both edges.

I believe that interrupts are best used with this encoder. Here is an example of pin change interrupts on your A and B pins (6 and 7). To make things simple I have used Nico Hoods library for the pinChange interrupts. It is available through the library manager. PinChangeInterrupt.h by Nico Hood. I recommend it because the syntax is parallel to that used with the ide external interrupts.

I am reading the interrupt counts every second in a manner which ensures the values won’t change during a reading. I have also used a pin reading method which is faster than digitalRead().

//Nico Hood library 
#include <PinChangeInterrupt.h>
const byte encoderPinA = 6;//PD6
const byte encoderPinB = 7;//PD7
#define readA bitRead(PIND,6)//faster than digitalRead()
#define readB bitRead(PIND,7)//faster than digitalRead()

volatile long count = 0; //depending on count rate, you may be able to use int
long copyCount = 0; //depending on count rate, you may be able to use int
long previousCount = 0;//depending on count rate, you may be able to use int

unsigned long previousReading;
const unsigned long interval = 1000;

void setup() {
  Serial.begin (115200);
  Serial.println("starting");
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(encoderPinB, INPUT_PULLUP);
  attachPCINT(digitalPinToPCINT(encoderPinA), isrA, CHANGE);
  attachPCINT(digitalPinToPCINT(encoderPinB), isrB, CHANGE);
}

void loop() {
  if (millis() - previousReading >= interval)
  {
    previousReading += interval;
    noInterrupts();
    copyCount = count;
    count = 0;
    interrupts();
  }
  if (copyCount != previousCount) 
  {
    Serial.println(copyCount);
  }
  previousCount = copyCount;
}

void isrA() {
  if (readB != readA) {
    count ++;
  } else {
    count --;
  }
}
void isrB() {
  if (readA == readB) {
    count ++;
  } else {
    count --;
  }
}

trojanhawrs,

Not that I know of. I have PWMA on 3 and PWMB on 11, both used by motors. If I try to set a random free pin (say, 10) as PWM pin motor doesn't start. If pin 3 is set as an interrupt pin, motor starts the moment I connect the encoders output wire. Perhaps I'm doing something wrong...

CrossRoads,

Good to know, I wasn't aware of that. Thanks for bringing it to my attention. I wasn't sure on how to actually do it but it seems that cattledog utilized it in their answer.

cattledog,

Thanks so much for updating the code. Also for the advice on the appropriate library and on how to set interrupt pins other than pin 3, which I need for the motor PWM. I'm still struggling to understand interrupt methods, especially if they call functions as arguments. So this is quite valuable. Also, I read about digitalRead() being slower then the direct read elsewhere on the forum and the Playground, but this is the first time I see an actual example on how to write it. So thank you for that too.

Pololu motor and the encoder you linked to is actually my other motor/encoder. I wanted to start with this no-name device first to better understand the problem.

I tried the sketch and it's giving fairly consistent readings at set PWM value. For motor of this range of RPM I believe it's quite appropriate. However, I tested the counter across the range of PWM values/RPM just for kicks. I got some interesting results, it seems as if there is a drop off of counts at higher RPM/PWM values. Here's a quick table and diagram:

Again, this is nothing critical for my use but it would be interesting to understand why this happens. I will test pololu motor and encoder next week when I get back to my work.

Which board are you using? Is it a leonardo, micro or yun by any chance?

If the "average count" is the count every second from the sketch, the data seems to sense with the 150:1 gear ratio and the reported "7 clicks" per revolution. The "seven clicks" would appear to mean 7 pulses per channel and 28 quadrature transitions when all 4 transitions per pulse are counted. The full 4 transition count is what the code uses.

150 * 28 = 4200 counts per revolution
100rpm = 420000 counts/minute
60 seconds/minute = 7000 counts/second

it seems as if there is a drop off of counts at higher RPM/PWM values

How are you determining rpm?

Quadrature encoders are generally overkill for rpm determination and are better for fine positioning. How are you planning to use the motors and the data from the encoders? Do you need to know the direction of rotation from the encoder, or can you determine that from the code which is running the motors? What accuracy/precision do you require?

There are algorithms which only count a reduced set of the quadrature transitions which give lower resolution with fewer interrupts and less processing. This helps free the mcu for other tasks.

trojanhawrs,

I have Arduino Uno and its Motor Shield, both R3. My A and B pins are 3, 9, 12 and 11, 8, 13, respectively (for PWM, BRAKE and DIR).

cattledog,

I see what you mean. I will try RPM = count * 60 / (150 * 7) next and see what I get. After that I will try the chart trendline expression and check which one is more accurate. I just hope this is not a mechanical issue, i.e. something off with the gearbox or similar.

I shot high speed videos with fixed framerate and I have enough capture points to check for errors and I'm pretty confident the RPM are correct. Until I'm comfortable with what's going on, the RPM might be the only number I really trust.

The motors drive mechanical parts submerged in a fluid that changes density and viscosity over time. My measurements were carried out for motor with no load. So I'm trying to figure out what is the correlation between encoder pulse counts and RPM. The end goal is to read RPM, say over period of 30 minutes, and adjust it if it changes by more than 5%, 10% or whatever. I will set the rotation via Arduino so I don't think I need to monitor it. Do you think that it would be better to reduce the number of encoder output channels to only one?

This might be an overkill for motors with 100RPM but I might end up with motors working at 400RPM or more. So if there is a better library for that, I'm all ears. Also, this reminds me, I forgot to ask: do you think this would also work on 9600 bits per second? Just curious.

I see what you mean. I will try RPM = count * 60 / (150 * 7) next and see what I get.

No. Given my understanding of the encoder, there are 28 pulses per revolution with the algorithm in the code provided which counts all quadrature transitions over 1 second.

RPM = count * 60 / (150 * 28)

So I’m trying to figure out what is the correlation between encoder pulse counts and RPM.

Depending on the algorithm used code can show either 7, 14, or 28 counts per motor revolution.

Do you think that it would be better to reduce the number of encoder output channels to only one?

Yes. You will need less pins, less processor time, and given the accuracy you need it does not really matter if you count 4200 counts per revolution, 2100 counts per revolution or 1050 counts per revolution.

Here is the previously supplied code modified to read one edge of one pin. I’ve also changed the counts to unsigned int instead of long. You can try both RISING or FALLING for the interrupt mode in case there is a cleaner edge to the signal.

//Nico Hood library
#include <PinChangeInterrupt.h>
const byte encoderPinA = 6;//PD6
#define readA bitRead(PIND,6)//faster than digitalRead()

volatile unsigned int count = 0; 
unsigned int copyCount = 0; 
unsigned int previousCount = 0;

unsigned long previousReading;
const unsigned long interval = 1000;

void setup() {
  Serial.begin (115200);
  Serial.println("starting");
  pinMode(encoderPinA, INPUT_PULLUP);
  attachPCINT(digitalPinToPCINT(encoderPinA), isrA, RISING);
}
void loop() {
  if (millis() - previousReading >= interval)
  {
    previousReading += interval;
    noInterrupts();
    copyCount = count;
    count = 0;
    interrupts();
  }
  if (copyCount != previousCount)
  {
    Serial.println(copyCount);
  }
  previousCount = copyCount;
}

void isrA()
{
  count ++;
}

If all you want to derive is RPM from a quadrature encoder I guess I don’t understand why you are looking at both the A and B signals. The idea behind A and B in your case is using both signals to determine rotational direction. Normally one leads the other by 90 degrees. For example if I view A & B on a scope and trigger off A then either A will lead B or B will lead A depending on rotational direction. If all you want to derive is RPM you can likely just use A or B. I see no need for both.

Frequency = 1 / Time and Time = 1 / Frequency so if we know the Time (Period) for the pulses we can derive the Frequency.

RPM = Frequency / PPR (Pulses Per Revolution) * 60.

You can use Interupts or measure on time + off time of a pulse to get the period. Here is an example using the latter:

#include <LiquidCrystal_I2C.h>

unsigned long highTime;    //integer for storing high time
unsigned long lowTime;     //integer for storing low time
float period;    //integer for storing period
float freq;      //intefer for storing frequency
float RPM;       //storing or calculating RPM 
LiquidCrystal_I2C lcd(0x27,20,4);  // set the LCD address to 0x27 for a 16 chars and 2 line display

void setup()
{
    lcd.init();                      // initialize the lcd
    Serial.begin(9600);
    pinMode(5,INPUT);  //Setting pin as input
}

void loop()
{
    highTime=pulseIn(5,HIGH);  //read high time
    lowTime=pulseIn(5,LOW);    //read low time
    period = highTime+lowTime; // Period = Ton + Toff
    freq=1000000/period;       //getting frequency with totalTime is in Micro seconds
    RPM=(freq/2)*60;           //we div by 2 since the fan tach outputs 2 pulses per revolution

    //Serial Print Data
    Serial.print("Frequency = ");
    Serial.print(freq);
    Serial.println("  Hertz");
    Serial.println("");
    Serial.print("RPM = ");
    Serial.println(RPM);
    
    // LCD Print Data
    lcd.backlight();
    lcd.setCursor(0,0);
    lcd.print(((String)"Freq. = "+freq + " Hz."));
    lcd.setCursor(0,1);
    lcd.print(((String)"Speed = "+RPM + " RPM"));
    
    delay(1000);
}

The example uses pin 5 which in your case can be A or B quadrature out. You would change:

RPM=(freq/2)*60; //we div by 2 since the fan tach outputs 2 pulses per revolution to read RPM=(freq/7)*60; //we div by 7 since the fan tach outputs 7 pulses per revolution. You can also use an Interupt as was covered. This was an old fan speed program I used a few years ago and it should work.

Forgot to mention you will need to set the pin you use for the internal pullup.

Ron

cattledog:
RPM = count * 60 / (150 * 28)

Arrrghghhhh my distracted brain! This is what I meant.

Anyhow, this is of great help, I'll give it a try and post the results.

Ron_Blain,

Thank you as well for the reply and for sharing the code. I guess as I'm progressing through the thread it's becoming obvious that some optimizations can be carried out for my practical case, dropping B output being one of them. Still, it's nice to know both how to approach both cases. Noted for the internal pullup.

EDIT:

OK, just a quick edit. I've been playing with this and I'm getting the results that are pretty close, albeit somewhat above the actual RPM.

I tried RISING and FALLING interrupt modes with RPM = count * 60.0 / (150.0 * 7.0) and CHANGE with RPM = count * 60.0 / (150.0 * 14.0) and they are all giving RPM that is anywhere from 4% (at higher PWM) to 10% (at lower PWM) higher than the actual RPM. Overall, RISING gave me the best results but it was a marginal improvement over other modes. I'll keep playing with this and with Ron's example.

And remember kids, if you declare RPM as a float, use the decimal zeros in your multiplication/division factors! Don't be like me!

OK, just a quick edit. I've been playing with this and I'm getting the results that are pretty close, albeit somewhat above the actual RPM.
I tried RISING and FALLING interrupt modes with RPM = count * 60.0 / (150.0 * 7.0) and CHANGE with RPM = count * 60.0 / (150.0 * 14.0) and they are all giving RPM that is anywhere from 4% (at higher PWM) to 10% (at lower PWM) higher than the actual RPM.

Do you see the same 4-10% percentage error across all methods when you use the three different algorithms for the 7/14/28 counts?

One potential issue is that the one second timing is performed with the millis() function of the Arduino, and if the system clock is not exactly 16MHz the timing is not accurate, but I would not expect this to be more than a 1% error. There are more accurate ways of timing pulses than with the millis() function, but the 4-10% error does not seem reasonable.

How many counts/second do you see at the lower speeds. Greater error at the lower speeds can be cause by the fact the the counts can vary slightly do to the integral number of counts in a time period. If the expected count reading one edge at 100 rpm is 1050/second, then at 30 rpm the expected value is 315/second. This is approximately one count every 3 milliseconds. There can be a variation of one or two counts with the timing, but again that's less than 1%.

Typically with high count rates, you can take counts per unit time. The alternative approach is to determine the unit of time for a specific number of counts. At the limit, it turns into the period of a single count as mentioned. In your case you would do better to take the time period for 1000 or 10000 or 100000 counts. Still, changing the routine from counts per unit time to time per a fixed number of counts will not explain the 4-10% error.

We are coming at this from different perspective. Magnetic encoders like you are using should not have any signal "bounce" creating extra edges, and I have confidence in the interrupt based encoder counts are not being missed or created. I certainly do not think the encoder or code is creating counts, certainly at the 4-10% level.

I do not have the confidence that you do in the rpm measurements. How are the motors powered? If there is variation in the supply voltage, that will lead to errors in the previously mapped pwm duty cycle to rpm values.

I could understand if the gear train was not exactly 150:1 and it would lead to a systemic error between the motor revolutions and the output shaft revolutions. However, I don't think that error would vary with the rpms.

We each have a different opinion. I think that 7/14/28 counts/rev is correct and the interrupt code counts accurately. You think that the rpm measurement is accurate and that for some reason the counts are not reflecting the rpm value in a way that varies with rpm.

I have the data only for 7 and 14 counts, haven't had the time to go back to using both outputs A and B.

For example, at PWM value of 30 (the lowest I'm using with this motor) I'm reading approx. 598 counts and measure 32.3 RPM. This is in comparison to calculated 35.9 RPM (RPM = 598 * 60.0 / (150.0 * 7.0)).

What I said about the RPM measurements was that I'm "fairly confident" they are correct. I don't want to talk in absolutes and I'm open to the idea that they might be incorrect. What I did to make everything as comparable as possible was to run the motor, with no load, while connected to Arduino's A power supply pins. Arduino was on 9V power supply. I changed the PWM value from 30 to 110 in increments of 10. I shot the videos and double-checked the frame rate before counting the frames between a full revolution. I got the data set for dozens of revolutions for each PWM and the maximum discrepancy I found after counting frames was 3 frames for 1000fps rate, which is 0.3%. But I actually monitored two points on a rotating shaft, with different starting times, and frames deltas between the two sets of data were typically 0 or 1. Which I believe is negligible. I understand that this still doesn't have to mean it's correct.

What I'm thinking of is that there is some builtin flaw either in the motor or the encoder, given that it's some no-name cheap thing with no data sheets or anything. While I was setting the equivalent PWM values for RPM measurements and pulse counts, there might've been something different in motor's operation (current draw, or some instability). Not sure. It would be good to repeat those tests if time will permit.

I will double check the 28 counts interrupts, and a couple of other things including how does the pololu motor behave. I would also like to see what I can come up with with the data I have right now to be able to set the RPM by setting the PWM value. The worst comes to the worst and I'm not able to figure out how exactly the counts correlate to the actual RPM, I will use PWM to set RPM directly, read and store the count at that point in time, and then just use the counts to monitor if shaft is slowing down or maintains the same RPM.

The motors drive mechanical parts submerged in a fluid that changes density and viscosity over time.

I will use PWM to set RPM directly, read and store the count at that point in time, and then just use the counts to monitor if shaft is slowing down or maintains the same RPM.

Sounds like a good plan. Do you know how the fluid changes, and how the motor behaves when loaded? How close do the two motors have to be to each other? Are they both in the same bath?

Is the actual rpm of the drive motor critical, or just the stability over time? Is it desired to keep the rpm constant over fluid changes, or is there a profile to the rpm with the process? Since the only feedback you will have in actual operation is encoder counts, maintaining them at a constant or varying value by adjusting the pwm duty cycle should work.

Regardless of the uncertainties about rpm and counts, you should be able to get reasonably constant rotation out of the motors.

I'm yet to do the rheometer measurements but even then it's a stochastic process (living organisms growth) that I might not be able to establish catch-all values.

I would guess that both RPM and the stability are critical as I'm using RPM to maintain the tangential velocity of the rotating part, and through it another controlled value (Reynolds number). At these RPM I guess I have some room for an error margin but ideally I should keep everything as tight as possible.

This thread gives me some comfort and I appreciate all the advice. From you and others who chimed in. Thanks again!