Using IR sensor and arduino to count RPM

Hello all,

I am attempting to write code to program an Arduino to calculate the RPM of a rotating disk. The issue I am having with my code is that I cannot get an accurate RPM value. Below is my code which I have edited and compiled from different sources. This is my first time programming an Arduino so sorry if I am not able to explain further. Any help is appreciated.

#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27,16,2);

    volatile int rev;
    unsigned int rpm;
    unsigned long oldtime;


void isr() //interrupt service routine
{
  rev++;
}

void setup()
{
   lcd.init();                    //initialize lcd
   lcd.backlight();
   attachInterrupt(0,isr,RISING); //attaching the interrupt

   lcd.setCursor(3,0);
   lcd.print("Tachometer");
   delay(1000);

   rev = 0;
   rpm = 0;
   oldtime = 0;

}

void loop()
{
   delay(1000);                             //update rpm every second
   detachInterrupt(0);                      //detaches the interrupt
   rpm = (rev/(millis()-oldtime))*60000;    //calculates rpm for blades
   oldtime = millis();                      //saves the current time
   rev=0;                                   //resets the counter
   
   
   
   lcd.clear();
   lcd.setCursor(3,0);
   lcd.print("TACHOMETER");
   lcd.setCursor(0,1);
   lcd.print("Speed: ");
   lcd.setCursor(6,1);
   lcd.print(rpm);
   lcd.setCursor(13,1);
   lcd.print("RPM");
   lcd.print("  ");
   
   attachInterrupt(0,isr,RISING);
}

Remove detachInterrupt/attachInterrupt from your loop(). Insert a cli(); line before the "rpm = ..." line and a sei(); line after that line which sets rev to 0.

If that doesn't work, post details (wiring diagram, pictures, etc.) of your hardware setup.

I've approached this with a multiplier rather than a bunch of math in the loop.
Heres my example code I refer to when I forget :slight_smile:

#define TacometerPin 2 // Must be pin 2 or 3

// My Encoder has 400 Clock pulses per revolution
// note that 150000.0 = (60 seonds * 1000000 microseconds)microseconds in a minute / 400 pulses in 1 revolution)
#define Multiplier 150000.0 // don't forget a decimal place to make this number a floating point number

volatile uint32_t _deltaTime; // Delt in time
volatile uint32_t _lastTime; // Saved Last Time of Last Pulse
volatile uint32_t _Time;

void CaputreDeltaT() {
  _deltaTime = (_Time = micros()) - _lastTime; 
  _lastTime = _Time;
}
void setup() {
  Serial.begin(115200); //115200
  pinMode(TacometerPin, INPUT);

  attachInterrupt(digitalPinToInterrupt(TacometerPin), CaputreDeltaT, RISING);
}

void loop() {
  float DeltaTime;
  float SpeedInRPM = 0;
  // Serial print is slow so only use it when you need to (10 times a second)
  static unsigned long SpamTimer;
  if ( (unsigned long)(millis() - SpamTimer) >= (100)) {
    SpamTimer = millis();
    noInterrupts ();
    // Because when the interrupt occurs the EncoderCounter and SpeedInRPM could be interrupted while they
    // are being used we need to command a "hold" for a split second while we copy these values down. This doesn't keep the
    // interrupt from occurring it just slightly delays it while we copy values.
    // if we don't do this we could be interrupted in the middle of copying a value and the result get a corrupted value.
    DeltaTime = _deltaTime;
    _deltaTime = 0; // if no pulses occure in the next 100 miliseconds then we must assume that the motor has stopped this allows a speed of zero to occure
    interrupts ();
    SpeedInRPM = Multiplier / DeltaTime; // Calculate the RPM 
    Serial.print(SpeedInRPM , 3);
    Serial.print(" RPM");
    Serial.println();
  }
}

Z

pylon:
Remove detachInterrupt/attachInterrupt from your loop(). Insert a cli(); line before the “rpm = …” line and a sei(); line after that line which sets rev to 0.

If that doesn’t work, post details (wiring diagram, pictures, etc.) of your hardware setup.

