optical encoder RPM measurements - guidance

Hi all! I've been using the arduino mega 2560 to count the number pulses (from an optical incremental encoder) occurring in 2 millisecond slots. This encoder is coupled to a relatively small dc motor.

Every 10 seconds, my arduino code gets the arduino to send the open-loop speed (in RPM) information (via serial), which I am able to see in the serial monitor window.

Those speed values look just fine except for cases where I introduce a "particular" Serial.print lines.

Let me explain my situation at bit. If I put a whole bunch of the same Serial.print lines for displaying the speed in RPM, then there appears to be no problem with the speed. The displayed speed is what I want to 'see' (ie. approximately 1500 RPM) every 10 seconds. So I do get (consistently) close to 1500 RPM every 10 seconds. For example, the following code has no problem.

    if ( micros() - r_time >= 10000000 ) {
      r_time = micros();
       Serial.print("rpm is ");  Serial.print( ((levdiff / (float) cpr) / (T * 1e-6)) * 60.0 ); Serial.print("  pwm_level is "); Serial.println( cpr );
       Serial.print("rpm is ");  Serial.print( ((levdiff / (float) cpr) / (T * 1e-6)) * 60.0 ); Serial.print("  pwm_level is "); Serial.println( cpr );
       Serial.print("rpm is ");  Serial.print( ((levdiff / (float) cpr) / (T * 1e-6)) * 60.0 ); Serial.print("  pwm_level is "); Serial.println( cpr );
       //Serial.print("pwm return = ");Serial.print(pwm_equiv);Serial.print("  error pwm = ");Serial.println(error_pwm);      
     }

The resulting output is around 1500 RPM every 10 seconds (just has values like the ones show below indefinitely ---never gets above values of around 1550),

rpm is 1508.79  pwm_level is 2048
rpm is 1508.79  pwm_level is 2048
rpm is 1508.79  pwm_level is 2048
rpm is 1494.14  pwm_level is 2048
rpm is 1494.14  pwm_level is 2048
rpm is 1494.14  pwm_level is 2048
rpm is 1552.73  pwm_level is 2048
rpm is 1552.73  pwm_level is 2048
rpm is 1552.73  pwm_level is 2048
rpm is 1494.14  pwm_level is 2048
rpm is 1494.14  pwm_level is 2048
rpm is 1494.14  pwm_level is 2048
rpm is 1538.09  pwm_level is 2048
rpm is 1538.09  pwm_level is 2048
rpm is 1538.09  pwm_level is 2048

The reason why I inserted replicated lines of code was just to see if extra serial print lines was causing an issue. I found that I could add many of the same replicated serial print lines, and no problem with the displayed speed....of around 1500 RPM.

But.....as soon as I add a different Serial.print line, some of the 'displayed' RPM values tend to be higher eg... up to 1655.

I believe that the "actual" RPM is still actually hanging around 1500 RPM. I'm just trying to figure out how to get my code under control

    if ( micros() - r_time >= 10000000 ) {
      r_time = micros();
       Serial.print("rpm is ");  Serial.print( ((levdiff / (float) cpr) / (T * 1e-6)) * 60.0 ); Serial.print("  pwm_level is "); Serial.println( cpr );
       Serial.print("rpm is ");  Serial.print( ((levdiff / (float) cpr) / (T * 1e-6)) * 60.0 ); Serial.print("  pwm_level is "); Serial.println( cpr );
       Serial.print("rpm is ");  Serial.print( ((levdiff / (float) cpr) / (T * 1e-6)) * 60.0 ); Serial.print("  pwm_level is "); Serial.println( cpr );
       Serial.print("pwm return = ");Serial.print(pwm_equiv);Serial.print("  error pwm = ");Serial.println(error_pwm);      
     }
