RPM sensor has an issue saving data on a SD card

Hi,

I am a beginner user of Arduino. I am using a GPS module, three limit switches, and a RPM hall effect sensor on an Arduino Nano to measure speed, height (three binary digits), and fan RPM. Please find the information of all components and also my code below.

When I read all data using serial monitor, the RPM data are consistent (for a specific RPM speed (for example 3000 RPM)). However, when I save all data on SD card, the RPM data is close to that specific RPM value with a variation range (for example 3000±500 RPM). I searched and I found that the Interrupt function for RPM has an issue with saving all data on SD card. I edited my code based on some suggestions that I found, but the issue was not solved yet.

How can I read consistent RPM values (without variation range) and also save all data on SD card?
Thank you for your help in advance.

Components information:

  1. Arduino Nano: https://www.amazon.com/dp/B0713XK923?psc=1&ref=ppx_yo2_dt_b_product_details
  2. Garmin GPS 18x, LVC (1 Hz) : Garmin GPS 18x OEM™ | Sensor
  3. Limit switch: McMaster-Carr
  4. Hall-effect RPM sensor: https://www.amazon.com/gp/product/B00VKAT8A2/ref=ppx_yo_dt_b_asin_title_o09_s00?ie=UTF8&psc=1
  5. SD card: https://www.amazon.com/dp/B073K14CVB?psc=1&ref=ppx_yo2_dt_b_product_details
  6. Micro SD TF Card Adapter Reader Module: https://www.amazon.com/dp/B07BJ2P6X6?psc=1&ref=ppx_yo2_dt_b_product_details

My code:

#include <SD.h>
#include <SPI.h>
#include <TinyGPS++.h> // Include the TinyGPS++ library
TinyGPSPlus tinyGPS; // Create a TinyGPSPlus object
#define GPS_BAUD 4800 // GPS module baud rate. 
#include <SoftwareSerial.h>
#define ARDUINO_GPS_RX 8 // GPS TX, Arduino RX pin
#define ARDUINO_GPS_TX 9 // GPS RX, Arduino TX pin
SoftwareSerial ssGPS(ARDUINO_GPS_TX, ARDUINO_GPS_RX); // Create a SoftwareSerial
#define gpsPort ssGPS  // Alternatively, use Serial1 on the Leonardo

//Limit switches (height measurement)
int noPin1 = 5;//Normally Open pin 
int buttonState1 = 0;
int noPin2 = 6;//Normally Open pin 
int buttonState2 = 0;
int noPin3 = 7;//Normally Open pin 
int buttonState3 = 0;

//RPM sensor
int sensor1 = 2; // Hall sensor at pin 2
volatile byte counts1;
unsigned int rpm1; //unsigned gives only positive values
unsigned long previoustime1; //it will store the last activation time to zero out the loop
void count_function1()
{ /*The ISR function
    Called on Interrupt
    Update counts*/
  counts1++;         // pulses counter
}

// SD card
File myFile;
int pinCS = 10; // Pin 10 on Arduino Uno



void setup() {
  
  gpsPort.begin(GPS_BAUD);
  Serial.begin(9600);
  
  //Limit switches
  pinMode(noPin1,INPUT_PULLUP);
  pinMode(noPin2,INPUT_PULLUP);
  pinMode(noPin3,INPUT_PULLUP);
  
  //RPM sensor
  attachInterrupt(0, count_function1, RISING); //Interrupts are called on Rise of Input
  pinMode(sensor1, INPUT); //Sets sensor as input
  counts1 = 0; // initial counts set zero
  rpm1 = 0;    // zero initial rpm
  previoustime1 = 0; //for the first loop should be equal zero

  // SD card
  pinMode(pinCS, OUTPUT);
  if (SD.begin())
  {
    Serial.println("SD card is ready to use.");
  } else
  {
    Serial.println("SD card initialization failed");
    return;
  }   
}



