Welcome to Hackster!
Hackster is a community dedicated to learning hardware, from beginner to pro. Join us, it's free!
Hackster is hosting Impact Spotlights: Smart Home. Watch the stream live on Thursday!Hackster is hosting Impact Spotlights: Smart Home. Stream on Thursday!
Alan De Windt
Published © GPL3+

Smart Multi-Mode PC Fan Speed Controller

Automatically control PC fan speed based on temperature or manually set to 50, 75 or 100% duty cycle

IntermediateFull instructions provided77
Smart Multi-Mode PC Fan Speed Controller

Things used in this project

Hardware components

Raspberry Pi Pico
Raspberry Pi Pico
×1
Pushbutton Switch, Momentary
Pushbutton Switch, Momentary
×1
LED (generic)
LED (generic)
×1
Resistor 220 ohm
Resistor 220 ohm
×1
PC Fan - 12 VDC, 4 pin PWM
×4
Relay Module - 3.3 VDC
×1

Story

Read more

Schematics

Smart Multi-Mode PC Fan Speed Controller Schematic

Code

Smart Multi-Mode PC Fan Speed Controller

Arduino
// Smart Multi-Mode PC Fan Speed Controller
// Written by Alan De Windt
// January 2024

// DISCLAIMER:  Use at your own risk!  This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

#include <DHT.h>  // DHT library from Adafruit
#define DHTPIN 18  // Digital pin for temperature and humidity sensor
#define DHTTYPE DHT22  // Type of sensor - DHT22
DHT dht(DHTPIN, DHTTYPE);  // Initialize temperature and humidity sensor

const int fanPwmPin[4] = {9, 11, 13, 15};  // Fan PWM pins
const int fanRpmPin[4] = {8, 10, 12, 14};  // Fan RPM pins
boolean isFanOperational[4] = {true, true, true, true};  // Assume all fans are connected and not faulty (not stuck, burned out or disconnected)

const int buttonPin = 17;  // Push-button pin
boolean lastButton = HIGH;
boolean currentButton = HIGH;

const int ledPin = 16;  // LED pin
const int fanPowerPin = 19;  // Fan 12 volt power pin connected to relay module

// Operating modes
const int TempControl = 0;  // Temperature controlled
const int DutyCycle50 = 1;  // 50% duty cycle
const int DutyCycle75 = 2;  // 75% duty cyclee
const int DutyCycle100 = 3;  // 100% duty cycle
int Mode = TempControl;  // Default mode which is set to temperature controlled

boolean isFansOn = false;  // To keep track of whether fans are currently off or on
boolean isCheckingFans = false;  // To keep track of whether or not we are currently checking fans
volatile int fanBeingChecked = 0;  // To keep track of which fan we are currently checking
boolean isFaultyFan = false;  // To keep track of whether or not we have one or more faulty fans (stuck, burned out or disconnected)
boolean isFansCheckedOnce = false;  // To keep track of whether or not fans have been checked at least once (to prevent mode change until fans have been checked at least once)

int dutyCycle = 0;  // Current duty cycle
int newDutyCycle = 0;  // Newly calculated duty cycle to compare to current duty cycle

unsigned long timeRpmStart = 0;  // Time we have started counting fan rotations to later compute RPM
volatile long int pulseCount = 0;  // Count of fan rotations (2 counts = one full rotation)
volatile boolean prevState = HIGH;  // Previous state of fan rotation/RPM pin
volatile boolean currentState = HIGH;  // Current state of fan rotation/RPM pin

unsigned long timeFanStart = 0;  // The time when fans were started (to check when they have been running for at least 3 seconds)
unsigned long timeDelayForNextTempCheck = 0;  // Used to check temperature every 10 seconds

unsigned long timePressedDown = 0;  // Time when push-button has been pressed down to check for short vs. long press
unsigned long timePressedDownIntervalStart = 0;  // Time when mode was last changed while push-button remained in the pressed down state