rpm is 1552.73  pwm_level is 2048
rpm is 1552.73  pwm_level is 2048
rpm is 1552.73  pwm_level is 2048
pwm return = 111.64  error pwm = -4
rpm is 1538.09  pwm_level is 2048
rpm is 1538.09  pwm_level is 2048
rpm is 1538.09  pwm_level is 2048
pwm return = 110.78  error pwm = -3
rpm is 1538.09  pwm_level is 2048
rpm is 1538.09  pwm_level is 2048
rpm is 1538.09  pwm_level is 2048
pwm return = 110.78  error pwm = -3
rpm is 1640.62  pwm_level is 2048
rpm is 1640.62  pwm_level is 2048
rpm is 1640.62  pwm_level is 2048
pwm return = 116.79  error pwm = -9
rpm is 1625.98  pwm_level is 2048
rpm is 1625.98  pwm_level is 2048
rpm is 1625.98  pwm_level is 2048
pwm return = 115.93  error pwm = -8
rpm is 1655.27  pwm_level is 2048
rpm is 1655.27  pwm_level is 2048
rpm is 1655.27  pwm_level is 2048
pwm return = 117.65  error pwm = -10
rpm is 1523.44  pwm_level is 2048
rpm is 1523.44  pwm_level is 2048
rpm is 1523.44  pwm_level is 2048
pwm return = 109.93  error pwm = -2
rpm is 1655.27  pwm_level is 2048
rpm is 1655.27  pwm_level is 2048
rpm is 1655.27  pwm_level is 2048
pwm return = 117.65  error pwm = -10
rpm is 1655.27  pwm_level is 2048
rpm is 1655.27  pwm_level is 2048
rpm is 1655.27  pwm_level is 2048
pwm return = 117.65  error pwm = -10
rpm is 1567.38  pwm_level is 2048
rpm is 1567.38  pwm_level is 2048
rpm is 1567.38  pwm_level is 2048
pwm return = 112.50  error pwm = -5

The pulse counting is done by interrupts.
The variables used are straight forward.... eg..

levdiff is an 'int', which stores the number of counts (in each 2 millisec window).
cpr is an 'int' as well, which is the number of counts per revolution I'm using... which is 2048
pwm_equiv and error_rpm are 'float'.

After all this time of thinking, and unable to yet figure out how to fix up this one, I thought I'd better ask for some help and recommendations on this.

I know that Serial.print can add delay to my loop. Does anyone have any ideas about why the very last 'additional/extra' (different) Serial.print line appears to have a noticeable influence on the displayed RPM?

Thanks for any recommendations in advance!!

I am sure I read somewhere that serial print can conflict with other interrupts on certain pins.

Not sure if that’s a issue on a mega, but shouldn’t be too hard to move everything around and see if that clears it up…

How are you timing the 2 ms window?
What baud rate are you using for the Serial print?

Serial uses interrupts by itself.
If printing from inside an ISR (can't see how you do this as it's just meaningless snippets) this can give all kinds of unwanted side effects.
Printing the same line three times over won't help either.

Slumpert:
I am sure I read somewhere that serial print can conflict with other interrupts on certain pins.

Not sure if that’s a issue on a mega, but shouldn’t be too hard to move everything around and see if that clears it up..

Thanks for your comment! I will definitely look into this. Much appreciated. I will later post my code as well.... the full code... for completeness. I definitely want to see what's going on here. Thanks again!

cattledog:
How are you timing the 2 ms window?
What baud rate are you using for the Serial print?

Hi cattledog! I'll be posting my code. The 2 ms timing is via a if statement in the short main loop. The micros() - reftime >= 2000 method. I have an interrupt routine that keeps counting the pulses. Then.. once the 2ms duration comes up.... the number of counts is then transferred to 'levdiff'. It's just the number of counts in a 2ms window. The transfer of the counts to 'levdiff' is done when interrupts are disabled. Interrupts are re-enabled straight afterward. I'll post my code soon... as I'm posting from my cell/mobile phone at the moment.

I'm using 115200 baud rate for serial.

Thanks again cattledog.

I suspect a more effective system would be for your ISR to count the pulses and record the value of micros() after a certain number of pulses - perhaps the number for a single revolution.

But you need to post the complete program.

I have an optical detector that produces one pulse per revolution and it works with this program

const byte fwdPin = 9;
const byte revPin = 10;
const byte potPin = A1;

int potVal;
int pwmVal;

unsigned long revMicros;
unsigned long prevRevMicros;
unsigned long revDuration;
unsigned long revCount;

unsigned long prevDisplayMillis;
unsigned long  displayInterval = 1000;

    // variables for the ISR
volatile unsigned long isrMicros;
volatile unsigned long isrCount;
volatile bool newIsrMicros = false;



void setup() {
    Serial.begin(115200);
    Serial.println("SimpleISRdemo.ino");
    pinMode (fwdPin, OUTPUT);
    pinMode (revPin, OUTPUT);

    isrCount = 0;
    attachInterrupt(0, revDetectorISR, RISING);
}

//==========

void loop() {
    getIsrData();
    if (millis() - prevDisplayMillis >= displayInterval) {
        prevDisplayMillis += displayInterval;
        showData();
        readPot();
        updateMotorSpeed();
    }
}

