Some choices for a fan control station

Hello good people. I am in the upstart of making a fan control station for a battery backup space in a storage room. It shall, if I get something finished, take a few temp readings here and there and control fans to act on the temp readings. Amongst other things there is a charger and a PSU that needs active cooling. I think I will end up with around five individual fans. I also want to be able to send some data from this in some way to be able to monitor it remotely. But that is a later subject. I have first started to think about how I will read that the fans are working. This was not so straight forward as I thought when I started. Most examples I found was reading revolutions over long periods with interrupts. This did not feel so applealing to me since I am not after any exact science around the revolutions and I did not want to weigh down the controller with long interrupts.

That made me start by instead reading the lengths of the hall sensor pulses. After some experimentation I think I have something working. I am very happy for comments both on the chosen method and on the code.

Here is the code for starters.

   /*
  This code reads the length of the DC fans built in hall sensor(s) activity. 
  The readings are in microseconds and with a formula this can be converted
  and presented as Revolutions Per Minute (RPM) and/or percentage spinning
  of the fans max revolutions per time unit.
  */

int hallSensorPin = 2;        // Pin 2 is used to read the Hall sensors activity
unsigned long pulseTime;      // Variable for storing the duration of a pulse.
                              // Since it is in microseconds it needs to be able
                              // to store a large value.
int revsPerMinute;            // Variable for storing RPM value
int smoothedRevsPerMinute;    // Variable for storing the smoothed RPM value
int percentage;               // Variable for storing percentage value
int samples = 4;              // Amount of samples that will be stored and used
                              // for smothing

  /*
  Set the pin that shall read the Hall Sensor to pull up its input to 5 V
  with the built in 20k resistor. I have seen some lifting to fan 12 V line but that is
  a much dirtyer voltage then the internal regulated 5 V line. Holding it internal also
  makes the electrical loop much smaller and less sensitive to interference.
  The hall sensors activates a sinking transistor that takes the voltage to LOW when
  the hall sensors are active, ie it shorts its input to ground.
  And start serial for test purpouse read outs to serial monitor.
  */
  
void setup() {
  pinMode(hallSensorPin, INPUT_PULLUP); 
  Serial.begin(9600);

}

  /*
  The pulseIn function reads a pulse, either the high part or the low part. My fan makes two 
  pulses per revolution, ie high-low-high-low so first I read the high pulse, then I read the 
  low pulse. Then in the calculation to revs per minute I sum the high and low pulse and after 
  that multiplies by factor 2. This shall be a full revlution that we time.
  
  The pulseIn way of measuring a fans revolutions per time unit is not very exact. A more 
  exact way would be to count the pulses over a longer time period. This is on the otther hand
  taking up a longer processing time of maybe a second to get good readings. I want t avoid
  that so I read the pulses instead which only takes a couple of milliseconds.
  1200 RPM is 20 RPS which meens a full revolution takes 50 ms. I measure half a revolution.
  
  The downside with this is that a fan is not very exact when you come down to microseconds
  and the hall sensor readings can once in a while bug out and give a strange value.
  I am on the other hand not interested in weather the fan spins with 1207 or or 1211 revs per
  minute, I am interested to see if they do the work they are set to do so I dont care about
  the exact value. On the other hand I do prefer a value that doesnt jump one step every time
  it renews itself so therefore I am smoothing the output values so every new reading adds
  the three readings before it and divides the total four readings with four to present a
  smoothed average. This makes it less jumpy on the last digit.
  */

