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.


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.


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.

void setup() {
  attachInterrupt(digitalPinToInterrupt(reedPin), countPulse, FALLING);
  pinMode(fanPin, OUTPUT); // probably PWM

unsigned long calculateRPM() {
  unsigned long RPM;
  float elapsedMS = (millis() - lastRPMmillis)/1000.0;
  unsigned long revolutions = pulses/2;
  float revPerMS = revolutions / elapsedMS;
  RPM = revPerMS * 60.0;
  lastRPMmillis = millis();
  /*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() {
  analogWrite(fanPin, pwmValue);
  if (millis() - previousMillis > interval) {
    Serial.print(F(" @ PWM="));
    previousMillis = millis();  

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

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

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

      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;
  if (printValue) {
    Serial.print(F("Current PWM = "));


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.

ALL comments submitted with fake or throw-away services are deleted, regardless of content.

Don't be a dweeb.

Leave a comment

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

  1. Hi, thank you for the article, really helpful. I have just one question. If I lower the analog signal down to low RPM, the FAN starts to return a noisy sound, like coil whining. Any idea of how to mitigate or remove this? thx