Temperature Control With Arduino and PWM Fans





Introduction: Temperature Control With Arduino and PWM Fans

Temperature control with PID on Arduino and PWM fans for DIY server/network rack cooling

A few weeks ago I needed to setup a rack with network devices and a few servers.

The rack is placed in a closed garage, so the temperature range between winter and summer is pretty high, and also dust could be a problem.

While browsing the Internet for cooling solutions, I found out that they're pretty expensive, in my place at least, being >100€ for 4 230V ceiling-mounted fans with a thermostat control. I didn't like the thermostat drive because it sucks in a lot of dust when powered, because of the fans going full power, and gives no ventilation at all when unpowered.

So, unsatisfied with these products, I decided to go the DIY way, building something that can smoothly mantain a certain temperature.

Step 1: How It Works

To make things a lot easier I went for DC fans: they're much less noisy than AC fans while baing a bit less powerful, but they're still more than enough for me.

The system uses a temperature sensor to control four fans that are driven by an Arduino controller. The Arduino throttles the fans using PID logic, and drives them through PWM.

The temperature and fan speed are reported through a 8-digit 7-segment display, fitted on a rack-mounted aluminium bar. Besides the display there are two buttons for tuning the target temperature.

Step 2: ​What I Used

Note: I tried to realize this project with things I had lying in the house, so not everything can be ideal. Budget was a concern.

Here are the components I used:

  • Hardware
    • One acrylic panel: used as the base (€ 1.50);
    • Four 3.6x1cm L shaped PVC profiles (€ 4.00);
    • One aluminum panel: cut at 19" in width (€ 3.00);
  • Electronics
    • Four 120mm PWM fans: I went for Arctic F12 PWM PST because of the ability to stack them in parallel (4x € 8.00);
    • One Pro Micro: Any ATMega 32u4 powered board should work fine with my code (€ 4.00);
    • One relay board: to switch off the fans when they're not needed (€ 1.50);
    • One 8 digit 7-segment MAX7219 display module (€ 2.00);
    • Three momentary push buttons, 1 is for reset (€ 2.00);
    • One 3A power switch (€ 1.50);
    • One LAN cable coupler: to easilly disconnect the main assembly to the display panel (€ 2.50);
    • One 5V and 12V dual output power supply: You can use 2 separated PSUs or a 12V with a step down converter to 5V (€ 15.00);

    • Cables, screws and other minor components (€ 5.00);

Total cost: € 74.00 (if I had to buy all the components on Ebay/Amazon).

Step 3: The Case

The case is made of 4 thin L-shaped plastic profiles glued and riveted to an acrylic board.

All the components of the box are glued with epoxy.

Four 120mm holes are cut in the acrylic to fit the fans. An additional hole is cut for letting the thermometer cables pass through.

The front panel has a power switch with an indicator light. On the left, two holes let the front panel cable and the USB cable go out. An additional reset button is added for easier programming (the Pro Micro doesn't have a reset button, and sometimes it's useful in order to upload a program onto it).

The box is held up by 4 screws passing through holes the acrylic base.

The front panel is made of a brushed aluminum panel, cut at 19" in width and with a height of ~4cm. The display hole was made with a Dremel and the other 4 holes for screws and buttons were made with a drill.

Step 4: Electronics

The control board is pretty simple and compact. During the making of the project, i found out that when I supply 0% PWM to the fans, they will run at full speed. To completely stop the fans from spinning, I added a relay that shuts off the fans when they're not needed.

The front panel is connected to the board through a network cable that, using a cable coupler, can be easilly detached from the main enclosure. The back of the panel is made of a 2.5x2.5 electrical conduit and fixed to the panel with double-sided tape. The display is also fixed to the panel with tape.

As you can see in the schematics, I've used some external pullup resistors. These provide a stronger pullup than the arduino's.

The Fritzing schematics can be found on my GitHub repo.

Step 5: The Code