I attempted your edits and now my IR sensor is not reading anything. I know my wiring is correct since I do get values when using the original code.

#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27,16,2);

    volatile int rev;
    unsigned int rpm;
    unsigned long oldtime;


void isr() //interrupt service routine
{
  rev++;
}

void setup()
{
   lcd.init();                    //initialize lcd
   lcd.backlight();
   attachInterrupt(0,isr,RISING); //attaching the interrupt

   lcd.setCursor(3,0);
   lcd.print("Tachometer");
   delay(1000);

   rev = 0;
   rpm = 0;
   oldtime = 0;

}

void loop()
{
   delay(1000);                             //update rpm every second
   cli();                                   //detaches the interrupt
   rpm = (rev/(millis()-oldtime))*60000;    //calculates rpm for blades
   oldtime = millis();                      //saves the current time
   rev=0;                                   //resets the counter
   sei();
   
   
   lcd.clear();
   lcd.setCursor(3,0);
   lcd.print("TACHOMETER");
   lcd.setCursor(0,1);
   lcd.print("Speed: ");
   lcd.setCursor(6,1);
   lcd.print(rpm);
   lcd.setCursor(13,1);
   lcd.print("RPM");
   lcd.print("  ");
   

}
   rpm = (rev/(millis()-oldtime))*60000;    //calculates rpm for blades

As millis()-oldtime is a bit more than 1000 you must have a lot of interrupt calls to have rpm greater than 0. Remember this is integer arithmetic! So you should change that line to

   rpm = rev * 60000 / (millis()-oldtime);    //calculates rpm for blades

and don't use ints but longs for the variables.

That way you avoid an information lost by the first division that cannot be compensated by the later multiplication.

I know my wiring is correct since I do get values when using the original code.

Did I write your wiring is wrong? We simply don't know it and the code should match the hardware.

BTW, I have some doubts that your original code did give you correct rpms.

Thank you for your suggestions. I was just stating the wiring appears to be correct to take that out of the pool of problems. I have changed my equation but still doesn’t give me the correct RPM value. Do I need to keep the attachInterrupt in the void setup?

#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27,16,2);

    volatile long rev;
    unsigned long rpm;
    unsigned long oldtime;


void isr() //interrupt service routine
{
  rev++;
}

void setup()
{
   lcd.init();                    //initialize lcd
   lcd.backlight();
   attachInterrupt(0,isr,RISING); //attaching the interrupt

   lcd.setCursor(3,0);
   lcd.print("Tachometer");
   delay(1000);

   rev = 0;
   rpm = 0;
   oldtime = 0;

}

void loop()
{
   delay(1000);                             //update rpm every second
   cli();                                   //detaches the interrupt
   rpm = rev * 60000 / (millis()-oldtime);    //calculates rpm for blades
   oldtime = millis();                      //saves the current time
   rev=0;                                   //resets the counter
   sei();
   
   
   lcd.clear();
   lcd.setCursor(3,0);
   lcd.print("TACHOMETER");
   lcd.setCursor(0,1);
   lcd.print("Speed: ");
   lcd.setCursor(6,1);
   lcd.print(rpm);
   lcd.setCursor(13,1);
   lcd.print("RPM");
   lcd.print("  ");
   

}

Noting your comment in the code: cli() or noInterrupts() doesn't detach the interrupt, it disables all interrupts (that includes the timer, pin change, ADC, UART, etc). sei() or interrupts() enables interrupts again.

Instead of calculating the rpm, start by printing rev, the raw count. See whether that number is correct to begin with. Then you know whether or not it's the calculation that's causing problems.

Another thing, as rev should never become negative, it's best to declare it unsigned (shouldn't be the cause of your problem, and up to just over 2 bln counts it doesn't matter really, it's mostly just habit: it's a count, can never be negative, so unsigned):

volatile unsigned long rev;

By the way, that you get some number doesn't necessarily mean your wiring is correct. You may still have a floating input, or a poorly connected wire. Both situations you're bound to get readings, but not what you expect. So in any case it's a good idea to post your project's complete schematic, links to the actual sensor you use, and maybe even some photos of the setup.

