Count RPM on motor

I am a hobbyist that is trying to control a relay based on a motor’s RPMs. I am using a Hall-Effect Sensor connected to pin 2. My sketch works but only if I keep the sample time a complete minute. I was hoping to reduce this sample time to 15 seconds. When I do this and multiply the results by 4, I get results in multiples of 4 which is unacceptable.

I am hoping someone can suggest a better way. I saw some discussion on measuring interrupts but that is beyond my skills.

const int hallSensorPin = 2;                      // connect the hall effect sensor on pin 2
const int relay = 4;                              // connect relay to pin 4
const unsigned long sampleTime = 60000;           // one minute of sample time 
int num = 0;
int totRPM = 0;

void setup() 
{
  pinMode(hallSensorPin,INPUT);
  pinMode(relay, OUTPUT);
  digitalWrite(relay, LOW);
  Serial.begin(9600);

  delay(1000);

}
         
void loop() 
{
  delay(10);
  int rpm = getRPM();
   
}
int getRPM()
{
  int count = 0;
  boolean countFlag = LOW;
  unsigned long currentTime = 0;
  unsigned long startTime = millis();
  while (currentTime <= sampleTime)
  {
    if (digitalRead(hallSensorPin) == HIGH){
      countFlag = HIGH;
    }
    if (digitalRead(hallSensorPin) == LOW && countFlag == HIGH){
      count++;
      countFlag=LOW;
    }
    currentTime = millis() - startTime;
  }
  int countRpm = 1 * count; // used a multiplier when changing the sample time
 Serial.println(countRpm);  
  if (countRpm > 168) {
   digitalWrite (relay, HIGH);
 }
  if (countRpm <= 163){
   digitalWrite (relay, LOW);
 }  
 
   return countRpm;
   
}

How about measuring the time taken for each revolution and deriving the RPM from that ? No need for a timing period and you could calculate an average over a number of revolutions to smooth the data.

You could use an interrupt but if all the Arduino is doing then it is not necessary.

Hi

Why not measure the length of time for one revolution and divide that into one minute?

e.g. if 1 revolution takes 1 second then 60/1 = 60rpm.

You could do this using millis(). See this tutorial on how to use millis() for timing.

You want to count from the time the hallSensorPin first goes from low to high to the next time it goes from low to high.

Just remember there are 60000 milliseconds in a minute.

Ian

Thank you both for your suggestions.

This is what I have come up with. The only bad part is the RPM is not quite accurate. For instance when true RPM is 300, I read 330 and when true RPM is 148, I read 168.

[byte sensor =2;
unsigned long startTime;
unsigned long endTime;
unsigned long duration;
byte timerRunning;
int RPM = 0;
int count = 0;
int totRPM = 0;
int avgRPM = 0;

void setup(){
pinMode (sensor, INPUT_PULLUP);
//digitalWrite (relay, LOW);
Serial.begin(9600);
}
void loop(){
  if (timerRunning == 0 && digitalRead(sensor) == LOW){ 
  startTime = millis();
  timerRunning = 1;
  }
  if (timerRunning == 1 && digitalRead(sensor) == HIGH){ 
  endTime = millis();
  timerRunning = 0;
  duration = endTime - startTime;
  RPM = 60000/duration;
  count = count + 1;
  totRPM = RPM + totRPM;
  avgRPM = totRPM/count;
  if (count > 60){
    totRPM = avgRPM;
    count = 1;
  }
  Serial.print ("avgRPM ");
  Serial.println (RPM);
  }
}/code]

How do you determine "true" RPM?

I have a second rpm monitor which I purchased that I was using as a control. It never occurred to me that it could be inaccurate. I just assumed some of the math in my code was wrong.

I thought that you would be capturing the value of millis() each time the magnet passes the sensor but from your code that is not what you seem to be doing.

It appears that the sensor pin goes LOW when the magnet passes the sensor and at that point you save the value of millis() and flag that the timer is running. Very soon afterwards the pin will go HIGH and you capture the value of millis() again and do calculations on the time between the two millis() values.