//===========

 void readPot() {
    potVal = analogRead(potPin);
}

//===========

void updateMotorSpeed() {
    pwmVal = potVal >> 2;

    digitalWrite(revPin,LOW);
    analogWrite(fwdPin, pwmVal);
 }

//===========

void getIsrData() {
    if (newIsrMicros == true) {
        prevRevMicros = revMicros; // save the previous value
        noInterrupts();
            revMicros = isrMicros;
            revCount = isrCount;
            newIsrMicros = false;
        interrupts();
        revDuration = revMicros - prevRevMicros;
    }
}

//===========

void showData() {
    Serial.println();
    Serial.println("===============");
    Serial.print("PWM Val "); Serial.println(pwmVal);
    Serial.print("  Rev Duration ");
    Serial.print(revDuration);
    Serial.print("  Rev Count ");
    Serial.print(revCount);
    Serial.println();
}

//===========

void revDetectorISR() {
    isrMicros = micros();
    isrCount ++;
    newIsrMicros = true;

}

...R

Southpark:
I'll be posting my code.

I look forward to seeing the code so I can see where it is going wrong. It looks like the timing and counting are dubious. And what is the value of "T"?

    if ( micros() - r_time >= 10000000 ) 
    {
      noInterrupts();
      r_time = micros();
      int localLevdiff = levdiff;
      levdiff = 0;
      interrupts();
      Serial.print("rpm is ");
      float pulsesPerMinute = localLevdiff * 6.0;  // Pulses per 10 seconds * 6
      Serial.println(pulsesPerMinute / (float) cpr);   // Revolutions per Minute

      Serial.print("pwm return = ");
      Serial.print(pwm_equiv);
      Serial.print("  error pwm = ");
      Serial.println(error_pwm);      
    }

Thanks John! I was in the middle of pasting the code, and noticed the issue. The issue (which I had created myself) turned out to be a mistake in the data type. For "error_pwm", I defined it as an 'int', but it should have correctly been assigned as a 'float'.

Robin2 ---- thanks for posting your code too. I will definitely make use of that! Greatly appreciated!

Yo, are you like, building up anticipation for your code? Too much and it'll be anticlimactic.

Southpark:
I was in the middle of pasting the code, and noticed the issue. The issue (which I had created myself) turned out to be a mistake in the data type.

That is why we ask to see a complete sketch. It is very hard to spot errors in the parts of a sketch that are not shown.

johnwasser:
That is why we ask to see a complete sketch. It is very hard to spot errors in the parts of a sketch that are not shown.

That's right. I was about to paste the code, and noticed while looking at the code. The code below is what I was using. I had error_pwm assigned to 'int', but should have been 'float'.

float grad = 17.07;  //measured slope
float y_int = -353; //measured y-intercept
float k = 34; //this is the adjustable value of 'k'


int pwmPin = 4;      // mega2560 pins 4 and 13 ... 976.56Hz pwm frequency. Other pins... 490.20 Hz.
int digitalPinA = 3;   // pin 3

int val = 128;         // variable to store the read value
int flag = 0;


unsigned long T = 2000; //sampling period in microseconds --eg. 20000 microseconds is 20 millisecond, or 0.02 second. 

unsigned long ref_time = 0;

unsigned long r_time = 0;

int C1 = 3; //hall out A

int ppr = 1024; //number of pulses per revolution of rotary encoder PER CHANNEL.

volatile int pulsecount = 0;
volatile int gencount = 0;
volatile int gencount_ref = 0;
volatile int dirA = 0; //right motor direction

float velA_rpm = 0;
float vel_reduced = 0;

int levdiff = 0;

int counter = 0;

volatile byte startflag = 0;

int setpoint_pwm = 0;


float error_rpm = 0;
float accum_error_rpm = 0;
float accum_error_pwm = 0;
float error_pwm = 0;
float error_rpm_prev = 0;
float error_pwm_prev = 0;

float rpm_meas = 0;
float pwm_equiv = 0;

