Motor RPM measurement using Optical Encoder for PI Control

Hey, guys! I need some guidance on how to measure the RPM of my brushed DC motor using an optical encoder. I will be using the RPM measurements in a PI controller as a feedback to compare to the target RPM value I will be specifying.

First, let me describe my current hardware setup.

  • I am currently using a 6V motor that is supposed to rotate at 100 RPM while drawing 120 mA of current at no-load conditions. The motor has a stall torque of 9 kg/cm. (These were the numbers I could find, but I can't verify if these numbers are extremely accurate).
  • I am using Sparkfun's GP1A57HRJ00F photo-interrupter with the breakout board they supply for it. I have soldered a 0.1 uF capacitor between the PWR and GND pins of the breakout board to prevent detection error.
  • The motor is driving the following gear train:

    Thus, the optical encoder should 192 counts per 1 revolution of the output shaft.

Now, I have tried 3 methods for measuring the RPM of the motor:

  1. Count the number of encoder pulses between each loop of the program. Divide the total count by the time it takes for the program to loop.
  2. Count the number of encoder pulses over specified period of time (I call this sampling time). Divide the total count by the specified time period.
  3. Measure the time elapses between encoder pulse. Take the reciprocal of this time interval.

Here, is my code for implementing all three methods:

#include <util/atomic.h>

//===== Encoder Pins =====//
const int encoderPin = 2;
volatile int encoderCount = 0;
int prevCount;

//===== Motor Pins & Variables =====//
const int DIR1 = 4;
const int EN1 = 5;

float motorVelocity;

//===== Timer Variables =====//
unsigned long currT;
unsigned long prevT;
unsigned long samplingT = 1.0e5;

//===== Motor Test Variables =====//
int testIndex = 1;
unsigned long prevIndexTimer;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(encoderPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(encoderPin), encoderPulse, RISING);

  pinMode(DIR1, OUTPUT);
  pinMode(EN1, OUTPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
  currT = micros();
  
  int localCount;
  float localVelocity;
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    localCount = encoderCount;
    localVelocity = motorVelocity;
  }

  // Method 1: Count the number of encoder pulses between each cycle of loop function
  /*
  float deltaT = ((float) (currT - prevT))/1.0e6;
  motorVelocity = (localCount - prevCount)/deltaT;
  prevCount = localCount;
  prevT = currT;
  */
  
  // Method 2: Count the number of encoder pulses over specified period of time
  /*
  if (currT - prevT >= samplingT) {
    motorVelocity = (localCount - prevCount)/((float) samplingT/1.0e6);
    prevT = currT;
    prevCount = localCount;
  }
  */

  motorRPM_Test(100, 10);
  
  Serial.print(currT/1.0e6);
  Serial.print(" ");
  // Serial.print(motorVelocity); // Output for Method 1 & 2
  // Serial.println(localVelocity); // Output for Method 3
  Serial.println();
}

void encoderPulse() {
  encoderCount += 1;

  // Method 3: Measure time elapsed between each encoder trigger
  
  long currT2 = micros();
  float deltaT = ((float) currT2 - prevT) / 1.0e6; // Calculate time interval between triggers
  motorVelocity = 1 / deltaT; 
  prevT = currT2;
  
}

void motorRPM_Test(int PWM_Value, int testTime) {
  // This function ramps up the motor speed from a chosen initial PWM value 
  // to 255 over a specified time interval.
  
  int basePWM = PWM_Value;
  int testInterval = testTime; // Time interval in seconds
  
  // testIndex variable is used to make sure the initial PWM value is maintained every time the test cycles
  unsigned long currIndexTimer = micros();
  if (currIndexTimer - prevIndexTimer >= (testTime*1.0e6)) {
    testIndex++;
    prevIndexTimer = currIndexTimer;
  }
  
  int motorPWM = basePWM * testIndex + ((255 - basePWM) * (micros() / 1.0e6)) / testInterval;

  digitalWrite(DIR1, HIGH);
  analogWrite(EN1, motorPWM);
}

Then, I tested each method by ramping up the motor speed from an initial PWM value of 100 to 255 over a time period of 10 seconds. The following are graphs of the results I obtained:

  1. Method 1
  2. Method 2
  3. Method 3

As you can see, method 1 seems to be just hot garbage. I'm not sure if its an issue of implementation, but there isn't any real data to be gleamed from that mess.

Method 2 gives pretty good data, but it feels very "coarse" (I know this is due to the nature of the calculation). I have a feeling this might be good enough to achieve an O.K level of speed control, but I don't really know as this is my first time using a PI/PID controller.

Method 3 gives the most detailed data, but it's fatal flaw is that it cannot detect an RPM of 0 due to the way it is implemented. As the calculation is done in the encoderPulse function, which is only carried out if there is an encoder pulse, it's impossible to detect 0 RPM.

