]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
pwm: Add support for pwmchip devices for faster and easier userspace access
authorUwe Kleine-König <u.kleine-koenig@baylibre.com>
Wed, 30 Apr 2025 11:56:01 +0000 (13:56 +0200)
committerUwe Kleine-König <ukleinek@kernel.org>
Mon, 7 Jul 2025 06:39:33 +0000 (08:39 +0200)
With this change each pwmchip defining the new-style waveform callbacks
can be accessed from userspace via a character device. Compared to the
sysfs-API this is faster and allows to pass the whole configuration in a
single ioctl allowing atomic application and thus reducing glitches.

On an STM32MP13 I see:

root@DistroKit:~ time pwmtestperf
real 0m 1.27s
user 0m 0.02s
sys 0m 1.21s
root@DistroKit:~ rm /dev/pwmchip0
root@DistroKit:~ time pwmtestperf
real 0m 3.61s
user 0m 0.27s
sys 0m 3.26s

pwmtestperf does essentially:

for i in 0 .. 50000:
pwm_set_waveform(duty_length_ns=i, period_length_ns=50000, duty_offset_ns=0)

and in the presence of /dev/pwmchip0 is uses the ioctls introduced here,
without that device it uses /sys/class/pwm/pwmchip0.

Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
Link: https://lore.kernel.org/r/ad4a4e49ae3f8ea81e23cac1ac12b338c3bf5c5b.1746010245.git.u.kleine-koenig@baylibre.com
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
drivers/pwm/core.c
include/linux/pwm.h
include/uapi/linux/pwm.h [new file with mode: 0644]

index edf776b8ad53b9820a7fd85b810b379bae92b5d2..50aa0528a2653b69efb632d5f3b9b78c0b957909 100644 (file)
 
 #include <dt-bindings/pwm/pwm.h>
 
+#include <uapi/linux/pwm.h>
+
 #define CREATE_TRACE_POINTS
 #include <trace/events/pwm.h>
 
+#define PWM_MINOR_COUNT 256
+
 /* protects access to pwm_chips */
 static DEFINE_MUTEX(pwm_lock);
 
@@ -2007,20 +2011,9 @@ struct pwm_device *pwm_get(struct device *dev, const char *con_id)
 }
 EXPORT_SYMBOL_GPL(pwm_get);
 
