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+).

Table 1: Raspberry Pi PWM Methods Comparison
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.

Component Warning: Do not use the common IRF520 MOSFET module sold in many beginner kits. The IRF520 requires 10V on the gate to fully turn on. At 3.3V, it will barely conduct, overheat, and your LEDs will be dim. You must use a logic-level MOSFET like the IRLZ44N, which has a gate threshold voltage (Vgs(th)) of 1.0V to 2.0V and fully saturates at 3.3V.

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.

  1. Prepare the MOSFET: Identify the pins on the IRLZ44N (facing you, tab to the back: Gate, Drain, Source). Insert it into your breadboard.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 pigpiod service is not running. Fix: Run sudo 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.GPIO library on a newer Raspberry Pi OS (Bullseye/Bookworm) without sudo privileges. Fix: Run your script with sudo python3 script.py, or better yet, migrate to pigpio or gpiozero which 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_HZ to at least 1000Hz.
The First Three Things to Check When PWM Fails:
  1. Is the pigpiod service actually active? (sudo systemctl status pigpiod)
  2. Are you physically wired to Pin 12 (BCM 18)? Count from the 3.3V pin (Pin 1) down the left side.
  3. 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.