void loop() {
  
  pulseTime = pulseIn(hallSensorPin, HIGH);           // Read for how long the pulse is high
  pulseTime += pulseIn(hallSensorPin, LOW);           // Read for how long the pulse is low
  revsPerMinute = (1000000 * 60)/(pulseTime * 2);     // Convert pulse time to RPM
  revsPerMinute = constrain(revsPerMinute, 0, 1560);  // Dont let a buggy value get it to way out

  // Smoothing out the value by adding earlyer values and divide to an average
  
  smoothedRevsPerMinute = smoothedRevsPerMinute + ((revsPerMinute - smoothedRevsPerMinute)/samples);
  
  // Calculate the fans spinning in percentage of its max which is approximately 1560 RPM
  
  percentage = (smoothedRevsPerMinute / 15.6);

  // Do some serial printing for checking the result for now
  
  Serial.print("Halfrev Microseconds");
  Serial.print("\t");
  Serial.println(pulseTime, DEC);
  Serial.print("Revolutions per minute");
  Serial.print("\t");
  Serial.println(smoothedRevsPerMinute, DEC);
  Serial.print("Percentage of max revs");
  Serial.print("\t");  
  Serial.println(percentage, DEC);
  delay(5000);

}

And here is a results from a serial print:

Halfrev Microseconds 23385
Revolutions per minute 1303
Percentage of max revs 83
Halfrev Microseconds 23090
Revolutions per minute 1302
Percentage of max revs 83
Halfrev Microseconds 23090
Revolutions per minute 1302
Percentage of max revs 83
Halfrev Microseconds 23024
Revolutions per minute 1302
Percentage of max revs 83
Halfrev Microseconds 23024
Revolutions per minute 1302
Percentage of max revs 83
Halfrev Microseconds 23388
Revolutions per minute 1297
Percentage of max revs 83
Halfrev Microseconds 23130
Revolutions per minute 1297
Percentage of max revs 83
Halfrev Microseconds 22956
Revolutions per minute 1299
Percentage of max revs 83
Halfrev Microseconds 23222
Revolutions per minute 1297
Percentage of max revs 83
Halfrev Microseconds 23321
Revolutions per minute 1295
Percentage of max revs 83

Today is a rainy day so it was time for some foul programming. I was unhappy with that if I stopped the fan manually the code started to say that the revs are increasing. That is ofc bec the pulseIn-command reads the pulse lengths and they usually get shorter when the revs are increasing, but with one exception: it returns zero pulselength if it doesnt find a pulse. And zero is short.

I just couldnt make things work with If/ElseIf/Else or While commands so I ended up doing Gotos. Is this considered foul programming, could it be done neater? I shall say the code works. It does alarm when I stop the fan and it goes back to normal if I release it.

      /*
   * This code reads the length of the DC fans built in hall sensor(s) activity.
   * The readings are in microseconds and with a formula this can be converted
   * and presented as Revolutions Per Minute (RPM) and/or percentage spinning
   * of the fans max revolutions per time unit.  
  */

int hallSensorPin = 2;            // Pin 2 is used to read the Hall sensors activity
unsigned long previousPulseTime;  // Variable for storing the duration of a pulse.
word highPulseTime;               // It stores the previous value to have something
word lowPulseTime;                // compare a new value with
unsigned long newPulseTime;       // Since it is in microseconds it needs to be able
                                  // to store a large value.

                              
int revsPerMinute;                // Variable for storing RPM value
int smoothedRevsPerMinute;        // Variable for storing the smoothed RPM value
int percentage;                   // Variable for storing percentage value
int samples = 3;                  // Amount of samples that will be stored and used
                                  // for smoothing

int ledPin = 13;                  // Pin 13 is used to test alarm

  /*
   * Set the pin that shall read the Hall Sensor to pull up its input to 5 V
   * with the built in 20k resistor. I have seen some lifting to fan 12 V line but that is
   * a much dirtyer voltage then the internal regulated 5 V line. Holding it internal also  
   * makes the electrical loop much smaller and less sensitive to interference.
   * The hall sensors activates a sinking transistor that takes the voltage to LOW when
   * the hall sensors are active, ie it shorts its input to ground.
   * And start serial for test purpouse read outs to serial monitor. 
  */
  