unsigned long timeLastLedBlinkSequence = 0;  // Time when the LED was last blinked to indicate that there is at least one faulty fan
unsigned long timeLastLedBlink = 0;  // Time when the LED was last blinked (set to on or off state)
int ledBlinkDuration = 100;  // How long the LED should remain on or off during a blinking operation
int countLedBlinks = 0;  // The number of times the LED has blinked / set on or off during a blink sequence operation
boolean isLedOn = false;  // To indicate if the LED is currently on or off

float startupTemp = 0.0;  // To store initial temperature when power first came on

void setup() {

  // Fan's RPM pin is "floating" (not LOW or HIGH), and is taken low twice per fan rotation when it is spinning
  // so we need to configure as INPUT_PULLUP
  for (int i=0; i <= 3; i++) {
    pinMode(fanRpmPin[i], INPUT_PULLUP);
    analogWrite(fanPwmPin[i], 0);  // Set each fan to 0% duty cycle (off)
  }

  pinMode(buttonPin, INPUT_PULLUP);

  pinMode(ledPin, OUTPUT);  // Pin to control LED to indicate when fans are on
  digitalWrite(ledPin, LOW);  // Set LED off

  pinMode(fanPowerPin, OUTPUT);  // Pin to control relay which powers all fans
  digitalWrite(fanPowerPin, LOW);  // Set to off / no 12 VDC power to fans

  // Initialize serial port
  //Serial.begin(115200);
  //while (!Serial) { }
  //Serial.println("Starting up...");

  // This fan controller will be installed in the SimBox and will come on when power is turned on for the entire SimBox
  // (therefore, before the PC is turned on, etc.).  We are going to get the temperature at this time (at initial startup)
  // and use this temperature as reference to determine if the PC compartment has warmed up since then, and by how much,
  // and will set the fan speeds accordingly whenever the fans are turned on.
  dht.begin();  // Activate the DHT22 temperature & humidity sensor
  //Serial.println("Waiting 5 seconds for temperature sensor.");
  delay(5000);
  float t = dht.readTemperature();
  if (!isnan(t)) {
    //Serial.print("Startup temperature is ");
    //Serial.print(t);
    //Serial.println(" degrees Celsius.");
    startupTemp = t;
  } else {
    //Serial.println("Startup temperature reading FAILED!");
    //Serial.println("Assuming 27 degrees Celsius (ideal temperature).");
    startupTemp = 27.0;
  }

  //Serial.println("Startup completed!");
}

