mirror of
https://github.com/torvalds/linux.git
synced 2026-01-25 07:47:50 +00:00
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:
committed by
Uwe Kleine-König
parent
505b730ede
commit
9c06f26ba5
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
53
include/uapi/linux/pwm.h
Normal 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_ */
|
||||
Reference in New Issue
Block a user