Skip to content

Instantly share code, notes, and snippets.

@Gadgetoid
Last active May 20, 2026 21:06
Show Gist options
  • Select an option

  • Save Gadgetoid/b92ad3db06ff8c264eef2abf0e09d569 to your computer and use it in GitHub Desktop.

Select an option

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)."
@naich58

naich58 commented Nov 19, 2025

Copy link
Copy Markdown

Thank you so much for the dtoverlay. I've been trying to get hardware PWM working for days now. Yours is the first solution I've found that works.

@db2053

db2053 commented May 20, 2026

Copy link
Copy Markdown

Hi,

I am submitting a README.md modification to fix a bug in the first script version:

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

echo "0" allow only PWM0 channel usage.
The following lines enable correct channel management for all channels:

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

This bug is fixed in the second script version.

I have updated the 2nd script version, with new comments and more explicit writing:

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

# *******************************
# Function to display script usage
# *******************************
function usage {
	printf "Usage: $0 <pin> <period> <duty_cycle>\n"
	printf "    pin - one of 12, 13, 14, 15, 18 or 19 (14 and 18 on the same channel, 15 and 19 on the same chanel\n"
	printf "    period - PWM period in nanoseconds\n"
	printf "    duty_cycle - Duty Cycle (on period) in nanoseconds\n"
	exit 1
}

# **************************************************
# Function to modify a setting value on the PWM node
#***************************************************
function pwmset {
	SETTING=$1
	VALUE=$2
	echo "$VALUE" | sudo tee -a "$NODE/$SETTING" > /dev/null
}

# ************
# *** Main ***
# ************
if [ -d "$NODE/device/consumer:platform:cooling_fan/" ]; then
	echo "$NODE is reserved for platform cooling fan. Use another free PWM node !"
	exit 1
fi

# Select PWM settings for the asqed PIN
case $PIN in
	"12")
	CHANNEL="0"
	FUNC="a0"
	;;
	"13")
	CHANNEL="1"
	FUNC="a0"
	;;
	"14")
	CHANNEL="2"
	FUNC="a0"
	;;
	"15")
	CHANNEL="3"
	FUNC="a0"
	;;
	"18")
	CHANNEL="2"
	FUNC="a3"
	;;
	"19")
	CHANNEL="3"
	FUNC="a3"
	;;
	*)
	echo "Unknown pin $PIN."
	exit 1
esac

# PWM pin desactivation if asked
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

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

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

# Activate channel (create drivers file)
if [ ! -d "$NODE/pwm$CHANNEL" ]; then
	pwmset "export" "$CHANNEL"
fi

# PWM channel configuration
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."

In my case, platform cooling fan is on NODE=/sys/class/pwm/pwmchip1 and I need to use NODE=/sys/class/pwm/pwmchip0

Thanks a lot for this project.

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