void setup() {

  pinMode(ledPin, OUTPUT);                        // Set LedPin to output
  pinMode(hallSensorPin, INPUT_PULLUP);           // Pull up the sensor pin to 5V
  highPulseTime = pulseIn(hallSensorPin, HIGH);   // Read once for how long the pulse is high
  lowPulseTime = pulseIn(hallSensorPin, LOW);     // Read once for how long the pulse is low
  newPulseTime = highPulseTime + lowPulseTime;    // Create a previous value by telling it to
  previousPulseTime = newPulseTime;               // be the same as the new value we just read.
                                                  // to get a starter position
  
  Serial.begin(9600);                             // We start a serial for debugging readouts

}

  /*
   * The pulseIn function reads a pulse, either the high part or the low part. My fan makes two
   * pulses per revolution, ie high-low-high-low so first I read the high pulse, then I read the
   * low pulse. Then in the calculation to revs per minute I sum the high and low pulse and after
   * that multiplies by factor 2. This shall be a full revlution that we time.
   * 
   * The pulseIn way of measuring a fans revolutions per time unit is not very exact. A more
   * exact way would be to count the pulses over a longer time period. This is on the other hand
   * taking up a longer processing time of maybe a second to get good readings. I want to avoid  
   * that so I read the pulses instead which only takes a couple of milliseconds.
   * 
   * 1200 RPM is 20 RPS which meens a full revolution takes 50 ms. I measure half a revolution.  
   * The downside with this is that a fan is not very exact when you come down to microseconds
   * and the hall sensor readings can once in a while bug out and give a strange value.
   * I am on the other hand not interested in weather the fan spins with 1207 or or 1211 revs per
   * minute, I am interested to see if they do the work they are set to do so I dont care about
   * the exact value. On the other hand I do prefer a value that doesnt jump one step every time
   * it renews itself so therefore I am smoothing the output values so every new reading adds  
   * the three readings before it and divides the total four readings with four to present a
   * smoothed average. This makes it less jumpy on the last digit.  
   * 
   * I also added an outlier corrector which is described below.
  */

void loop() {  
  
run:                                                // goto tag, see below

  digitalWrite(13, LOW);

  highPulseTime = pulseIn(hallSensorPin, HIGH);     // Read for how long the pulse is high

  lowPulseTime = pulseIn(hallSensorPin, LOW);       // Read for how long the pulse is low

  newPulseTime = highPulseTime + lowPulseTime;

  /*
   * Here I tried with alot of If, Else If and Else commands but it would not work for me.
   * If I tried If => Pulse Time = 0 and Else If if it was not 0, somehow the smoothing
   * algorithms below bugged out so it just told me that the revs are going up all the way
   * to 100% while tha fan was actually stopped and the Led Pin did not want to shine. I
   * guess the interpretation of If => Else If and then new If commands does not work.
   * I also tried While for the alarm if the fan stoppes and it worked so far but when I
   * released the fan it was stuck in the While-loop.
   */

      if (newPulseTime == 0) {                      // A goto command. If statement is true
        goto alarm;                                 // we jump down to alarm: in the end
  }
  
      if (newPulseTime < (previousPulseTime * 0.9)) {   //Kind of a high-pass filter
        (newPulseTime = (previousPulseTime * 0.9));     //Takes outlier values down
    }

      if (newPulseTime > (previousPulseTime * 1.1)) {   //Kind of a high-pass filter
        (newPulseTime = (previousPulseTime * 1.1));     //Takes outlier values down
    }   

  revsPerMinute = (1000000 * 60)/(newPulseTime * 2);  // Convert pulse time to RPM
                             
  // Smoothing out the value by adding earlyer values and divide to an average
  
  smoothedRevsPerMinute = smoothedRevsPerMinute + ((revsPerMinute - smoothedRevsPerMinute)/samples);
  
  // Calculate the fans spinning in percentage of its max which is approximately 1560 RPM
  
  percentage = (smoothedRevsPerMinute / 15.55);       //The fans max revs are about 1550
  percentage =  constrain(percentage, 0, 100);        //Constrains percentage scale

  // Do some serial printing for checking the result for now
  
  Serial.print("High Pulsetime");
  Serial.print("\t");
  Serial.print("\t");
  Serial.println(highPulseTime, DEC);
  Serial.print("Low Pulsetime");
  Serial.print("\t");
  Serial.print("\t");
  Serial.println(lowPulseTime, DEC);
  Serial.print("Halfrev Microseconds");
  Serial.print("\t");
  Serial.println(newPulseTime, DEC);
  Serial.print("Revolutions per minute");
  Serial.print("\t");
  Serial.println(smoothedRevsPerMinute, DEC);
  Serial.print("Percentage of max revs");
  Serial.print("\t");  
  Serial.println(percentage, DEC);

  previousPulseTime = newPulseTime;

  alarm:
  if (newPulseTime > 0) {           //If the pulse is not zero wait a while and go to run
     delay(5000);
     goto run;
  }
  else {                            //otherwise activate alarm, wait a bit and check again
  digitalWrite(ledPin, HIGH); 
  }
  delay(5000);
}

