Skip to content

Instantly share code, notes, and snippets.

@Gadgetoid
Last active December 11, 2024 11:41
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)."
@Gadgetoid
Copy link
Author

Gadgetoid commented Mar 6, 2024

Possibly a better way to handle the script:

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

function usage {
	printf "Usage: $0 <channel> <period> <duty_cycle>\n"
	printf "    pin - one of 12, 13, 14, 15, 18 or 19\n"
	printf "    period - PWM period in nanoseconds\n"
	printf "    duty_cycle - Duty Cycle (on period) in nanoseconds\n"
	exit 1
}

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

case $PIN in
	"12")
	CHANNEL="0"
	;;
	"13")
	CHANNEL="1"
	;;
	"14")
	CHANNEL="2"
	;;
	"15")
	CHANNEL="3"
	;;
	"18")
	CHANNEL="2"
	FUNC="a3"
	;;
	"19")
	CHANNEL="3"
	FUNC="a3"
	;;
	*)
	echo "Unknown pin $PIN."
	exit 1
esac

function pwmset {
	echo "$2" | sudo tee -a "$NODE/$1" > /dev/null
}

if [[ "$PERIOD" == "off" ]]; then	
	if [ -d "$NODE/pwm$CHANNEL" ]; then
		pinctrl set $PIN no
		pwmset "pwm$CHANNEL/enable" "0"
		pwmset "unexport" "$CHANNEL"
	fi
	exit 0
fi

if [[ ! $PERIOD =~ ^[0-9]+$ ]]; then
	usage
fi

if [[ ! $DUTY_CYCLE =~ ^[0-9]+$ ]]; then
	usage
fi

if [ ! -d "$NODE/pwm$CHANNEL" ]; then
	pwmset "export" "$CHANNEL"
fi

pwmset "pwm$CHANNEL/enable" "0"
pwmset "pwm$CHANNEL/period" "$PERIOD"
if [ $? -ne 0 ]; then
	echo "^ don't worry, handling it!"
	pwmset "pwm$CHANNEL/duty_cycle" "$DUTY_CYCLE"
	pwmset "pwm$CHANNEL/period" "$PERIOD"
else
	pwmset "pwm$CHANNEL/duty_cycle" "$DUTY_CYCLE"
fi
pwmset "pwm$CHANNEL/enable" "1"

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

echo "GPIO $PIN (Ch. $CHANNEL, Fn. $FUNC) set to $PERIOD ns, $DUTY_CYCLE."

@elgeeko1
Copy link

elgeeko1 commented May 4, 2024

Kind soul, thank you for this, I tried many different solutions, including using the raspian devicetree overlays and directly setting memory-mapped registers to no avail. Your solution was the only one that worked, and it worked out of the box for me.

It's crazy to think it is this difficult to unlock PWM, and that there isn't a better tutorial for it.

@elgeeko1
Copy link

elgeeko1 commented May 4, 2024

In my case my chip was pwmchip2 (I also had a pwmchip0 and pwmchip6)

@easytarget
Copy link

There are some pwm overlays available by default in RaspiOS
https://raspberrypi.stackexchange.com/a/143644

I was able to just run sudo dtoverlay pwm pin=12 func=4 and the pwmchip0 device appeared in /sys/class/pwm and works as expected.

At least this works on my Pi3a; but I think this is common with the Pi4/5.

After that I can control it from userland (commandline and a python lib) with a little pwm daemon tool I wrote:
https://github.com/easytarget/pyPWMd

  • I'm still finishing the readme.. but I just tested this and it works on the PI.
  • It was written for a rather different SBC, a MangoPi MQ-Pro, which is risc-v based and raspi.GPIO control of PWM is not an option.

@Gadgetoid
Copy link
Author

FWIW I nudged a mailing list patch to add a kernel driver for arbitrary GPIO pin PWM support a while back and it's slowly making its way through the regular channels and should arrive in Raspberry Pi OS eventually: raspberrypi/linux@7f61257

It's actually pretty decent, giving much better stability and jitter than any userspace software approaches.

A chardev ABI for PWM is also coming, which should hopefully clean up this eldritch horror of catch-22 sysfs poking. I can't find the original discussion, but see: https://lore.kernel.org/linux-pwm/7490e64bbe12e2046d92716dadef7070881592e6.1720435656.git.u.kleine-koenig@baylibre.com/

@easytarget
Copy link

easytarget commented Sep 30, 2024

WIW I nudged a mailing list patch to add a kernel driver for arbitrary GPIO pin PWM support a while back and it's slowly making its way through the regular channels and should arrive in Raspberry Pi OS eventually: raspberrypi/linux@7f61257

It's actually pretty decent, giving much better stability and jitter than any userspace software approaches.

That sounds good, bit-bang PWM is best done from the kernel :-)

There is also lg, which is cross-platform and has some software PWM functions too.
https://github.com/joan2937/lg

A chardev ABI for PWM is also coming, which should hopefully clean up this eldritch horror of catch-22 sysfs poking. I can't find the original discussion, but see: https://lore.kernel.org/linux-pwm/7490e64bbe12e2046d92716dadef7070881592e6.1720435656.git.u.kleine-koenig@baylibre.com/

Cool.. what we need. Properly cross-platform too. 😄

I sort of knew as I was putting my simple PWM tool together that it will get superseded by a proper solution. But I couldn't find one already existing and fancied a bit of hobby programming..

@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 ?

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