void setup() {
  // put your setup code here, to run once:
  pinMode(pwmPin, OUTPUT);   // sets the pin as output
  analogWrite(pwmPin, 0);  //level 0 out of 255

  pinMode(digitalPinA, INPUT_PULLUP);  //using internal pullup. Will choose to go for 'falling edge' interrupts, so no need to add delay and no need to clear interrupt pending flag, since the pullup can only cause a low-to-high transition, and won't cause a falling edge interrupt.

  Serial.begin(115200); //show this if serial data is to be displayed in the serial monitor window
  delay(10);

  levdiff = 0;

  analogWrite(pwmPin, 70);  //set initial non-zero speed (simply to give the system a nudge to get around the dead-band area).
  delay(1000);
  analogWrite(pwmPin, 70);  //set initial non-zero speed (simply to give the system a nudge to get around the dead-band area).
  delay(5000);


  setpoint_pwm = 107;
  //setpoint_pwm = setpoint_pwm1;
  
  Serial.print("setpoint_pwm= "); Serial.println(setpoint_pwm);
  Serial.print("sampling period (us) = ");Serial.println(T);
  Serial.println("counts  RPM  time_index");

  attachInterrupt(1, State_A, CHANGE);  // interrupt number 1 translates to pin 3 on the MEGA2560

  noInterrupts();
  gencount = 0;
  gencount_ref = gencount;
  interrupts();

  analogWrite(pwmPin, setpoint_pwm);  

  ref_time = micros();   // initialise reference time
  r_time = micros();
}


void loop() {

     if ( micros() - ref_time  >= 2000 ) {
        noInterrupts();
        levdiff = gencount - gencount_ref;
        gencount_ref = 0;
        gencount = 0;
        interrupts();
        ref_time = micros();
     }


     rpm_meas = ((levdiff / (float) cpr) / (T * 1e-6)) * 60.0;
     pwm_equiv = (rpm_meas - y_int)/grad;
     error_pwm = setpoint_pwm - pwm_equiv;

     if ( micros() - r_time >= 10000000 ) {
      r_time = micros();
       Serial.print("rpm is ");  Serial.print( ((levdiff / (float) cpr) / (T * 1e-6)) * 60.0 ); Serial.print("  pwm_level is "); Serial.println( cpr );
       Serial.print("pwm return = ");Serial.print(pwm_equiv);Serial.print("  error pwm = ");Serial.println(error_pwm);      
     }

      
} //loop


void State_A() 
{
  gencount++;
  if (startflag == 0) {
    startflag = 1;
  }
}

I'm still in the middle of testing and debugging an extension of the code for the closed loop system, where I'm using the arduino to digitally implement an integrator function, and also for adjusting the open loop gain. Not working perfectly at the moment. The feedback system (controlled by the arduino) is getting the correct value of fed-back PWM level, but the steady-state RPM readings are significantly higher than what I'm expecting to see. Eg. RPM readings end up at around 1800 RPM, while I'm expecting to see 1500 RPM, even though the steady-state PWM level being applied (eg pwm level of 107 out of 255) using analogWrite is at the correct expected level. Still slowly trying to trace my mistake(s). But this all relates to extended code.

The extended code seems to be nearly there....but falling short due to wrong RPM reading for feedback mode. The open loop mode is working nicely, where I apply a particular rpm level (eg. 107) using analogWrite, and the motor will spin up to approximately 1500 rpm (for a 12V fixed supply) repeatabily. I measured output rpm and input pwm level relations for the motor, so applying the appropriate pwm level leads to the expected associated output RPM level. But that's for open-loop.

For closed loop (feedback control), the automatically determined input PWM level correctly approaches the setpoint value (eg. 107), but the "measured" RPM is 'too high'. The 'measurement' could be wrong - due to something I'm doing wrong. A good thing is, the output RPM overshoot is always 2 times higher than what I designed for. The plan is to just change the overshoot or damping ratio by using the arduino to change an open-loop multiplying factor (linear gain factor). At the moment, it's consistently turning out that if I design for 5% overshoot, the plotted RPM versus time results shows 10% overshoot. If I design for 8% overshoot, the plots show 16%. Always doubled the output. That's why I'm going back to square 1, to make sure that my rpm measurements aren't getting messed up due to my own mistakes, or something else, or both. It looks like my own mistakes! (as usual).

For the moment, I usually do the feedback response measurements (eg. step response) with running start. So the motor is usually spin up in feedback mode before stepping up to some other level.....just to avoid dead-zone. If I get stuck and can't trace the source of the issue after putting enough effort into it, I'll definitely ask for help on this one!

Referring to the code in Reply #11 ...

IMHO this piece

    if ( micros() - ref_time  >= 2000 ) {
        noInterrupts();
        levdiff = gencount - gencount_ref;
        gencount_ref = 0;
        gencount = 0;
        interrupts();
        ref_time = micros();
     }