// Save data on SD card
void loop() {
  
  myFile = SD.open("test.txt", FILE_WRITE);
  if (myFile) { 
    
     //RPM sensor 
    detachInterrupt(0); //Interrupts are disabled. No other interrupts will be counted till we say to.
    rpm1 = (counts1 * 60000) / (millis() - previoustime1); //counts the rate of pulses per ms so we need to make pulse per ms to pulses per minute by the factor of 60000
    previoustime1 = millis(); //Resets the clock as said earlier
    counts1 = 0; //Resets the counter
    
    //Limit Switches (height measurement)
    buttonState1 = digitalRead(noPin1);
    buttonState2 = digitalRead(noPin2);
    buttonState3 = digitalRead(noPin3);

    //GPS data
    myFile.print(tinyGPS.date.month());  
    myFile.print("/"); 
    myFile.print(tinyGPS.date.day());  
    myFile.print("/"); 
    myFile.print(tinyGPS.date.year());  
    myFile.print(","); 
    myFile.print(tinyGPS.time.hour());
    myFile.print(":");
    if (tinyGPS.time.minute() < 10) myFile.print('0');
    myFile.print(tinyGPS.time.minute());
    myFile.print(":");
    if (tinyGPS.time.second() < 10) myFile.print('0');
    myFile.print(tinyGPS.time.second());
    myFile.print(","); 
    myFile.print(tinyGPS.location.lat(), 6);
    myFile.print(",");    
    myFile.print(tinyGPS.location.lng(), 6);
    myFile.print(","); 
    myFile.print(tinyGPS.altitude.feet());
    myFile.print(","); 
    myFile.print(tinyGPS.speed.mph());
    myFile.print(",");
    myFile.print(buttonState1);
    myFile.print(buttonState2);
    myFile.print(buttonState3);
    myFile.print(",");

    // RPM sensor 
    myFile.println(rpm1); 
    attachInterrupt(0, count_function1, RISING); //Counter restarted so we can evaluate the rpm again.  

    myFile.close(); // close the file
  }
  // if the file didn't open, print an error:
  else {
    Serial.println("error opening test.txt");
  }
  smartDelay(1000); 
}


//GPS module
static void smartDelay(unsigned long ms)
{
  unsigned long start = millis();
  do
  {
    // If data has come in from the GPS module
    while (gpsPort.available())
      tinyGPS.encode(gpsPort.read()); 
  } while (millis() - start < ms);
}

I did not look at your code, but your comment here tells me you already have problems. Never try to save data to an SD card inside the interrupt code. Interrupts are off and likely the SD code wants to use interrupts.
Examine the logic of the program and save the SD card data in the main loop() function.
Paul

Hi Paul,

Thank you for your comment. I brought the Interrupt function out of the saving process (I hope I edited the loop section correctly based on your suggestion). The values became a little bit more consistent, however it still has variation range and it is different from the ground truth RPM (Arduino serial monitor and sensor LCD shows 3000 RPM, however I can see 4000 RPM on SD card). Please find my edited loop section below and let me know if I can improve it more.

// Save data on SD card
void loop() {
      
  //RPM sensor 
  detachInterrupt(0); //Interrupts are disabled. No other interrupts will be counted till we say to.
  rpm1 = (counts1 * 60000) / (millis() - previoustime1); //counts the rate of pulses per ms so we need to make pulse per ms to pulses per minute by the factor of 60000
    
  //Limit Switches (height measurement)
  buttonState1 = digitalRead(noPin1);
  buttonState2 = digitalRead(noPin2);
  buttonState3 = digitalRead(noPin3);
    
  myFile = SD.open("test.txt", FILE_WRITE);
  if (myFile) { 
    //GPS data
    myFile.print(tinyGPS.date.month());  
    myFile.print("/"); 
    myFile.print(tinyGPS.date.day());  
    myFile.print("/"); 
    myFile.print(tinyGPS.date.year());  
    myFile.print(","); 
    myFile.print(tinyGPS.time.hour());
    myFile.print(":");
    if (tinyGPS.time.minute() < 10) myFile.print('0');
    myFile.print(tinyGPS.time.minute());
    myFile.print(":");
    if (tinyGPS.time.second() < 10) myFile.print('0');
    myFile.print(tinyGPS.time.second());
    myFile.print(","); 
    myFile.print(tinyGPS.location.lat(), 6);
    myFile.print(",");    
    myFile.print(tinyGPS.location.lng(), 6);
    myFile.print(","); 
    myFile.print(tinyGPS.altitude.feet());
    myFile.print(","); 
    myFile.print(tinyGPS.speed.mph());
    myFile.print(",");
    myFile.print(buttonState1);
    myFile.print(buttonState2);
    myFile.print(buttonState3);
    myFile.print(",");
    myFile.println(rpm1);  
    myFile.close(); // close the file
     
    
  }
  // if the file didn't open, print an error:
  else {
    Serial.println("error opening test.txt");
  }
  
  // RPM sensor
  previoustime1 = millis(); //Resets the clock as said earlier
  counts1 = 0; //Resets the counter
  attachInterrupt(0, count_function1, RISING); //Counter restarted so we can evaluate the rpm again. 
   
  smartDelay(1000); 
}

Where in your sketch are you printing the rpm value to the Serial Monitor?

