The Direct Answer: Raspberry Pi Pulse Width Modulation Basics
Raspberry Pi pulse width modulation (PWM) allows you to simulate analog voltage outputs by rapidly switching a digital 3.3V GPIO pin on and off. The ratio of "on" time to the total cycle time is the duty cycle (0% to 100%). For a 12V PC fan, a 50% duty cycle runs the fan at roughly half speed.
The most critical distinction in Raspberry Pi PWM is Software vs. Hardware PWM. By default, libraries like gpiozero use software PWM, which relies on the CPU to toggle the pin. This introduces microsecond jitter that causes audible, high-pitched whining in motors and fans. Hardware PWM offloads this to a dedicated peripheral on the BCM2711 (Pi 4) or BCM2712 (Pi 5) SoC, yielding a perfectly clean square wave.
Target Board Variant: This guide targets the Raspberry Pi 4 Model B (4GB) and the Raspberry Pi 5 running Raspberry Pi OS Bookworm. Note that the legacy RPi.GPIO library is deprecated and broken on Pi 5 / Bookworm OS; therefore, we use the modern gpiozero library paired with the pigpio daemon for reliable hardware PWM.
Parts List & Spec Sheet for 12V Fan Control
Controlling a 12V fan with a 3.3V logic pin requires a logic-level MOSFET. Do not use the common IRF520 module found in beginner kits; its gate threshold voltage (Vgs) is typically 4V+, meaning a 3.3V Pi pin will barely turn it on, causing the MOSFET to overheat and fail. Use the IRLZ44N instead.
| Component | Exact Model / Variant | Technical Notes |
|---|---|---|
| Microcontroller | Raspberry Pi 4 Model B (4GB) or Pi 5 | Must have hardware PWM0 on GPIO 18. |
| Fan | Noctua NF-A12x25 PWM (12V) | 4-pin PWM fan. Minimum start voltage ~4V. |
| MOSFET | IRLZ44N (Logic-Level N-Channel) | Vgs(th) is 1V-2V. Fully saturates at 3.3V logic. |
| Gate Resistor | 1kΩ (1/4W) | Prevents ringing and limits inrush current to the gate. |
| Pull-Down Resistor | 10kΩ (1/4W) | Keeps MOSFET off during Pi boot when GPIO is floating. |
| Power Supply | 12V 2A DC Adapter | Do not power 12V fans from the Pi's 5V rail. |
Pin Mapping & Wiring Steps
Hardware PWM on the Raspberry Pi is restricted to specific pins. According to the official Raspberry Pi GPIO documentation, GPIO 12, 13, 18, and 19 support hardware PWM. We will use GPIO 18 (Physical Pin 12) as it is the default for PWM0.
| Pi GPIO / Pin | Function | Connection Destination |
|---|---|---|
| GPIO 18 (Pin 12) | Hardware PWM0 Output | 1kΩ Resistor → MOSFET Gate |
| GND (Pin 14) | Logic Ground | MOSFET Source & 10kΩ Pull-down |
| 5V (Pin 2) | Fan Tachometer Pull-up | Fan Pin 3 (Optional, for RPM reading) |
- Place the MOSFET: Insert the IRLZ44N into a breadboard. Identify the pins (facing you, tab up): Gate (left), Drain (middle), Source (right).
- Wire the Gate: Connect Pi GPIO 18 through a 1kΩ resistor to the MOSFET Gate.
- Wire the Pull-Down: Connect a 10kΩ resistor between the MOSFET Gate and Source (GND). This prevents the fan from spinning at 100% while the Pi boots up and configures the GPIO.
- Wire the Source: Connect the MOSFET Source to the Pi GND (Pin 14) and the negative terminal of your 12V power supply.
- Wire the Drain: Connect the MOSFET Drain to the black wire (GND) of the 12V fan.
- Wire Fan Power: Connect the yellow wire (12V) of the fan to the positive terminal of the 12V power supply.
- Verify: Use a multimeter to check continuity between the Pi GND and the 12V supply GND. Ensure no shorts exist between 12V and any Pi logic pins.
Complete Python Code (Hardware PWM via gpiozero)
To force gpiozero to use hardware PWM, we must instantiate the PiGPIOFactory. This requires the pigpio daemon to be running in the background. Install it via terminal: sudo apt install pigpio python3-pigpio, then enable it: sudo systemctl enable pigpiod && sudo systemctl start pigpiod.
Install the library: pip install gpiozero.
#!/usr/bin/env python3
"""
Raspberry Pi Hardware PWM Fan Controller
Targets: Pi 4 / Pi 5 (Bookworm OS)
Library: gpiozero with pigpio backend
"""
import time
import sys
from gpiozero import PWMOutputDevice
from gpiozero.pins.pigpio import PiGPIOFactory
# --- PIN DEFINITIONS ---
FAN_PWM_PIN = 18 # Physical Pin 12 (Hardware PWM0)
PWM_FREQUENCY = 25000 # 25kHz is the standard for 4-pin PC fans
def initialize_fan():
"""Initialize hardware PWM factory and fan device."""
try:
# Force hardware PWM via the pigpio daemon
factory = PiGPIOFactory()
fan = PWMOutputDevice(
FAN_PWM_PIN,
pin_factory=factory,
frequency=PWM_FREQUENCY,
initial_value=0 # Start at 0% duty cycle
)
return fan
except Exception as e:
print(f"[FATAL] Failed to initialize PiGPIOFactory: {e}")
print("Ensure pigpiod is running: sudo systemctl start pigpiod")
sys.exit(1)
def main():
fan = initialize_fan()
print("Hardware PWM initialized on GPIO 18 at 25kHz.")
try:
# Ramp up from 20% to 100% duty cycle
# Note: Many 12V fans stall below 20-30% duty cycle
for duty in range(20, 101, 10):
normalized_duty = duty / 100.0
fan.value = normalized_duty
print(f"Setting fan duty cycle to {duty}%")
time.sleep(2)
# Hold at 100% for 5 seconds
time.sleep(5)
# Ramp down
for duty in range(90, 9, -10):
normalized_duty = duty / 100.0
fan.value = normalized_duty
print(f"Setting fan duty cycle to {duty}%")
time.sleep(2)
except KeyboardInterrupt:
print("\nInterrupted by user.")
finally:
fan.off()
fan.close()
print("Fan stopped and GPIO resources released.")
if __name__ == "__main__":
main()
Debugging: Connection Errors and Motor Whine
When working with Raspberry Pi pulse width modulation, the most common failure point is the interface between the Python script and the underlying C daemon. If your script crashes immediately upon execution, look for this exact error string:
ConnectionError: [Errno 111] Connection refused
or
gpiozero.exc.PinFactoryFallback: Falling back from rpigpio: No module named 'pigpio'
First 3 Things to Check When It Fails
- Is the daemon running? Run
systemctl status pigpiod. If it is inactive, the Python script cannot connect to the hardware PWM peripheral. - Are you in a virtual environment? On Raspberry Pi OS Bookworm, PEP 668 prevents global
pip install. If you installedgpiozeroin a venv but installedpython3-pigpioviaaptglobally, the venv cannot see the system bindings. Install the bindings inside the venv:pip install pigpio. - Is the pin correct? If you changed
FAN_PWM_PINto 17 or 27, hardware PWM will fail. It must be 12, 13, 18, or 19.
Ranked Causes for PWM Jitter and Motor Whine
If the code runs but the fan emits a high-pitched squeal, you are accidentally using software PWM. Ranked causes:
- PiGPIOFactory fallback: The script failed to connect to
pigpiodand silently fell back to the default softwareRPiGPIOFactory. Check your console for thePinFactoryFallbackwarning. - Frequency too low: The
frequencyparameter is set below 20,000 Hz (20kHz). Human hearing caps around 20kHz. Set it to 25,000 Hz to push the switching noise out of the audible spectrum. - CPU Throttling: If using software PWM on a Pi Zero W under heavy load, the OS scheduler delays the GPIO toggle. Migrate to hardware PWM immediately.
Extending and Simplifying the Build
How to Extend (Closed-Loop Thermal Control):
To make this a smart cooling system, add a BME280 I2C temperature sensor. Wire the BME280 VCC to 3.3V, GND to GND, SDA to GPIO 2, and SCL to GPIO 3. Use the adafruit-circuitpython-bme280 library to read the ambient temperature. Implement a PID controller in Python that maps the temperature (e.g., 30°C to 60°C) directly to the fan.value duty cycle (0.2 to 1.0). This creates a silent, demand-driven cooling system for enclosed Pi server racks.
How to Simplify (5V LED Strip Dimming):
If you don't need a 12V fan and just want to dim a 5V LED strip, drop the 12V power supply and the MOSFET. Instead, use a single 2N2222 NPN transistor. Connect the Pi GPIO 18 through a 1kΩ resistor to the 2N2222 base. Connect the emitter to GND, and the collector to the LED strip's GND pad. Power the LED strip's VCC directly from the Pi's 5V pin (Pin 2), provided the strip draws less than 500mA. This reduces the parts count to three components.
Frequently Asked Questions
Which Raspberry Pi pins support hardware pulse width modulation?
Only four GPIO pins on the Raspberry Pi 4 and 5 support true hardware PWM: GPIO 12, 13, 18, and 19. These map to the PWM0 and PWM1 channels on the Broadcom SoC. If you attempt to use hardware PWM libraries on any other pin (like GPIO 17 or 27), the library will either throw an error or silently fall back to CPU-driven software PWM, which introduces timing jitter.
Why does my fan make a high-pitched whine with Raspberry Pi PWM?
Motor whine occurs when the PWM switching frequency falls within the human hearing range (20Hz to 20kHz) and the motor coils physically vibrate at that frequency. Software PWM often defaults to frequencies like 100Hz or 1kHz, causing severe whine. By explicitly setting the frequency=25000 (25kHz) parameter in gpiozero and using the PiGPIOFactory for hardware-level precision, you push the vibration above human hearing, resulting in silent operation.
Can I use Raspberry Pi pulse width modulation to dim an LED strip?
Yes, but you must use a MOSFET or transistor as an intermediary. A Raspberry Pi GPIO pin can only safely source or sink about 16mA (with a total board limit of 50mA across all pins). A typical 12V LED strip draws several amps. Use the exact same IRLZ44N MOSFET circuit shown in this guide, but connect the LED strip's GND wire to the MOSFET Drain. For gpiozero LED PWM documentation, use the PWMLED class instead of PWMOutputDevice for slightly cleaner syntax, though both work identically under the hood.
How do I fix the RPi.GPIO deprecation error on Raspberry Pi OS Bookworm?
If you see RuntimeError: This module can only be run on a Raspberry Pi! or ModuleNotFoundError: No module named 'RPi.GPIO' on a Pi 5 or Bookworm OS, it is because the legacy RPi.GPIO library is no longer maintained and lacks support for the BCM2712 chip. The official pigpio library and the lgpio backend are the modern replacements. Migrate your code to gpiozero (which abstracts the backend) and ensure you are using gpiozero.pins.pigpio.PiGPIOFactory or gpiozero.pins.lgpio.LGPIOFactory to interact with the hardware safely.






