Introduction: Temperature Control With Arduino and PWM Fans

Picture of 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

Picture of 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

Picture of ​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

Picture of 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

Picture of 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.

A little note on LedControl library: to make it work with my code, you need to include this pull request #13 which simply extends the default character map of the library. This is only neccessary to display the startup screen and the "Set" word when setting the temperature, so it can be easily avoided.

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.



Maralme (author)2018-01-21


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.

Bonny97 (author)Maralme2018-01-22

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.

Maralme (author)2018-01-21


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 ( - All Rights Reserved


* fancontrol is available under the GNU LGPLv3 License which is available at <>

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> //

#include <DHT.h> //

#include <LedControl.h> //

#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, &, 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 ( + 0) == CONFIG_VERSION[0] && + 1) == CONFIG_VERSION[1] && + 2) == CONFIG_VERSION[2])

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

*((char*)&storage + t) = + 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(, 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);




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 && < 255)



if (down && !lastDown == down && targetMode && > 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

Bonny97 (author)Maralme2018-01-21

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

Maralme (author)2018-01-20


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

Bonny97 (author)Maralme2018-01-21

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

FrédérikD2 (author)2017-12-26

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!

Vlad6511 (author)2017-05-29

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.

Bonny97 (author)Vlad65112017-05-30

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

CPUDOCTHE1. (author)2017-02-27

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.

tutdude98 (author)2017-02-24

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

Bonny97 (author)tutdude982017-02-24

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.

tutdude98 (author)Bonny972017-02-24

well transistor should be alot cheaper than pwm fans,and you wont have to use relay, since you can just set it to 0%, maybe you could use lcd to show duty cycle

Bonny97 (author)tutdude982017-02-24

I just checked on Amazon, and the difference between PWM and non PWM fans is just 16cents, so yes, you would save about 1€ but you won't be able to see the actual RPM. Don't know if worth, but if you have 3-pin fans lying around the house it'll just work fine.

booga007 (author)Bonny972017-02-26

Actually, a regular 3-pin fan will show you the RPM on the 3rd wire.

The 4-pin PWM fan has, Power (+12V), GND (0v), RPM monitor and PWM signal; a pulsed level to regulate the speed of the fan between 0-100% duty cycle, time based not voltage based.

The advantage is that it is still the full 12v that the fan see's, thus it can sustain much slower speeds than a 3-pin fan. As the 3-pin fan you regulate the voltage going to the power pin and fans will have a minimum turn on voltage.

Thanks for the idea's in this 'ible tho :)

Bonny97 (author)booga0072017-02-27

Il you drive the fan with PWM, the RPM reading will be wrong because you're basically switching the fan on and off. Some fans might still report a correct measure if the RPM reading works with the fan turned off, but it's not guaranteed. Happy that you enjoyed my instructable ?

Jakes workshop (author)2017-02-24

thanks for an awesome tutorial, this is a great way to make an external laptop cooling platform

About This Instructable




More by Bonny97:Temperature control with Arduino and PWM fansRaspberry Pi Audio Player
Add instructable to: