Skip to content

Instantly share code, notes, and snippets.

@Gadgetoid
Last active April 4, 2025 12:46
Show Gist options
  • Save Gadgetoid/b92ad3db06ff8c264eef2abf0e09d569 to your computer and use it in GitHub Desktop.
Save Gadgetoid/b92ad3db06ff8c264eef2abf0e09d569 to your computer and use it in GitHub Desktop.
Raspberry Pi 5 - All channels on pwm0

Hardware PWM on the Raspberry Pi 5 b

Since PWM is a little fraught with gotchas, this is mostly a message to future me-

(Note to self, rtfm - https://datasheets.raspberrypi.com/rp1/rp1-peripherals.pdf)

pin a0 a3
GPIO19 PWM0_CHAN3
GPIO18 PWM0_CHAN2
GPIO15 PWM0_CHAN3
GPIO14 PWM0_CHAN2
GPIO13 PWM0_CHAN1
GPIO12 PWM0_CHAN0

TODO: Figure out how to tell if pwm0 is on /sys/class/pwm/pwmchip1 or /sys/class/pwm/pwmchip2. pwm1 on the Pi 5 might have device/consumer:platform:cooling_fan/

Life is short, this single dtoverlay configures GPIO12, GPIO13, GPIO18 and GPIO19 to their respective alt modes on boot and enables pwm0:

/dts-v1/;
/plugin/;

/{
	compatible = "brcm,bcm2712";

	fragment@0 {
		target = <&rp1_gpio>;
		__overlay__ {
			pwm_pins: pwm_pins {
				pins = "gpio12", "gpio13", "gpio18", "gpio19";
				function = "pwm0", "pwm0", "pwm0", "pwm0";
			};
		};
	};

	fragment@1 {
		target = <&rp1_pwm0>;
		frag1: __overlay__ {
			pinctrl-names = "default";
			pinctrl-0 = <&pwm_pins>;
			status = "okay";
		};
	};
};

Save as "pwm-pi5-overlay.dts" and compile with:

dtc -I dts -O dtb -o pwm-pi5.dtbo pwm-pi5-overlay.dts

Install:

sudo cp pwm-pi5.dtbo /boot/firmware/overlays/

Don't forget to add dtoverlay=pwm-pi5 to /boot/firmware/config.txt...

Then use this janky script to stick some safety rails on poking PWM:

#!/bin/bash
NODE=/sys/class/pwm/pwmchip1
CHANNEL="$1"
PERIOD="$2"
DUTY_CYCLE="$3"

function usage {
	printf "Usage: $0 <channel> <period> <duty_cycle>\n"
	printf "    channel - number from 0-3\n"
	printf "    period - PWM period in nanoseconds\n"
	printf "    duty_cycle - Duty Cycle (on period) in nanoseconds\n"
	exit 1
}

if [[ ! $CHANNEL =~ ^[0-3]+$ ]]; then
	usage
fi

if [ -d "$NODE/device/consumer:platform:cooling_fan/" ]; then
	echo "Hold your horses, looks like this is pwm1?"
	exit 1
fi

if [ ! -d "$NODE/pwm$CHANNEL" ]; then
	echo "0" | sudo tee -a "$NODE/export"
fi

echo "0" | sudo tee -a "$NODE/pwm$CHANNEL/enable" > /dev/null
echo "$PERIOD" | sudo tee -a "$NODE/pwm$CHANNEL/period" > /dev/null
if [ $? -ne 0 ]; then
	echo "^ don't worry, handling it!"
	echo "$DUTY_CYCLE" | sudo tee -a "$NODE/pwm$CHANNEL/duty_cycle" > /dev/null
	echo "$PERIOD" | sudo tee -a "$NODE/pwm$CHANNEL/period" > /dev/null
else
	echo "$DUTY_CYCLE" | sudo tee -a "$NODE/pwm$CHANNEL/duty_cycle" > /dev/null
fi
echo "1" | sudo tee -a "$NODE/pwm$CHANNEL/enable" > /dev/null


case $CHANNEL in
	"0")
	PIN="12"
	FUNC="a0"
	;;
	"1")
	PIN="13"
	FUNC="a0"
	;;
	"2")
	PIN="18"
	FUNC="a3"
	;;
	"3")
	PIN="19"
	FUNC="a3"
