Raspberry Pi 4 PWM: Jitter-Free Servo and DC Motor Speed Control
Pulse-width modulation (PWM) switches a pin on and off rapidly. By changing how long the pin stays high in each cycle, you control the average power delivered to a motor, or send a position command to a servo. The Raspberry Pi 4 can generate PWM in two ways: in software, where Linux toggles a pin on a timer, and in hardware, where a dedicated PWM peripheral inside the BCM2711 produces the waveform with no CPU involvement.
This tutorial shows when each method is good enough. You will drive an SG90 servo from the hardware PWM peripheral for a steady, jitter-free position, and control the speed and direction of a DC motor through an L298N driver using gpiozero's software PWM.
PWM Concepts You Need
A PWM signal has a period (or frequency) and a duty cycle. Hobby servos expect a 50 Hz signal, a 20 ms period, and read the width of the high pulse: roughly 1.0 ms for one end, 1.5 ms for centre and 2.0 ms for the other end. Many SG90 clones accept about 0.5 ms to 2.5 ms for their full travel, so always find the limits of your servo gently.
Servo: period = 1 / 50 Hz = 20 ms
pulse 1.5 ms -> duty = 1.5 / 20 = 7.5 % (centre)
Motor: average voltage = supply x duty
12 V supply at 40 % duty -> about 4.8 V averageSoftware PWM is fine for motors and LEDs because a small timing wobble only changes the speed or brightness slightly. A servo, however, reacts to every microsecond of pulse-width error, so software PWM makes it twitch. That is why the servo uses the hardware peripheral.
| Hardware PWM channel | Usable GPIO pins | Physical pins |
|---|---|---|
| PWM0 | GPIO12 or GPIO18 | 32 or 12 |
| PWM1 | GPIO13 or GPIO19 | 33 or 35 |
Hardware and Wiring
| Part | Connection | Notes |
|---|---|---|
| SG90 servo signal (orange) | GPIO18, physical pin 12 | 3.3 V pulses are accepted by most servos |
| SG90 power (red) | External 5 V supply | Not the Pi's 5 V pin under load |
| SG90 ground (brown) | Supply GND and Pi GND | Grounds must be common |
| L298N ENA | GPIO13, physical pin 33 | Speed (PWM) input; remove the ENA jumper |
| L298N IN1 / IN2 | GPIO5 / GPIO6 | Direction inputs |
| L298N 12V and GND | Motor battery, 7 to 12 V | Connect L298N GND to Pi GND |
A servo stalling or starting can pull several hundred milliamps in spikes. Feeding that from the Pi's 5 V header pins causes brown-outs, so use a separate 5 V supply and join only the grounds.
Enabling Hardware PWM and Driving the Servo
Add this line to /boot/firmware/config.txt and reboot. It enables both channels, PWM0 on GPIO18 and PWM1 on GPIO19 by default; here only channel 0 is used for the servo.
dtoverlay=pwm-2chan
After the reboot, ls /sys/class/pwm should show pwmchip0. The kernel exposes each channel through simple files, which the following class writes to. Times are in nanoseconds.
#!/usr/bin/env python3
# servo_hwpwm.py - drive an SG90 on GPIO18 using hardware PWM (pwmchip0, channel 0)
# Requires dtoverlay=pwm-2chan in /boot/firmware/config.txt
import os
import time
CHIP = '/sys/class/pwm/pwmchip0'
PERIOD_NS = 20_000_000 # 20 ms = 50 Hz
MIN_PULSE_NS = 500_000 # 0.5 ms - adjust for your servo
MAX_PULSE_NS = 2_500_000 # 2.5 ms - adjust for your servo
class HardwareServo:
def __init__(self, channel=0):
self.path = f'{CHIP}/pwm{channel}'
# Export the channel if it is not already visible
if not os.path.isdir(self.path):
self._write(f'{CHIP}/export', channel)
time.sleep(0.2) # give udev time to set permissions
# duty_cycle must never exceed period, so clear it first
self._write(f'{self.path}/duty_cycle', 0)
self._write(f'{self.path}/period', PERIOD_NS)
self._write(f'{self.path}/enable', 1)
@staticmethod
def _write(path, value):
with open(path, 'w') as f:
f.write(str(value))
def angle(self, degrees):
"""Move to an angle between 0 and 180 degrees."""
degrees = max(0, min(180, degrees))
pulse = MIN_PULSE_NS + (MAX_PULSE_NS - MIN_PULSE_NS) * degrees / 180
self._write(f'{self.path}/duty_cycle', int(pulse))
def close(self):
self._write(f'{self.path}/enable', 0)
if __name__ == '__main__':
servo = HardwareServo(0)
try:
# Sweep 0 -> 180 -> 0 in 10 degree steps
for a in list(range(0, 181, 10)) + list(range(180, -1, -10)):
servo.angle(a)
print(f'angle {a:3d}')
time.sleep(0.15)
servo.angle(90) # park at centre
time.sleep(0.5)
finally:
servo.close()
If you get a permission error, run the script with sudo once to confirm the wiring, then add your user to the gpio group, which on Raspberry Pi OS is normally given access to the PWM files.
DC Motor Speed and Direction with gpiozero
For the motor, gpiozero's Motor class takes the two direction pins plus an enable pin, and applies PWM to the enable pin. Values run from 0 (stopped) to 1 (full speed).
#!/usr/bin/env python3
# motor_speed.py - L298N: IN1=GPIO5, IN2=GPIO6, ENA=GPIO13 (jumper removed)
from gpiozero import Motor
from time import sleep
# pwm=True (default) applies PWM on the enable pin for speed control
motor = Motor(forward=5, backward=6, enable=13, pwm=True)
try:
# Ramp up forwards in 10 % steps
for step in range(0, 11):
motor.forward(step / 10)
print(f'forward {step * 10}%')
sleep(0.5)
motor.stop()
sleep(1) # let the motor stop before reversing
# Run backwards at 60 % for 3 seconds
motor.backward(0.6)
sleep(3)
finally:
motor.stop()
motor.close()
Most small motors will not start below a certain duty cycle because of friction; you will hear a hum but see no movement. Note that minimum value and treat it as the motor's real zero in your project.
Troubleshooting
- No pwmchip0: the overlay line is missing or misspelled, or was put under a section header like
[cm4]that does not apply to a Pi 4. Place it under[all]. - Invalid argument when writing period: the duty cycle is larger than the new period. Set duty to 0 first, as the class does.
- Servo buzzes at the ends: the pulse limits exceed its mechanical range. Narrow
MIN_PULSE_NSandMAX_PULSE_NS. - Pi resets when the motor starts: the motor and Pi share a weak supply. Use a separate battery for the L298N.
- Motor is weaker than expected: the L298N uses bipolar transistors and drops around 2 V, so a 6 V motor needs a higher supply.
Summary
Use software PWM from gpiozero where small timing errors do not matter, such as DC motor speed and LED brightness. For servos, enable dtoverlay=pwm-2chan and write period and duty cycle through /sys/class/pwm/pwmchip0, which gives a stable 50 Hz signal generated entirely in hardware. Always power servos and motors separately and share only the ground with the Pi.