I believe I have two main routes of moving forward with this:

  1. Keep experimenting with the sampling time for method 2 to get as much detailed data as I can.
  2. Come up with a way to detect 0 RPM for method 3. The only way I could think of is if the encoderCount variable doesn't change for like 0.5 s, the micro-controller sets the value of motorRPM to 0. I'm not sure if this would work though.

What do you guys think would be the best method? If you have a better idea than the three I have shared above, I would love to hear it as well.

Thank you in advance.

I have not checked in details but motorVelocity should be declared as volatile

I'm not too sure what volatile actually does. Can you give me a brief run down of what it does?

Happy to Google that for you…

Have you verified the sensor is providing a usable signal?
Your system should be run at DC i.e. no movement. Can you rotate the gear by hand and see if the output is really detecting the teeth? Thinking the 96 tooth gear may have some pretty small teeth.

I had the same concerns as you, so I wrote this quick test:

#include <util/atomic.h>

//===== Encoder Pins =====//
const int encoderPin = 2;
volatile int encoderCount = 0;

//===== Motor Pins & Variables =====//
const int DIR1 = 4;
const int EN1 = 5;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(encoderPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(encoderPin), encoderPulse, RISING);

  pinMode(DIR1, OUTPUT);
  pinMode(EN1, OUTPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(DIR1, HIGH);
  if (encoderCount <= 192) {
    analogWrite(EN1, 200);
  }
  else {
    analogWrite(EN1, 0);
  }
}

void encoderPulse() {
  encoderCount += 1;
}

It runs the motor until the encoder has detected 192 counts and then turns the motor off.

Here is the result:

As you can see in the video, the output link stops pretty much at the position where it starts. So, I think the encoder is fine.

Did you consider the data sheet warning :To prevent photointerrupter from faulty operation caused by external light, do not set the detecting face to the external light.".

In other words, do you have the sensor in the dark?

I did not consider that, but as you can see from the video I shared, I don't think there are issues with getting data off the photo-interrupter.

The problem for me is calculating a good RPM value from the photo-interrupter count. As I explained above, I believe either method 2 or method 3 will be the best, but both have their drawbacks:

  1. Method 2 gives relatively good stable RPM values, but as the calculation is done over a relatively large time interval, it doesn't pick up small changes in the RPM value very well.
  2. Method 3 gives the most sensitive data, but the way it's implemented right now makes it impossible to detect a 0 RPM state.

I was hoping someone on the forums, who has more experience than me, could share either what they did for their project or suggest how I can modify method 2/3 to mitigate their respective weaknesses.

I wonder why you did not count the pulses in an interrupt function. Then once a second save and reset the interrupt count. Use the copy to compute the RPM for that second.

1 Like

I count the pulses in an interrupt function in Method 3. This is the method that gives me the best data, but it can't detect when the motor stops moving as the function doesn't execute if there isn't an interrupt from the photo-interrupter.

Nonsense. The count will be zero the next time you measure it. The loop() function is where you save a copy and reset the counter used by the interrupt function. No interrupts mean the counter will always stay zero. So in loop, test for zero count and if true, the motor has stopped.

1 Like