How many blades are on your IR sensor for 1 revolution?
your code implies 1.

Z

I am using a piece of reflective tape on one of the black fan blades. Sorry I havnt replied recently this project got put on the back burner.

Right. A few more questions.

  1. are you sure that this reflective tape is reflective not only for visible light but also for IR, and that it reflects the IR back to the sensor?
  2. are you sure the black fan blades themselves are not reflective to IR?
  3. are you sure there are no ambient sources of IR that may trigger your sensor?

Attached is a document of my wiring and set up. The led lights up when it passes over the tape and goes dark once the tape has gone by. This is my whole set up I wont be using this fan I’m just trying to get the code to read RPM accurately before I move to the next step. Also to reply to zhomeslice I used one because I only put tape on 1 fan blade. I believe that is correct way to do it please advice if I am incorrect thanks!

Tach pics.doc (1000 KB)

Likayuu:
I am using a piece of reflective tape on one of the black fan blades. Sorry I havnt replied recently this project got put on the back burner.

Try this code out:

#define TacometerPin 2 // Must be pin 2 or 3

// My Encoder has 400 Clock pulses per revolution
// note that 150000.0 = (60 seonds * 1000000 microseconds)microseconds in a minute / 400 pulses in 1 revolution)
// note that 60000000.0 = (60 seonds * 1000000 microseconds)microseconds in a minute / 1 pulses in 1 revolution)
#define Multiplier 60000000.0 // don't forget a decimal place to make this number a floating point number

volatile uint32_t _deltaTime; // Delt in time
volatile uint32_t _lastTime; // Saved Last Time of Last Pulse
volatile uint32_t _Time;

void CaputreDeltaT() {
  _deltaTime = (_Time = micros()) - _lastTime; 
  _lastTime = _Time;
}
void setup() {
  Serial.begin(115200); //115200
  pinMode(TacometerPin, INPUT);

  attachInterrupt(digitalPinToInterrupt(TacometerPin), CaputreDeltaT, RISING);
}

void loop() {
  float DeltaTime;
  float SpeedInRPM = 0;
  // Serial print is slow so only use it when you need to (10 times a second)
  static unsigned long SpamTimer;
  if ( (unsigned long)(millis() - SpamTimer) >= (100)) {
    SpamTimer = millis();
    noInterrupts ();
    // Because when the interrupt occurs the EncoderCounter and SpeedInRPM could be interrupted while they
    // are being used we need to command a "hold" for a split second while we copy these values down. This doesn't keep the
    // interrupt from occurring it just slightly delays it while we copy values.
    // if we don't do this we could be interrupted in the middle of copying a value and the result get a corrupted value.
    DeltaTime = _deltaTime;
    _deltaTime = 0; // if no pulses occure in the next 100 miliseconds then we must assume that the motor has stopped this allows a speed of zero to occure
    interrupts ();
    SpeedInRPM = (DeltaTime) ? Multiplier / DeltaTime: 0; // Calculate the RPM
    Serial.print(SpeedInRPM , 3);
    Serial.print(" RPM");
    Serial.println();
  }
}

Z

the Arduino pulseIn() function works a treat for determining rpm

pulseIn() detects the transition in the sensor pin, measures the duration of the new state and returns that duration

this simple code should give the rpm;

pulseIn(pin, High); //wait for transition to HI & measure pulse duration
markOne = millis(); //pulse over, save ms count
pulseIn(pin, High); //repeat
markTwo = millis(); /get ms count when second pulse is over

deltaT = (markTwo - markOne)/1000; //time for one rev in sec
rps = 1/deltaT //revs per second
rpm = rps*60 //revs per minute

no need to mess with the intricacies of interrupts, unless you have a particular reason to use them

john.

That is blocking code which blocks for at least two rotations - and for some reason you decide to not use the microsecond pulse duration the pulseIn() function returns... instead going for the lower resolution (probably too low for this application) millisecond counter.
Replacement for this suggested code is to me a good enough reason to mess with the intricacies of interrupts, which is not that hard, really, as long as you actually understand what you're working with.

