If you want to dim an LED, control a servo, or generate a clean audio tone on a Raspberry Pi, you need Pulse Width Modulation (PWM). But here is the direct answer that saves most builders hours of frustration: The Raspberry Pi has exactly one hardware PWM pin available on the standard GPIO header (BCM 18 / Physical Pin 12). Every other pin relies on software PWM, which is inherently jittery because Linux is not a real-time operating system.
If you use software PWM to drive a MOSFET for LED dimming, you will likely see visible flickering at low duty cycles. If you use it for a servo, the motor will twitch. This guide breaks down the exact hardware limitations, provides a complete wiring and Python implementation for flicker-free hardware PWM, and gives you the exact debugging steps when your signals degrade.
The Hardware vs. Software PWM Divide
To understand why your PWM signal might look messy on an oscilloscope, you have to understand how the Pi generates it. Hardware PWM is handled by a dedicated peripheral on the Broadcom SoC. It runs independently of the CPU. Software PWM relies on the Linux kernel scheduling a timer interrupt to toggle a GPIO pin high and low. If the CPU gets busy handling a network packet or a USB interrupt, that timer gets delayed, stretching or shrinking your pulse width.
Here is the exact breakdown of your PWM options on the Raspberry Pi 4 Model B (and Pi 3B+).
| Method | Pin(s) | Frequency Range | Jitter / Stability | CPU Overhead | Best Use Case |
|---|---|---|---|---|---|
| Hardware PWM0 | BCM 18 (Pin 12) | 1Hz - 19.2MHz | Zero (Perfect square wave) | 0% | Servos, audio, high-power LED dimming |
| Hardware PWM1 | BCM 13, 19 (Pins 33, 35) | 1Hz - 19.2MHz | Low (Shared clock channel) | 0% | Secondary independent PWM channel |
| Software PWM (RPi.GPIO) | All GPIO pins | 1Hz - 5kHz (practical) | High (Visible flicker >100Hz) | High (Blocks CPU) | Basic heater control, slow DC motor speed |
| Software PWM (pigpio) | All GPIO pins | 1Hz - 10kHz | Medium (DMA timed, ~10us jitter) | Low (Daemon handles it) | RGB LEDs where perfect sync isn't critical |
| I2C PWM Driver (PCA9685) | Any I2C pins (BCM 2, 3) | 24Hz - 1526Hz | Zero (Dedicated IC) | Near 0% | Multi-servo robots, 16-channel LED arrays |
Source: Raspberry Pi Foundation GPIO Documentation and pigpio library specifications.
Parts List and Pin Mapping
For this build, we are driving a 12V high-power LED strip. The Pi outputs 3.3V logic, which cannot directly power the LEDs, nor can it provide the current required. We use a logic-level N-channel MOSFET to switch the 12V load based on the Pi's 3.3V PWM signal.
Bill of Materials
- Microcontroller: Raspberry Pi 4 Model B (4GB RAM) - Code also targets Pi 3B+ and Pi 5 (with minor config.txt overlay adjustments for Pi 5).
- Switching Component: IRLZ44N Logic-Level N-Channel MOSFET (TO-220 package)
- Load: 12V 5050 SMD LED Strip (1 meter segment, ~1.5A draw)
- Power Supply: 12V 5A Switching PSU (Mean Well LRS-60-12 or similar)
- Resistors: 1x 1kΩ (Gate pull-up/limit), 1x 10kΩ (Gate-to-Source pull-down)
- Wiring: 22 AWG solid core for breadboard, 18 AWG stranded for 12V power lines
Pin Mapping Table
| Pi Physical Pin | BCM GPIO | Function | Connected To |
|---|---|---|---|
| 12 | 18 | Hardware PWM0 Output | 1kΩ Resistor -> MOSFET Gate |
| 6 | GND | System Ground | MOSFET Source & 10kΩ Resistor |
| 2 | 5V Power | (Not used in this circuit) | - |
Wiring the 12V LED PWM Circuit
Safety Callout: While 12V DC is not a shock hazard, a 5A power supply can deliver enough current to melt 22 AWG wire and start a fire if a short circuit occurs. Double-check your MOSFET pinout (Gate, Drain, Source) before applying power. Ensure your 12V PSU ground is tied to the Pi's GND to establish a common reference voltage.
- Prepare the MOSFET: Identify the pins on the IRLZ44N (facing you, tab to the back: Gate, Drain, Source). Insert it into your breadboard.
- Wire the Gate Drive: Connect a jumper from Pi Physical Pin 12 (BCM 18) through a 1kΩ resistor to the MOSFET Gate. This resistor prevents high-frequency ringing and protects the Pi GPIO pin from inductive spikes.
- Wire the Pull-Down: Connect a 10kΩ resistor between the MOSFET Gate and Source. This ensures the MOSFET stays OFF during Pi boot-up when GPIO pins are floating.
- Establish Common Ground: Connect Pi Physical Pin 6 (GND) to the MOSFET Source. Connect this same node to the negative (-) terminal of your 12V Power Supply.
- Connect the Load: Connect the positive (+) terminal of the 12V PSU to the positive lead of the LED strip. Connect the negative lead of the LED strip to the MOSFET Drain.
- Verify: Before plugging in the 12V PSU, use a multimeter in continuity mode to verify there is no short between the 12V positive rail and ground.
Python Implementation: Hardware PWM via pigpio
To get true hardware PWM on BCM 18, we bypass the standard RPi.GPIO library and use pigpio. The pigpio daemon runs in the background, utilizing the Pi's DMA (Direct Memory Access) controller to generate flawless timing signals without CPU intervention.
Prerequisite: Install the daemon and Python library via terminal:
sudo apt update && sudo apt install pigpio python3-pigpio
Then enable the service:
sudo systemctl enable pigpiod && sudo systemctl start pigpiod
The following code targets the Raspberry Pi 4 Model B. It initializes hardware PWM, ramps the LED up and down, and includes robust error handling to ensure the GPIO pin is safely reset if the script crashes.
import pigpio
import time
import sys
# --- PIN & PARAMETER DEFINITIONS ---
# BCM 18 is Physical Pin 12, the primary Hardware PWM0 pin
PWM_PIN = 18
# 1000Hz is ideal for LEDs (high enough to avoid camera flicker, low enough to avoid MOSFET switching losses)
FREQUENCY_HZ = 1000
# We set a custom range of 1,000,000 for microsecond-level duty cycle precision
PWM_RANGE = 1000000
def run_pwm_sweep():
pi = None
try:
# Connect to the pigpio daemon
pi = pigpio.pi()
if not pi.connected:
raise ConnectionError('Failed to connect to pigpio daemon at localhost:8888')
# Configure Hardware PWM parameters
pi.set_mode(PWM_PIN, pigpio.OUTPUT)
pi.set_PWM_range(PWM_PIN, PWM_RANGE)
pi.set_PWM_frequency(PWM_PIN, FREQUENCY_HZ)
print(f'Hardware PWM active on GPIO {PWM_PIN} at {FREQUENCY_HZ}Hz.')
print('Starting 0% to 100% sweep...')
# Sweep Duty Cycle from 0% to 100%
for duty in range(0, PWM_RANGE + 1, 10000):
pi.set_PWM_dutycycle(PWM_PIN, duty)
time.sleep(0.02) # 20ms delay per step
# Hold at 100% for 2 seconds
time.sleep(2)
except pigpio.error as e:
print(f'[FATAL] pigpio hardware error: {e}')
sys.exit(1)
except ConnectionError as e:
print(f'[FATAL] Daemon connection error: {e}')
print('Fix: Run sudo systemctl start pigpiod')
sys.exit(1)
except KeyboardInterrupt:
print('\n[INFO] User interrupted script.')
except Exception as e:
print(f'[ERROR] Unexpected exception: {e}')
sys.exit(1)
finally:
# ALWAYS clean up to prevent the LED from staying on if the script crashes
if pi is not None and pi.connected:
pi.set_PWM_dutycycle(PWM_PIN, 0)
pi.stop()
print('[INFO] PWM stopped, GPIO cleaned up, and daemon disconnected.')
if __name__ == '__main__':
run_pwm_sweep()
Debugging: Exact Error Strings and Jitter Fixes
When working with Pi PWM, things will go wrong. Here are the exact error strings you will encounter, ranked by probability, and how to fix them.
1. The Daemon Connection Error
Exact Error String: Can't connect to pigpio at localhost(8888)
Causes & Fixes:
- Cause A (90%): The
pigpiodservice is not running. Fix: Runsudo systemctl start pigpiod. - Cause B (10%): Another process (like an old instance of your script or Node-RED) is hogging the daemon or the DMA channels. Fix: Reboot the Pi or kill the rogue process.
2. The Permission Error (If using RPi.GPIO instead)
Exact Error String: RuntimeError: No access to /dev/mem. Try running as root!
Causes & Fixes:
- Cause: You are using the older
RPi.GPIOlibrary on a newer Raspberry Pi OS (Bullseye/Bookworm) without sudo privileges. Fix: Run your script withsudo python3 script.py, or better yet, migrate topigpioorgpiozerowhich handle user-space permissions gracefully via the daemon.
3. The Physical Jitter (No Error String, Visual Symptom)
Symptom: LEDs flicker visibly at 50% duty cycle, or servos twitch randomly.
Causes & Fixes:
- Cause A: You wired your load to BCM 17 (Pin 11) instead of BCM 18 (Pin 12), forcing the Pi to use software PWM. Fix: Move the wire to Physical Pin 12.
- Cause B: Your PWM frequency is set too low (e.g., 50Hz) for an LED. Fix: Increase
FREQUENCY_HZto at least 1000Hz.
- Is the
pigpiodservice actually active? (sudo systemctl status pigpiod) - Are you physically wired to Pin 12 (BCM 18)? Count from the 3.3V pin (Pin 1) down the left side.
- Is your MOSFET logic-level? Check the datasheet for Vgs(th). If it's > 3.5V, the Pi cannot turn it on.
Extending and Simplifying the Build
How to Simplify (For Basic, Non-Critical Loads)
If you are just controlling a slow DC motor or a heating element where microsecond jitter doesn't matter, you don't need pigpio. You can simplify your code by using the gpiozero library, which comes pre-installed on Raspberry Pi OS. It abstracts the daemon connection and uses software PWM by default.
from gpiozero import PWMLED
from time import sleep
# gpiozero uses BCM numbering by default
led = PWMLED(18)
led.value = 0.5 # 50% duty cycle
sleep(5)
led.off()
How to Extend (For Multi-Channel Robotics and Lighting)
The Pi only has one primary hardware PWM channel (PWM0) that operates completely independently. If you need to drive 4 servos for a robot arm, or 3 channels for an RGB amplifier, hardware PWM0 won't cut it.
The Solution: Add a PCA9685 16-Channel I2C PWM Driver board (approx. $5 to $10 on Amazon or Adafruit). This IC generates hardware PWM for 16 independent channels and communicates with the Pi over I2C (using just BCM 2 and BCM 3). It completely offloads PWM generation from the Pi's SoC, guaranteeing zero jitter across all 16 channels regardless of CPU load. You can control it easily using the adafruit-circuitpython-pca9685 library.
By understanding the hard line between Linux software timing and Broadcom hardware peripherals, you can stop fighting the OS and start building reliable, flicker-free embedded systems.