I didnt like goto so I went to function blocks with millis instead. Much better for the coming functions. Now the usual (non alarm) functions ticks on even if the alarm triggers.

  /*
   * This code reads the length of the DC fans built in hall sensor(s) activity.
   * The readings are in microseconds and with a formula this can be converted
   * and presented as Revolutions Per Minute (RPM) and/or percentage spinning
   * of the fans max revolutions per time unit.  
  */

const int hallSensorPin = 2;      // Pin 2 is used to read the Hall sensors activity
const int readFanInterval = 5000; // number of millisecs between reading the fan
const int alarmInterval = 1000;   // number of millisecs for the alarm to go

unsigned long previousPulseTime;  // Variable for storing the duration of a pulse.
word highPulseTime;               // It stores the previous value to have something
word lowPulseTime;                // compare a new value with
unsigned long newPulseTime;       // Since it is in microseconds it needs to be able
                                  // to store a large value.
unsigned long currentMillis = 0;  // stores the value of millis() in each iteration of loop()
unsigned long previousReadFan = 0;// will store last time the LED was updated
unsigned long previousAlarm = 0;  // will store last time the LED was updated
                              
int revsPerMinute;                // Variable for storing RPM value
int smoothedRevsPerMinute;        // Variable for storing the smoothed RPM value
int percentage;                   // Variable for storing percentage value
const int samples = 3;            // Amount of samples that will be stored and used
                                  // for smoothing

const int ledPin = 13;            // Pin 13 is used to test alarm

  /*
   * Set the pin that shall read the Hall Sensor to pull up its input to 5 V
   * with the built in 20k resistor. I have seen some lifting to fan 12 V line but that is
   * a much dirtyer voltage then the internal regulated 5 V line. Holding it internal also  
   * makes the electrical loop much smaller and less sensitive to interference.
   * The hall sensors activates a sinking transistor that takes the voltage to LOW when
   * the hall sensors are active, ie it shorts its input to ground.
   * And start serial for test purpouse read outs to serial monitor. 
  */
  