How are you powering the tachometer, and how are you picking up the interrupt signal for the Arduino?

Only do the attach interrupt ONCE in the setup() code. Then use code similar to this:
// make a working copy of the counter while disabling interrupts
cli();
revs = icounter; // Two interrupts per revolution.
icounter = 0; // reset interupt count
sei(); "
in your loop() when you want to compute the RPM and save the value.
Paul

Hi cattledog,

Thank you for your help. In my uploaded code, I do not have serial printing; however I just used another sperate code with serial printing to see the RPM values.
I am using a 12v battery to power the sensor. I use Interrupt function on digital pin 2 to get the interrupt signal.

Paul,

I became a little bit confused. I appreciate it if you can show me where and how exactly I put your piece of code to my loop section below.

// Save data on SD card
void loop() {
      
  //RPM sensor 
  detachInterrupt(0); //Interrupts are disabled. No other interrupts will be counted till we say to.
  rpm1 = (counts1 * 60000) / (millis() - previoustime1); //counts the rate of pulses per ms so we need to make pulse per ms to pulses per minute by the factor of 60000
    
  //Limit Switches (height measurement)
  buttonState1 = digitalRead(noPin1);
  buttonState2 = digitalRead(noPin2);
  buttonState3 = digitalRead(noPin3);
    
  myFile = SD.open("test.txt", FILE_WRITE);
  if (myFile) { 
    //GPS data
    myFile.print(tinyGPS.date.month());  
    myFile.print("/"); 
    myFile.print(tinyGPS.date.day());  
    myFile.print("/"); 
    myFile.print(tinyGPS.date.year());  
    myFile.print(","); 
    myFile.print(tinyGPS.time.hour());
    myFile.print(":");
    if (tinyGPS.time.minute() < 10) myFile.print('0');
    myFile.print(tinyGPS.time.minute());
    myFile.print(":");
    if (tinyGPS.time.second() < 10) myFile.print('0');
    myFile.print(tinyGPS.time.second());
    myFile.print(","); 
    myFile.print(tinyGPS.location.lat(), 6);
    myFile.print(",");    
    myFile.print(tinyGPS.location.lng(), 6);
    myFile.print(","); 
    myFile.print(tinyGPS.altitude.feet());
    myFile.print(","); 
    myFile.print(tinyGPS.speed.mph());
    myFile.print(",");
    myFile.print(buttonState1);
    myFile.print(buttonState2);
    myFile.print(buttonState3);
    myFile.print(",");
    myFile.println(rpm1);  
    myFile.close(); // close the file
     
    
  }
  // if the file didn't open, print an error:
  else {
    Serial.println("error opening test.txt");
  }
  
  // RPM sensor
  previoustime1 = millis(); //Resets the clock as said earlier
  counts1 = 0; //Resets the counter
  attachInterrupt(0, count_function1, RISING); //Counter restarted so we can evaluate the rpm again. 
   
  smartDelay(1000); 
}

I am concerned about the connection to both the display and the Arduino. Typically, the output from the sensors is npn open collector with a pull up to the operating voltage.

Are you certain that pin 2 input is not being pulled up to 12v in your arrangement. Do you have a scope or multimeter to look at signal levels?

however I just used another sperate code with serial printing to see the RPM values.

Can you post that code? How did you manage the transfer of data from the interrupt?

Right here. But instead of doing the RPM calculation every time through the loop, why not do it every second or 1000 milliseconds.
Paul

I think what is required is a complete view of the differences between the application that works correctly and the application that does not work as expected... There are obviously differences in software AND hardware that may influence the results.

  • Hardware configuration and software (working)
  • Hardware configuration and software (not working as expected)

Everything else ends up in just more or less sophisticated guess work ... :wink:

And of course some helpful amendmends as already stated in other posts...

Luckily the compiler seems to solve (byte) x 60000 which can be e.g. 3.000.000 as long ... :wink:

cattledog,

I am really sorry for the confusion since I used LCD separately to monitor the data. The LCD comes with the sensor and it does not need Arduino to read the RPM values. I do not have LCD while saving data using Arduino.

Unfortunately, in the settings (I packaged all sensors for the test) that I have now I can not take the signal from pin2 and I will do it later.

Please find my code below using serial print :