esac

# Sure, the pin is set to the correct alt mode by the dtoverlay at startup...
# But we'll do this to protect the user (me, the user is me) from themselves:
pinctrl set $PIN $FUNC

echo "PWM$CHANNEL set to $PERIOD ns, $DUTY_CYCLE, on pin $PIN (func $FUNC)."
@mohamedsemz
Copy link

mohamedsemz commented Dec 11, 2024

I'm using raspberry pi 5 and did all what was written in the here. but when I run the bash script it shows this error
tee: /sys/class/pwm/pwmchip0/pwm0/enable: Invalid argument
even when I try to write in the enable file by myself it shows the same error

small note: In /sys/class/pwm there are pwmchip0, pwmchip2 so I changed the script to pwmchip0 instead of 1

I tried to check if the overlay loaded or not by removing it
sudo dtoverlay -r pwm-pi5
and the response is

  • Overlay 'pwm-pi5' is not loaded

although it's added to the path /boot/firmware/overlays and I added "dtoverlay=pwm-pi5" in /boot/firmware/config.txt and rebooted several times

and when I try to add the overlay manually by
sudo dtoverlay pwm-pi5
I get

  • Failed to apply overlay '0_pwm-pi5' (kernel)
    with no info in the dmesg

and I get the same responce when I try to use
sudo dtoverlay pwm-2chan

Does anybody know what is the problem ?

@sirpewe2
Copy link

Hi.
I have the exact same problem.
Trying this on RPI5 with bookworm.
I've tried adding in /boot/firmware/config.txt under section [all]
dtoverlay=pwm-2chan
or
dtoverlay=pwm
or
dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4

But none some to work.
lsmod | grep pwm returns nothing.

Will test some more and report.

@tkunchick
Copy link

I messed with this HardwarePWM for about 4 days on and off for my RaspPi5 to control the fan speed (120mm) in the mini server rack I have before I got it working. I have the Pi5 monitoring all the rasp pi temps and controlling the fan based on the highest temperature.

01 - 1 (1)

Using: dtoverlay=pwm-2chan

pinctrl returns

pinctrl funcs 12-13,18-19
12, GPIO12, PWM0_CHAN0, DPI_D8, TXD4, SDA2, AAUD_LEFT, SYS_RIO012, PROC_RIO012, PIO12, SPI5_CE0
13, GPIO13, PWM0_CHAN1, DPI_D9, RXD4, SCL2, AAUD_RIGHT, SYS_RIO013, PROC_RIO013, PIO13, SPI5_SIO1
18, GPIO18, SPI1_CE0, DPI_D14, I2S0_SCLK, PWM0_CHAN2, I2S1_SCLK, SYS_RIO018, PROC_RIO018, PIO18, GPCLK1
19, GPIO19, SPI1_MISO, DPI_D15, I2S0_WS, PWM0_CHAN3, I2S1_WS, SYS_RIO019, PROC_RIO019, PIO19, -

pinctrl 12
12: no    pd | -- // GPIO12 = none

I got my script working with the following:

from rpi_hardware_pwm import HardwarePWM
import time                                # Calling time to allow delays to be used
import subprocess                          # Calling subprocess to get the CPU temperatures


pwm = HardwarePWM(2, 1500, chip=2)         # channel 0 1 2 3 for GPIO12 13 18 19 respectively?? Working on channel 2 for some reason (RaspPi5)
                                           # 1500 = hz
                                           # chip=2 This indicates that the PWM channel is mapped to the PWM chip 2
                                           # which controls GPIO 12 and 13. For Rpi 1,2,3,4, use chip=0; For Rpi 5, use chip=2

pwm.start(50)                              # Start PWM with a 50% duty cycle

I found the chip information on https://pypi.org/project/rpi-hardware-pwm/

I don't know why channel 2 works when it should be channel 0 for pin 12 that I am using and indicated by pinctrl. I also forget why I even tried a different channel.

Maybe this will help someone.

I am just a tinkering guy, no real experience with python. But been PC/Server admin for over 40 years (don't we miss DOS and OS2 Warp) and messing with Rasp Pis/Linux since 2012.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment