PWM a 3-pin PC fan with an Arduino

Note: 4-pin fans already have a PWM signal

Aeroscope Measuring a Fan

A question came up on IRC regarding how to PWM a 3-pin PC fan with an Arduino using analogWrite(). Controlling the fan was seemingly straightforward. The problem was that the hall effect sensor, or TACH signal, was incredibly noisy. The noise made it impossible to measure the fan’s rotation. Working through the question, I found three issues to tackle:

  1. You need to use a PNP transistor
  2. Filter capacitors help
  3. Create a non-blocking RPM measurement (with millis())

This post addresses all three issues regarding how to PWM a 3-pin PC fan with an Arduino.

1. You need a PNP Transistor

Most tutorials show an NPN transistor driving a motor as a low-side switch. However, the problem with this approach is that you are switching the ground (GND) path. This method doesn’t provide a return path when the FAN is turned off. Regarding the fan’s operation, it will turn on and off as you expect. However, the lack of ground path means the switching can create electromagnetic interference (EMI).

NPN Low Side vs High Side Switch

Figure 1: Don’t do this!

The proper way to power a fan is with a high-side switch. This circuit type switches the high voltage on and off, instead of ground. But you can’t use a NPN. Figure 1 shows the problem. When configured as a high-side switch the voltage from across VBE remains 0.7 volts. Which means if there is 5 volts at the base, you only get 4.3 volts at the collector. You were probably expecting it to be 11.3 volts, weren’t you? Well, that’s not how a NPN BJT works.

Instead, we can use a PNP transistor for the high-side switch. This circuit does present a problem when using 5 volts on the base and 12 V on the emitter.

PNP Only

Figure 2: Oops. 12V rail with a 5V control.

So the problem is that when the I/O pin is HIGH or LOW, there isn’t enough voltage to turn off the transistor. And you create an awkward voltage divider between the I/O pin and the base of the transistor. If you looked at the collector (shown as pin 2 in this diagram) on an oscilloscope, you’d see it stay a steady DC voltage. Not very useful is it?

To solve the problem of how to use a PNP transistor with an Arduino, you need to add an NPN driver. It seems kind of silly doesn’t it? Well, doing things right isn’t always easy. Or something to that effect.

NPN-PNP Driver Example

If we add a NPN transistor before the PNP, it can be used to switch the high-voltage supply on and off. This driver would allow the PNP’s base to see a wide voltage range. The NPN can pull the PNP’s base, with its current limiting resistor, to 12 volts. This mode will prevent 0.7 volts from dropping across the Base-Emitter diode keeping the transistor OFF. When the NPN drops to ground, then VBE becomes active, and the transistor turns on.

NPN-PNP Driver HIGH

Figure 3: I/O Pin is HIGH, Motor is ON

In Figure 3, I’m showing what happens with the NPN-PNP driver when the Arduino drives 5 volts. It turns on the NPN, which connected the PNP’s base resistor to ground. This path allows 0.7 volts to drop across the PNP’s Emitter to Base junction. The transistor turns ON and the motor spins.

NPN-PNP Driver LOW

Figure 4: I/O Pin is LOW, Motor is OFF.

Now, when the I/O pin goes LOW, something more interesting happens. The NPN turns into an open because it’s Base-Emitter junction is off. So that leaves the PNP’s base resistor floating. So that means if there is 12 volts on the Emitter, that is the anode of the junction diode. Since current can’t flow through the NPN, the cathode of the junction diode is effectively floating, meaning it will show the 12V connected to its anode. So the VBE becomes: 12v – 12v = 0v. This relationship keeps the PNP, and the motor, OFF.

2. Filter Capacitors Help when you PWM a 3-Pin PC fan with an Arduino

Once this was all setup, I connected my scope and saw the following:

Noisy RPM SIgnal with PWM

Figure 5: Noisy RPM Signal with PWM

All of the noise spikes shown are finding their way into the I/O pin. It falsely triggered the interrupt, messing up an RPM measurement. So what’s going on here? Where are those spikes coming from? The spikes are EMI from high frequencies in the PWM signal.

But wait, the PWM signal from the Arduino is only about 600 Hz. That isn’t very fast. The 600 Hz isn’t our issue. Instead, it is how fast the PWM signal switches from OFF to ON.

EMI comes from the Edge

The edge rate from the Arduino I/O pin, and the NPN connected to it, transitions from LOW to HIGH incredibly fast. There is a lot of what we call “high-frequency content” in that edge. That’s what is making its way onto the hall-effect sensor. All of those little spikes are EMI noise. A simple solution is to slow down the edge to the OFF to ON transition. Connecting a capacitor from the NPN’s collector to ground slows down the edge into the base of the PNP. The current limiting resistor (R1) and capacitor (C2) form an RC network, which reduces much of the noise.

pwm-3-pin-pc-fan-schematic v2

Figure 6: Final Arduino PWM PC Fan Circuit

Picking the PWM Filter Capacitor

Normally you could do a bunch of math to figure out an ideal resistor-capacitor combination. In my case, I just connected a 390nF Ceramic capacitor for C2 and all was good. I did look at using a 470nF and 1uF electrolytic capacitor. However, those had enough capacitance to even out the voltage creating a constant ~10V, preventing any switching. Additionally, their ESR causes substantial “shelves” to appear on the edge. Stick with ceramics or film capacitors for this type of filtering.

Depending on your circuit, you may need to play with that value. Basically, make it big enough to reduce the noise spikes, but low enough that the NPN can still switch the PNP off.

You’re probably going to want a scope for this measurement.

01 - Multiple Cycles from Hall Effect