#include <SD.h>
#include <SPI.h>
#include <TinyGPS++.h> // Include the TinyGPS++ library
TinyGPSPlus tinyGPS; // Create a TinyGPSPlus object
#define GPS_BAUD 4800 // GPS module baud rate. GP3906 defaults to 9600.
#include <SoftwareSerial.h>
#define ARDUINO_GPS_RX 8 // GPS TX, Arduino RX pin
#define ARDUINO_GPS_TX 9 // GPS RX, Arduino TX pin
SoftwareSerial ssGPS(ARDUINO_GPS_TX, ARDUINO_GPS_RX); // Create a SoftwareSerial
#define gpsPort ssGPS  // Alternatively, use Serial1 on the Leonardo
//Limit switches (height measurement)
int noPin1 = 5;//Normally Open pin 
int buttonState1 = 0;
int noPin2 = 6;//Normally Open pin 
int buttonState2 = 0;
int noPin3 = 7;//Normally Open pin 
int buttonState3 = 0;
//RPM/airflow 
int sensor1 = 2; // Hall sensor at pin 2(or 0 in the code)///pin 3 (or 1 in the code)
volatile byte counts1;
unsigned int rpm1; //unsigned gives only positive values
unsigned long previoustime1; //it will store the last activation time to zero out the loop
void count_function1()
{ /*The ISR function
    Called on Interrupt
    Update counts*/
  counts1++;         // pulses counter
}


void setup() {
  gpsPort.begin(GPS_BAUD);
  Serial.begin(9600);
  //Limit switches
  pinMode(noPin1,INPUT_PULLUP);
  pinMode(noPin2,INPUT_PULLUP);
  pinMode(noPin3,INPUT_PULLUP);
  attachInterrupt(0, count_function1, RISING); //Interrupts are called on Rise of Input
  pinMode(sensor1, INPUT); //Sets sensor as input
  counts1 = 0; // initial counts set zero
  rpm1 = 0;    // zero initial rpm
  previoustime1 = 0; //for the first loop should be equal zero
}

void loop() {
  {

    detachInterrupt(0); //Interrupts are disabled. No other interrupts will be counted till we say to.
    buttonState1 = digitalRead(noPin1);
    buttonState2 = digitalRead(noPin2);
    buttonState3 = digitalRead(noPin3);
    Serial.print(tinyGPS.date.month());  
    Serial.print("/"); 
    Serial.print(tinyGPS.date.day());  
    Serial.print("/"); 
    Serial.print(tinyGPS.date.year());  
    Serial.print(","); 
    Serial.print(tinyGPS.time.hour());
    Serial.print(":");
    if (tinyGPS.time.minute() < 10) myFile.print('0');
    Serial.print(tinyGPS.time.minute());
    Serial.print(":");
    if (tinyGPS.time.second() < 10) myFile.print('0');
    Serial.print(tinyGPS.time.second());
    Serial.print(","); 
    Serial.print(tinyGPS.location.lat(), 6);
    Serial.print(",");    
    Serial.print(tinyGPS.location.lng(), 6);
    Serial.print(","); 
    Serial.print(tinyGPS.altitude.feet());
    Serial.print(","); 
    Serial.print(tinyGPS.speed.mph());
    Serial.print(",");
    Serial.print(buttonState1);
    Serial.print(buttonState2);
    Serial.print(buttonState3);
    Serial.print(",");
    rpm1 = (counts1 * 60000) / (millis() - previoustime1); //counts the rate of pulses per ms so we need to make pulse per ms to pulses per minute by the factor of 60000
    previoustime1 = millis(); //Resets the clock as said earlier
    counts1 = 0; //Resets the counter
    Serial.println(rpm1); //Calibration equation Airflow=0.0155*rmp
    attachInterrupt(0, count_function1, RISING); //Counter restarted so we can evaluate the rpm again.
   }
smartDelay(1000); 
}


static void smartDelay(unsigned long ms)
{
  unsigned long start = millis();
  do
  {
    // If data has come in from the GPS module
    while (gpsPort.available())
      tinyGPS.encode(gpsPort.read()); 
  } while (millis() - start < ms);
}

Paul,

I modified the code based on your suggestion. Please check the edited code below and let me know if I modified it correctly. The data became more consistent, but they are off compare to the ground truth RPM (when the input voltage of the fan is 5v and 12v, the ground truth are 800 and 3000 rpm respectively, however the code saved the data as 1000 and 4000 rpm on SD card). Any idea to improve it more?

Thank you!

The modified version:

#include <SD.h>
#include <SPI.h>
#include <TinyGPS++.h> // Include the TinyGPS++ library
TinyGPSPlus tinyGPS; // Create a TinyGPSPlus object
#define GPS_BAUD 4800 // GPS module baud rate. 
#include <SoftwareSerial.h>
#define ARDUINO_GPS_RX 8 // GPS TX, Arduino RX pin
#define ARDUINO_GPS_TX 9 // GPS RX, Arduino TX pin
SoftwareSerial ssGPS(ARDUINO_GPS_TX, ARDUINO_GPS_RX); // Create a SoftwareSerial
#define gpsPort ssGPS  // Alternatively, use Serial1 on the Leonardo