void setup() {

  currentMillis = millis();                       // capture the latest value of millis()
  pinMode(ledPin, OUTPUT);                        // Set LedPin to output
  pinMode(hallSensorPin, INPUT_PULLUP);           // Pull up the sensor pin to 5V
  highPulseTime = pulseIn(hallSensorPin, HIGH);   // Read once for how long the pulse is high
  lowPulseTime = pulseIn(hallSensorPin, LOW);     // Read once for how long the pulse is low
  newPulseTime = highPulseTime + lowPulseTime;    // Create a previous value by telling it to
  previousPulseTime = newPulseTime;               // be the same as the new value we just read.
                                                  // to get a starter position
  
  Serial.begin(9600);                             // We start a serial for debugging readouts

}

  /*
   * The pulseIn function reads a pulse, either the high part or the low part. My fan makes two
   * pulses per revolution, ie high-low-high-low so first I read the high pulse, then I read the
   * low pulse. Then in the calculation to revs per minute I sum the high and low pulse and after
   * that multiplies by factor 2. This shall be a full revlution that we time.
   * 
   * The pulseIn way of measuring a fans revolutions per time unit is not very exact. A more
   * exact way would be to count the pulses over a longer time period. This is on the other hand
   * taking up a longer processing time of maybe a second to get good readings. I want to avoid  
   * that so I read the pulses instead which only takes a couple of milliseconds.
   * 
   * 1200 RPM is 20 RPS which meens a full revolution takes 50 ms. I measure half a revolution.  
   * The downside with this is that a fan is not very exact when you come down to microseconds
   * and the hall sensor readings can once in a while bug out and give a strange value.
   * I am on the other hand not interested in weather the fan spins with 1207 or or 1211 revs per
   * minute, I am interested to see if they do the work they are set to do so I dont care about
   * the exact value. On the other hand I do prefer a value that doesnt jump one step every time
   * it renews itself so therefore I am smoothing the output values so every new reading adds  
   * the three readings before it and divides the total four readings with four to present a
   * smoothed average. This makes it less jumpy on the last digit.  
   * 
   * I also added an outlier corrector which is described below.
  */



void loop() {
  
  read_fan_rpm();

}

void read_fan_rpm() {                               // Separate function block for reading fan

  if (millis() - previousReadFan >= readFanInterval) {

  highPulseTime = pulseIn(hallSensorPin, HIGH);     // Read for how long the pulse is high

  lowPulseTime = pulseIn(hallSensorPin, LOW);       // Read for how long the pulse is low

  newPulseTime = highPulseTime + lowPulseTime;


      if (newPulseTime == 0) {                      // If both high and low pulsetime 
        fan_alarm();                                // returns zero call function alarm
  }

  digitalWrite(13, LOW);
  
      if (newPulseTime < (previousPulseTime * 0.9)) {   //Kind of a high-pass filter
        (newPulseTime = (previousPulseTime * 0.9));     //Takes outlier values down
    }

      if (newPulseTime > (previousPulseTime * 1.1)) {   //Kind of a high-pass filter
        (newPulseTime = (previousPulseTime * 1.1));     //Takes outlier values down
    }   

  revsPerMinute = (1000000 * 60)/(newPulseTime * 2);  // Convert pulse time to RPM
                             
  // Smoothing out the value by adding earlyer values and divide to an average
  
  smoothedRevsPerMinute = smoothedRevsPerMinute + ((revsPerMinute - smoothedRevsPerMinute)/samples);
  
  // Calculate the fans spinning in percentage of its max which is approximately 1560 RPM
  
  percentage = (smoothedRevsPerMinute / 15.55);       //The fans max revs are about 1550
  percentage =  constrain(percentage, 0, 100);        //Constrains percentage scale

  // Do some serial printing for debugging
  
  Serial.print("High Pulsetime");
  Serial.print("\t");
  Serial.print("\t");
  Serial.println(highPulseTime, DEC);
  
  Serial.print("Low Pulsetime");
  Serial.print("\t");
  Serial.print("\t");
  Serial.println(lowPulseTime, DEC);
  
  Serial.print("Halfrev Microseconds");
  Serial.print("\t");
  Serial.println(newPulseTime, DEC);

  Serial.print("Unsmoothed per minute");
  Serial.print("\t");
  Serial.println(revsPerMinute, DEC);
  
  Serial.print("Revolutions per minute");
  Serial.print("\t");
  Serial.println(smoothedRevsPerMinute, DEC);
  
  Serial.print("Percentage of max revs");
  Serial.print("\t");  
  Serial.println(percentage, DEC);

  previousPulseTime = newPulseTime;

  previousReadFan += readFanInterval;

  }
}

void fan_alarm() {                      // Separate function block for alarm

  if (millis() - previousAlarm >= alarmInterval) {  
  
  digitalWrite(ledPin, HIGH);           // Activates the LedPin

  Serial.println("Fan has stopped!");   // Prints a warning message

  previousAlarm += alarmInterval;  
  
  }
}