I'm using this approach with a speedometer on my car and it works a treat from zero to 110 kph.

The application doesn't have any other tasks to do so blocking for two revs doesn't matter -- there's nothing being blocked.

With my speedometer, I do indeed use the pulse duration returned by the pulseIn() function. Multiplying this duration by the correct factor gives the speed of the car. Surprisingly simple. Surprisingly reliable.

The OP isn't aiming to get the speed of the fan, so the pulse duration isn't helpful, is it? It's the time between two successive pulses that is required.

The fan is a snail compared to the Arduino, don't you think? Surely millis() is good enuf? We don't know the accuracy specs the OP is working to.

John.

Snail compared to Arduino is not necessarily snail compared to millisecond times. A computer fan may do 4,000 rpm, this is almost 67 Hz, or 15 ms per rotation - definitely calling for microsecond resolution. Your car's speedometer may produce pulses at a much slower rate, where milliseconds have sufficient resolution.

To measure a full revolution you need falling edge to falling edge, while pulseIn() measures rising to falling (high pulse) or falling to rising (low pulse).

HillmanImp:
the Arduino pulseIn() function works a treat for determining rpm

pulseIn() detects the transition in the sensor pin, measures the duration of the new state and returns that duration

this simple code should give the rpm;

pulseIn(pin, High); //wait for transition to HI & measure pulse duration
markOne = millis(); //pulse over, save ms count
pulseIn(pin, High); //repeat
markTwo = millis(); /get ms count when second pulse is over

deltaT = (markTwo - markOne)/1000; //time for one rev in sec
rps = 1/deltaT //revs per second
rpm = rps*60 //revs per minute

no need to mess with the intricacies of interrupts, unless you have a particular reason to use them

john.

Why use pulsein() it blocks all other code
use a multiplier and Delta T

#define Multiplier 60000000.0 // don't forget a decimal place to make this number a floating-point number

60 seconds in a minute * 1000000 microseconds in a second equals 60000000 microseconds in a minute
Divide that by the number of pulses per revolution with your example OP you have 1 pulse in 1 revolution

Capture Delta T

void CaputreDeltaT() {
 _deltaTime = (_Time = micros()) - _lastTime;
 _lastTime = _Time;
}

Using interrupts

 attachInterrupt(digitalPinToInterrupt(TacometerPin), CaputreDeltaT, RISING);

In the loop properly get the Delta T and Calculate the RPM

   noInterrupts ();
   DeltaTime = _deltaTime;
   _deltaTime = 0; // if no pulses occure this allows a speed of zero 
   interrupts ();
   SpeedInRPM = (DeltaTime) ? Multiplier / DeltaTime: 0; // Calculate the RPM

This is a fast and simple way to capture RPM without blocking you can delay as long as you want between samples so your other code can do what needs to be done. the latest reading will be waiting for your use.

Z

zhomeslice:

   noInterrupts ();

DeltaTime = _deltaTime;
  _deltaTime = 0; // if no pulses occure this allows a speed of zero
  interrupts ();
  SpeedInRPM = Multiplier / DeltaTime; // Calculate the RPM

Don't forget to take the possibility of a zero DeltaTime into account... or you can get a division by zero crash.

You can also achieve accurate measurement of interval timings using the input capture register. See the section “Timing an interval using the input capture unit” in https://www.gammon.com.au/timers for an example. You simply capture the time interval between say the rising edges of two pulses and, from that, calculate the rotational speed. The input capture method has an even better resolution than microseconds. The resolution is processor clock ticks which, for example at 16MHz is units of 62.5 nanoseconds.

wvmarle:
Don't forget to take the possibility of a zero DeltaTime into account... or you can get a division by zero crash.

yes, I forgot about that Easy Fix.

SpeedInRPM = (DeltaTime) ? Multiplier / DeltaTime: 0; // Calculate the RPM

Thanks for the Catch
z