//Limit switches (height measurement)
int noPin1 = 5;//Normally Open pin 
int buttonState1 = 0;
int noPin2 = 6;//Normally Open pin 
int buttonState2 = 0;
int noPin3 = 7;//Normally Open pin 
int buttonState3 = 0;

//RPM sensor
int sensor1 = 2; // Hall sensor at pin 2
volatile byte counts1;
unsigned int rpm1; //unsigned gives only positive values
unsigned long previoustime1; //it will store the last activation time to zero out the loop
void count_function1()
{ /*The ISR function
    Called on Interrupt
    Update counts*/
  counts1++;         // pulses counter
}

// SD card
File myFile;
int pinCS = 10; // Pin 10 on Arduino Uno



void setup() {
  
  gpsPort.begin(GPS_BAUD);
  Serial.begin(9600);
  
  //Limit switches
  pinMode(noPin1,INPUT_PULLUP);
  pinMode(noPin2,INPUT_PULLUP);
  pinMode(noPin3,INPUT_PULLUP);
  
  //RPM sensor
  attachInterrupt(0, count_function1, RISING); //Interrupts are called on Rise of Input
  pinMode(sensor1, INPUT); //Sets sensor as input
  counts1 = 0; // initial counts set zero
  rpm1 = 0;    // zero initial rpm
  previoustime1 = 0; //for the first loop should be equal zero

  // SD card
  pinMode(pinCS, OUTPUT);
  if (SD.begin())
  {
    Serial.println("SD card is ready to use.");
  } else
  {
    Serial.println("SD card initialization failed");
    return;
  }   
}



// Save data on SD card
void loop() {
      
   //RPM sensor 
  cli();
  delay(1000);
  rpm1 = (counts1 * 60000) / (millis() - previoustime1); //counts the rate of pulses per ms so we need to make pulse per ms to pulses per minute by the factor of 60000
  previoustime1 = millis(); //Resets the clock as said earlier
  counts1 = 0; //Resets the counter
  sei();
    
  //Limit Switches (height measurement)
  buttonState1 = digitalRead(noPin1);
  buttonState2 = digitalRead(noPin2);
  buttonState3 = digitalRead(noPin3);
  
  myFile = SD.open("test.txt", FILE_WRITE);
  if (myFile) { 
    //GPS data
    myFile.print(tinyGPS.date.month());  
    myFile.print("/"); 
    myFile.print(tinyGPS.date.day());  
    myFile.print("/"); 
    myFile.print(tinyGPS.date.year());  
    myFile.print(","); 
    myFile.print(tinyGPS.time.hour());
    myFile.print(":");
    if (tinyGPS.time.minute() < 10) myFile.print('0');
    myFile.print(tinyGPS.time.minute());
    myFile.print(":");
    if (tinyGPS.time.second() < 10) myFile.print('0');
    myFile.print(tinyGPS.time.second());
    myFile.print(","); 
    myFile.print(tinyGPS.location.lat(), 6);
    myFile.print(",");    
    myFile.print(tinyGPS.location.lng(), 6);
    myFile.print(","); 
    myFile.print(tinyGPS.altitude.feet());
    myFile.print(","); 
    myFile.print(tinyGPS.speed.mph());
    myFile.print(",");
    myFile.print(buttonState1);
    myFile.print(buttonState2);
    myFile.print(buttonState3);
    myFile.print(",");
    myFile.println(rpm1); 
    myFile.close(); // close the file
  }
  // if the file didn't open, print an error:
  else {
    Serial.println("error opening test.txt");
  }

  smartDelay(1000); 
}


//GPS module
static void smartDelay(unsigned long ms)
{
  unsigned long start = millis();
  do
  {
    // If data has come in from the GPS module
    while (gpsPort.available())
      tinyGPS.encode(gpsPort.read()); 
  } while (millis() - start < ms);
}

Reverse the sequence of these two lines! You turn off all interrupts for 1 second! Don't do that!

1 Like

Paul,

Thank you for your helpful suggestion. I edited the code and the data became pretty much consistent; however, the data is still off values compare to my ground-truth data (Arduino serial monitoring data and also sensor LCD). Any suggestion to improve the readings?

Yes, as a matter of fact.
I see you are opening and closing the SD card file every time through the loop(). Combined with your 1 second delay could possibly take more than a second. Think about why you need to do the open and close so many times.
Paul