Raspberry Pi PWM Support in User or Kernel Space - cu-ecen-aeld/buildroot-assignments-base GitHub Wiki
This section was last tested on Buildroot release 2022.02.11 on a Raspberry Pi 4 (RPi4).
Documentation:
The simplest way to use PWM on the RPi4 from userspace is via the sysfs interface. To enable the PWM controller, add the following to your config.txt
:
-
dtoverlay=pwm
— for one channel (PWM0) -
dtoverlay=pwm-2chan
— for both channels (PWM0 and PWM1)
Once enabled, PWM devices can be accessed through /sys/class/pwm/pwmchip<number>/
. You must export each channel before configuring it.
Example for PWM0:
# Export pwm0
echo 0 > /sys/class/pwm/pwmchip0/export
# Set period in nanoseconds
echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/period
# Set duty cycle in nanoseconds
echo 500000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
# Enable PWM output
echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
# Disable PWM output
echo 0 > /sys/class/pwm/pwmchip0/pwm0/enable
To access a PWM device from kernel space, you need to define it in the Device Tree. A working overlay example is shown below. The overlay must be compiled using (dtc -@ -I dts -O dtb -o my-overlay.dtbo my-overlay.dts
), then placed in buildroot/output/images/rpi-firmware/overlays
, and finally enabled by adding dtoverlay=my-overlay
to config.txt
. While Buildroot can be configured to automatically build and install overlays, this was not tested at the time of writing.
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2835";
fragment@0 {
target-path = "/soc/gpio@7e200000";
__overlay__ {
my_pwm_pins: my_pwm_pins {
brcm,pins = <18 19>;
brcm,function = <2 2>; // ALT5
};
};
};
fragment@1 {
target-path = "/soc/pwm@7e20c000";
__overlay__ {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&my_pwm_pins>;
};
};
fragment@2 {
target-path = "/soc";
__overlay__ {
my_pwm_device: my_pwm_device {
compatible = "mycompany,my-pwm-device";
pwms = <&pwm 0 20000000>, <&pwm 1 20000000>;
pwm-names = "pwm0", "pwm1";
status = "okay";
};
};
};
};
- Fragment 0 sets up the correct GPIO pinmux for PWM by assigning ALT5 mode to the GPIOs used (typically GPIO18 and GPIO19).
- Fragment 1 enables the PWM controller and attaches the pin configuration.
-
Fragment 2 defines a custom platform device (
my_pwm_device
) that uses both PWM channels and assigns human-readable names (pwm0
,pwm1
).
Fragments 0 and 1 are similar to the pwm-2chan
overlay used for userspace control. A reference implementation is available here.
Fragment 2 creates a platform device that can be accessed by a kernel module using the PWM interface described in [1]. The compatible
property identifies the device, following the format "vendor,device"
(e.g., "mycompany,my-pwm-device"
). This string is matched in the driver via the of_device_id
table.
static const struct of_device_id pwm_dt_ids[] = {
{.compatible = "mycompany,my-pwm-device"}, {}}; // From device tree property
MODULE_DEVICE_TABLE(of, pwm_dt_ids); // Matches driver to device
The driver must declare an of_device_id
match table and call MODULE_DEVICE_TABLE(of, ...)
so the kernel can match the overlay to the module. A platform_driver
structure is then used to register the driver, providing pointers to the probe
and remove
functions. These are similar in purpose to file_operations
used in character drivers.
static struct platform_driver pwm_driver = {
.probe = pwm_probe,
.remove = pwm_remove,
.driver =
{
.name = DEVICE_NAME,
.of_match_table = pwm_dt_ids,
},
};
module_platform_driver(pwm_driver); // Handles init and exit
When the kernel matches the device from the overlay with the loaded driver, it calls the driver's probe()
function. Inside probe()
, the driver can call devm_pwm_get()
using the names defined in the pwm-names
property to retrieve handles to the PWM channels. Once acquired, the driver can use the standard PWM API to enable, configure, and disable the PWM signals (pwm_enable()
, pwm_config()
, pwm_get_state()
, and pwm_disable()
). At this point, the driver is fully in control of the PWM hardware from kernel space.
#include <linux/pwm.h>
struct pwm_device *pwm_0 = devm_pwm_get(pdev, "pwm0");
pwm_enable(pwm_0);
pwm_config(pwm_0, 10000000 /* duty cycle in ns */, 20000000 /* period in ns */);
struct pwm_state state;
pwm_get_state(pwm_0, &state);
pwm_disable(pwm_0);