would be better like this as the interrups are off for a shorter period and the time period will follow more accurately

   if ( micros() - ref_time  >= 2000 ) {
        noInterrupts();
            latestGenCount = gencount;  // make a copy of the value
            gencount = 0;
        interrupts();
        ref_time += 2000;
        levdiff = latestGenCount - gencount_ref; // use the copy
        gencount_ref = 0;
    }

...R

For the time: I would not count on it being exactly 2000 µs, instead calculate the interval. That way it doesn't matter if you're say 100 µs late due to some other process that took a bit longer, as you know the number of ticks is for 2,100 µs instead of 2,000 µs.

   if ( micros() - ref_time  >= 2000 ) {
        noInterrupts();
            latestGenCount = gencount;  // make a copy of the value
            gencount = 0;
        interrupts();
        timePassed = micros() - ref_time;  // timePassed could be a bit more than 2000 - use that number to later calculate the rpm.
        ref_time = micros();
    }

wvmarle:
For the time: I would not count on it being exactly 2000 µs, instead calculate the interval.

I am inclined to agree.

In Reply #6 I suggested measuring the time for a given number of pulses rather than counting the number of pulses within an interval of time. That remains my preferred approach.

…R

Robin2 … thanks very much for your suggestion regarding minimising the de-activation of the interrupt handling routine(s). Really nice suggestion.

Also, I’ll be trying the other velocity estimation method — the one you suggested too. I was using the ‘number of counts per 2 millisec’ approach because I was initially going down the path of getting estimates of instantaneous (or close to instantaneous) RPM values every 2 millisecond. Sort of a sampling system.

But now realise that counting number of pulses per 2 millisec when a shaft is accelerating or decelerating probably won’t provide a decent value of ‘instantaneous’ velocity. That’s because if a rotating shaft is speeding up or slowing down, then counting the number of pulses over 2 millisec is probably not going to give me accurate values of instantaneous velocity. I haven’t looked into this or thought about this properly yet.

But…assuming the motor is already spinning (ie. not zero speed or very slow speed), then the method you suggest — ie. time difference between two consecutive pulses… or time difference between a number of pulses… should be much better than what I had been doing. I will try that one out. Thanks again.

wvmarle:
For the time: I would not count on it being exactly 2000 µs, instead calculate the interval. That way it doesn't matter if you're say 100 µs late due to some other process that took a bit longer, as you know the number of ticks is for 2,100 µs instead of 2,000 µs.

Thanks very much for your help and comments as well wv. I'll take this excellent advice and use it too.
For a small motor that gets up to about 4000 RPM only, I've been working toward measuring the high-speed shaft RPM, and using the 2048 pulse per rev encoder to measure the RPM in order to control the rpm step response (overshoot mainly) for a closed loop case....just using integral control. At the moment, I just treat the motor as a first order system. My first step is to try get reliable instantaneous rpm measurements. The 'n pulse per 2 millisec' method I was using is probably not the right way to go, although, the integral gain that I set turns out to give an overshoot of exactly two-times what I intended. So the nice thing is that there's repeatable predictability. So now I'm just working towards fixing up my wrong things.

I'm guessing that one thing that isn't helping me (for close loop) is the analogWrite PWM is in steps of '1', so I can't really do something like analogWrite a value of 100.43 etc.

I'm currently using pin 4 on the MEGA for analogWrite --- for outputting pwm to the motor. So analogWrite is what I'm using right now. I might turn toward using lower level timer code for generating PWM instead (if needed) of using analogWrite.

In any case, I'll definitely use the method you mentioned for better timing! Thanks very much!

Southpark:
I've been working toward measuring the high-speed shaft RPM, and using the 2048 pulse per rev encoder to measure the RPM in order to control the rpm step response (overshoot mainly) for a closed loop case.

I have some small DC motors with rev rates between about 1500 and 15000 RPM. The detector produces 1 pulse per revolution and the Arduino measures the time for each revolution. The Arduino comfortably holds the speed constant +/- 1%

The business of reading an encoder is hard work for an Arduino so I suggest you keep the PPR as low as possible consistent with meeting your project requirement.

...R

Southpark:
For a small motor that gets up to about 4000 RPM only, I've been working toward measuring the high-speed shaft RPM, and using the 2048 pulse per rev encoder

That would be a pulse rate of over 8 MHz! The absolute maximum you can count pulses with a regular Arduino is 8 MHz (half the clock frequency)... counting the pulses at that rate is not hard work for an Arduino (use the T1 pin and TCNT1 set to external clock); doing something useful with that data is another matter. 1-2 pulses per rotation should be enough.