Skip to content

Instantly share code, notes, and snippets.

@Gadgetoid
Last active May 8, 2024 20:12
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)

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