If you need to dim an LED or control the speed of a DC motor, Pulse Width Modulation (PWM) is the standard technique. This practical Arduino PWM example demonstrates how to drive a 12V PC cooling fan using an Arduino Nano and a logic-level MOSFET. Rather than just blinking an onboard LED, this guide tackles the real-world hardware challenges of switching inductive loads, provides a complete, compilable codebase, and breaks down the exact debugging steps when your output fails to behave.
Project Overview & Difficulty Rating
| Parameter | Specification |
|---|---|
| Target Board | Arduino Nano V3 (ATmega328P, 5V logic) |
| Difficulty | 2/5 (Intermediate Beginner) |
| Estimated Time | 45 minutes |
| Core Concept | Hardware PWM via analogWrite() driving an N-channel MOSFET |
Exact Parts List
- Microcontroller: Arduino Nano V3 (ATmega328P variant). Note: Do not use the ESP32 for this specific code without modifying the PWM API.
- Switching Component: IRLZ44N Logic-Level N-Channel MOSFET. (Avoid the IRF520; it requires 10V on the gate to fully turn on and will overheat on a 5V Arduino).
- Load: 12V 4-pin or 3-pin PC Cooling Fan (max 1.5A draw).
- Resistors: 1x 100Ω (gate series resistor), 1x 10kΩ (gate pull-down resistor).
- Protection: 1x 1N4007 rectifier diode (flyback protection).
- Power: 12V 2A DC power supply.
Hardware Wiring & Pin Mapping
The Arduino's ATmega328P microcontroller can only source about 20mA per pin at 5V. A 12V fan draws hundreds of milliamps. We use the Arduino to output a 5V PWM signal to the gate of the MOSFET, which then switches the 12V power to the fan.
| Component Pin | Connects To | Notes |
|---|---|---|
| Arduino D9 | 100Ω Resistor -> MOSFET Gate | D9 is a hardware PWM pin (marked with ~ on silkscreen). |
| Arduino GND | MOSFET Source & 12V PSU GND | Critical: The 12V supply and Arduino must share a common ground. |
| MOSFET Gate | 10kΩ Resistor -> GND | Pull-down resistor keeps the fan off during Arduino boot-up. |
| MOSFET Drain | Fan Negative Wire (Black) | Switches the ground path for the fan. |
| Fan Positive (Red) | 12V PSU Positive (+12V) | Fan receives constant 12V; the MOSFET pulses the ground. |
| 1N4007 Diode | Across Fan Red & Black wires | Cathode (stripe) to Red. Protects MOSFET from inductive kickback. |
The Code: Complete Arduino PWM Example
This code targets the Arduino Nano V3 (ATmega328P). It ramps the fan speed from 0% to 100% duty cycle, holds it, and ramps back down. It includes serial feedback so you can monitor the duty cycle in the IDE Serial Monitor.
// Pin Definitions
const int PWM_PIN = 9; // Hardware PWM pin on Nano/Uno
const int POT_PIN = A0; // Analog input reserved for future manual override
// PWM Parameters
const int PWM_MIN = 0; // 0% duty cycle
const int PWM_MAX = 255; // 100% duty cycle (8-bit resolution)
const int RAMP_DELAY = 20; // Milliseconds between steps
void setup() {
Serial.begin(115200);
pinMode(PWM_PIN, OUTPUT);
// Initial state: ensure fan is off immediately
analogWrite(PWM_PIN, PWM_MIN);
Serial.println("System Initialized. Starting PWM ramp test...");
}
void loop() {
// Ramp up from 0 to 255
for (int duty = PWM_MIN; duty <= PWM_MAX; duty += 5) {
analogWrite(PWM_PIN, duty);
Serial.print("Duty Cycle: ");
Serial.print(map(duty, 0, 255, 0, 100));
Serial.println("%");
delay(RAMP_DELAY);
}
delay(1000); // Hold at 100% for 1 second
// Ramp down from 255 to 0
for (int duty = PWM_MAX; duty >= PWM_MIN; duty -= 5) {
analogWrite(PWM_PIN, duty);
Serial.print("Duty Cycle: ");
Serial.print(map(duty, 0, 255, 0, 100));
Serial.println("%");
delay(RAMP_DELAY);
}
delay(2000); // Hold at 0% for 2 seconds before repeating
}
For deeper theory on how the microcontroller achieves this by toggling the pin high and low faster than the mechanical system can react, refer to the All About Circuits guide on Pulse Width Modulation. The official Arduino analogWrite() documentation also details the 490Hz default frequency on most pins.
Debugging: When Your PWM Output Fails
Hardware PWM rarely fails in simulation, but it frequently fails on the bench. If your fan isn't spinning, is stuck at 100%, or the Arduino is throwing errors, follow this decision path.
The First Three Things to Check
- Is the pin actually a hardware PWM pin? On the Nano/Uno, only pins 3, 5, 6, 9, 10, and 11 support hardware PWM via
analogWrite(). If you wired the gate to D8, it will output a static HIGH or LOW, not a modulated signal. - Is the common ground connected? The 12V power supply ground must be physically wired to the Arduino GND pin. Without this, the 5V signal from D9 has no reference point to turn on the MOSFET gate.
- Are you using a logic-level MOSFET? If you swapped the IRLZ44N for an IRF520 or IRFZ44N, the 5V from the Arduino is insufficient to fully open the gate channel. The fan will either not spin, or the MOSFET will get burning hot.
Ranked Causes for Compile & Runtime Errors
Error String: error: 'ledcSetup' was not declared in this scope
- Cause 1 (Most Likely): You copied ESP32 PWM code into an Arduino AVR project. The ESP32 uses the LEDC peripheral for PWM, requiring
ledcSetup()andledcAttachPin(). - Fix: Delete the LEDC functions and use
analogWrite(pin, value)for the ATmega328P, or change your board manager target to an ESP32 variant.
Hardware Symptom: Motor whines loudly at low speeds and refuses to spin below 40% duty cycle.
- Cause 1: The default 490Hz PWM frequency falls within the audible range, causing the motor windings to vibrate (acoustic noise).
- Cause 2: Low duty cycles don't provide enough peak voltage to overcome the fan's static friction (stiction).
- Fix: Implement a 'kickstart' routine in your code. Apply
analogWrite(PWM_PIN, 255)for 150 milliseconds before dropping to your target low PWM value. This overcomes stiction without running the fan at full speed continuously.
Extending and Simplifying the Build
How to Simplify: If you don't have a 12V fan and just want to test the code, remove the MOSFET, 12V supply, and diode entirely. Connect a standard 5mm LED with a 220Ω series resistor directly between Arduino D9 and GND. The exact same code will dim the LED smoothly.
How to Extend:
To make this a manual fan controller, wire a 10kΩ potentiometer to 5V, GND, and the wiper to POT_PIN (A0). Replace the for loops in the loop() function with:
int potValue = analogRead(POT_PIN);
int pwmValue = map(potValue, 0, 1023, 0, 255);
analogWrite(PWM_PIN, pwmValue);
delay(10); // Small delay for ADC stability
Frequently Asked Questions
What is the default PWM frequency on an Arduino Uno or Nano?
On pins 5 and 6, the default frequency is approximately 980Hz. On pins 3, 9, 10, and 11, it is approximately 490Hz. This is fast enough for incandescent bulbs and basic DC motors, but can cause audible whining in small fans or piezo buzzers. You can alter the timer prescalers in the setup block to push this to 31kHz if acoustic noise is an issue.
Why does my motor whine or squeal when using analogWrite()?
The squeal is magnetostriction and coil vibration caused by the 490Hz switching frequency. The rapid pulsing of the magnetic field causes the physical windings inside the motor to vibrate at an audible pitch. To fix this, you either need to increase the PWM frequency above the human hearing range (typically >20kHz) or add a low-pass LC filter to smooth the PWM into a true DC analog voltage before it reaches the motor.
Can I use any digital pin for an Arduino PWM example?
No. Only the pins marked with a tilde (~) on the Arduino silkscreen support hardware PWM via the analogWrite() function. If you attempt to use analogWrite() on a non-PWM pin like D8 or D12, the Arduino will simply treat it as a digital HIGH if the value is >127, or LOW if the value is <128. Software PWM libraries exist (like SoftwareServo), but they consume significant CPU cycles and introduce flickering.
How do I change the PWM frequency to 25kHz for a 4-pin PC fan?
Intel's 4-Wire PWM Fan Specification mandates a 25kHz target frequency on the PWM control wire to eliminate acoustic noise and standardize thermal management. To achieve this on an Arduino Nano using Timer 1 (pins 9 and 10), you must bypass analogWrite() and manipulate the hardware registers directly in your setup() function:
// Set Timer 1 to Phase and Frequency Correct PWM, no prescaler
TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM11);
TCCR1B = _BV(WGM13) | _BV(CS10);
ICR1 = 320; // Sets frequency to ~25kHz
After setting this, you use OCR1A = value; (where value is 0 to 320) instead of analogWrite() to control pin 9.