Is that what you are doing ? If so then you are basing the calculations on the time that it takes the magnet to pass the sensor, not on the time it takes to do one revolution of the wheel.

Rats. I thought I had it.

I understand I need to measure from starttime to startime. But making both start time and end time when the sensor reads HIGH or LOW isn't working.

How can I measure to read (duration = HIGH + LOW + HIGH)?

You are looking to time from one state change to the next one. If the transition for the magnet coming in front of the sensor is HIGH to LOW, then the logic looks like this EDIT: reverse HIGH/LOW for change to be consistent with code

lastReading = reading;
reading = digitalRead(sensor);
if (reading == LOW && lastReading == HIGH)//pick up appropriate state change
{ 
  stateChangeTime = millis();
  period = stateChangeTime - previousStateChangeTime;
  previousStateChangeTime = stateChangeTime;
}

This code is derived from a program I use for measuring the speed of a small DC motor. It measures the time for each revolution in microsecs and works fine up to 15,000 RPM, and probably much higher.

I use an optical detector that produces 1 pulse per revolution but the code should work with any sensor that produces pulses.

volatile unsigned long isrMicros;
unsigned long latestIsrMicros;
unsigned long previousIsrMicros;
volatile boolean newISR = false;


void loop() {
   if (newISR == true) {
     previousIsrMicros = latestIsrMicros; // save the old value
     noInterrupts(); // pause interrupts while we get the new value
        latestIsrMicros = isrMicros;
        newISR = false;
     interrupts();
     microsThisRev = latestIsrMicros - previousIsrMicos;
}

void myISR() {
   isrMicros = micros();
   newISR = true;
}

...R

Something like

previous start time = millis()

start of loop()
  if the magnet becomes detected
    save millis() as new start time
    calculate time for 1 revolution (difference between new start time and previous start time) and derive RPM
    copy new start time to previous start time
  end if
end of loop()

I understand what you are saying. I am having trouble coding it correctly.

Here is what I have:

#include <LiquidCrystal.h>
LiquidCrystal lcd(13,12,11,10,9,8);
byte sensor =2;
unsigned long startTime;
unsigned long endTime;
unsigned long PstartTime;
unsigned long duration;
byte timerRunning;
int RPM = 0;
int count = 0;
int totRPM = 0;
int avgRPM = 0;
const int relay = 4;

unsigned long lastReading;
unsigned long reading;
unsigned long stateChangeTime;

void setup(){
pinMode (sensor, INPUT_PULLUP);
pinMode (relay, OUTPUT);
digitalWrite (relay, LOW);
Serial.begin(9600);
  lcd.begin(16, 2);
  lcd.print("Initializing");
  delay(1000);
  lcd.clear();
  

}
void loop(){

   
  
  if (timerRunning == 0 && digitalRead(sensor) == LOW){ 
  startTime = millis();
  timerRunning = 1;
  PstartTime = startTime;
  
  }
  
  if (timerRunning == 1 && digitalRead(sensor) == LOW){ 
  startTime = millis();
  timerRunning = 0;
  duration = Pstartime - startTime;
  RPM = 60000/duration;
  count = count + 1;
  totRPM = RPM + totRPM;
  avgRPM = totRPM/count;
  if (count > 60){
    totRPM = avgRPM;
    count = 1;
  } 
    if (avgRPM > 181){
   digitalWrite (relay, HIGH);
  }
  if (avgRPM <= 178){
   digitalWrite (relay, LOW);
 }  
   
  Serial.print ("avgRPM ");
  Serial.println (RPM);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(avgRPM);
  lcd.setCursor(5,0);
  lcd.print("RPM");
  }
}

I understand what you are saying. I am having trouble coding it correctly.

As a start, have a look at the StateChangeDetection example in the IDE to see how to detect when an input [u]becomes[/u] HIGH or LOW rather than when it [u]is[/u] HIGH or LOW

Try this. I put the display on a millis() timer, because the lcd is slow, and I thought it was getting in the way of the hall sensor readings.

#include <LiquidCrystal.h>
LiquidCrystal lcd(13, 12, 11, 10, 9, 8);
byte sensor = 2;
//unsigned long startTime;
//unsigned long endTime;
//unsigned long PstartTime;
//unsigned long duration;
//byte timerRunning;
int RPM = 0;
int count = 0;
int totRPM = 0;
int avgRPM = 0;
const int relay = 4;

//unsigned long lastReading;
byte lastReading;
//unsigned long reading;
byte reading;
unsigned long stateChangeTime;
unsigned long previousStateChangeTime;
unsigned long duration;
unsigned long lastDisplay;

void setup() {
  pinMode (sensor, INPUT_PULLUP);
  pinMode (relay, OUTPUT);
  digitalWrite (relay, LOW);
  Serial.begin(115200);
  lcd.begin(16, 2);
  lcd.print("Initializing");
  delay(1000);
  lcd.clear();
}
void loop() {
  lastReading = reading;
  reading = digitalRead(sensor);
  if (reading == LOW && lastReading == HIGH)//pick up appropriate state change
  {
    stateChangeTime = millis();
    duration = stateChangeTime - previousStateChangeTime;
    previousStateChangeTime = stateChangeTime;
  }
  /*
    if (timerRunning == 0 && digitalRead(sensor) == LOW) {
      startTime = millis();
      timerRunning = 1;
      PstartTime = startTime;

    }

    if (timerRunning == 1 && digitalRead(sensor) == LOW) {
      startTime = millis();
      timerRunning = 0;
      duration = Pstartime - startTime;
  */
  RPM = 60000 / duration;
  count = count + 1;
  totRPM = RPM + totRPM;
  avgRPM = totRPM / count;
  if (count > 60) {
    totRPM = avgRPM;
    count = 1;
  }
  if (avgRPM > 181) {
    digitalWrite (relay, HIGH);
  }
  if (avgRPM <= 178) {
    digitalWrite (relay, LOW);
  }

  if (millis() - lastDisplay >= 1000)
  {
    Serial.print ("avgRPM ");
    Serial.println (RPM);
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(avgRPM);
    lcd.setCursor(5, 0);
    lcd.print("RPM");
    lastDisplay += 1000;
  }
}

Cattledog - Thank you!! I was looking into UKHeliBob's suggestions and trying to make it wash but that code explains alot. One question - My serial print is a bunch of gibberish and the lcd never zeros back out. Is this something in my settings?

UKHeliBob - Thank you !! As you can probably tell, I am novice and self taught. I did not know about the IDE. I am still studying it.

Robin2 - Thank you for your code. I am assuming there is more to it than that, as I couldn't identify your input but is very intriguing. I would be interested in learning more about it.

Cattledog - Thank you!! I was looking into UKHeliBob's suggestions and trying to make it wash but that code explains alot. One question - My serial print is a bunch of gibberish and the lcd never zeros back out. Is this something in my settings?

I did not test the code with an lcd connected. I used a test signal and saw correct Serial data. NOTE: I did change the serial baud rate in your sketch from 9600 to 115200 for faster printing. Set it with the pull down box at the bottom of the serial monitor.

When you are trying to use polling with digitalRead() to catch signals you want your program to loop as fast as possible so as not to miss an pulses.

I'm still not certain that the code I provided has the sensor readings correct for the magnet triggering the sensor. It may be picking up the falling edge as the magnet leaves, rather than a rising edge as it approaches but it really should not matter.

dillingerkt:
Robin2 - Thank you for your code. I am assuming there is more to it than that, as I couldn’t identify your input but is very intriguing. I would be interested in learning more about it.

I think the only substantial thing that is missing is the line (in setup() ) that would attach the interrupt to the ISR function myISR(). Something like

void setup() {
   // other stuff
   attachInterrupt(digitalPinToInterrupt(2), myISR, RISING);
}

…R

I thought I had this working but have discovered something is amiss. Every 5th or 6th reading will be 30 to 50 percent less. For example. With the motor running at a steady 181 rpm, the readings will be 181, 181, 181, 181, 165, 181, 181, 181, 181, 164, 181 …

Is this a product of the code?

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// Set the LCD address to 0x27 for a 16 chars and 2 line display
LiquidCrystal_I2C lcd(0x27, 16, 2);

byte sensor =2;
int tempPin = 0;
int RPM = 0;
int count = 0;
int totRPM = 0;
int avgRPM = 0;
const int relay = 4;
const int fan = 3;
byte lastReading;
byte reading;
unsigned long stateChangeTime;
unsigned long previousStateChangeTime;
unsigned long duration;
unsigned long lastDisplay;

void setup(){
pinMode (sensor, INPUT_PULLUP);
pinMode (relay, OUTPUT);
pinMode (fan, OUTPUT);
digitalWrite (relay, HIGH);
digitalWrite (fan, LOW);
Serial.begin(115200);
  lcd.begin();
  lcd.setBacklight((uint8_t)1);
  lcd.print("Initializing");
  delay(1000);
  lcd.clear();
  

}
void loop(){
  lastReading = reading;
  reading = digitalRead(sensor);
  if (reading == LOW && lastReading == HIGH)
  { 
    stateChangeTime = millis();
    duration = stateChangeTime - previousStateChangeTime;
    previousStateChangeTime = stateChangeTime;
  }
  
  RPM = 60000/duration;
  count = count + 1;
  totRPM = RPM + totRPM;
  avgRPM = totRPM/count;
  if (count > 60)
  {
    totRPM = avgRPM;
    count = 1;
  } 
  if (avgRPM > 183)
  {
    digitalWrite (relay, LOW);
  }
  if (avgRPM <= 181)
  {
    digitalWrite (relay, HIGH);
  }
  {
    int tempReading = analogRead(tempPin);
    double tempK = log(10000.0 * ((1024.0 / tempReading - 1)));
    tempK = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * tempK * tempK)) * tempK );
    float tempC = tempK - 273.15;
    float tempF = (tempC * 9.0)/ 5.0 + 32.0; 
    if (tempF > 110)
    {
      digitalWrite (fan, HIGH);
    }
    if (tempF <= 110)
    {
      digitalWrite (fan, LOW);
    }
    
  //Serial.print ("F ");
 // Serial.println (tempF);
  lcd.setCursor(0 , 1);
  lcd.print("Temp          F");
  lcd.setCursor(6, 1);
  lcd.print(tempF);
  }

  if (millis() - lastDisplay >= 1000)
  {  
 Serial.print ("avgRPM ");
 Serial.println (RPM);
 lcd.clear();
 lcd.setCursor(0, 0);
 lcd.print(avgRPM);
 lcd.setCursor(5,0);
 lcd.print("RPM");
  
 lastDisplay += 1000;
  }
}

I don't know and I don't think I could answer your question without being able to run the program with your hardware, and with an oscilloscope to hand.

If the "wrong" value arises at a regular interval you should consider what else might be happening in the program at that frequency.

What happens if you take all the temperature stuff out of the program?

What happens if you create an array of maybe 50 elements and save each successive reading of millis() when the pulse is detected. When you have all 50 readings print them all. Then collect the next 50.

Also, I would micros() for more precise timing.

...R

Every 5th or 6th reading will be 30 to 50 percent less. For example. With the motor running at a steady 181 rpm, the readings will be 181, 181, 181, 181, 165, 181, 181, 181, 181, 164, 181 …

Are you talking about the value derived from the individual cycle time
RPM = 60000/duration; or something involving the average?

Robin2 make a very good suggestion to record the individual rpm and milllis() values. If you are missing a pulse, the rpm will be half.

The lcd print of the temperature does not be every pass through loop. I would put it in the one second display update section.