void loop() {

  // If fans are on...
  if (isFansOn == true) {

    // If current mode is temperature controlled then we need to regularly check the temperature, adjust duty cycle, etc.
    // ...but only after we have given the fans 3 seconds to spin up and stabilize a bit
    if (Mode == TempControl && millis() > timeFanStart + 3000) {

      // If we are not currently checking fans then we should get temperature...
      if (isCheckingFans == false) {

        // ...but only once every 10 seconds!
        if (millis() > timeDelayForNextTempCheck + 10000) {

          // Read temperature
          float t = dht.readTemperature();

          // Check if read failed and proceed only if successful
          if (!isnan(t)) {
            //Serial.print("Current temperature is ");
            //Serial.print(t);
            //Serial.println(" degrees Celsius.");
            // Update PWM based on temperature, but only if we were able to get an initial temperature at startup
            // Calculate a new duty cycle if the current temperature is higher than the initial temperature at startup
            if (t > startupTemp) {
              // Using "* 40" in the below formula means that the fan will increase speed up to max speed when temp reaches
              // 5 degrees above startupTemp.  
              // Reduce from 40 to less if fan should be more reactive / should increase speed with less rise in temperature.
              newDutyCycle = 51 + ((t - startupTemp) * 40);
              if (newDutyCycle > 255) {
                newDutyCycle = 255;
              }
            } else {
              newDutyCycle = 51;
            }
            if (newDutyCycle != dutyCycle) {
              dutyCycle = newDutyCycle;
              //Serial.print("New duty cycle!  ");
              //Serial.print("analogWrite = ");
              //Serial.println(dutyCycle);
              for (int i=0; i <= 3; i++) {
                if (isFanOperational[i] == true) {  // Change PWM on operational fans only
                  analogWrite(fanPwmPin[i], dutyCycle);
                }
              }
            }
          } else {
            //Serial.println("Temperature reading FAILED!");
          }
          timeDelayForNextTempCheck = millis();

          // Now that we have checked the temperature, lets start checking the fans again
          isCheckingFans = true;
          // Get the number of the first operational fan to check...
          fanBeingChecked = 0;
          while (isFanOperational[fanBeingChecked] == false && fanBeingChecked < 3) {
            fanBeingChecked++;
          } 
          // If none of the fans are operational, turn off fans to stop checking temperature and whether or not they are turning (pointless)
          if (isFanOperational[fanBeingChecked] == false) {
            //Serial.println("No fans functioning / connected!");
            isCheckingFans = false;  // Stop checking fans
            isFansOn = false;  // Fans are all off now
            dutyCycle = 0; // 0% duty cycle (off)
            for (int i=0; i <= 3; i++) {
              analogWrite(fanPwmPin[i], dutyCycle);  // Set each fan to 0% duty cycle (off)
            }
            digitalWrite(fanPowerPin, LOW);  // Cut 12 volt power to fans
            digitalWrite(ledPin, LOW);  // Turn off LED
            isLedOn = false;  // LED is off
          } else {
            // ...otherwise add hardware interrupt for first fan to be checked.
            //Serial.println("Checking fans...");
            timeRpmStart = millis();  // Save current time to count fan rotations for 1 second
            pulseCount = 0;  // Zero out rotation count
            prevState = HIGH;
            currentState = HIGH;
            attachInterrupt(digitalPinToInterrupt(fanRpmPin[fanBeingChecked]), add_pulse, CHANGE);  // Attach hardware interrupt on fan rotation pin
          }
        }

      // ...otherwise we are in the process of checking fans
      } else {

        // We need to count fan revolutions for 1 second (done by add_pulse being called by hardware interrupt on fan's RPM pin)
        if (millis() > timeRpmStart + 1000) {

          detachInterrupt(digitalPinToInterrupt(fanRpmPin[fanBeingChecked]));

          // Disable fan if it is not rotating which means it is physically stuck, burned out or not connected
          if (pulseCount == 0) {
            isFaultyFan = true;  // We have a faulty fan (stuck, burned out or not connected)
            isFanOperational[fanBeingChecked] = false;  // Set fan to not operational so that we don't control it anymore
            analogWrite(fanPwmPin[fanBeingChecked], 0);  // Set it to 0% duty cycle (off)
            //Serial.print("Fan ");
            //Serial.print(fanBeingChecked);
            //Serial.println(" not operational!");
            timeLastLedBlinkSequence = millis();  // We want LED to start blinking 3 short pulses in 10 seconds from now to indicate that we have at least one faulty fan
          } else {
            // ...otherwise indicate current RPM
            //Serial.print("Fan ");
            //Serial.print(fanBeingChecked);
            //Serial.print(" RPM = ");
            // One fan rotation generates two pulses, so / 2 for number of rotations, * 60 because we sampled for only 1 second
            //Serial.println((pulseCount / 2) * 60);  
          }

          // If all 4 fans have been checked, stop checking fans and get new temperature reading when it is time again
          if (fanBeingChecked == 3) {
            isCheckingFans = false;  
            isFansCheckedOnce = true;
            //Serial.println("Finished checking fans.");
          } else {
            // ...otherwise check the next fan
            do {
              fanBeingChecked++;
            } while (isFanOperational[fanBeingChecked] == false && fanBeingChecked < 3);

            // If we have run out of operational fans, stop checking fans and get temperature reading
            if (isFanOperational[fanBeingChecked] == false) {
              isCheckingFans = false;  
              isFansCheckedOnce = true;
              //Serial.println("Finished checking fans.");
            } else {
              // ...otherwise lets start getting RPM readings for this next fan by activating/configuring a hardware interrupt
              timeRpmStart = millis();
              pulseCount = 0;
              prevState = HIGH;
              currentState = HIGH;
              attachInterrupt(digitalPinToInterrupt(fanRpmPin[fanBeingChecked]), add_pulse, CHANGE);
            }
          }
        }
      }
    }

    // Blink LED while in temperature controlled mode if one or more fans are faulty
    // ...but only every 10 seconds and as long as the push-button is not being pressed down
    if (isFaultyFan == true && Mode == TempControl && currentButton == HIGH && millis() > timeLastLedBlinkSequence + 10000) {
      if (millis() > timeLastLedBlink + 100) {  // Flip the LED on or off every 100 milliseconds
        if (isLedOn == true) {
          digitalWrite(ledPin, LOW);  // Turn off LED
          isLedOn = false;
          countLedBlinks++;
        } else {
          digitalWrite(ledPin, HIGH);  // Turn on LED
          isLedOn = true;
          countLedBlinks++;
          if (countLedBlinks > 7) {  // If we blinked the LED 3 times on/off...
            timeLastLedBlinkSequence = millis();  // Save current time so that we start blinking again in 10 seconds
            countLedBlinks = 0;  // Re-initialize blink counter
          }
        }
        timeLastLedBlink = millis();  // Save current time so that next blink on/off occurs in 100 milliseconds
      }
    }

    // Blink LED slowly, faster or fastest (depending on fixed speed/PWM duty cycle mode) when not in temperature controlled mode
    if (Mode != TempControl && millis() > timeLastLedBlink + ledBlinkDuration) {
      if (isLedOn == true) {
        digitalWrite(ledPin, LOW);  // Turn off LED
        isLedOn = false;
      } else {
        digitalWrite(ledPin, HIGH);  // Turn on LED
        isLedOn = true;
      }
      timeLastLedBlink = millis();  // Save current time so that next blink on/off occurs in 100 milliseconds
    }

  }

  // Check push-button  
  currentButton = debounce(lastButton, buttonPin);

  // If push-button has just been pressed down, save current time
  if (lastButton == HIGH && currentButton == LOW) {
    timePressedDown = millis();  // Used on release of push-button to determine if it was a short press vs. long press to change mode
    timePressedDownIntervalStart = millis();  // Used to change mode every 2 seconds while button remains pressed down
  }

  // If push-button is being kept down to change mode...
  if (isFansOn == true && lastButton == LOW && currentButton == LOW) {
    // If 2 seconds have elapsed since it was pushed down or since last mode change while remaining down, and fans
    // have been checked at least once...
    if (millis() > (timePressedDownIntervalStart + 2000) && isFansCheckedOnce == true) {
      Mode++;  // Switch to next mode
      if (Mode > 3) {
        Mode = 0;  // Loop back to default temperature controlled mode
      }
      switch (Mode) {
        case TempControl:  // Configure temperature controlled mode
          //Serial.println("Switching to temperature controlled mode.");
          dutyCycle = 51; // 20% duty cycle
          for (int i=0; i <= 3; i++) {
            if (isFanOperational[i] == true) {  // If fan is operational...
              analogWrite(fanPwmPin[i], dutyCycle);  // Set each fan to 20% duty cycle
            }
          }
          isCheckingFans = false;  // We are not checking fans yet (we want to get current temperature first)
          digitalWrite(ledPin, HIGH);  // Turn on LED
          isLedOn = true;  // LED is on
          timeFanStart = millis();  // Save current time to start checking temperature and fans in 3 seconds (to allow them time to spin up and stabilize)
          timeLastLedBlinkSequence = millis();  // Save current time so that we start blinking LED 3 short pulses in 10 seconds if we have one or more faulty fans
          break;
        case DutyCycle50:  // Configure 50% duty cycle mode
          //Serial.println("Switching to 50% duty cycle.");
          dutyCycle = 128; // 50% duty cycle
          for (int i=0; i <= 3; i++) {
            if (isFanOperational[i] == true) {  // If fan is operational...
              analogWrite(fanPwmPin[i], dutyCycle);  // Set each fan to 50% duty cycle
            }
          }
          digitalWrite(ledPin, HIGH);  // Turn on LED
          isLedOn = true;  // LED is on
          ledBlinkDuration = 100;  // Blink LED every 100 milliseconds
          break;
        case DutyCycle75:  // Configure 75% duty cycle mode
          //Serial.println("Switching to 75% duty cycle.");
          dutyCycle = 192; // 75% duty cycle
          for (int i=0; i <= 3; i++) {
            if (isFanOperational[i] == true) {  // If fan is operational...
              analogWrite(fanPwmPin[i], dutyCycle);  // Set each fan to 75% duty cycle
            }
          }
          digitalWrite(ledPin, HIGH);  // Turn on LED
          isLedOn = true;  // LED is on
          ledBlinkDuration = 75;  // Blink LED every 75 milliseconds (faster)
          break;
        case DutyCycle100:  // Configure 100% duty cycle mode
          //Serial.println("Switching to 100% duty cycle.");
          dutyCycle = 255; // 100% duty cycle
          for (int i=0; i <= 3; i++) {
            if (isFanOperational[i] == true) {  // If fan is operational...
              analogWrite(fanPwmPin[i], dutyCycle);  // Set each fan to 100% duty cycle
            }
          }
          digitalWrite(ledPin, HIGH);  // Turn on LED
          isLedOn = true;  // LED is on
          ledBlinkDuration = 50;  // Blink LED every 50 milliseconds (fastest)
          break;
      }
      timePressedDownIntervalStart = millis();
    }
  }

  // If push-button has been released and it was a short press (shorter than 2 seconds - not a long press to change mode)
  if (lastButton == LOW && currentButton == HIGH && millis() < timePressedDown + 2000) {
    if (isFansOn == false){  // If fans are currently off then turn them on
      //Serial.println("Turning fans on giving them 3 seconds to spin up before we start checking them.");
      digitalWrite(fanPowerPin, HIGH);  // Turn 12 volt power on for fans
      Mode = TempControl;  // Set/reset mode to default temperature controlled mode
      dutyCycle = 51; // 20% duty cycle
      for (int i=0; i <= 3; i++) {
        isFanOperational[i] = true; // Assume all fans are functional to recheck them all
        analogWrite(fanPwmPin[i], dutyCycle);  // Set each fan to 20% duty cycle
      }
      isFansOn = true;  // Fans are now on
      isFaultyFan = false;  // We don't have any faulty fans (current assumption)
      isCheckingFans = false;  // We are not checking fans yet (we want to get current temperature first)
      isFansCheckedOnce = false;  // To prevent change of mode until fans have been checked or re-checked at least once
      digitalWrite(ledPin, HIGH);  // Turn on LED
      isLedOn = true;  // LED is on
      timeFanStart = millis();  // Save current time to start checking temperature and fans in 3 seconds (to allow them time to spin up and stabilize)
    } else {  // Fans are currently on so we should turn them off...
      //Serial.println("Turning fans off...");
      digitalWrite(fanPowerPin, LOW);  // Cut off 12 volt power to fans
      dutyCycle = 0; // 0% duty cycle (off)
      for (int i=0; i <= 3; i++) {
        analogWrite(fanPwmPin[i], dutyCycle);  // Set each fan to 0% duty cycle (off)
      }
      isFansOn = false;  // Fans are now off
      digitalWrite(ledPin, LOW);  // Turn off LED
      isLedOn = false;  // LED is off
      //Serial.println("Fans turned off.");
    }
  }
  lastButton = currentButton;

}

void add_pulse() {
  // For some reason, when a non-zero and non-max duty cycle is set, the fan RPM pin fluctuates/spikes a few microvolts
  // up/down at every PWM transition which causes an interrupt.  For this reason, it is important to use the CHANGE 
  // mode interrupt, read the rpmPin and add a pulse only when there is a transition from (previously) LOW to (now) HIGH.
  // Note:  The voltage "spike" up/down lasts for a few microseconds,so by the time this code runs the voltage has
  // stabilized once again.
  currentState = digitalRead(fanRpmPin[fanBeingChecked]);
  if (prevState == LOW && currentState == HIGH) {
    pulseCount++;  
  }
  prevState = currentState;
}

// Button debouncer
boolean debounce(boolean last, int pin) {
  boolean current = digitalRead(pin);
  if (last != current) {
    delay(15);
    current = digitalRead(pin);
  }
  return current;
}

Credits

Alan De Windt
4 projects • 23 followers
Currently a Business Analyst and UI/UX Designer. Started career as a developer, still coding but as a hobby only.
Contact

Comments

Please log in or sign up to comment.