Computers have been using PWM-controlled fans for ages now (they're the ones with a 4 pin connector). This allows the BIOS to change the fan speed according to the current temperatures using a PWM signal instead of changing the voltage of the fan, which means that motherboards are cheaper to make (less voltage regulators), but also that fans are much easier to control, since DC motors won't even move below a certain voltage threshold.
The exact specs of these fans were made by Intel in the mid-00s and are available here: Original | Latest version | Noctua
We can connect up to 3 PWM fans to a single Arduino.
This is the pinout of a standard PWM fan:
Notice the presence of a notch on the connector: this is to ensure that you don't connect it backwards, and also to ensure compatibility with older 3 pin connectors. Polarity protection is a requirement of the spec so even if you force it, you won't damage the motherboard or the fan.
If you have a 12V fan, the best way to power it is to put the Arduino and the fan in parallel, using the VIN pin to power the Arduino. It's a good idea to put a diode in front of the VIN pin if you have it, that way you can connect both the 12V and the USB without damaging anything.
If you have a 5V fan, you can power it directly from the 5V pin on the Arduino, but I don't recommend it if it draws more than 4-500 mA, as it could damage the voltage regulator on the Arduino, and also generate noise that could cause the Arduino to be unstable. If unsure, power the Arduino and the fan in parallel with a 5V supply connected to both the fan and the 5V pin.
If you have a 24V fan, you'll have to power both the fan and the Arduino externally. Note that the Arduino can only take up to 12V as input in the VIN pin, and the Arduino and the fan MUST share the same ground!
Note: if you're using really shitty fans, you should add a flyback diode between the ground and power pins of each fan.
Important: the VIN pin on the Arduino has no polarity protection, double check your connections before turning it on!
Example schematic for a single 12V fan:
This is the tricky part. The specs require a PWM signal with a frequency of 25 kHz (with tolerance, 21-28 kHz), but our usual analogWrite function doesn't output anywhere near that frequency. By using some timer tricks, we can make it generate 3 PWM signals at the correct frequency. I'll show you the code first and then explain it.
//configure Timer 1 (pins 9,10) to output 25kHz PWM
void setupTimer1(){
//Set PWM frequency to about 25khz on pins 9,10 (timer 1 mode 10, no prescale, count to 320)
TCCR1A = (1 << COM1A1) | (1 << COM1B1) | (1 << WGM11);
TCCR1B = (1 << CS10) | (1 << WGM13);
ICR1 = 320;
OCR1A = 0;
OCR1B = 0;
}
//configure Timer 2 (pin 3) to output 25kHz PWM. Pin 11 will be unavailable for output in this mode
void setupTimer2(){
//Set PWM frequency to about 25khz on pin 3 (timer 2 mode 7, prescale 8, count to 79)
TIMSK2 = 0;
TIFR2 = 0;
TCCR2A = (1 << COM2B1) | (1 << WGM21) | (1 << WGM20);
TCCR2B = (1 << WGM22) | (1 << CS21);
OCR2A = 79;
OCR2B = 0;
}
//equivalent of analogWrite on pin 9
void setPWM1A(float f){
f=f<0?0:f>1?1:f;
OCR1A = (uint16_t)(320*f);
}
//equivalent of analogWrite on pin 10
void setPWM1B(float f){
f=f<0?0:f>1?1:f;
OCR1B = (uint16_t)(320*f);
}
//equivalent of analogWrite on pin 3
void setPWM2(float f){
f=f<0?0:f>1?1:f;
OCR2B = (uint8_t)(79*f);
}
void setup(){
//enable outputs for Timer 1
pinMode(9,OUTPUT); //1A
pinMode(10,OUTPUT); //1B
setupTimer1();
//enable outputs for Timer 2
pinMode(3,OUTPUT); //2
setupTimer2();
//note that pin 11 will be unavailable for output in this mode!
//example...
setPWM1A(0.5f); //set duty to 50% on pin 9
setPWM1B(0.2f); //set duty to 20% on pin 10
setPWM2(0.8f); //set duty to 80% on pin 3
}
void loop(){
//do what you want...
}
The PWM signals on the Arduino Uno (and others based on the ATmega328p, such as the Nano) are generated by 3 internal timers. With the default settings and using analogWrite with a value of 127, this is what we see on an oscilloscope:
As you can see it's nowhere near the required frequency. You can run a fan with this signal but it will behave erratically.
This is the PWM output on the 3 channels generated by the example code:
As you can see in the bottom left corner, the oscilloscope detects a 25 kHz signal on all outputs.
Apart from having to remember not to use functions and libraries that use Timers 1 and 2, there are no real drawbacks from using this code. The signal looks a bit less clean, but that's because the frequency is over 50 times higher.
Note that the official spec says that the minimum duty cycle for the fans should be 20%. Decent fans like the Noctua ones I used in my tests can take any duty cycle, but you should keep it in mind if you're using cheaper fans.
The rotation sensor on the fan can interface with the Arduino. It expects to be connected with a pullup resistor and it generates 2 impulses per rotation.
The best way to read this is to use one of the Arduino pins that can do interrupts (2 and 3 on the Arduino Uno). I will use pin 2 in this example:
#define PIN_SENSE 2 //where we connected the fan sense pin. Must be an interrupt capable pin (2 or 3 on Arduino Uno)
#define DEBOUNCE 0 //0 is fine for most fans, crappy fans may require 10 or 20 to filter out noise
#define FANSTUCK_THRESHOLD 500 //if no interrupts were received for 500ms, consider the fan as stuck and report 0 RPM
//Interrupt handler. Stores the timestamps of the last 2 interrupts and handles debouncing
unsigned long volatile ts1=0,ts2=0;
void tachISR() {
unsigned long m=millis();
if((m-ts2)>DEBOUNCE){
ts1=ts2;
ts2=m;
}
}
//Calculates the RPM based on the timestamps of the last 2 interrupts. Can be called at any time.
unsigned long calcRPM(){
if(millis()-ts2<FANSTUCK_THRESHOLD&&ts2!=0){
return (60000/(ts2-ts1))/2;
}else return 0;
}
void setup(){
pinMode(PIN_SENSE,INPUT_PULLUP); //set the sense pin as input with pullup resistor
attachInterrupt(digitalPinToInterrupt(PIN_SENSE),tachISR,FALLING); //set tachISR to be triggered when the signal on the sense pin goes low
Serial.begin(9600); //enable serial so we can see the RPM in the serial monitor
}
void loop(){
delay(100);
Serial.print("RPM:");
Serial.println(calcRPM());
}
You are free to do what you want with the code on this page.
I made a simple fan controller to put in a computer case, powered directly from one of the fan headers on the motherboard. This controls up to 3 fans to keep the temperature inside the case in check.