Built-in project · Arduino Uno R3

PC fan speed and tachometer on an Arduino Uno

A 25 kHz PWM on pin 9 steps a 4-pin PC fan through 100, 75, 50, 25 and 0 percent, and its tachometer on pin 2 reads the rpm back. The whole build, an Arduino Uno and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $39. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • PWM duty cycle and frequency
  • Setting up a hardware timer by hand
  • Counting pulses with an interrupt
  • Turning pulses per second into rpm
  • A 12 V load sharing ground with the Uno
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 3

How it works

A 4-pin PC fan takes its speed from a PWM signal. Intel's fan spec asks for 25 kHz, but analogWrite on an Uno only gives 490 Hz, so the sketch sets up Timer1 itself. It counts to 639 and back to 0 at 16 MHz, which makes 25 kHz, and the compare value sets the duty.

The duty steps through 100, 75, 50, 25 and 0 percent, six seconds each, and repeats. The fan's PWM wire is on pin 9. At 0 percent the pin is taken off the timer and held low.

The fan's tach wire pulls pin 2 low twice per turn. An interrupt counts each falling edge, and once a second the sketch multiplies the count by 30 to get rpm and prints it with the duty.

Parts list

3 parts, plus the jumper wires. They retail for about $39; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
PC fan, 4-pin PWM pin GNDArduino Uno R3 pin GND
PC fan, 4-pin PWM pin PWMArduino Uno R3 pin 9
PC fan, 4-pin PWM pin TACHArduino Uno R3 pin 2
DC jack and adapter pin V-Arduino Uno R3 pin GND
DC jack and adapter pin V+PC fan, 4-pin PWM pin +12V

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// PC fan speed and tachometer: a 4-pin 12 V PWM fan, its speed set by a
// 25 kHz PWM on pin 9 and read back from its tachometer on pin 2.
//
//   9       the fan's PWM wire (blue), Timer1's OC1A
//   2       the fan's TACH wire (green), with the Uno's own pull-up
//   GND     the fan's GND (black), shared with the 12 V adapter's minus
//   12 V    the adapter's plus to the fan's +12V (yellow)
//
// The fan's PWM input is pulled up inside it: left open it runs flat out,
// which is how a fan with no controller keeps a machine cool. Intel's fan
// spec asks for 25 kHz. analogWrite() on an Uno is 490 Hz, which most fans
// follow but sing at, so the sketch sets Timer1 itself: fast PWM, mode 14,
// TOP in ICR1 = 639, no prescaler, so 16 MHz / 640 = 25 kHz, and OCR1A sets
// the duty in 640 steps.
//
// TACH is an open collector that pulls low twice a turn, so rpm is the
// falling edges in a second times 30. The fan here is a Noctua NF-A12x25 PWM:
// 2000 rpm at 100 %, 450 rpm at 20 % and under, and stopped at 0 %.
//
// Press Run and watch the serial monitor: the duty steps 100, 75, 50, 25, 0
// percent, six seconds each, and the rpm follows as the rotor spins down.

const int PWM_PIN = 9;   // OC1A
const int TACH_PIN = 2;  // INT0

const int STEPS[] = {100, 75, 50, 25, 0};
const int STEP_COUNT = 5;
const unsigned long STEP_MS = 6000;

volatile unsigned int pulses = 0;

void countPulse() {
  pulses++;
}

// Duty in percent. 0 % takes the pin off the timer and holds it low: fast PWM
// with OCR1A at 0 still gives one high tick each period.
void setDuty(int percent) {
  if (percent <= 0) {
    TCCR1A &= ~(1 << COM1A1);
    digitalWrite(PWM_PIN, LOW);
    return;
  }
  OCR1A = (unsigned int)((long)percent * 640 / 100 - 1);
  TCCR1A |= (1 << COM1A1);
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Arduino Uno: PC fan speed and tachometer");
  pinMode(PWM_PIN, OUTPUT);
  TCCR1A = (1 << WGM11);
  TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);
  ICR1 = 639;
  pinMode(TACH_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(TACH_PIN), countPulse, FALLING);
  setDuty(STEPS[0]);
}

int step = 0;
unsigned long stepStart = 0;
unsigned long lastPrint = 0;

void loop() {
  unsigned long now = millis();
  if (now - stepStart >= STEP_MS) {
    stepStart = now;
    step = (step + 1) % STEP_COUNT;
    setDuty(STEPS[step]);
  }
  if (now - lastPrint >= 1000) {
    lastPrint = now;
    noInterrupts();
    unsigned int n = pulses;
    pulses = 0;
    interrupts();
    Serial.print("duty ");
    Serial.print(STEPS[step]);
    Serial.print("%  ");
    Serial.print((long)n * 30);
    Serial.println(" rpm");
  }
}

Code walkthrough

The key functions of the sketch, and what each one does.

countPulse sketch.ino

countPulse is the interrupt handler. It runs on every falling edge of the tach wire and adds one to pulses. It does nothing else, so it finishes fast.

void countPulse() {
  pulses++;
}

setDuty sketch.ino

setDuty turns a percent into a compare value out of 640 steps and connects pin 9 to the timer. At 0 percent it disconnects the pin and drives it low, because the timer would otherwise still give a tiny pulse.

// Duty in percent. 0 % takes the pin off the timer and holds it low: fast PWM
// with OCR1A at 0 still gives one high tick each period.
void setDuty(int percent) {
  if (percent <= 0) {
    TCCR1A &= ~(1 << COM1A1);
    digitalWrite(PWM_PIN, LOW);
    return;
  }
  OCR1A = (unsigned int)((long)percent * 640 / 100 - 1);
  TCCR1A |= (1 << COM1A1);
}

setup sketch.ino

setup sets Timer1 to fast PWM with ICR1 as the top value, 639, and no prescaler. It also turns on the tach pin's pull-up, attaches the interrupt and starts at 100 percent.

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Arduino Uno: PC fan speed and tachometer");
  pinMode(PWM_PIN, OUTPUT);
  TCCR1A = (1 << WGM11);
  TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);
  ICR1 = 639;
  pinMode(TACH_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(TACH_PIN), countPulse, FALLING);
  setDuty(STEPS[0]);
}

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The fan runs flat out all the time. This project
    A fan with an open PWM wire runs at full speed. Check that pin 9 goes to the fan's PWM wire.
  • The rpm always reads 0. This project
    The tach wire goes to pin 2. Also, the fan is stopped at 0 percent duty, so wait for the next step.
  • The fan will not start. This project
    The adapter's plus goes to +12V on the fan and its minus must be joined to the Uno's GND.
  • The fan does not spin or only runs full speed. PC fan
    A 4-pin fan needs 12 V on its power pin, with the grounds joined to the controller. PWM wants about 25 kHz, which is far above analogWrite on an Uno. Set a timer for that frequency.
  • The tach reading is wrong. PC fan
    TACH gives two pulses per turn and is open collector, so use a pull-up resistor or INPUT_PULLUP. Count pulses in a fixed time with an interrupt, then divide by two for turns.
  • Board gets too much voltage and overheats or dies. DC jack and adapter
    Check the adapter voltage before plugging in. Boards like the Uno want 7 to 12 V on the jack. A 5 V part wired to a 12 V supply can be destroyed.
  • Nothing powers on, or the polarity is backward. DC jack and adapter
    Most barrel jacks are center positive, but some are not. Check the label on the adapter, and make sure the adapter can supply enough current for the load.
  • Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
    Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.