Hardware
GPIO 12 → Physical Pin 32
PWM Channel
PWM0_CHAN0 → chip=2, channel=0
Frequency
50 Hz (standard servo)
01
Wire the Servo
| Servo Wire | Connect to |
|---|---|
| Signal (orange / yellow) | GPIO 12 — Physical Pin 32 |
| Power (red) | 5V — Pin 2 or 4 (use external supply for metal-gear servos) |
| Ground (brown / black) | GND — Pin 6 or 9 |
Important
Share a common ground between the Pi and any external servo power supply. Without a shared ground, the PWM signal has no reference voltage and the servo will not respond.
02
Edit config.txt
Open the boot configuration file:
sudo nano /boot/firmware/config.txt
Add this line to the [all] section (or at the bottom of the file):
dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4
Save and reboot:
sudo reboot
Verify the overlay loaded
After rebooting, confirm the PWM chip is visible:
ls /sys/class/pwm/ # Expected output: pwmchip0 pwmchip2 pinctrl get 12 # Expected: 12: a4 pd | lo // GPIO12 = PWM0_CHAN0
Pi 5 Note
lsmod | grep pwm returns nothing on the Pi 5 — this is normal. The RP1 southbridge uses a device-tree driver that doesn't appear in lsmod. Use ls /sys/class/pwm/ to verify instead.
03
Add User to gpio Group
sudo usermod -aG gpio $USER
Log out and back in (or reboot) for the group change to take effect, then verify:
groups
# output should include: gpio
04
Set Up PWM Permissions
The Pi 5 RP1 driver doesn't reliably fire udev events on channel export, so a systemd oneshot service is the most robust approach.
Create the helper script
sudo nano /usr/local/bin/pwm-permissions.sh
Paste the following contents:
#!/bin/bash chown -R root:gpio /sys/class/pwm chmod -R 770 /sys/class/pwm # Export channel 0 on pwmchip2 (GPIO 12) if [ ! -d /sys/class/pwm/pwmchip2/pwm0 ]; then echo 0 > /sys/class/pwm/pwmchip2/export sleep 0.1 fi chown -R root:gpio /sys/class/pwm chmod -R 770 /sys/class/pwm
Make it executable:
sudo chmod +x /usr/local/bin/pwm-permissions.sh
Create the systemd service
sudo nano /etc/systemd/system/pwm-permissions.service
Paste the following:
[Unit] Description=Hardware PWM permissions for gpio group After=multi-user.target [Service] Type=oneshot ExecStart=/usr/local/bin/pwm-permissions.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload sudo systemctl enable pwm-permissions.service sudo systemctl start pwm-permissions.service
05
Install rpi-hardware-pwm
pip install rpi-hardware-pwm --break-system-packages
Why --break-system-packages?
Trixie (Debian 13) uses an externally-managed Python environment by default. The --break-system-packages flag tells pip to install into the system site-packages anyway. This is the simplest approach for our single-purpose Pi 5 setups.
06
Control the Servo
Critical Pi 5 Parameter
Always pass chip=2 when creating the HardwarePWM object. The RP1 controller registers as pwmchip2, not pwmchip0 as on older Pi models.
from rpi_hardware_pwm import HardwarePWM import time # Pi 5: chip=2 always. GPIO 12 = channel 0. pwm = HardwarePWM(pwm_channel=0, hz=50, chip=2) pwm.start(7.5) # Start at centre (90°) def set_angle(degrees): """Map 0-180 degrees to duty cycle (2.5%-12.5%)""" duty = 2.5 + (degrees / 180.0) * 10.0 pwm.change_duty_cycle(duty) set_angle(0) # Full left time.sleep(1) set_angle(90) # Centre time.sleep(1) set_angle(180) # Full right time.sleep(1) set_angle(90) # Return to centre time.sleep(0.5) pwm.stop()
Understanding the Duty Cycle Math
| Angle | Duty Cycle | Pulse Width | Servo Position |
|---|---|---|---|
| 0° | 2.5% | 0.5 ms | Full left |
| 90° | 7.5% | 1.5 ms | Centre |
| 180° | 12.5% | 2.5 ms | Full right |
At 50 Hz the period is 20 ms. A 2.5% duty cycle means the signal is HIGH for 0.5 ms out of every 20 ms cycle — that's what tells the servo to go to 0°.
—
Troubleshooting
| Symptom | Fix |
|---|---|
PermissionError on export |
Re-run the systemd service; confirm groups includes gpio; log out and back in |
FileNotFoundError on pwmchip2 |
Overlay didn't load — check config.txt spelling, reboot, then run ls /sys/class/pwm/ |
| Servo jitters / vibrates | Use a separate 5V supply for servo power; share GND with Pi. Never power servos from the Pi's GPIO header. |
| Servo doesn't reach full range | Adjust the 2.5 / 12.5 duty cycle limits in set_angle() — exact range varies by servo model |
chip=0 works but chip=2 doesn't |
Your overlay may have loaded on a different chip number. Run ls /sys/class/pwm/ and check which pwmchip directories exist. |