pwm: Add support for pwmchip devices for faster and easier userspace access

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>
This commit is contained in:
Uwe Kleine-König
2025-04-30 13:56:01 +02:00
committed by Uwe Kleine-König
parent 505b730ede
commit 9c06f26ba5
3 changed files with 363 additions and 15 deletions

View File

@@ -23,9 +23,13 @@
#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;
}

View 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;

53
include/uapi/linux/pwm.h Normal file
View File

@@ -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_ */