Figure 7: Multiple Cycles from Hall Effect

The hall sensor is still a bit noisy, especially on the rising edge. The falling edge is pretty clean. So even though this signal is active HIGH, I decided to measure the falling edge. I did find adding some additional capacitance on the sensor signal helps clean it up a bit more. You could also consider playing around with an external pull-up resistor to for a similar RC like filter.

With my PWM signal driving working well and the hall sensor signal cleaned up, it was time to measure RPMs on the Arduino.

3. RPM Measurement with millis()

I created a simple serial interface (single character commands) to adjust the PWM speeds. There is a one-second interval used to display current RPM. The interrupt is used only for counting pulses from the hall effect sensor. However, I don’t think even that is necessary. (Maybe something to tackle in another tutorial.) RPM calculation is pretty basic, but seems to work. My fan is rated for 1200 RPM and I’m measuring 1250ish. The slowest I can seem to run this fan is 600 RPM.

As usual, no delays means you can easily incorporate this code into other Arduino sketches.

unsigned long previousRPMMillis;
unsigned long previousMillis;
float RPM;

unsigned long interval = 1000;
byte pwmValue = 125;
byte pwmInterval = 5;
const byte pwmMax = 255;
const byte pwmMin = 0;

const int reedPin = 2;
const int fanPin = 6;

volatile unsigned long pulses=0;
unsigned long lastRPMmillis = 0;


void countPulse() {
  // just count each pulse we see
  // ISRs should be short, not like
  // these comments, which are long.
  pulses++;
}

void setup() {
  Serial.begin(9600);
  pinMode(reedPin,INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(reedPin), countPulse, FALLING);
  
  pinMode(fanPin, OUTPUT); // probably PWM
}

unsigned long calculateRPM() {
  unsigned long RPM;
  noInterrupts();
  float elapsedMS = (millis() - lastRPMmillis)/1000.0;
  unsigned long revolutions = pulses/2;
  float revPerMS = revolutions / elapsedMS;
  RPM = revPerMS * 60.0;
  lastRPMmillis = millis();
  pulses=0;
  interrupts();
  /*Serial.print(F("elpasedMS = ")); Serial.println(elapsedMS);
  Serial.print(F("revolutions = ")); Serial.println(revolutions);
  Serial.print(F("revPerMS = ")); Serial.println(revPerMS); */
  return RPM;
}

void loop() {
  handleSerial();
  analogWrite(fanPin, pwmValue);
  
  if (millis() - previousMillis > interval) {
    Serial.print("RPM=");
    Serial.print(calculateRPM());
    Serial.print(F(" @ PWM="));
    Serial.println(pwmValue);
    previousMillis = millis();  
  }
}

void handleSerial() {
boolean printValue = false;
  while(Serial.available()) {
    switch (Serial.read()) {
      case '+':
        pwmValue = pwmValue+pwmInterval;
        printValue = true;
      break;      
      
      case '-':
        pwmValue = pwmValue-pwmInterval;
        printValue = true;
      break;
  
      case '!':
        pwmValue = pwmMax;
        printValue = true;
      break;

      case '=':
        pwmValue = 125;
        printValue = true;
     break;

      case '0':
      case '@':
        pwmValue = pwmMin;
        printValue = true;
      break;

      case '?':
        Serial.println(F("+ Increase"));
        Serial.println(F("- Decrease"));
        Serial.print(F("! PWM to ")); Serial.println(pwmMax);
        Serial.print(F("@ PWM to ")); Serial.println(pwmMin);
        Serial.println(F("= PWM to 125"));
        printValue = true;
      break;
    }
  }
  if (printValue) {
    Serial.print(F("Current PWM = "));
    Serial.println(pwmValue);
    Serial.flush();
  }
}

Conclusion

While this was a fun academic exercise in the proper way to switch motors, it really wasn’t necessary. This entire tutorial is based on a 3-pin PC fan. If you buy a 4-pin PC fan, the 4th pin is for PWM control. The filtering has already been taken care of in that fan. So while this works, it isn’t necessary if you buy the right fan at the start.

Long comments, URLs, and code tend to get flagged for spam moderation. No need to resubmit.

Leave a comment

2 thoughts on “PWM a 3-pin PC fan with an Arduino

  1. You can actually run the PNP-transistor without an “awkward voltage divider” by simply adding a single diode instead of an additional NPN-transistor. Even for your open-collector driver you actually should provide a resistor between base and +12V for the PNP for highest reliability.

    • Sorry, uwezimmermann, I don’t follow your first comment. What “awkward voltage divider”? When the NPN transistor is driven with enough base current the C/E junction will saturate, effectively pulling the collector to ground (almost, maybe with a potential across the C/E junction of, say, 100mV, no transistor is a perfect switch). If the base isn’t driven hard enough, then yes, a “voltage divider” might be formed as the C/E junction isn’t fully saturated. When the C/E junction is saturated I really would class the transistor as a switch and not really forming an “awkward voltage divider”. Don’t you agree? I also don’t understand what you mean by replacing the NPN transistor with a single diode? This removes the switching capability if it was to replace the C/E junction of the transistor. If it was to used in place of the B/C of the NPN and was forward biased, then when the input was high the PNP transistor would be forced off (opposite the current configuration) and if the input was low, you would need to have a path to ground to still turn on the PNP transistor.

      I agree that adding a resistor from the base of the PNP transistor to Vcc would ensure that the transistor is truly “off” when the NPN is not driven, although I think you might find that the “leakage” current through the NPN (when off) is tiny and is not enough to switch the PNP transistor on (in this case).