Hardware PWM on Raspberry Pi: When Software Toggling Stops Being Good Enough
Software-toggled PWM on a Raspberry Pi jitters whenever Linux deschedules your process. Here's how the SoC's hardware PWM channels fix that, with a working sysfs example on GPIO 18 and the trade-offs to plan for.
18 Jul 2025, 23:52 UTC

You dim an LED from Python with a simple loop — set the pin high, sleep, set it low, sleep. It works until the Pi decides to run a cron job, flush a filesystem cache, or garbage-collect, and your smooth fade turns into visible flicker. That jitter is the whole argument for hardware PWM: the Broadcom SoC has dedicated PWM channels that generate the waveform without the CPU, so timing doesn't depend on what Linux happens to be doing.
The useful takeaway: if your project involves time-critical output — LED dimming, servo or motor speed control, buzzer tones — move the waveform generation off your process and onto the hardware PWM peripheral. It costs you a specific pin choice and a bit of pin-mux configuration, and buys you a stable, CPU-independent signal.
What hardware PWM actually gives you
Pulse-width modulation (PWM) controls average power by switching a pin between high and low at a fixed frequency with a variable duty cycle (the fraction of each period spent high). The Pi's SoC includes PWM channels that can be routed to specific GPIO pins — commonly GPIO 12, 13, 18, and 19 on 40-pin models, with additional pins such as 40, 41, 45, and 52 on some variants. Not every model exposes the same set, so check the pinout for your exact board revision before committing a PCB or wiring loom.
Contrast this with software PWM, where your code (or a library) toggles the pin from userspace. Even with a real-time-ish library, a preemptible OS means your toggling thread can be descheduled at any moment. The result is jitter — irregular pulse widths — plus measurable CPU load just to hold a pin high and low. Hardware PWM runs from a clock divider and comparator inside the SoC; once configured, it keeps going even if your program crashes.
Two practical ways to drive it
There are two well-established interfaces, and both are fine choices with different ergonomics.
sysfs interface. The kernel exposes PWM chips under /sys/class/pwm/. You export a channel, then write the period and duty cycle (both in nanoseconds) as plain text. This needs no extra library and works from a shell script, which is handy for quick experiments. You must run as root or arrange permissions via udev rules, and the pin's alternate function must be set correctly first — on many setups this is handled by enabling the PWM overlay in /boot/config.txt (or /boot/firmware/config.txt on newer images), e.g. dtoverlay=pwm,pin=18,func=2, then rebooting.
pigpio library. The pigpio daemon handles the pin muxing for you and offers hardware-timed PWM with fine granularity (down to roughly 1 µs on newer Pis) on a broader set of pins, plus DMA-assisted features. You talk to it from Python, C, or over a socket. The trade-off is a running daemon and one more dependency.
Worked example: sysfs PWM on GPIO 18
Assuming a Raspberry Pi 4 or 5 with the PWM overlay enabled and a reboot done, run these as root (or with sudo) in a shell on the Pi itself:
cd /sys/class/pwm/pwmchip0
echo 0 > export # creates pwm0/
cd pwm0
echo 1000000 > period # 1,000,000 ns = 1 kHz
echo 250000 > duty_cycle # 25% duty cycle
echo 1 > enable
Placeholders to adjust: period is the full cycle time in nanoseconds (1 kHz here); duty_cycle must be ≤ period. A reasonable expected check: read the values back with cat period duty_cycle and confirm they match what you wrote. For real verification, put an oscilloscope or logic analyzer on GPIO 18 (physical pin 12) and confirm the measured frequency and duty cycle are within about ±1% of the configured values — a multimeter on DC mode should read roughly 25% of 3.3 V (about 0.8 V) as a sanity check. Risk to note: GPIO is 3.3 V logic with limited current; drive LEDs through a resistor and motors through a transistor or driver, never directly.
To stop: echo 0 > enable, and echo 0 > /sys/class/pwm/pwmchip0/unexport to release the channel. This is a state change (it configures the pin and peripheral), so the unexport is the meaningful rollback if you're testing.
Trade-offs worth knowing before you commit
Hardware PWM is not free of constraints:
- Pin lock-in. Only certain pins carry PWM channels, and those pins may conflict with other functions you want (I2S audio, for instance, shares some). Plan pin allocation early.
- Pin muxing matters. The alternate-function registers must route the PWM peripheral to the pin. A wrong ALT setting can silently enable a different peripheral function or leave the pin as a plain GPIO — always verify with a scope or by reading back the sysfs state rather than assuming.
- Frequency vs. resolution. Higher carrier frequencies reduce the number of distinct duty-cycle steps available, since the divider runs from a fixed clock. For LED dimming, a few hundred Hz to 1 kHz is usually plenty.
- Noise and power. Frequencies above ~1 kHz can increase switching losses, and PWM in the audible range can cause coil whine in motors or inductors — a real annoyance in audio-adjacent projects. Push the carrier above ~20 kHz if whine is a problem, accepting the resolution trade-off.
- Two channels, shared. The classic PWM peripheral offers a limited number of independent channels. If you need many independent hardware-timed outputs, pigpio's DMA-based approach or an external PWM driver chip (e.g., over I2C) scales better.
Deciding which route to take
Use software PWM (or a library like RPi.GPIO's PWM mode) for anything where jitter doesn't matter: a status LED, a one-shot beep. Reach for hardware PWM the moment a human eye, ear, or a motor's behavior will notice timing irregularities. Start with the sysfs interface for a single channel — it's transparent, scriptable, and easy to verify — and graduate to pigpio when you need more channels, finer control, or dynamic updates from application code. Whatever you choose, verify the actual waveform on the pin at least once; "I wrote the value" and "the pin is doing it" are separated by pin muxing, overlays, and permissions, and a five-minute scope check settles all three.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.