Intel's specification for 4-pin fans suggests a 25KHz target PWM frequency and 21 kHz to 28 kHz acceptable range. The problem is that Arduino's default frequency is 488Hz or 976Hz, but the ATMega 32u4 is perfectly capable of delivering higher frequencies, so we only need to set it up correctly. I referred to this article about the Leonardo's PWM to clock the fourth timer to 23437Hz which is the closest it can get to 25KHz.

I used various libraries for the display, the temperature sensor and the PID logic.

The full updated code can be found on my GitHub repo.

Step 6: Conclusion

So here it is! I have to wait till this summer to actually see it in action, but I'm pretty confident it'll work fine.

I'm planning on making a program to see the temperature from the USB port that I connected to a Raspberry Pi.

I hope that everything was understandable, If not let me know and I will explain better.




    • Oil Contest

      Oil Contest
    • Clocks Contest

      Clocks Contest
    • Water Contest

      Water Contest

    25 Discussions

    Hi, first of all great project! Im trying to turn this into a cheap version of DBE for my home heating. So all I basicly need is two temperature sensors, relay and fan to control.

    But im having some trouble stripiing out your LCD code, anyway you can help me out with it?

    1 reply

    Hi, thank you! what issue are you having?

    Yes, I used all the libraryes you indicated. I tested the software before commiting to the display library and the presentation was confusing, lacking character, etc ... After the commit, everything was ok.
    When I finish I show the photos.
    Thank you


    Yes, I installed because without this library, the display does not work right, mainly the messages.

    But I noticed that your project does not work with DHT11, only with DHT22.

    But, as I said, my contouring solution was to turn off the specific display, and change the startup message to fit, without that digit, and it is working on the Protoboard to content.

    I also tested with the fan and it was very good

    The use of this project will be in a Rack of 3 U that I am doing to accommodate my equipment ... In the vacant hours, I like to play bass guitar ... I will make a panel integrated to the Rack.

    Once you have something ready, post the photos.

    Thanks a lot for the help.

    1 reply

    Have you tried this https://github.com/giech/LedControl
    As I stated in the instructable you will need to edit the library in order to make some text to work. the one above is the link to the branch with the proper modifications


    Tip 5 months ago

    Today the DHT22 sensor arrived.
    To my happiness, it worked okay: The startup, the correct temperature indication and the proportional display of PWM fan operation.

    But the indication of when I try the set temperature, the digit number 5 appears with the indication of the number nine ... = SET 930c

    This should be something wrong with some MAX7219 configuration table ...
    But if there is no way, I can inhibit this digit 5 .. I put the initial message to = FAn__ctL.

    But here my congratulations for the great project.

    As soon as I finish, I can post some photos ok?

    Thank you

    1 reply


    I did what I suggested: Test the DHT11 sensor ...

    I used the example contained in the library, running on an Arduino Pro-Mini # 328 16Mhz ...

    The result is below:

    Status Humidity (%) Temperature (C) (F)

    OK 63.0 30.0 86.0

    OK 63.0 30.0 86.0

    OK 62.0 30.0 86.0

    OK 62.0 30.0 86.0

    What I see in the temperature reading, in your sketch is something close to the value indicated here in Fahrenheit ...

    It may not be compatible since you used the DHT22 ..

    Anyway, I await the arrival of the DHT22 that I bought ... when I arrive, I test and put the result.

    Thank you


    I'm using the Arduino Pro Micro (32u4), the display is the same, with the CI MAX7219.

    I'm testing on a proto-board. The fan is already in 100% of the rotation when turning on, because the temperature indication is high and the display of the fan is 100.

    The temperature reading is crazy, which I believe is sensor failure. But when I turn the sensor off, the reading drops to zero and the fan goes OFF.

    But the Temperature Set functions are strange, as I said ... The setting is between 900 and 999.

    I just changed the pins on the Arduino, to those I had defined in your software, and it's working that way too. Maybe you did something wrong the first time.

    Thank you.

    1 reply

    Have you tested the thermometer alone? Try loading an example sketch from the DHT library and see if the behaviour is correct. I have this code running just fine and I also know of a few people that are having no problems with it, so I doubt that's a fault on my part but we shall see.


    I had a DHT11 sensor, but the reading is crazy ... showing an ambient temperature of 98 Degrees ... As I can be with a sensor with problems, I am getting the DHT22, according to your design.

    I also changed the pins for connecting the display:

    // 7-segment display

    #define SEG_DIN 14

    #define SEG_CLK 15

    #define SEG_CS 10

    and includes the modification to display your code in the diaplay.

    The rest kept its code available in the repository, which follows :


    * fancontrol is free software.

    * Copyright (C) 2017 Matteo Bonora (bonora.matteo@gmail.com) - All Rights Reserved


    * fancontrol is available under the GNU LGPLv3 License which is available at <http://www.gnu.org/licenses/lgpl.html>

    This is a temperature-based fan controller using PID logic and PWM signals to control PC fans.

    Check out my instructable on this project for more info


    The PWM frequency is set to 23437 Hz that is within the 21k-25k Hz range so it should work with any PC fan.

    For more details on how PWM works on the 32u4, check this link:


    Note that the 32u4 has 2 pins (6 and 13) hooked to Timer4, but since my board doesn't have pin 13 I only configure pin 6.

    A RPM reading system is also featured in this example (although it has proven to be not that accurate, at least on my setup).

    This code has been tested on a SparkFun Pro Micro 16MHz Clone with 4 Arctic F12 PWM PST Fans connected to the same connector.


    #include <PID_v1.h> // https://github.com/br3ttb/Arduino-PID-Library

    #include <DHT.h> // https://github.com/markruys/arduino-DHT

    #include <LedControl.h> // https://github.com/wayoda/LedControl

    #include <EEPROM.h>

    // Change this if you want your current settings to be overwritten.

    #define CONFIG_VERSION "f01"

    // Where to store config data in EEPROM

    #define CONFIG_START 32

    // Pin 6 shortcut

    #define PWM6 OCR4D

    // Terminal count

    #define PWM6_MAX OCR4C

    /* Pinouts */

    #define SPD_IN 7 // RPM input pin

    // 7-segment display

    #define SEG_DIN 14 //select by marcos

    #define SEG_CLK 15 //select by marcos

    #define SEG_CS 10 //select by marcos

    #define RELAY 9 // Relay output pin

    #define TEMP_IN 5 // Temperature sensor input pin // select by marcos

    #define TARGET_UP 18 // Up/Down buttons pins

    #define TARGET_DOWN 19

    #define DBG false

    // Debug macro to print messages to serial

    #define DEBUG(x) if(DBG && Serial) { Serial.print (x); }

    // Tells the amount of time (in ms) to wait between updates

    #define WAIT 500

    #define DUTY_MIN 64 // The minimum fans speed (0...255)

    #define DUTY_DEAD_ZONE 64 // The delta between the minimum output for the PID and DUTY_MIN (DUTY_MIN - DUTY_DEAD_ZONE).

    #define KP 0.4

    #define KI 0.4

    #define KD 0.05

    /* Target set vars */

    bool targetMode = false;

    bool lastUp = false, lastDown = false;

    bool up, down = false;

    /* RPM calculation */

    volatile unsigned long duration = 0; // accumulates pulse width

    volatile unsigned int pulsecount = 0;

    volatile unsigned long previousMicros = 0;

    int ticks = 0, speed = 0;

    unsigned long prev1, prev2, prev3 = 0; // Time placeholders

    double duty;

    // Display temp, .5 rounded and Compute temp, integer (declared as double because of PID library input);

    double dtemp, ctemp;

    // Fan status

    bool fanRunning = true;

    // Settings

    struct StoreStruct


    // This is for mere detection if they are your settings

    char version[4];

    // The variables of your settings

    double target;

    } storage = { // Default values




    // Initialize all the libraries.

    PID fanPID(&ctemp, &duty, &storage.target, KP, KI, KD, REVERSE);

    LedControl lc = LedControl(SEG_DIN, SEG_CLK, SEG_CS, 1);

    DHT sensor;

    /* Configure the PWM clock */

    void pwm6configure()


    //TCCR4B configuration

    TCCR4B = 4; /* 4 sets 23437Hz */

    // TCCR4C configuration

    TCCR4C = 0;

    // TCCR4D configuration

    TCCR4D = 0;

    // PLL Configuration

    PLLFRQ = (PLLFRQ & 0xCF) | 0x30;

    // Terminal count for Timer 4 PWM

    OCR4C = 255;


    // Set PWM to D6 (Timer4 D)

    // Argument is PWM between 0 and 255

    void pwmSet6(int value)


    OCR4D = value; // Set PWM value

    DDRD |= 1 << 7; // Set Output Mode D7

    TCCR4C |= 0x09; // Activate channel D


    /* Called when hall sensor pulses */

    void pickRPM ()


    volatile unsigned long currentMicros = micros();

    if (currentMicros - previousMicros > 20000) // Prevent pulses less than 20k micros far.


    duration += currentMicros - previousMicros;

    previousMicros = currentMicros;




    /* Settings management on the EEPROM */

    void loadConfig()


    // Check if saved bytes have the same "version" and loads them. Otherwise it will load the default values.

    if (EEPROM.read(CONFIG_START + 0) == CONFIG_VERSION[0] &&



    for (unsigned int t = 0; t < sizeof(storage); t++)

    *((char*)&storage + t) = EEPROM.read(CONFIG_START + t);


    void saveConfig()


    for (unsigned int t = 0; t < sizeof(storage); t++)

    EEPROM.update(CONFIG_START + t, *((char*)&storage + t));



    /* Writes 'str' to the lcd, starting at 'index' */

    void writeSeg(const char str[], byte index)


    int size = strlen(str);

    for (int i = 0; i < size; i++)


    lc.setChar(0, index + i, str[(size - 1) - i], false);



    /* writes the temperature on the lcd. 'off' defines the offset and dInt defines whether the temp is an int or a float */

    void writeTemp(float temp, byte off, bool dInt = false)


    byte t[3];

    if (!dInt) // If it's a float, then multiply by 10 to get rid of the decimal value


    temp *= 10;


    // Split the value in an array of bytes

    t[0] = (int)temp % 10;

    temp /= 10;

    t[1] = (int)temp % 10;

    if (!dInt)


    temp /= 10;

    t[2] = (int)temp % 10;


    // Do the actual printing

    for (byte i = 1; i < 4; i++)


    lc.setDigit(0, i + off, t[i - 1], (i == 2 && !dInt));


    lc.setChar(0, off, 'C', false);


    /* Calls the right functions to fill the left half of the lcd */

    void writeLeft()


    if (targetMode)


    writeSeg("Set ", 4);




    writeTemp(dtemp, 4);



    /* Calls the right functions to fill the right half of the lcd */

    void writeRight()


    if (targetMode)


    writeTemp(storage.target, 0, true);




    char tmp[5];

    if (fanRunning)


    sprintf(tmp, "%4u", map(round(duty), 0, 255, 0, 100));




    strcpy(tmp, " 0ff");


    writeSeg(tmp, 0);



    /* Routine that updates the display */

    void printSeg()





    void setup()



    if (DBG)





    pinMode(SPD_IN, INPUT);

    pinMode(RELAY, OUTPUT);



    attachInterrupt(digitalPinToInterrupt(SPD_IN), pickRPM, FALLING);



    lc.shutdown(0, false);

    lc.setIntensity(0, 15);

    writeSeg("Fan ctrl", 0);




    // Setup the PID to work with our settings


    fanPID.SetOutputLimits(DUTY_MIN - DUTY_DEAD_ZONE, 255);




    // Let the fan run for 5s. Here we could add a fan health control to see if the fan revs to a certain value.






    prev1 = millis();


    void loop()


    unsigned long cur = millis();

    bool shouldPrint = false;

    lastUp = up;

    lastDown = down;

    up = !digitalRead(TARGET_UP);

    down = !digitalRead(TARGET_DOWN);

    if (cur - prev3 >= sensor.getMinimumSamplingPeriod())


    if (sensor.getStatus() == 0)


    prev3 = cur;

    double t = sensor.getTemperature();

    /* Sometimes I get a checksum error from my DHT-22.

    To avoid exceptions I check if the reported temp is a number.

    This should work only with the "getStatus() == 0" above, but it gave me errors anyway, So I doublecheck */

    if (!isnan(t))


    dtemp = round(t * 2.0) / 2.0;

    ctemp = round(t);





    // If there's an error in the sensor, wait 5 seconds to let the communication reset

    prev3 += 5000;




    fanPID.Compute(); // Do magic

    if (cur - prev1 >= WAIT)


    prev1 = cur;

    unsigned long _duration = duration;

    unsigned long _ticks = ticks;

    duration = 0;

    // Calculate fan speed

    float Freq = (1e6 / float(_duration) * _ticks) / 2;

    speed = Freq * 60;

    ticks = 0;

    // Turn the fans ON/OFF

    if (round(duty) < DUTY_MIN)


    digitalWrite(RELAY, HIGH);

    PWM6 = 0;

    fanRunning = false;




    fanRunning = true;

    PWM6 = duty;

    digitalWrite(RELAY, LOW);


    shouldPrint = true; // Things have changed. remind to update the display


    DEBUG(" - Target: ");


    DEBUG(" - Temp: ");


    DEBUG(" - Duty: ");

    DEBUG(map(round(duty), 0, 255, 0, 100));



    /* Checks if the +/- buttons are pressed and if it's not the first time they've been pressed. */

    if (up && !lastUp == up && targetMode && storage.target < 255)




    if (down && !lastDown == down && targetMode && storage.target > 0)




    /* If either + or - buttons are pressed, enter target mode and display the current target on the lcd. */

    if (up || down)


    targetMode = true;

    shouldPrint = true;

    prev2 = cur;


    /* If 3 secs have elapsed and no button has been pressed, exit target mode. */

    if (targetMode && cur - prev2 >= 3000)


    targetMode = false;

    shouldPrint = true;

    saveConfig(); // Save the config only when exiting targetMode to reduce EEPROM wear


    if (shouldPrint)




    Is It Something That Needs Adjusted?

    Thank you

    1 reply

    Have you tried plugging the display to my original pins? What board are you running this on? Does the rest of the program work?


    Thank you for sharing this project.

    I build the control.

    But when I press the temperature set buttons, the display shows: "SET 900" to "SET 999" ... is this correct?

    What should I change in the code?

    thank you

    1 reply

    I don't think that's correct. PM me with further info on how you implemented the controller (components/code).

    Thank you for sharing your code and schematic. I built the control circuit and everything works!I initially omitted the diode on the PWM output and the fan was emitting a very high pitch sound. I then added a 1N914 diode and the noise disappeared.

    I guess my problem is solved, but I'm really curious as to what the diode does and why it makes the fan quieter. Thank you!

    Good afternoon. Sorry, but the sketch is compiled. Libraries are installed. Error in line 86 (DHT dht sensor; expected initializer before 'sensor'). Please advise the problem. Thank you.

    1 reply

    Mhm... this error usually means that you have forgotten a semicolon or a bracket somewhere... Hope that fixes it. PM me if you have further problems

    My son and I are building a box for the electronics for his CNC plasma table. I figure you can't have too much cooling. The air in the shop is anywhere from 30 to 95 degrees F. We are using a 13"x2.75" automotive air filter and two 120V 100 cfm fans. The fans are on all of the time when the box is powered up. I can see where you would save some energy only running the fans when cooling is needed, but with a 4 KVA air compressor and a 7 KVA plasma torch a couple of fans will not make much difference.


    1 year ago

    i was just searching something like this couple of days ago lol, but wouldnt be cheaper if you used just regular 3 pin fan, and pwm control them on input pins? just like regular motors

    1 reply

    Well, you will actually need a transistor to drive a 12v fan through PWM, And you will not be able to read the speed of the fan. Another solution is to drive the fan using a voltage regulator, but this adds complexity, inefficiency and cost.