I'd say don't measure RPM. Instead measure in what you can sense (

taken with the 15-35counts/sec on graph gives 15/19260=4.6RPM
to 35/192
60=10.9 RPM for maybe 10-100% PWM.

The Arduino PID library has a default update time of 200ms
( Arduino Playground - PIDLibrarySetSampleTime ) . If you use 200ms as a sampling/update/dT interval, your system seems to be stable between 3-7 pulse per 0.2s.

Interval-wise, 35 to 15pps is 28 to 67 ms/pulse.

If you time out measuring the interval between pulses at 0.5s, a trick is converting it to the minimum detectable RPM: 1/192 / 0.5 *60s/m = 0.625 RPM. Or halfway between the minimum detectable and zero: 0.5 * 1/192/0.5*60s/M = 0.3125 RPM

I think I'd convert the target RPM into pulses per second or pulses per update period. Or convert target RPM to the interval between pulses and then measure that directly and avoid the conversions in the control loop. You can always convert whatever you measure, back to RPM for the UI.

If you care about absolute position and a dead-accurate average RPM, you could try a phase locked loop between encoder position and time, instead of a PID on RPM. See PIC project DC motor like crystal clockwork! or Stepper motor going out of time - #31 by luni64 for some hints.

1 Like

Try something along those lines

//===== Encoder Pins =====//
const byte encoderPin = 2;
volatile unsigned long encoderCount = 0;
const unsigned long checkPeriod = 1000; // 1s in ms
//===== Motor Pins & Variables =====//
const byte DIR1 = 4;
const byte EN1 = 5;

void encoderPulse() {
  encoderCount++;
}

void setup() {

  pinMode(DIR1, OUTPUT);
  pinMode(EN1, OUTPUT);
  pinMode(encoderPin, INPUT); // not really needed as this is the default state.

  Serial.begin(115200); // 9600 is soooo last century, don’t go slow

  attachInterrupt(digitalPinToInterrupt(encoderPin), encoderPulse, RISING);

  digitalWrite(DIR1, HIGH); // whatever is needed to run 
  analogWrite(EN1, 200);
}

void loop() {:
  static unsigned long lastCheck;
  unsigned long now = millis();
  if (now -  lastCheck  >= checkPeriod) { // every checkPeriod
    noInterrupts();
    unsigned long countCopy = encoderCount;
    encoderCount = 0;
    interrupts();

    // you know you got countCopy ticks in the last (now -  lastCheck) ms
    // do your maths for speed

    lastCheck = now;
  }
}

If countCopy Is 0 you know there was no tick since last check. You can adjust 1000 (checkPeriod) to suit your timing needs

(Typed from my iPhone, fully untested)

1 Like

@DaveX I wanted to clarify what you mean by this section. The math you are doing here is:
(1 revolution / 192 counts) * (1 count / 0.5 seconds) * (60 seconds / 1 minute) = 0.625 RPM.

My question is where did you get 1 count / 0.5 seconds? Maybe this is a dumb question, but I didn't understand how your obtained the equation above.

@DaveX took a random reference for 0.5 second. if you detect one pulse during the sampling time you select, that's your maximum resolution / minimum detectable RPM

if your sampling time for calculating the speed is too short compared to the actual speed, you might get 0 tick during an interval and you'll falsely conclude that the motor is not spinning

I got the 0.5 seconds from your post with the 0.5s timeout:

If you use a timeout like that, you can only guess about the actual value, but whatever it is is bounded above by the 1 count/timeout rate.

If you can allow smoothing, you might try an Exponentially Weighted Moving Average (Moving average - Wikipedia):

ewma_alpha = 0.2;  // Adjustable smoothing parameter lower is smoother, higher is more responsive
ewma_counts_per_period = ewma_alpha* counts_per_period + (1-ewma_alpha) * ewma_counts_per_period;

An EWMA filter is an inexpensive way to keep a moving average of a measurement. It can be useful if you are measuring low-rate or near-zero processes.

1 Like

@J-M-L I implemented your suggestion with a sampling period of 200 ms and ran another test with an initial PWM value of 80, ramping up to a value of 255 over a period of 10 seconds.

The data looks like below:

While this data is relatively clean, it's also very "discrete" - the measured motor speed falls into discrete levels. Does this mean when I use a PID controller, should I use set points that fall into these discrete levels i.e 10, 15, 20, 25, 30, or 35 counts/second?

No. Choose the setpoint you want, and the PID with the attached controlled system, will smooth things out. (Assuming the the right choice of parameters)

ETA: Also, those discrete levels of counts/sec correspond exactly to the the highest resolution you can measure in a sampling period. 1 count/0.200s = 5 counts/s

For a given sampling period, your system can't measure more accurately than discrete counts.

1 Like

This is a great method! Note that ...

  • there's no longer any need to count pulses (you're just measuring pulse period)
  • typical brushed DC motor rpm = 0-20,000 or 0-333.33Hz or ∞-3ms or ∞-3000µs
  • use micros() to improve time measurement resolution
  • use a timeout to allow continuous measurements and to indicate 0 rpm
  • should now easily achieve ± 1 PWM duty cycle resolution for control
  • if using timeout of 2000000µs (2 sec), should now achieve 0 for stopped, then 2000001 - 3000 representing 0.5Hz - 333.33Hz or 30-20000 rpm
  • could use a separate main loop (non-interrupt) function that handles 1-30 rpm

I've been following this thread trying to get a handle on the actual counts and goal.
Are the below valid"

  1. Motor turns at 100 RPM (max) (aka 1.66 rev/sec)

  2. Sense gear (max) is 1.666 * 24/96 = 0.4166 rev/sec.

  3. sensor count is 0.4166 * 96 = 40 counts /sec (max)

  4. Motor speed is approx 100 RPM at PWM of 255

What is the lowest speed you expect to operate at and control?

Do you really care what the RPM is? Or do you just need to control the speed relative to some control?

Could you do something like this:

  • two milli's counters ( I don't think you really benefit from micros)
  • one interrupt counting gear teeth signals.

Counter1 is read then reset by the interrupt. This reading is millis / count.
Counter2 is a millis counter also reset by the interrupt.

Counter2 in the main program is monitored and if it counts to a number that corresponds to the lowest equivalent RPM then it will set that to that minimum millis / count.

I would do no float or division in the code unless there is some requirement to read the RPM out. And then I would make only one calculation, just before the display.