How to properly control PWM fans with Arduino

Introduction

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

Connecting the fan to the Arduino

We can connect up to 3 PWM fans to a single Arduino.

This is the pinout of a standard PWM fan: Fan connector

  • Black: Ground
  • Yellow: +5V, +12V or +24V (depends on fan model, usually 12V for desktops, 5V for laptops)
  • Green: Sense. Used to measure RPM
  • Blue: PWM control signal at 5V, 25kHz

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: Schematic

PWM control

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.

Code

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

Explanation

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: Oscilloscope outputs of the various pins showing 50% PWM with default settings

As you can see it's nowhere near the required frequency. You can run a fan with this signal but it will behave erratically.

  • Timer 1 (Pins 9,10) is a high resolution 16 bit timer. This is used by libraries like Servo. We want to take the clock as it is (no prescale) and feed it to the counter; we use mode 10, which counts up to the value of register ICR1, which we set to 320 instead of 65535, giving us a period of roughly 25 kHz. OCR1A and OCR1B control the duty cycle of our output PWM on pins 9 and 10 respectively, independently. We have 320 possible values for the duty cycle with this timer.
  • Timer 2 (Pins 3,11) is a low resolution 8 bit timer. This is used by many libraries and functions such as tone. This timer doesn't have an ICR register like Timer 1, so instead we divide the clock by 8 (prescale 8) and feed it to the counter; we use mode 7 and set output A to trigger a reset of the counter when it reaches 79 instead of 255, resulting in a period of roughly 25 kHz. OCR2B controls the duty cycle of our output PWM on pin 3. Pin 11 is unusable for output because of this mode. We only have 79 possible values for the duty cycle with this timer.
  • Timer 0 (Pins 5,6) is identical to Timer 2, so we could apply the same settings that we used for it and get an extra output for a fourth fan; however, it is used for all timing functions such as delay, millis, etc. and touching it would cause everything that depends on this to behave erratically, including our Serial output. My code doesn't touch this timer.

This is the PWM output on the 3 channels generated by the example code: Oscilloscope output of 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.

RPM detection

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());
}

License

You are free to do what you want with the code on this page.

Project based on this code

Nano Fan Controller

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.

Device

Additional reading on Arudino timers and PWM

Share this article

Comments