Commit 9c06f26b authored by Uwe Kleine-König's avatar Uwe Kleine-König Committed by Uwe Kleine-König
Browse files

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: default avatarUwe 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: default avatarUwe Kleine-König <ukleinek@kernel.org>
parent 505b730e
Loading
Loading
Loading
Loading
+307 −15
Original line number Diff line number Diff line
@@ -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;
	}

+3 −0
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
/* 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_ */