-/**
- * pwm_put() - release a PWM device
- * @pwm: PWM device
- */
-void pwm_put(struct pwm_device *pwm)
+static void __pwm_put(struct pwm_device *pwm)
 {
-       struct pwm_chip *chip;
-
-       if (!pwm)
-               return;
-
-       chip = pwm->chip;
-
-       guard(mutex)(&pwm_lock);
+       struct pwm_chip *chip = pwm->chip;
 
        /*
         * Trigger a warning if a consumer called pwm_put() twice.
@@ -2041,6 +2034,20 @@ void pwm_put(struct pwm_device *pwm)
 
        module_put(chip->owner);
 }
+
+/**
+ * pwm_put() - release a PWM device
+ * @pwm: PWM device
+ */
+void pwm_put(struct pwm_device *pwm)
+{
+       if (!pwm)
+               return;
+
+       guard(mutex)(&pwm_lock);
+
+       __pwm_put(pwm);
+}
 EXPORT_SYMBOL_GPL(pwm_put);
 
 static void devm_pwm_release(void *pwm)
@@ -2110,6 +2117,274 @@ struct pwm_device *devm_fwnode_pwm_get(struct device *dev,
 }
 EXPORT_SYMBOL_GPL(devm_fwnode_pwm_get);
 
+struct pwm_cdev_data {
+       struct pwm_chip *chip;
+       struct pwm_device *pwm[];
+};
+
+static int pwm_cdev_open(struct inode *inode, struct file *file)
+{
+       struct pwm_chip *chip = container_of(inode->i_cdev, struct pwm_chip, cdev);
+       struct pwm_cdev_data *cdata;
+
+       guard(mutex)(&pwm_lock);
+
+       if (!chip->operational)
+               return -ENXIO;
+
+       cdata = kzalloc(struct_size(cdata, pwm, chip->npwm), GFP_KERNEL);
+       if (!cdata)
+               return -ENOMEM;
+
+       cdata->chip = chip;
+
+       file->private_data = cdata;
+
+       return nonseekable_open(inode, file);
+}
+
+static int pwm_cdev_release(struct inode *inode, struct file *file)
+{
+       struct pwm_cdev_data *cdata = file->private_data;
+       unsigned int i;
+
+       for (i = 0; i < cdata->chip->npwm; ++i) {
+               struct pwm_device *pwm = cdata->pwm[i];
+
+               if (pwm) {
+                       const char *label = pwm->label;
+
+                       pwm_put(cdata->pwm[i]);
+                       kfree(label);
+               }
+       }
+       kfree(cdata);
+
+       return 0;
+}
+
+static int pwm_cdev_request(struct pwm_cdev_data *cdata, unsigned int hwpwm)
+{
+       struct pwm_chip *chip = cdata->chip;
+
+       if (hwpwm >= chip->npwm)
+               return -EINVAL;
+
+       if (!cdata->pwm[hwpwm]) {
+               struct pwm_device *pwm = &chip->pwms[hwpwm];
+               const char *label;
+               int ret;
+
+               label = kasprintf(GFP_KERNEL, "pwm-cdev (pid=%d)", current->pid);
+               if (!label)
+                       return -ENOMEM;
+
+               ret = pwm_device_request(pwm, label);
+               if (ret < 0) {
+                       kfree(label);
+                       return ret;
+               }
+
+               cdata->pwm[hwpwm] = pwm;
+       }
+
+       return 0;
+}
+
+static int pwm_cdev_free(struct pwm_cdev_data *cdata, unsigned int hwpwm)
+{
+       struct pwm_chip *chip = cdata->chip;
+
+       if (hwpwm >= chip->npwm)
+               return -EINVAL;
+
+       if (cdata->pwm[hwpwm]) {
+               struct pwm_device *pwm = cdata->pwm[hwpwm];
+               const char *label = pwm->label;
+
+               __pwm_put(pwm);
+
+               kfree(label);
+
+               cdata->pwm[hwpwm] = NULL;
+       }
+
+       return 0;
+}
+
+static struct pwm_device *pwm_cdev_get_requested_pwm(struct pwm_cdev_data *cdata,
+                                                    u32 hwpwm)
+{
+       struct pwm_chip *chip = cdata->chip;
+
+       if (hwpwm >= chip->npwm)
+               return ERR_PTR(-EINVAL);
+
+       if (cdata->pwm[hwpwm])
+               return cdata->pwm[hwpwm];
+
+       return ERR_PTR(-EINVAL);
+}
+
+static long pwm_cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+{
+       int ret = 0;
+       struct pwm_cdev_data *cdata = file->private_data;
+       struct pwm_chip *chip = cdata->chip;
+
+       guard(mutex)(&pwm_lock);
+
+       if (!chip->operational)
+               return -ENODEV;
+
+       switch (cmd) {
+       case PWM_IOCTL_REQUEST:
+               {
+                       unsigned int hwpwm = arg;
+
+                       return pwm_cdev_request(cdata, hwpwm);
+               }
+
+       case PWM_IOCTL_FREE:
+               {
+                       unsigned int hwpwm = arg;
+
+                       return pwm_cdev_free(cdata, hwpwm);
+               }
+
+       case PWM_IOCTL_ROUNDWF:
+               {
+                       struct pwmchip_waveform cwf;
+                       struct pwm_waveform wf;
+                       struct pwm_device *pwm;
+
+                       ret = copy_from_user(&cwf,
+                                            (struct pwmchip_waveform __user *)arg,
+                                            sizeof(cwf));
+                       if (ret)
+                               return -EFAULT;
+
+                       if (cwf.__pad != 0)
+                               return -EINVAL;
+
+                       pwm = pwm_cdev_get_requested_pwm(cdata, cwf.hwpwm);
+                       if (IS_ERR(pwm))
+                               return PTR_ERR(pwm);
+
+                       wf = (struct pwm_waveform) {
+                               .period_length_ns = cwf.period_length_ns,
+                               .duty_length_ns = cwf.duty_length_ns,
+                               .duty_offset_ns = cwf.duty_offset_ns,
+                       };
+
+                       ret = pwm_round_waveform_might_sleep(pwm, &wf);
+                       if (ret < 0)
+                               return ret;
+
+                       cwf = (struct pwmchip_waveform) {
+                               .hwpwm = cwf.hwpwm,
+                               .period_length_ns = wf.period_length_ns,
+                               .duty_length_ns = wf.duty_length_ns,
+                               .duty_offset_ns = wf.duty_offset_ns,
+                       };
+
+                       return copy_to_user((struct pwmchip_waveform __user *)arg,
+                                           &cwf, sizeof(cwf));
+               }
+
+       case PWM_IOCTL_GETWF:
+               {
+                       struct pwmchip_waveform cwf;
+                       struct pwm_waveform wf;
+                       struct pwm_device *pwm;
+
+                       ret = copy_from_user(&cwf,
+                                            (struct pwmchip_waveform __user *)arg,
+                                            sizeof(cwf));
+                       if (ret)
+                               return -EFAULT;
+
+                       if (cwf.__pad != 0)
+                               return -EINVAL;
+
+                       pwm = pwm_cdev_get_requested_pwm(cdata, cwf.hwpwm);
+                       if (IS_ERR(pwm))
+                               return PTR_ERR(pwm);
+
+                       ret = pwm_get_waveform_might_sleep(pwm, &wf);
+                       if (ret)
+                               return ret;
+
+                       cwf = (struct pwmchip_waveform) {
+                               .hwpwm = cwf.hwpwm,
+                               .period_length_ns = wf.period_length_ns,
+                               .duty_length_ns = wf.duty_length_ns,
+                               .duty_offset_ns = wf.duty_offset_ns,
+                       };
+
+                       return copy_to_user((struct pwmchip_waveform __user *)arg,
+                                           &cwf, sizeof(cwf));
+               }
+
+       case PWM_IOCTL_SETROUNDEDWF:
+       case PWM_IOCTL_SETEXACTWF:
+               {
+                       struct pwmchip_waveform cwf;
+                       struct pwm_waveform wf;
+                       struct pwm_device *pwm;
+
+                       ret = copy_from_user(&cwf,
+                                            (struct pwmchip_waveform __user *)arg,
+                                            sizeof(cwf));
+                       if (ret)
+                               return -EFAULT;
+
+                       if (cwf.__pad != 0)
+                               return -EINVAL;
+
+                       wf = (struct pwm_waveform){
+                               .period_length_ns = cwf.period_length_ns,
+                               .duty_length_ns = cwf.duty_length_ns,
+                               .duty_offset_ns = cwf.duty_offset_ns,
+                       };
+
+                       if (!pwm_wf_valid(&wf))
+                               return -EINVAL;
+
+                       pwm = pwm_cdev_get_requested_pwm(cdata, cwf.hwpwm);
+                       if (IS_ERR(pwm))
+                               return PTR_ERR(pwm);
+
+                       ret = pwm_set_waveform_might_sleep(pwm, &wf,
+                                                          cmd == PWM_IOCTL_SETEXACTWF);
+
+                       /*
+                        * If userspace cares about rounding deviations it has
+                        * to check the values anyhow, so simplify handling for
+                        * them and don't signal uprounding. This matches the
+                        * behaviour of PWM_IOCTL_ROUNDWF which also returns 0
+                        * in that case.
+                        */
+                       if (ret == 1)
+                               ret = 0;
+
+                       return ret;
+               }
+
+       default:
+               return -ENOTTY;
+       }
+}
+
+static const struct file_operations pwm_cdev_fileops = {
+       .open = pwm_cdev_open,
+       .release = pwm_cdev_release,
+       .owner = THIS_MODULE,
+       .unlocked_ioctl = pwm_cdev_ioctl,
+};
+
+static dev_t pwm_devt;
+
 /**
  * __pwmchip_add() - register a new PWM chip
  * @chip: the PWM chip to add
@@ -2162,7 +2437,17 @@ int __pwmchip_add(struct pwm_chip *chip, struct module *owner)
        scoped_guard(pwmchip, chip)
                chip->operational = true;
 
-       ret = device_add(&chip->dev);
+       if (chip->ops->write_waveform) {
+               if (chip->id < PWM_MINOR_COUNT)
+                       chip->dev.devt = MKDEV(MAJOR(pwm_devt), chip->id);
+               else
+                       dev_warn(&chip->dev, "chip id too high to create a chardev\n");
+       }
+
+       cdev_init(&chip->cdev, &pwm_cdev_fileops);
+       chip->cdev.owner = owner;
+
+       ret = cdev_device_add(&chip->cdev, &chip->dev);
        if (ret)
                goto err_device_add;
 
@@ -2213,7 +2498,7 @@ void pwmchip_remove(struct pwm_chip *chip)
                idr_remove(&pwm_chips, chip->id);
        }
 
-       device_del(&chip->dev);
+       cdev_device_del(&chip->cdev, &chip->dev);
 }
 EXPORT_SYMBOL_GPL(pwmchip_remove);
 
@@ -2357,9 +2642,16 @@ static int __init pwm_init(void)
 {
        int ret;
 
+       ret = alloc_chrdev_region(&pwm_devt, 0, PWM_MINOR_COUNT, "pwm");
+       if (ret) {
+               pr_err("Failed to initialize chrdev region for PWM usage\n");
+               return ret;
+       }
+
        ret = class_register(&pwm_class);
        if (ret) {
                pr_err("Failed to initialize PWM class (%pe)\n", ERR_PTR(ret));
+               unregister_chrdev_region(pwm_devt, 256);
                return ret;
        }
 
index 63a17d2b4ec8d0d34a02b05240efb75b637dd99f..2492c91452f9641881e9923e5a97e0705047da59 100644 (file)
@@ -2,6 +2,7 @@
 #ifndef __LINUX_PWM_H
 #define __LINUX_PWM_H
 
+#include <linux/cdev.h>
 #include <linux/device.h>
 #include <linux/err.h>
 #include <linux/module.h>
@@ -311,6 +312,7 @@ struct pwm_ops {
 /**
  * struct pwm_chip - abstract a PWM controller
  * @dev: device providing the PWMs
+ * @cdev: &struct cdev for this device
  * @ops: callbacks for this PWM controller
  * @owner: module providing this chip
  * @id: unique number of this PWM chip
@@ -325,6 +327,7 @@ struct pwm_ops {
  */
 struct pwm_chip {
        struct device dev;
+       struct cdev cdev;
        const struct pwm_ops *ops;
        struct module *owner;
        unsigned int id;
diff --git a/include/uapi/linux/pwm.h b/include/uapi/linux/pwm.h
new file mode 100644 (file)
index 0000000..182d59c
--- /dev/null
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note */
+
+#ifndef _UAPI_PWM_H_
+#define _UAPI_PWM_H_
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+/**
+ * struct pwmchip_waveform - Describe a PWM waveform for a pwm_chip's PWM channel
+ * @hwpwm: per-chip relative index of the PWM device
+ * @__pad: padding, must be zero
+ * @period_length_ns: duration of the repeating period.
+ *    A value of 0 represents a disabled PWM.
+ * @duty_length_ns: duration of the active part in each period
+ * @duty_offset_ns: offset of the rising edge from a period's start
+ */
+struct pwmchip_waveform {
+       __u32 hwpwm;
+       __u32 __pad;
+       __u64 period_length_ns;
+       __u64 duty_length_ns;
+       __u64 duty_offset_ns;
+};
+
+/* Reserves the passed hwpwm for exclusive control. */
+#define PWM_IOCTL_REQUEST      _IO(0x75, 1)
+
+/* counter part to PWM_IOCTL_REQUEST */
+#define PWM_IOCTL_FREE         _IO(0x75, 2)
+
+/*
+ * Modifies the passed wf according to hardware constraints. All parameters are
+ * rounded down to the next possible value, unless there is no such value, then
+ * values are rounded up. Note that zero isn't considered for rounding down
+ * period_length_ns.
+ */
+#define PWM_IOCTL_ROUNDWF      _IOWR(0x75, 3, struct pwmchip_waveform)
+
+/* Get the currently implemented waveform */
+#define PWM_IOCTL_GETWF                _IOWR(0x75, 4, struct pwmchip_waveform)
+
+/* Like PWM_IOCTL_ROUNDWF + PWM_IOCTL_SETEXACTWF in one go. */
+#define PWM_IOCTL_SETROUNDEDWF _IOW(0x75, 5, struct pwmchip_waveform)
+
+/*
+ * Program the PWM to emit exactly the passed waveform, subject only to rounding
+ * down each value less than 1 ns. Returns 0 on success, -EDOM if the waveform
+ * cannot be implemented exactly, or other negative error codes.
+ */
+#define PWM_IOCTL_SETEXACTWF   _IOW(0x75, 6, struct pwmchip_waveform)
+
+#endif /* _UAPI_PWM_H_ */