mirror of
https://github.com/VIKINGYFY/immortalwrt.git
synced 2025-12-16 17:15:26 +00:00
rockchip: backport pending PWMv4 support
Used by RK3576 SoCs. Signed-off-by: Tianling Shen <cnsztl@immortalwrt.org>
This commit is contained in:
parent
2151ced240
commit
f2c5abcda3
@ -0,0 +1,113 @@
|
||||
From 21368fcbb124d51b5d8bd8fa0a286a23c34a0888 Mon Sep 17 00:00:00 2001
|
||||
From: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
Date: Mon, 25 Aug 2025 10:28:21 +0200
|
||||
Subject: [PATCH] bitmap: introduce hardware-specific bitfield operations
|
||||
|
||||
Hardware of various vendors, but very notably Rockchip, often uses
|
||||
32-bit registers where the upper 16-bit half of the register is a
|
||||
write-enable mask for the lower half.
|
||||
|
||||
This type of hardware setup allows for more granular concurrent register
|
||||
write access.
|
||||
|
||||
Over the years, many drivers have hand-rolled their own version of this
|
||||
macro, usually without any checks, often called something like
|
||||
HIWORD_UPDATE or FIELD_PREP_HIWORD, commonly with slightly different
|
||||
semantics between them.
|
||||
|
||||
Clearly there is a demand for such a macro, and thus the demand should
|
||||
be satisfied in a common header file. As this is a convention that spans
|
||||
across multiple vendors, and similar conventions may also have
|
||||
cross-vendor adoption, it's best if it lives in a vendor-agnostic header
|
||||
file that can be expanded over time.
|
||||
|
||||
Add hw_bitfield.h with two macros: FIELD_PREP_WM16, and
|
||||
FIELD_PREP_WM16_CONST. The latter is a version that can be used in
|
||||
initializers, like FIELD_PREP_CONST.
|
||||
|
||||
Suggested-by: Yury Norov (NVIDIA) <yury.norov@gmail.com>
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
Acked-by: Jakub Kicinski <kuba@kernel.org>
|
||||
Acked-by: Heiko Stuebner <heiko@sntech.de>
|
||||
Signed-off-by: Yury Norov (NVIDIA) <yury.norov@gmail.com>
|
||||
---
|
||||
MAINTAINERS | 1 +
|
||||
include/linux/hw_bitfield.h | 62 +++++++++++++++++++++++++++++++++++++
|
||||
2 files changed, 63 insertions(+)
|
||||
create mode 100644 include/linux/hw_bitfield.h
|
||||
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -3904,6 +3904,7 @@ F: include/linux/bits.h
|
||||
F: include/linux/cpumask.h
|
||||
F: include/linux/cpumask_types.h
|
||||
F: include/linux/find.h
|
||||
+F: include/linux/hw_bitfield.h
|
||||
F: include/linux/nodemask.h
|
||||
F: include/linux/nodemask_types.h
|
||||
F: include/vdso/bits.h
|
||||
--- /dev/null
|
||||
+++ b/include/linux/hw_bitfield.h
|
||||
@@ -0,0 +1,62 @@
|
||||
+/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
+/*
|
||||
+ * Copyright (C) 2025, Collabora Ltd.
|
||||
+ */
|
||||
+
|
||||
+#ifndef _LINUX_HW_BITFIELD_H
|
||||
+#define _LINUX_HW_BITFIELD_H
|
||||
+
|
||||
+#include <linux/bitfield.h>
|
||||
+#include <linux/build_bug.h>
|
||||
+#include <linux/limits.h>
|
||||
+
|
||||
+/**
|
||||
+ * FIELD_PREP_WM16() - prepare a bitfield element with a mask in the upper half
|
||||
+ * @_mask: shifted mask defining the field's length and position
|
||||
+ * @_val: value to put in the field
|
||||
+ *
|
||||
+ * FIELD_PREP_WM16() masks and shifts up the value, as well as bitwise ORs the
|
||||
+ * result with the mask shifted up by 16.
|
||||
+ *
|
||||
+ * This is useful for a common design of hardware registers where the upper
|
||||
+ * 16-bit half of a 32-bit register is used as a write-enable mask. In such a
|
||||
+ * register, a bit in the lower half is only updated if the corresponding bit
|
||||
+ * in the upper half is high.
|
||||
+ */
|
||||
+#define FIELD_PREP_WM16(_mask, _val) \
|
||||
+ ({ \
|
||||
+ typeof(_val) __val = _val; \
|
||||
+ typeof(_mask) __mask = _mask; \
|
||||
+ __BF_FIELD_CHECK(__mask, ((u16)0U), __val, \
|
||||
+ "HWORD_UPDATE: "); \
|
||||
+ (((typeof(__mask))(__val) << __bf_shf(__mask)) & (__mask)) | \
|
||||
+ ((__mask) << 16); \
|
||||
+ })
|
||||
+
|
||||
+/**
|
||||
+ * FIELD_PREP_WM16_CONST() - prepare a constant bitfield element with a mask in
|
||||
+ * the upper half
|
||||
+ * @_mask: shifted mask defining the field's length and position
|
||||
+ * @_val: value to put in the field
|
||||
+ *
|
||||
+ * FIELD_PREP_WM16_CONST() masks and shifts up the value, as well as bitwise ORs
|
||||
+ * the result with the mask shifted up by 16.
|
||||
+ *
|
||||
+ * This is useful for a common design of hardware registers where the upper
|
||||
+ * 16-bit half of a 32-bit register is used as a write-enable mask. In such a
|
||||
+ * register, a bit in the lower half is only updated if the corresponding bit
|
||||
+ * in the upper half is high.
|
||||
+ *
|
||||
+ * Unlike FIELD_PREP_WM16(), this is a constant expression and can therefore
|
||||
+ * be used in initializers. Error checking is less comfortable for this
|
||||
+ * version.
|
||||
+ */
|
||||
+#define FIELD_PREP_WM16_CONST(_mask, _val) \
|
||||
+ ( \
|
||||
+ FIELD_PREP_CONST(_mask, _val) | \
|
||||
+ (BUILD_BUG_ON_ZERO(const_true((u64)(_mask) > U16_MAX)) + \
|
||||
+ ((_mask) << 16)) \
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+#endif /* _LINUX_HW_BITFIELD_H */
|
||||
@ -29,7 +29,7 @@ Signed-off-by: David S. Miller <davem@davemloft.net>
|
||||
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -14427,8 +14427,8 @@ M: Qingfang Deng <dqfext@gmail.com>
|
||||
@@ -14428,8 +14428,8 @@ M: Qingfang Deng <dqfext@gmail.com>
|
||||
M: SkyLake Huang <SkyLake.Huang@mediatek.com>
|
||||
L: netdev@vger.kernel.org
|
||||
S: Maintained
|
||||
|
||||
@ -24,7 +24,7 @@ Signed-off-by: David S. Miller <davem@davemloft.net>
|
||||
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -14428,7 +14428,9 @@ M: SkyLake Huang <SkyLake.Huang@mediatek
|
||||
@@ -14429,7 +14429,9 @@ M: SkyLake Huang <SkyLake.Huang@mediatek
|
||||
L: netdev@vger.kernel.org
|
||||
S: Maintained
|
||||
F: drivers/net/phy/mediatek/mtk-ge-soc.c
|
||||
|
||||
@ -36,7 +36,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -23668,6 +23668,12 @@ F: Documentation/filesystems/ubifs-authe
|
||||
@@ -23669,6 +23669,12 @@ F: Documentation/filesystems/ubifs-authe
|
||||
F: Documentation/filesystems/ubifs.rst
|
||||
F: fs/ubifs/
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -14419,7 +14419,9 @@ M: Daniel Golle <daniel@makrotopia.org>
|
||||
@@ -14420,7 +14420,9 @@ M: Daniel Golle <daniel@makrotopia.org>
|
||||
L: netdev@vger.kernel.org
|
||||
S: Maintained
|
||||
F: drivers/net/pcs/pcs-mtk-lynxi.c
|
||||
|
||||
@ -413,6 +413,7 @@ CONFIG_MFD_CORE=y
|
||||
CONFIG_MFD_RK8XX=y
|
||||
CONFIG_MFD_RK8XX_I2C=y
|
||||
CONFIG_MFD_RK8XX_SPI=y
|
||||
CONFIG_MFD_ROCKCHIP_MFPWM=y
|
||||
CONFIG_MFD_SYSCON=y
|
||||
CONFIG_MIGRATION=y
|
||||
CONFIG_MMC=y
|
||||
@ -571,6 +572,7 @@ CONFIG_PTP_1588_CLOCK=y
|
||||
CONFIG_PTP_1588_CLOCK_OPTIONAL=y
|
||||
CONFIG_PWM=y
|
||||
CONFIG_PWM_ROCKCHIP=y
|
||||
CONFIG_PWM_ROCKCHIP_V4=y
|
||||
# CONFIG_QFMT_V2 is not set
|
||||
CONFIG_QUEUED_RWLOCKS=y
|
||||
CONFIG_QUEUED_SPINLOCKS=y
|
||||
@ -615,6 +617,7 @@ CONFIG_ROCKCHIP_MBOX=y
|
||||
CONFIG_ROCKCHIP_MFPWM=y
|
||||
CONFIG_ROCKCHIP_PHY=y
|
||||
CONFIG_ROCKCHIP_PM_DOMAINS=y
|
||||
CONFIG_ROCKCHIP_PWM_CAPTURE=y
|
||||
# CONFIG_ROCKCHIP_SARADC is not set
|
||||
CONFIG_ROCKCHIP_THERMAL=y
|
||||
CONFIG_ROCKCHIP_TIMER=y
|
||||
|
||||
@ -0,0 +1,265 @@
|
||||
From 1cc2e1faafb3b5a2be25112559bdb495736b5af7 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:57:57 +0200
|
||||
Subject: [PATCH] pwm: Add more locking
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This ensures that a pwm_chip that has no corresponding driver isn't used
|
||||
and that a driver doesn't go away while a callback is still running.
|
||||
|
||||
In the presence of device links this isn't necessary yet (so this is no
|
||||
fix) but for pwm character device support this is needed.
|
||||
|
||||
To not serialize all pwm_apply_state() calls, this introduces a per chip
|
||||
lock. An additional complication is that for atomic chips a mutex cannot
|
||||
be used (as pwm_apply_atomic() must not sleep) and a spinlock cannot be
|
||||
held while calling an operation for a sleeping chip. So depending on the
|
||||
chip being atomic or not a spinlock or a mutex is used.
|
||||
|
||||
An additional change implemented here is that on driver remove the
|
||||
.free() callback is called for each requested pwm_device. This is the
|
||||
right time because later (e.g. when the consumer calls pwm_put()) the
|
||||
free function is (maybe) not available any more.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/026aa891c8270a11723a1ba7e4256f456f7e1e86.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 100 ++++++++++++++++++++++++++++++++++++++++----
|
||||
include/linux/pwm.h | 13 ++++++
|
||||
2 files changed, 105 insertions(+), 8 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -31,6 +31,24 @@ static DEFINE_MUTEX(pwm_lock);
|
||||
|
||||
static DEFINE_IDR(pwm_chips);
|
||||
|
||||
+static void pwmchip_lock(struct pwm_chip *chip)
|
||||
+{
|
||||
+ if (chip->atomic)
|
||||
+ spin_lock(&chip->atomic_lock);
|
||||
+ else
|
||||
+ mutex_lock(&chip->nonatomic_lock);
|
||||
+}
|
||||
+
|
||||
+static void pwmchip_unlock(struct pwm_chip *chip)
|
||||
+{
|
||||
+ if (chip->atomic)
|
||||
+ spin_unlock(&chip->atomic_lock);
|
||||
+ else
|
||||
+ mutex_unlock(&chip->nonatomic_lock);
|
||||
+}
|
||||
+
|
||||
+DEFINE_GUARD(pwmchip, struct pwm_chip *, pwmchip_lock(_T), pwmchip_unlock(_T))
|
||||
+
|
||||
static void pwm_apply_debug(struct pwm_device *pwm,
|
||||
const struct pwm_state *state)
|
||||
{
|
||||
@@ -224,6 +242,7 @@ static int __pwm_apply(struct pwm_device
|
||||
int pwm_apply_might_sleep(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
int err;
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
|
||||
/*
|
||||
* Some lowlevel driver's implementations of .apply() make use of
|
||||
@@ -234,7 +253,12 @@ int pwm_apply_might_sleep(struct pwm_dev
|
||||
*/
|
||||
might_sleep();
|
||||
|
||||
- if (IS_ENABLED(CONFIG_PWM_DEBUG) && pwm->chip->atomic) {
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && chip->atomic) {
|
||||
/*
|
||||
* Catch any drivers that have been marked as atomic but
|
||||
* that will sleep anyway.
|
||||
@@ -258,9 +282,16 @@ EXPORT_SYMBOL_GPL(pwm_apply_might_sleep)
|
||||
*/
|
||||
int pwm_apply_atomic(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
- WARN_ONCE(!pwm->chip->atomic,
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+
|
||||
+ WARN_ONCE(!chip->atomic,
|
||||
"sleeping PWM driver used in atomic context\n");
|
||||
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
return __pwm_apply(pwm, state);
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(pwm_apply_atomic);
|
||||
@@ -338,8 +369,18 @@ static int pwm_capture(struct pwm_device
|
||||
if (!ops->capture)
|
||||
return -ENOSYS;
|
||||
|
||||
+ /*
|
||||
+ * Holding the pwm_lock is probably not needed. If you use pwm_capture()
|
||||
+ * and you're interested to speed it up, please convince yourself it's
|
||||
+ * really not needed, test and then suggest a patch on the mailing list.
|
||||
+ */
|
||||
guard(mutex)(&pwm_lock);
|
||||
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
return ops->capture(chip, pwm, result, timeout);
|
||||
}
|
||||
|
||||
@@ -372,6 +413,14 @@ static int pwm_device_request(struct pwm
|
||||
if (test_bit(PWMF_REQUESTED, &pwm->flags))
|
||||
return -EBUSY;
|
||||
|
||||
+ /*
|
||||
+ * This function is called while holding pwm_lock. As .operational only
|
||||
+ * changes while holding this lock, checking it here without holding the
|
||||
+ * chip lock is fine.
|
||||
+ */
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
if (!try_module_get(chip->owner))
|
||||
return -ENODEV;
|
||||
|
||||
@@ -400,7 +449,9 @@ err_get_device:
|
||||
*/
|
||||
struct pwm_state state = { 0, };
|
||||
|
||||
- err = ops->get_state(chip, pwm, &state);
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ err = ops->get_state(chip, pwm, &state);
|
||||
+
|
||||
trace_pwm_get(pwm, &state, err);
|
||||
|
||||
if (!err)
|
||||
@@ -1024,6 +1075,7 @@ struct pwm_chip *pwmchip_alloc(struct de
|
||||
|
||||
chip->npwm = npwm;
|
||||
chip->uses_pwmchip_alloc = true;
|
||||
+ chip->operational = false;
|
||||
|
||||
pwmchip_dev = &chip->dev;
|
||||
device_initialize(pwmchip_dev);
|
||||
@@ -1129,6 +1181,11 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
|
||||
chip->owner = owner;
|
||||
|
||||
+ if (chip->atomic)
|
||||
+ spin_lock_init(&chip->atomic_lock);
|
||||
+ else
|
||||
+ mutex_init(&chip->nonatomic_lock);
|
||||
+
|
||||
guard(mutex)(&pwm_lock);
|
||||
|
||||
ret = idr_alloc(&pwm_chips, chip, 0, 0, GFP_KERNEL);
|
||||
@@ -1142,6 +1199,9 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
if (IS_ENABLED(CONFIG_OF))
|
||||
of_pwmchip_add(chip);
|
||||
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = true;
|
||||
+
|
||||
ret = device_add(&chip->dev);
|
||||
if (ret)
|
||||
goto err_device_add;
|
||||
@@ -1149,6 +1209,9 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
return 0;
|
||||
|
||||
err_device_add:
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = false;
|
||||
+
|
||||
if (IS_ENABLED(CONFIG_OF))
|
||||
of_pwmchip_remove(chip);
|
||||
|
||||
@@ -1168,11 +1231,27 @@ void pwmchip_remove(struct pwm_chip *chi
|
||||
{
|
||||
pwmchip_sysfs_unexport(chip);
|
||||
|
||||
- if (IS_ENABLED(CONFIG_OF))
|
||||
- of_pwmchip_remove(chip);
|
||||
+ scoped_guard(mutex, &pwm_lock) {
|
||||
+ unsigned int i;
|
||||
+
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = false;
|
||||
+
|
||||
+ for (i = 0; i < chip->npwm; ++i) {
|
||||
+ struct pwm_device *pwm = &chip->pwms[i];
|
||||
+
|
||||
+ if (test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
+ dev_warn(&chip->dev, "Freeing requested PWM #%u\n", i);
|
||||
+ if (pwm->chip->ops->free)
|
||||
+ pwm->chip->ops->free(pwm->chip, pwm);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_OF))
|
||||
+ of_pwmchip_remove(chip);
|
||||
|
||||
- scoped_guard(mutex, &pwm_lock)
|
||||
idr_remove(&pwm_chips, chip->id);
|
||||
+ }
|
||||
|
||||
device_del(&chip->dev);
|
||||
}
|
||||
@@ -1542,12 +1621,17 @@ void pwm_put(struct pwm_device *pwm)
|
||||
|
||||
guard(mutex)(&pwm_lock);
|
||||
|
||||
- if (!test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
+ /*
|
||||
+ * Trigger a warning if a consumer called pwm_put() twice.
|
||||
+ * If the chip isn't operational, PWMF_REQUESTED was already cleared in
|
||||
+ * pwmchip_remove(). So don't warn in this case.
|
||||
+ */
|
||||
+ if (chip->operational && !test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
pr_warn("PWM device already freed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
- if (chip->ops->free)
|
||||
+ if (chip->operational && chip->ops->free)
|
||||
pwm->chip->ops->free(pwm->chip, pwm);
|
||||
|
||||
pwm->label = NULL;
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -275,6 +275,9 @@ struct pwm_ops {
|
||||
* @of_xlate: request a PWM device given a device tree PWM specifier
|
||||
* @atomic: can the driver's ->apply() be called in atomic context
|
||||
* @uses_pwmchip_alloc: signals if pwmchip_allow was used to allocate this chip
|
||||
+ * @operational: signals if the chip can be used (or is already deregistered)
|
||||
+ * @nonatomic_lock: mutex for nonatomic chips
|
||||
+ * @atomic_lock: mutex for atomic chips
|
||||
* @pwms: array of PWM devices allocated by the framework
|
||||
*/
|
||||
struct pwm_chip {
|
||||
@@ -290,6 +293,16 @@ struct pwm_chip {
|
||||
|
||||
/* only used internally by the PWM framework */
|
||||
bool uses_pwmchip_alloc;
|
||||
+ bool operational;
|
||||
+ union {
|
||||
+ /*
|
||||
+ * depending on the chip being atomic or not either the mutex or
|
||||
+ * the spinlock is used. It protects .operational and
|
||||
+ * synchronizes the callbacks in .ops
|
||||
+ */
|
||||
+ struct mutex nonatomic_lock;
|
||||
+ spinlock_t atomic_lock;
|
||||
+ };
|
||||
struct pwm_device pwms[] __counted_by(npwm);
|
||||
};
|
||||
|
||||
@ -0,0 +1,408 @@
|
||||
From 17e40c25158f2505cbcdeda96624afcbab4af368 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:57:58 +0200
|
||||
Subject: [PATCH] pwm: New abstraction for PWM waveforms
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Up to now the configuration of a PWM setting is described exclusively by
|
||||
a struct pwm_state which contains information about period, duty_cycle,
|
||||
polarity and if the PWM is enabled. (There is another member usage_power
|
||||
which doesn't completely fit into pwm_state, I ignore it here for
|
||||
simplicity.)
|
||||
|
||||
Instead of a polarity the new abstraction has a member duty_offset_ns
|
||||
that defines when the rising edge happens after the period start. This
|
||||
is more general, as with a pwm_state the rising edge can only happen at
|
||||
the period's start or such that the falling edge is at the end of the
|
||||
period (i.e. duty_offset_ns == 0 or duty_offset_ns == period_length_ns -
|
||||
duty_length_ns).
|
||||
|
||||
A disabled PWM is modeled by .period_length_ns = 0. In my eyes this is a
|
||||
nice usage of that otherwise unusable setting, as it doesn't define
|
||||
anything about the future which matches the fact that consumers should
|
||||
consider the state of the output as undefined and it's just there to say
|
||||
"No further requirements about the output, you can save some power.".
|
||||
|
||||
Further I renamed period and duty_cycle to period_length_ns and
|
||||
duty_length_ns. In the past there was confusion from time to time about
|
||||
duty_cycle being measured in nanoseconds because people expected a
|
||||
percentage of period instead. With "length_ns" as suffix the semantic
|
||||
should be more obvious to people unfamiliar with the pwm subsystem.
|
||||
period is renamed to period_length_ns for consistency.
|
||||
|
||||
The API for consumers doesn't change yet, but lowlevel drivers can
|
||||
implement callbacks that work with pwm_waveforms instead of pwm_states.
|
||||
A new thing about these callbacks is that the calculation of hardware
|
||||
settings needed to implement a certain waveform is separated from
|
||||
actually writing these settings. The motivation for that is that this
|
||||
allows a consumer to query the hardware capabilities without actually
|
||||
modifying the hardware state.
|
||||
|
||||
The rounding rules that are expected to be implemented in the
|
||||
round_waveform_tohw() are: First pick the biggest possible period not
|
||||
bigger than wf->period_length_ns. For that period pick the biggest
|
||||
possible duty setting not bigger than wf->duty_length_ns. Third pick the
|
||||
biggest possible offset not bigger than wf->duty_offset_ns. If the
|
||||
requested period is too small for the hardware, it's expected that a
|
||||
setting with the minimal period and duty_length_ns = duty_offset_ns = 0
|
||||
is returned and this fact is signaled by a return value of 1.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Tested-by: Trevor Gamblin <tgamblin@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/df0faa33bf9e7c9e2e5eab8d31bbf61e861bd401.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
[ukleinek: Update pwm_check_rounding() to return bool instead of int.]
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 234 ++++++++++++++++++++++++++++++++++++++++----
|
||||
include/linux/pwm.h | 36 +++++++
|
||||
2 files changed, 249 insertions(+), 21 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -49,6 +49,102 @@ static void pwmchip_unlock(struct pwm_ch
|
||||
|
||||
DEFINE_GUARD(pwmchip, struct pwm_chip *, pwmchip_lock(_T), pwmchip_unlock(_T))
|
||||
|
||||
+static void pwm_wf2state(const struct pwm_waveform *wf, struct pwm_state *state)
|
||||
+{
|
||||
+ if (wf->period_length_ns) {
|
||||
+ if (wf->duty_length_ns + wf->duty_offset_ns < wf->period_length_ns)
|
||||
+ *state = (struct pwm_state){
|
||||
+ .enabled = true,
|
||||
+ .polarity = PWM_POLARITY_NORMAL,
|
||||
+ .period = wf->period_length_ns,
|
||||
+ .duty_cycle = wf->duty_length_ns,
|
||||
+ };
|
||||
+ else
|
||||
+ *state = (struct pwm_state){
|
||||
+ .enabled = true,
|
||||
+ .polarity = PWM_POLARITY_INVERSED,
|
||||
+ .period = wf->period_length_ns,
|
||||
+ .duty_cycle = wf->period_length_ns - wf->duty_length_ns,
|
||||
+ };
|
||||
+ } else {
|
||||
+ *state = (struct pwm_state){
|
||||
+ .enabled = false,
|
||||
+ };
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static void pwm_state2wf(const struct pwm_state *state, struct pwm_waveform *wf)
|
||||
+{
|
||||
+ if (state->enabled) {
|
||||
+ if (state->polarity == PWM_POLARITY_NORMAL)
|
||||
+ *wf = (struct pwm_waveform){
|
||||
+ .period_length_ns = state->period,
|
||||
+ .duty_length_ns = state->duty_cycle,
|
||||
+ .duty_offset_ns = 0,
|
||||
+ };
|
||||
+ else
|
||||
+ *wf = (struct pwm_waveform){
|
||||
+ .period_length_ns = state->period,
|
||||
+ .duty_length_ns = state->period - state->duty_cycle,
|
||||
+ .duty_offset_ns = state->duty_cycle,
|
||||
+ };
|
||||
+ } else {
|
||||
+ *wf = (struct pwm_waveform){
|
||||
+ .period_length_ns = 0,
|
||||
+ };
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static bool pwm_check_rounding(const struct pwm_waveform *wf,
|
||||
+ const struct pwm_waveform *wf_rounded)
|
||||
+{
|
||||
+ if (!wf->period_length_ns)
|
||||
+ return true;
|
||||
+
|
||||
+ if (wf->period_length_ns < wf_rounded->period_length_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ if (wf->duty_length_ns < wf_rounded->duty_length_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ if (wf->duty_offset_ns < wf_rounded->duty_offset_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+static int __pwm_round_waveform_tohw(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf, void *wfhw)
|
||||
+{
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+
|
||||
+ return ops->round_waveform_tohw(chip, pwm, wf, wfhw);
|
||||
+}
|
||||
+
|
||||
+static int __pwm_round_waveform_fromhw(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const void *wfhw, struct pwm_waveform *wf)
|
||||
+{
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+
|
||||
+ return ops->round_waveform_fromhw(chip, pwm, wfhw, wf);
|
||||
+}
|
||||
+
|
||||
+static int __pwm_read_waveform(struct pwm_chip *chip, struct pwm_device *pwm, void *wfhw)
|
||||
+{
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+
|
||||
+ return ops->read_waveform(chip, pwm, wfhw);
|
||||
+}
|
||||
+
|
||||
+static int __pwm_write_waveform(struct pwm_chip *chip, struct pwm_device *pwm, const void *wfhw)
|
||||
+{
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+
|
||||
+ return ops->write_waveform(chip, pwm, wfhw);
|
||||
+}
|
||||
+
|
||||
+#define WFHWSIZE 20
|
||||
+
|
||||
static void pwm_apply_debug(struct pwm_device *pwm,
|
||||
const struct pwm_state *state)
|
||||
{
|
||||
@@ -186,6 +282,7 @@ static bool pwm_state_valid(const struct
|
||||
static int __pwm_apply(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
struct pwm_chip *chip;
|
||||
+ const struct pwm_ops *ops;
|
||||
int err;
|
||||
|
||||
if (!pwm || !state)
|
||||
@@ -209,6 +306,7 @@ static int __pwm_apply(struct pwm_device
|
||||
}
|
||||
|
||||
chip = pwm->chip;
|
||||
+ ops = chip->ops;
|
||||
|
||||
if (state->period == pwm->state.period &&
|
||||
state->duty_cycle == pwm->state.duty_cycle &&
|
||||
@@ -217,18 +315,69 @@ static int __pwm_apply(struct pwm_device
|
||||
state->usage_power == pwm->state.usage_power)
|
||||
return 0;
|
||||
|
||||
- err = chip->ops->apply(chip, pwm, state);
|
||||
- trace_pwm_apply(pwm, state, err);
|
||||
- if (err)
|
||||
- return err;
|
||||
+ if (ops->write_waveform) {
|
||||
+ struct pwm_waveform wf;
|
||||
+ char wfhw[WFHWSIZE];
|
||||
|
||||
- pwm->state = *state;
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
- /*
|
||||
- * only do this after pwm->state was applied as some
|
||||
- * implementations of .get_state depend on this
|
||||
- */
|
||||
- pwm_apply_debug(pwm, state);
|
||||
+ pwm_state2wf(state, &wf);
|
||||
+
|
||||
+ /*
|
||||
+ * The rounding is wrong here for states with inverted polarity.
|
||||
+ * While .apply() rounds down duty_cycle (which represents the
|
||||
+ * time from the start of the period to the inner edge),
|
||||
+ * .round_waveform_tohw() rounds down the time the PWM is high.
|
||||
+ * Can be fixed if the need arises, until reported otherwise
|
||||
+ * let's assume that consumers don't care.
|
||||
+ */
|
||||
+
|
||||
+ err = __pwm_round_waveform_tohw(chip, pwm, &wf, &wfhw);
|
||||
+ if (err) {
|
||||
+ if (err > 0)
|
||||
+ /*
|
||||
+ * This signals an invalid request, typically
|
||||
+ * the requested period (or duty_offset) is
|
||||
+ * smaller than possible with the hardware.
|
||||
+ */
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ return err;
|
||||
+ }
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG)) {
|
||||
+ struct pwm_waveform wf_rounded;
|
||||
+
|
||||
+ err = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf_rounded);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ if (!pwm_check_rounding(&wf, &wf_rounded))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ wf.duty_length_ns, wf.period_length_ns, wf.duty_offset_ns,
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
+ }
|
||||
+
|
||||
+ err = __pwm_write_waveform(chip, pwm, &wfhw);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ pwm->state = *state;
|
||||
+
|
||||
+ } else {
|
||||
+ err = ops->apply(chip, pwm, state);
|
||||
+ trace_pwm_apply(pwm, state, err);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ pwm->state = *state;
|
||||
+
|
||||
+ /*
|
||||
+ * only do this after pwm->state was applied as some
|
||||
+ * implementations of .get_state() depend on this
|
||||
+ */
|
||||
+ pwm_apply_debug(pwm, state);
|
||||
+ }
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -296,6 +445,41 @@ int pwm_apply_atomic(struct pwm_device *
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(pwm_apply_atomic);
|
||||
|
||||
+static int pwm_get_state_hw(struct pwm_device *pwm, struct pwm_state *state)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret = -EOPNOTSUPP;
|
||||
+
|
||||
+ if (ops->read_waveform) {
|
||||
+ char wfhw[WFHWSIZE];
|
||||
+ struct pwm_waveform wf;
|
||||
+
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
+
|
||||
+ scoped_guard(pwmchip, chip) {
|
||||
+
|
||||
+ ret = __pwm_read_waveform(chip, pwm, &wfhw);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ ret = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ pwm_wf2state(&wf, state);
|
||||
+
|
||||
+ } else if (ops->get_state) {
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ ret = ops->get_state(chip, pwm, state);
|
||||
+
|
||||
+ trace_pwm_get(pwm, state, ret);
|
||||
+ }
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* pwm_adjust_config() - adjust the current PWM config to the PWM arguments
|
||||
* @pwm: PWM device
|
||||
@@ -439,7 +623,7 @@ err_get_device:
|
||||
}
|
||||
}
|
||||
|
||||
- if (ops->get_state) {
|
||||
+ if (ops->read_waveform || ops->get_state) {
|
||||
/*
|
||||
* Zero-initialize state because most drivers are unaware of
|
||||
* .usage_power. The other members of state are supposed to be
|
||||
@@ -449,11 +633,7 @@ err_get_device:
|
||||
*/
|
||||
struct pwm_state state = { 0, };
|
||||
|
||||
- scoped_guard(pwmchip, chip)
|
||||
- err = ops->get_state(chip, pwm, &state);
|
||||
-
|
||||
- trace_pwm_get(pwm, &state, err);
|
||||
-
|
||||
+ err = pwm_get_state_hw(pwm, &state);
|
||||
if (!err)
|
||||
pwm->state = state;
|
||||
|
||||
@@ -1140,12 +1320,24 @@ static bool pwm_ops_check(const struct p
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
|
||||
- if (!ops->apply)
|
||||
- return false;
|
||||
+ if (ops->write_waveform) {
|
||||
+ if (!ops->round_waveform_tohw ||
|
||||
+ !ops->round_waveform_fromhw ||
|
||||
+ !ops->write_waveform)
|
||||
+ return false;
|
||||
+
|
||||
+ if (WFHWSIZE < ops->sizeof_wfhw) {
|
||||
+ dev_warn(pwmchip_parent(chip), "WFHWSIZE < %zu\n", ops->sizeof_wfhw);
|
||||
+ return false;
|
||||
+ }
|
||||
+ } else {
|
||||
+ if (!ops->apply)
|
||||
+ return false;
|
||||
|
||||
- if (IS_ENABLED(CONFIG_PWM_DEBUG) && !ops->get_state)
|
||||
- dev_warn(pwmchip_parent(chip),
|
||||
- "Please implement the .get_state() callback\n");
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && !ops->get_state)
|
||||
+ dev_warn(pwmchip_parent(chip),
|
||||
+ "Please implement the .get_state() callback\n");
|
||||
+ }
|
||||
|
||||
return true;
|
||||
}
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -49,6 +49,31 @@ enum {
|
||||
PWMF_EXPORTED = 1,
|
||||
};
|
||||
|
||||
+/**
|
||||
+ * struct pwm_waveform - description of a PWM waveform
|
||||
+ * @period_length_ns: PWM period
|
||||
+ * @duty_length_ns: PWM duty cycle
|
||||
+ * @duty_offset_ns: offset of the rising edge from the period's start
|
||||
+ *
|
||||
+ * This is a representation of a PWM waveform alternative to struct pwm_state
|
||||
+ * below. It's more expressive than struct pwm_state as it contains a
|
||||
+ * duty_offset_ns and so can represent offsets other than zero (with .polarity =
|
||||
+ * PWM_POLARITY_NORMAL) and period - duty_cycle (.polarity =
|
||||
+ * PWM_POLARITY_INVERSED).
|
||||
+ *
|
||||
+ * Note there is no explicit bool for enabled. A "disabled" PWM is represented
|
||||
+ * by .period_length_ns = 0. Note further that the behaviour of a "disabled" PWM
|
||||
+ * is undefined. Depending on the hardware's capabilities it might drive the
|
||||
+ * active or inactive level, go high-z or even continue to toggle.
|
||||
+ *
|
||||
+ * The unit for all three members is nanoseconds.
|
||||
+ */
|
||||
+struct pwm_waveform {
|
||||
+ u64 period_length_ns;
|
||||
+ u64 duty_length_ns;
|
||||
+ u64 duty_offset_ns;
|
||||
+};
|
||||
+
|
||||
/*
|
||||
* struct pwm_state - state of a PWM channel
|
||||
* @period: PWM period (in nanoseconds)
|
||||
@@ -259,6 +284,17 @@ struct pwm_ops {
|
||||
void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
|
||||
int (*capture)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
struct pwm_capture *result, unsigned long timeout);
|
||||
+
|
||||
+ size_t sizeof_wfhw;
|
||||
+ int (*round_waveform_tohw)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf, void *wfhw);
|
||||
+ int (*round_waveform_fromhw)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const void *wfhw, struct pwm_waveform *wf);
|
||||
+ int (*read_waveform)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ void *wfhw);
|
||||
+ int (*write_waveform)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const void *wfhw);
|
||||
+
|
||||
int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
const struct pwm_state *state);
|
||||
int (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
@ -0,0 +1,323 @@
|
||||
From 6c5126c6406d1c31e91f5b925c621c1c785366be Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:57:59 +0200
|
||||
Subject: [PATCH] pwm: Provide new consumer API functions for waveforms
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Provide API functions for consumers to work with waveforms.
|
||||
|
||||
Note that one relevant difference between pwm_get_state() and
|
||||
pwm_get_waveform*() is that the latter yields the actually configured
|
||||
hardware state, while the former yields the last state passed to
|
||||
pwm_apply*() and so doesn't account for hardware specific rounding.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Tested-by: Trevor Gamblin <tgamblin@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/6c97d27682853f603e18e9196043886dd671845d.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 261 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
include/linux/pwm.h | 6 +-
|
||||
2 files changed, 266 insertions(+), 1 deletion(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -49,6 +49,30 @@ static void pwmchip_unlock(struct pwm_ch
|
||||
|
||||
DEFINE_GUARD(pwmchip, struct pwm_chip *, pwmchip_lock(_T), pwmchip_unlock(_T))
|
||||
|
||||
+static bool pwm_wf_valid(const struct pwm_waveform *wf)
|
||||
+{
|
||||
+ /*
|
||||
+ * For now restrict waveforms to period_length_ns <= S64_MAX to provide
|
||||
+ * some space for future extensions. One possibility is to simplify
|
||||
+ * representing waveforms with inverted polarity using negative values
|
||||
+ * somehow.
|
||||
+ */
|
||||
+ if (wf->period_length_ns > S64_MAX)
|
||||
+ return false;
|
||||
+
|
||||
+ if (wf->duty_length_ns > wf->period_length_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ /*
|
||||
+ * .duty_offset_ns is supposed to be smaller than .period_length_ns, apart
|
||||
+ * from the corner case .duty_offset_ns == 0 && .period_length_ns == 0.
|
||||
+ */
|
||||
+ if (wf->duty_offset_ns && wf->duty_offset_ns >= wf->period_length_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
static void pwm_wf2state(const struct pwm_waveform *wf, struct pwm_state *state)
|
||||
{
|
||||
if (wf->period_length_ns) {
|
||||
@@ -95,6 +119,29 @@ static void pwm_state2wf(const struct pw
|
||||
}
|
||||
}
|
||||
|
||||
+static int pwmwfcmp(const struct pwm_waveform *a, const struct pwm_waveform *b)
|
||||
+{
|
||||
+ if (a->period_length_ns > b->period_length_ns)
|
||||
+ return 1;
|
||||
+
|
||||
+ if (a->period_length_ns < b->period_length_ns)
|
||||
+ return -1;
|
||||
+
|
||||
+ if (a->duty_length_ns > b->duty_length_ns)
|
||||
+ return 1;
|
||||
+
|
||||
+ if (a->duty_length_ns < b->duty_length_ns)
|
||||
+ return -1;
|
||||
+
|
||||
+ if (a->duty_offset_ns > b->duty_offset_ns)
|
||||
+ return 1;
|
||||
+
|
||||
+ if (a->duty_offset_ns < b->duty_offset_ns)
|
||||
+ return -1;
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
static bool pwm_check_rounding(const struct pwm_waveform *wf,
|
||||
const struct pwm_waveform *wf_rounded)
|
||||
{
|
||||
@@ -145,6 +192,220 @@ static int __pwm_write_waveform(struct p
|
||||
|
||||
#define WFHWSIZE 20
|
||||
|
||||
+/**
|
||||
+ * pwm_round_waveform_might_sleep - Query hardware capabilities
|
||||
+ * Cannot be used in atomic context.
|
||||
+ * @pwm: PWM device
|
||||
+ * @wf: waveform to round and output parameter
|
||||
+ *
|
||||
+ * Typically a given waveform cannot be implemented exactly by hardware, e.g.
|
||||
+ * because hardware only supports coarse period resolution or no duty_offset.
|
||||
+ * This function returns the actually implemented waveform if you pass wf to
|
||||
+ * pwm_set_waveform_might_sleep now.
|
||||
+ *
|
||||
+ * Note however that the world doesn't stop turning when you call it, so when
|
||||
+ * doing
|
||||
+ *
|
||||
+ * pwm_round_waveform_might_sleep(mypwm, &wf);
|
||||
+ * pwm_set_waveform_might_sleep(mypwm, &wf, true);
|
||||
+ *
|
||||
+ * the latter might fail, e.g. because an input clock changed its rate between
|
||||
+ * these two calls and the waveform determined by
|
||||
+ * pwm_round_waveform_might_sleep() cannot be implemented any more.
|
||||
+ *
|
||||
+ * Returns 0 on success, 1 if there is no valid hardware configuration matching
|
||||
+ * the input waveform under the PWM rounding rules or a negative errno.
|
||||
+ */
|
||||
+int pwm_round_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+ struct pwm_waveform wf_req = *wf;
|
||||
+ char wfhw[WFHWSIZE];
|
||||
+ int ret_tohw, ret_fromhw;
|
||||
+
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
+
|
||||
+ if (!pwm_wf_valid(wf))
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
+ ret_tohw = __pwm_round_waveform_tohw(chip, pwm, wf, wfhw);
|
||||
+ if (ret_tohw < 0)
|
||||
+ return ret_tohw;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && ret_tohw > 1)
|
||||
+ dev_err(&chip->dev, "Unexpected return value from __pwm_round_waveform_tohw: requested %llu/%llu [+%llu], return value %d\n",
|
||||
+ wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns, ret_tohw);
|
||||
+
|
||||
+ ret_fromhw = __pwm_round_waveform_fromhw(chip, pwm, wfhw, wf);
|
||||
+ if (ret_fromhw < 0)
|
||||
+ return ret_fromhw;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && ret_fromhw > 0)
|
||||
+ dev_err(&chip->dev, "Unexpected return value from __pwm_round_waveform_fromhw: requested %llu/%llu [+%llu], return value %d\n",
|
||||
+ wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns, ret_tohw);
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) &&
|
||||
+ ret_tohw == 0 && !pwm_check_rounding(&wf_req, wf))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns,
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns);
|
||||
+
|
||||
+ return ret_tohw;
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(pwm_round_waveform_might_sleep);
|
||||
+
|
||||
+/**
|
||||
+ * pwm_get_waveform_might_sleep - Query hardware about current configuration
|
||||
+ * Cannot be used in atomic context.
|
||||
+ * @pwm: PWM device
|
||||
+ * @wf: output parameter
|
||||
+ *
|
||||
+ * Stores the current configuration of the PWM in @wf. Note this is the
|
||||
+ * equivalent of pwm_get_state_hw() (and not pwm_get_state()) for pwm_waveform.
|
||||
+ */
|
||||
+int pwm_get_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+ char wfhw[WFHWSIZE];
|
||||
+ int err;
|
||||
+
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
+
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
+ err = __pwm_read_waveform(chip, pwm, &wfhw);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ return __pwm_round_waveform_fromhw(chip, pwm, &wfhw, wf);
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(pwm_get_waveform_might_sleep);
|
||||
+
|
||||
+/* Called with the pwmchip lock held */
|
||||
+static int __pwm_set_waveform(struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf,
|
||||
+ bool exact)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+ char wfhw[WFHWSIZE];
|
||||
+ struct pwm_waveform wf_rounded;
|
||||
+ int err;
|
||||
+
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
+
|
||||
+ if (!pwm_wf_valid(wf))
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ err = __pwm_round_waveform_tohw(chip, pwm, wf, &wfhw);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ if ((IS_ENABLED(CONFIG_PWM_DEBUG) || exact) && wf->period_length_ns) {
|
||||
+ err = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf_rounded);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && !pwm_check_rounding(wf, &wf_rounded))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
+
|
||||
+ if (exact && pwmwfcmp(wf, &wf_rounded)) {
|
||||
+ dev_dbg(&chip->dev, "Requested no rounding, but %llu/%llu [+%llu] -> %llu/%llu [+%llu]\n",
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
+
|
||||
+ return 1;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ err = __pwm_write_waveform(chip, pwm, &wfhw);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ /* update .state */
|
||||
+ pwm_wf2state(wf, &pwm->state);
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && ops->read_waveform && wf->period_length_ns) {
|
||||
+ struct pwm_waveform wf_set;
|
||||
+
|
||||
+ err = __pwm_read_waveform(chip, pwm, &wfhw);
|
||||
+ if (err)
|
||||
+ /* maybe ignore? */
|
||||
+ return err;
|
||||
+
|
||||
+ err = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf_set);
|
||||
+ if (err)
|
||||
+ /* maybe ignore? */
|
||||
+ return err;
|
||||
+
|
||||
+ if (pwmwfcmp(&wf_set, &wf_rounded) != 0)
|
||||
+ dev_err(&chip->dev,
|
||||
+ "Unexpected setting: requested %llu/%llu [+%llu], expected %llu/%llu [+%llu], set %llu/%llu [+%llu]\n",
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns,
|
||||
+ wf_set.duty_length_ns, wf_set.period_length_ns, wf_set.duty_offset_ns);
|
||||
+ }
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * pwm_set_waveform_might_sleep - Apply a new waveform
|
||||
+ * Cannot be used in atomic context.
|
||||
+ * @pwm: PWM device
|
||||
+ * @wf: The waveform to apply
|
||||
+ * @exact: If true no rounding is allowed
|
||||
+ *
|
||||
+ * Typically a requested waveform cannot be implemented exactly, e.g. because
|
||||
+ * you requested .period_length_ns = 100 ns, but the hardware can only set
|
||||
+ * periods that are a multiple of 8.5 ns. With that hardware passing exact =
|
||||
+ * true results in pwm_set_waveform_might_sleep() failing and returning 1. If
|
||||
+ * exact = false you get a period of 93.5 ns (i.e. the biggest period not bigger
|
||||
+ * than the requested value).
|
||||
+ * Note that even with exact = true, some rounding by less than 1 is
|
||||
+ * possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
+ * exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
+ */
|
||||
+int pwm_set_waveform_might_sleep(struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf, bool exact)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ int err;
|
||||
+
|
||||
+ might_sleep();
|
||||
+
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && chip->atomic) {
|
||||
+ /*
|
||||
+ * Catch any drivers that have been marked as atomic but
|
||||
+ * that will sleep anyway.
|
||||
+ */
|
||||
+ non_block_start();
|
||||
+ err = __pwm_set_waveform(pwm, wf, exact);
|
||||
+ non_block_end();
|
||||
+ } else {
|
||||
+ err = __pwm_set_waveform(pwm, wf, exact);
|
||||
+ }
|
||||
+
|
||||
+ return err;
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(pwm_set_waveform_might_sleep);
|
||||
+
|
||||
static void pwm_apply_debug(struct pwm_device *pwm,
|
||||
const struct pwm_state *state)
|
||||
{
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -358,7 +358,11 @@ static inline void pwmchip_set_drvdata(s
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_PWM)
|
||||
-/* PWM user APIs */
|
||||
+
|
||||
+/* PWM consumer APIs */
|
||||
+int pwm_round_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf);
|
||||
+int pwm_get_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf);
|
||||
+int pwm_set_waveform_might_sleep(struct pwm_device *pwm, const struct pwm_waveform *wf, bool exact);
|
||||
int pwm_apply_might_sleep(struct pwm_device *pwm, const struct pwm_state *state);
|
||||
int pwm_apply_atomic(struct pwm_device *pwm, const struct pwm_state *state);
|
||||
int pwm_adjust_config(struct pwm_device *pwm);
|
||||
@ -0,0 +1,236 @@
|
||||
From 1afd01db1a76cdd1d96696e3790d66c79621784c Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:58:00 +0200
|
||||
Subject: [PATCH] pwm: Add tracing for waveform callbacks
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This adds trace events for the recently introduced waveform callbacks.
|
||||
With the introduction of some helper macros consistency among the
|
||||
different events is ensured.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/1d71879b0de3bf01459c7a9d0f040d43eb5ace56.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 24 +++++--
|
||||
include/trace/events/pwm.h | 134 ++++++++++++++++++++++++++++++++++---
|
||||
2 files changed, 146 insertions(+), 12 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -164,30 +164,46 @@ static int __pwm_round_waveform_tohw(str
|
||||
const struct pwm_waveform *wf, void *wfhw)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret;
|
||||
|
||||
- return ops->round_waveform_tohw(chip, pwm, wf, wfhw);
|
||||
+ ret = ops->round_waveform_tohw(chip, pwm, wf, wfhw);
|
||||
+ trace_pwm_round_waveform_tohw(pwm, wf, wfhw, ret);
|
||||
+
|
||||
+ return ret;
|
||||
}
|
||||
|
||||
static int __pwm_round_waveform_fromhw(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
const void *wfhw, struct pwm_waveform *wf)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = ops->round_waveform_fromhw(chip, pwm, wfhw, wf);
|
||||
+ trace_pwm_round_waveform_fromhw(pwm, wfhw, wf, ret);
|
||||
|
||||
- return ops->round_waveform_fromhw(chip, pwm, wfhw, wf);
|
||||
+ return ret;
|
||||
}
|
||||
|
||||
static int __pwm_read_waveform(struct pwm_chip *chip, struct pwm_device *pwm, void *wfhw)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret;
|
||||
|
||||
- return ops->read_waveform(chip, pwm, wfhw);
|
||||
+ ret = ops->read_waveform(chip, pwm, wfhw);
|
||||
+ trace_pwm_read_waveform(pwm, wfhw, ret);
|
||||
+
|
||||
+ return ret;
|
||||
}
|
||||
|
||||
static int __pwm_write_waveform(struct pwm_chip *chip, struct pwm_device *pwm, const void *wfhw)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = ops->write_waveform(chip, pwm, wfhw);
|
||||
+ trace_pwm_write_waveform(pwm, wfhw, ret);
|
||||
|
||||
- return ops->write_waveform(chip, pwm, wfhw);
|
||||
+ return ret;
|
||||
}
|
||||
|
||||
#define WFHWSIZE 20
|
||||
--- a/include/trace/events/pwm.h
|
||||
+++ b/include/trace/events/pwm.h
|
||||
@@ -8,15 +8,135 @@
|
||||
#include <linux/pwm.h>
|
||||
#include <linux/tracepoint.h>
|
||||
|
||||
+#define TP_PROTO_pwm(args...) \
|
||||
+ TP_PROTO(struct pwm_device *pwm, args)
|
||||
+
|
||||
+#define TP_ARGS_pwm(args...) \
|
||||
+ TP_ARGS(pwm, args)
|
||||
+
|
||||
+#define TP_STRUCT__entry_pwm(args...) \
|
||||
+ TP_STRUCT__entry( \
|
||||
+ __field(unsigned int, chipid) \
|
||||
+ __field(unsigned int, hwpwm) \
|
||||
+ args)
|
||||
+
|
||||
+#define TP_fast_assign_pwm(args...) \
|
||||
+ TP_fast_assign( \
|
||||
+ __entry->chipid = pwm->chip->id; \
|
||||
+ __entry->hwpwm = pwm->hwpwm; \
|
||||
+ args)
|
||||
+
|
||||
+#define TP_printk_pwm(fmt, args...) \
|
||||
+ TP_printk("pwmchip%u.%u: " fmt, __entry->chipid, __entry->hwpwm, args)
|
||||
+
|
||||
+#define __field_pwmwf(wf) \
|
||||
+ __field(u64, wf ## _period_length_ns) \
|
||||
+ __field(u64, wf ## _duty_length_ns) \
|
||||
+ __field(u64, wf ## _duty_offset_ns) \
|
||||
+
|
||||
+#define fast_assign_pwmwf(wf) \
|
||||
+ __entry->wf ## _period_length_ns = wf->period_length_ns; \
|
||||
+ __entry->wf ## _duty_length_ns = wf->duty_length_ns; \
|
||||
+ __entry->wf ## _duty_offset_ns = wf->duty_offset_ns
|
||||
+
|
||||
+#define printk_pwmwf_format(wf) \
|
||||
+ "%lld/%lld [+%lld]"
|
||||
+
|
||||
+#define printk_pwmwf_formatargs(wf) \
|
||||
+ __entry->wf ## _duty_length_ns, __entry->wf ## _period_length_ns, __entry->wf ## _duty_offset_ns
|
||||
+
|
||||
+TRACE_EVENT(pwm_round_waveform_tohw,
|
||||
+
|
||||
+ TP_PROTO_pwm(const struct pwm_waveform *wf, void *wfhw, int err),
|
||||
+
|
||||
+ TP_ARGS_pwm(wf, wfhw, err),
|
||||
+
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
+ __field_pwmwf(wf)
|
||||
+ __field(void *, wfhw)
|
||||
+ __field(int, err)
|
||||
+ ),
|
||||
+
|
||||
+ TP_fast_assign_pwm(
|
||||
+ fast_assign_pwmwf(wf);
|
||||
+ __entry->wfhw = wfhw;
|
||||
+ __entry->err = err;
|
||||
+ ),
|
||||
+
|
||||
+ TP_printk_pwm(printk_pwmwf_format(wf) " > %p err=%d",
|
||||
+ printk_pwmwf_formatargs(wf), __entry->wfhw, __entry->err)
|
||||
+);
|
||||
+
|
||||
+TRACE_EVENT(pwm_round_waveform_fromhw,
|
||||
+
|
||||
+ TP_PROTO_pwm(const void *wfhw, struct pwm_waveform *wf, int err),
|
||||
+
|
||||
+ TP_ARGS_pwm(wfhw, wf, err),
|
||||
+
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
+ __field(const void *, wfhw)
|
||||
+ __field_pwmwf(wf)
|
||||
+ __field(int, err)
|
||||
+ ),
|
||||
+
|
||||
+ TP_fast_assign_pwm(
|
||||
+ __entry->wfhw = wfhw;
|
||||
+ fast_assign_pwmwf(wf);
|
||||
+ __entry->err = err;
|
||||
+ ),
|
||||
+
|
||||
+ TP_printk_pwm("%p > " printk_pwmwf_format(wf) " err=%d",
|
||||
+ __entry->wfhw, printk_pwmwf_formatargs(wf), __entry->err)
|
||||
+);
|
||||
+
|
||||
+TRACE_EVENT(pwm_read_waveform,
|
||||
+
|
||||
+ TP_PROTO_pwm(void *wfhw, int err),
|
||||
+
|
||||
+ TP_ARGS_pwm(wfhw, err),
|
||||
+
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
+ __field(void *, wfhw)
|
||||
+ __field(int, err)
|
||||
+ ),
|
||||
+
|
||||
+ TP_fast_assign_pwm(
|
||||
+ __entry->wfhw = wfhw;
|
||||
+ __entry->err = err;
|
||||
+ ),
|
||||
+
|
||||
+ TP_printk_pwm("%p err=%d",
|
||||
+ __entry->wfhw, __entry->err)
|
||||
+);
|
||||
+
|
||||
+TRACE_EVENT(pwm_write_waveform,
|
||||
+
|
||||
+ TP_PROTO_pwm(const void *wfhw, int err),
|
||||
+
|
||||
+ TP_ARGS_pwm(wfhw, err),
|
||||
+
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
+ __field(const void *, wfhw)
|
||||
+ __field(int, err)
|
||||
+ ),
|
||||
+
|
||||
+ TP_fast_assign_pwm(
|
||||
+ __entry->wfhw = wfhw;
|
||||
+ __entry->err = err;
|
||||
+ ),
|
||||
+
|
||||
+ TP_printk_pwm("%p err=%d",
|
||||
+ __entry->wfhw, __entry->err)
|
||||
+);
|
||||
+
|
||||
+
|
||||
DECLARE_EVENT_CLASS(pwm,
|
||||
|
||||
TP_PROTO(struct pwm_device *pwm, const struct pwm_state *state, int err),
|
||||
|
||||
TP_ARGS(pwm, state, err),
|
||||
|
||||
- TP_STRUCT__entry(
|
||||
- __field(unsigned int, chipid)
|
||||
- __field(unsigned int, hwpwm)
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
__field(u64, period)
|
||||
__field(u64, duty_cycle)
|
||||
__field(enum pwm_polarity, polarity)
|
||||
@@ -24,9 +144,7 @@ DECLARE_EVENT_CLASS(pwm,
|
||||
__field(int, err)
|
||||
),
|
||||
|
||||
- TP_fast_assign(
|
||||
- __entry->chipid = pwm->chip->id;
|
||||
- __entry->hwpwm = pwm->hwpwm;
|
||||
+ TP_fast_assign_pwm(
|
||||
__entry->period = state->period;
|
||||
__entry->duty_cycle = state->duty_cycle;
|
||||
__entry->polarity = state->polarity;
|
||||
@@ -34,8 +152,8 @@ DECLARE_EVENT_CLASS(pwm,
|
||||
__entry->err = err;
|
||||
),
|
||||
|
||||
- TP_printk("pwmchip%u.%u: period=%llu duty_cycle=%llu polarity=%d enabled=%d err=%d",
|
||||
- __entry->chipid, __entry->hwpwm, __entry->period, __entry->duty_cycle,
|
||||
+ TP_printk_pwm("period=%llu duty_cycle=%llu polarity=%d enabled=%d err=%d",
|
||||
+ __entry->period, __entry->duty_cycle,
|
||||
__entry->polarity, __entry->enabled, __entry->err)
|
||||
|
||||
);
|
||||
@ -0,0 +1,367 @@
|
||||
From 65406de2b0d059d44472ad6f3f88a9b4a9894833 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:58:03 +0200
|
||||
Subject: [PATCH] pwm: Reorder symbols in core.c
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This moves pwm_get() and friends above the functions handling
|
||||
registration of pwmchips. The motivation is that character device
|
||||
support needs pwm_get() and pwm_put() and so ideally is defined below
|
||||
these and when a pwmchip is registered this registers the character
|
||||
device. So the natural order is
|
||||
|
||||
pwm_get() and friend
|
||||
pwm character device symbols
|
||||
pwm_chip functions
|
||||
|
||||
. The advantage of having these in their natural order is that static
|
||||
functions don't need to be forward declared.
|
||||
|
||||
Note that the diff that git produces for this change some functions are
|
||||
moved down instead. This is technically equivalent, but not how this
|
||||
change was created.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/193b3d933294da34e020650bff93b778de46b1c5.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 312 ++++++++++++++++++++++-----------------------
|
||||
1 file changed, 156 insertions(+), 156 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -1619,132 +1619,6 @@ static bool pwm_ops_check(const struct p
|
||||
return true;
|
||||
}
|
||||
|
||||
-/**
|
||||
- * __pwmchip_add() - register a new PWM chip
|
||||
- * @chip: the PWM chip to add
|
||||
- * @owner: reference to the module providing the chip.
|
||||
- *
|
||||
- * Register a new PWM chip. @owner is supposed to be THIS_MODULE, use the
|
||||
- * pwmchip_add wrapper to do this right.
|
||||
- *
|
||||
- * Returns: 0 on success or a negative error code on failure.
|
||||
- */
|
||||
-int __pwmchip_add(struct pwm_chip *chip, struct module *owner)
|
||||
-{
|
||||
- int ret;
|
||||
-
|
||||
- if (!chip || !pwmchip_parent(chip) || !chip->ops || !chip->npwm)
|
||||
- return -EINVAL;
|
||||
-
|
||||
- /*
|
||||
- * a struct pwm_chip must be allocated using (devm_)pwmchip_alloc,
|
||||
- * otherwise the embedded struct device might disappear too early
|
||||
- * resulting in memory corruption.
|
||||
- * Catch drivers that were not converted appropriately.
|
||||
- */
|
||||
- if (!chip->uses_pwmchip_alloc)
|
||||
- return -EINVAL;
|
||||
-
|
||||
- if (!pwm_ops_check(chip))
|
||||
- return -EINVAL;
|
||||
-
|
||||
- chip->owner = owner;
|
||||
-
|
||||
- if (chip->atomic)
|
||||
- spin_lock_init(&chip->atomic_lock);
|
||||
- else
|
||||
- mutex_init(&chip->nonatomic_lock);
|
||||
-
|
||||
- guard(mutex)(&pwm_lock);
|
||||
-
|
||||
- ret = idr_alloc(&pwm_chips, chip, 0, 0, GFP_KERNEL);
|
||||
- if (ret < 0)
|
||||
- return ret;
|
||||
-
|
||||
- chip->id = ret;
|
||||
-
|
||||
- dev_set_name(&chip->dev, "pwmchip%u", chip->id);
|
||||
-
|
||||
- if (IS_ENABLED(CONFIG_OF))
|
||||
- of_pwmchip_add(chip);
|
||||
-
|
||||
- scoped_guard(pwmchip, chip)
|
||||
- chip->operational = true;
|
||||
-
|
||||
- ret = device_add(&chip->dev);
|
||||
- if (ret)
|
||||
- goto err_device_add;
|
||||
-
|
||||
- return 0;
|
||||
-
|
||||
-err_device_add:
|
||||
- scoped_guard(pwmchip, chip)
|
||||
- chip->operational = false;
|
||||
-
|
||||
- if (IS_ENABLED(CONFIG_OF))
|
||||
- of_pwmchip_remove(chip);
|
||||
-
|
||||
- idr_remove(&pwm_chips, chip->id);
|
||||
-
|
||||
- return ret;
|
||||
-}
|
||||
-EXPORT_SYMBOL_GPL(__pwmchip_add);
|
||||
-
|
||||
-/**
|
||||
- * pwmchip_remove() - remove a PWM chip
|
||||
- * @chip: the PWM chip to remove
|
||||
- *
|
||||
- * Removes a PWM chip.
|
||||
- */
|
||||
-void pwmchip_remove(struct pwm_chip *chip)
|
||||
-{
|
||||
- pwmchip_sysfs_unexport(chip);
|
||||
-
|
||||
- scoped_guard(mutex, &pwm_lock) {
|
||||
- unsigned int i;
|
||||
-
|
||||
- scoped_guard(pwmchip, chip)
|
||||
- chip->operational = false;
|
||||
-
|
||||
- for (i = 0; i < chip->npwm; ++i) {
|
||||
- struct pwm_device *pwm = &chip->pwms[i];
|
||||
-
|
||||
- if (test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
- dev_warn(&chip->dev, "Freeing requested PWM #%u\n", i);
|
||||
- if (pwm->chip->ops->free)
|
||||
- pwm->chip->ops->free(pwm->chip, pwm);
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- if (IS_ENABLED(CONFIG_OF))
|
||||
- of_pwmchip_remove(chip);
|
||||
-
|
||||
- idr_remove(&pwm_chips, chip->id);
|
||||
- }
|
||||
-
|
||||
- device_del(&chip->dev);
|
||||
-}
|
||||
-EXPORT_SYMBOL_GPL(pwmchip_remove);
|
||||
-
|
||||
-static void devm_pwmchip_remove(void *data)
|
||||
-{
|
||||
- struct pwm_chip *chip = data;
|
||||
-
|
||||
- pwmchip_remove(chip);
|
||||
-}
|
||||
-
|
||||
-int __devm_pwmchip_add(struct device *dev, struct pwm_chip *chip, struct module *owner)
|
||||
-{
|
||||
- int ret;
|
||||
-
|
||||
- ret = __pwmchip_add(chip, owner);
|
||||
- if (ret)
|
||||
- return ret;
|
||||
-
|
||||
- return devm_add_action_or_reset(dev, devm_pwmchip_remove, chip);
|
||||
-}
|
||||
-EXPORT_SYMBOL_GPL(__devm_pwmchip_add);
|
||||
-
|
||||
static struct device_link *pwm_device_link_add(struct device *dev,
|
||||
struct pwm_device *pwm)
|
||||
{
|
||||
@@ -1923,36 +1797,6 @@ static DEFINE_MUTEX(pwm_lookup_lock);
|
||||
static LIST_HEAD(pwm_lookup_list);
|
||||
|
||||
/**
|
||||
- * pwm_add_table() - register PWM device consumers
|
||||
- * @table: array of consumers to register
|
||||
- * @num: number of consumers in table
|
||||
- */
|
||||
-void pwm_add_table(struct pwm_lookup *table, size_t num)
|
||||
-{
|
||||
- guard(mutex)(&pwm_lookup_lock);
|
||||
-
|
||||
- while (num--) {
|
||||
- list_add_tail(&table->list, &pwm_lookup_list);
|
||||
- table++;
|
||||
- }
|
||||
-}
|
||||
-
|
||||
-/**
|
||||
- * pwm_remove_table() - unregister PWM device consumers
|
||||
- * @table: array of consumers to unregister
|
||||
- * @num: number of consumers in table
|
||||
- */
|
||||
-void pwm_remove_table(struct pwm_lookup *table, size_t num)
|
||||
-{
|
||||
- guard(mutex)(&pwm_lookup_lock);
|
||||
-
|
||||
- while (num--) {
|
||||
- list_del(&table->list);
|
||||
- table++;
|
||||
- }
|
||||
-}
|
||||
-
|
||||
-/**
|
||||
* pwm_get() - look up and request a PWM device
|
||||
* @dev: device for PWM consumer
|
||||
* @con_id: consumer name
|
||||
@@ -2178,6 +2022,162 @@ struct pwm_device *devm_fwnode_pwm_get(s
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(devm_fwnode_pwm_get);
|
||||
|
||||
+/**
|
||||
+ * __pwmchip_add() - register a new PWM chip
|
||||
+ * @chip: the PWM chip to add
|
||||
+ * @owner: reference to the module providing the chip.
|
||||
+ *
|
||||
+ * Register a new PWM chip. @owner is supposed to be THIS_MODULE, use the
|
||||
+ * pwmchip_add wrapper to do this right.
|
||||
+ *
|
||||
+ * Returns: 0 on success or a negative error code on failure.
|
||||
+ */
|
||||
+int __pwmchip_add(struct pwm_chip *chip, struct module *owner)
|
||||
+{
|
||||
+ int ret;
|
||||
+
|
||||
+ if (!chip || !pwmchip_parent(chip) || !chip->ops || !chip->npwm)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ /*
|
||||
+ * a struct pwm_chip must be allocated using (devm_)pwmchip_alloc,
|
||||
+ * otherwise the embedded struct device might disappear too early
|
||||
+ * resulting in memory corruption.
|
||||
+ * Catch drivers that were not converted appropriately.
|
||||
+ */
|
||||
+ if (!chip->uses_pwmchip_alloc)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ if (!pwm_ops_check(chip))
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ chip->owner = owner;
|
||||
+
|
||||
+ if (chip->atomic)
|
||||
+ spin_lock_init(&chip->atomic_lock);
|
||||
+ else
|
||||
+ mutex_init(&chip->nonatomic_lock);
|
||||
+
|
||||
+ guard(mutex)(&pwm_lock);
|
||||
+
|
||||
+ ret = idr_alloc(&pwm_chips, chip, 0, 0, GFP_KERNEL);
|
||||
+ if (ret < 0)
|
||||
+ return ret;
|
||||
+
|
||||
+ chip->id = ret;
|
||||
+
|
||||
+ dev_set_name(&chip->dev, "pwmchip%u", chip->id);
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_OF))
|
||||
+ of_pwmchip_add(chip);
|
||||
+
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = true;
|
||||
+
|
||||
+ ret = device_add(&chip->dev);
|
||||
+ if (ret)
|
||||
+ goto err_device_add;
|
||||
+
|
||||
+ return 0;
|
||||
+
|
||||
+err_device_add:
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = false;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_OF))
|
||||
+ of_pwmchip_remove(chip);
|
||||
+
|
||||
+ idr_remove(&pwm_chips, chip->id);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(__pwmchip_add);
|
||||
+
|
||||
+/**
|
||||
+ * pwmchip_remove() - remove a PWM chip
|
||||
+ * @chip: the PWM chip to remove
|
||||
+ *
|
||||
+ * Removes a PWM chip.
|
||||
+ */
|
||||
+void pwmchip_remove(struct pwm_chip *chip)
|
||||
+{
|
||||
+ pwmchip_sysfs_unexport(chip);
|
||||
+
|
||||
+ scoped_guard(mutex, &pwm_lock) {
|
||||
+ unsigned int i;
|
||||
+
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = false;
|
||||
+
|
||||
+ for (i = 0; i < chip->npwm; ++i) {
|
||||
+ struct pwm_device *pwm = &chip->pwms[i];
|
||||
+
|
||||
+ if (test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
+ dev_warn(&chip->dev, "Freeing requested PWM #%u\n", i);
|
||||
+ if (pwm->chip->ops->free)
|
||||
+ pwm->chip->ops->free(pwm->chip, pwm);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_OF))
|
||||
+ of_pwmchip_remove(chip);
|
||||
+
|
||||
+ idr_remove(&pwm_chips, chip->id);
|
||||
+ }
|
||||
+
|
||||
+ device_del(&chip->dev);
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(pwmchip_remove);
|
||||
+
|
||||
+static void devm_pwmchip_remove(void *data)
|
||||
+{
|
||||
+ struct pwm_chip *chip = data;
|
||||
+
|
||||
+ pwmchip_remove(chip);
|
||||
+}
|
||||
+
|
||||
+int __devm_pwmchip_add(struct device *dev, struct pwm_chip *chip, struct module *owner)
|
||||
+{
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = __pwmchip_add(chip, owner);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ return devm_add_action_or_reset(dev, devm_pwmchip_remove, chip);
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(__devm_pwmchip_add);
|
||||
+
|
||||
+/**
|
||||
+ * pwm_add_table() - register PWM device consumers
|
||||
+ * @table: array of consumers to register
|
||||
+ * @num: number of consumers in table
|
||||
+ */
|
||||
+void pwm_add_table(struct pwm_lookup *table, size_t num)
|
||||
+{
|
||||
+ guard(mutex)(&pwm_lookup_lock);
|
||||
+
|
||||
+ while (num--) {
|
||||
+ list_add_tail(&table->list, &pwm_lookup_list);
|
||||
+ table++;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * pwm_remove_table() - unregister PWM device consumers
|
||||
+ * @table: array of consumers to unregister
|
||||
+ * @num: number of consumers in table
|
||||
+ */
|
||||
+void pwm_remove_table(struct pwm_lookup *table, size_t num)
|
||||
+{
|
||||
+ guard(mutex)(&pwm_lookup_lock);
|
||||
+
|
||||
+ while (num--) {
|
||||
+ list_del(&table->list);
|
||||
+ table++;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
static void pwm_dbg_show(struct pwm_chip *chip, struct seq_file *s)
|
||||
{
|
||||
unsigned int i;
|
||||
@ -0,0 +1,117 @@
|
||||
From 4f3d1be4c2f8a22470f3625cbc778ba2e2130def Mon Sep 17 00:00:00 2001
|
||||
From: Vincent Mailhol <mailhol.vincent@wanadoo.fr>
|
||||
Date: Thu, 14 Nov 2024 02:18:32 +0900
|
||||
Subject: [PATCH] compiler.h: add const_true()
|
||||
|
||||
__builtin_constant_p() is known for not always being able to produce
|
||||
constant expression [1] which led to the introduction of
|
||||
__is_constexpr() [2]. Because of its dependency on
|
||||
__builtin_constant_p(), statically_true() suffers from the same
|
||||
issues.
|
||||
|
||||
For example:
|
||||
|
||||
void foo(int a)
|
||||
{
|
||||
/* fail on GCC */
|
||||
BUILD_BUG_ON_ZERO(statically_true(a));
|
||||
|
||||
/* fail on both clang and GCC */
|
||||
static char arr[statically_true(a) ? 1 : 2];
|
||||
}
|
||||
|
||||
For the same reasons why __is_constexpr() was created to cover
|
||||
__builtin_constant_p() edge cases, __is_constexpr() can be used to
|
||||
resolve statically_true() limitations.
|
||||
|
||||
Note that, somehow, GCC is not always able to fold this:
|
||||
|
||||
__is_constexpr(x) && (x)
|
||||
|
||||
It is OK in BUILD_BUG_ON_ZERO() but not in array declarations nor in
|
||||
static_assert():
|
||||
|
||||
void bar(int a)
|
||||
{
|
||||
/* success */
|
||||
BUILD_BUG_ON_ZERO(__is_constexpr(a) && (a));
|
||||
|
||||
/* fail on GCC */
|
||||
static char arr[__is_constexpr(a) && (a) ? 1 : 2];
|
||||
|
||||
/* fail on GCC */
|
||||
static_assert(__is_constexpr(a) && (a));
|
||||
}
|
||||
|
||||
Encapsulating the expression in a __builtin_choose_expr() switch
|
||||
resolves all these failed tests.
|
||||
|
||||
Define a new const_true() macro which, by making use of the
|
||||
__builtin_choose_expr() and __is_constexpr(x) combo, always produces a
|
||||
constant expression.
|
||||
|
||||
It should be noted that statically_true() is the only one able to fold
|
||||
tautological expressions in which at least one on the operands is not a
|
||||
constant expression. For example:
|
||||
|
||||
statically_true(true || var)
|
||||
statically_true(var == var)
|
||||
statically_true(var * 0 + 1)
|
||||
statically_true(!(var * 8 % 4))
|
||||
|
||||
always evaluates to true, whereas all of these would be false under
|
||||
const_true() if var is not a constant expression [3].
|
||||
|
||||
For this reason, usage of const_true() should be the exception.
|
||||
Reflect in the documentation that const_true() is less powerful and
|
||||
that statically_true() is the overall preferred solution.
|
||||
|
||||
[1] __builtin_constant_p cannot resolve to const when optimizing
|
||||
Link: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=19449
|
||||
|
||||
[2] commit 3c8ba0d61d04 ("kernel.h: Retain constant expression output for max()/min()")
|
||||
Link: https://git.kernel.org/torvalds/c/3c8ba0d61d04
|
||||
|
||||
[3] https://godbolt.org/z/c61PMxqbK
|
||||
|
||||
CC: Linus Torvalds <torvalds@linux-foundation.org>
|
||||
CC: Rasmus Villemoes <linux@rasmusvillemoes.dk>
|
||||
CC: Luc Van Oostenryck <luc.vanoostenryck@gmail.com>
|
||||
Reviewed-by: Yury Norov <yury.norov@gmail.com>,
|
||||
Signed-off-by: Vincent Mailhol <mailhol.vincent@wanadoo.fr>
|
||||
Signed-off-by: Yury Norov <yury.norov@gmail.com>
|
||||
---
|
||||
include/linux/compiler.h | 22 ++++++++++++++++++++++
|
||||
1 file changed, 22 insertions(+)
|
||||
|
||||
--- a/include/linux/compiler.h
|
||||
+++ b/include/linux/compiler.h
|
||||
@@ -295,6 +295,28 @@ static inline void *offset_to_ptr(const
|
||||
#define statically_true(x) (__builtin_constant_p(x) && (x))
|
||||
|
||||
/*
|
||||
+ * Similar to statically_true() but produces a constant expression
|
||||
+ *
|
||||
+ * To be used in conjunction with macros, such as BUILD_BUG_ON_ZERO(),
|
||||
+ * which require their input to be a constant expression and for which
|
||||
+ * statically_true() would otherwise fail.
|
||||
+ *
|
||||
+ * This is a trade-off: const_true() requires all its operands to be
|
||||
+ * compile time constants. Else, it would always returns false even on
|
||||
+ * the most trivial cases like:
|
||||
+ *
|
||||
+ * true || non_const_var
|
||||
+ *
|
||||
+ * On the opposite, statically_true() is able to fold more complex
|
||||
+ * tautologies and will return true on expressions such as:
|
||||
+ *
|
||||
+ * !(non_const_var * 8 % 4)
|
||||
+ *
|
||||
+ * For the general case, statically_true() is better.
|
||||
+ */
|
||||
+#define const_true(x) __builtin_choose_expr(__is_constexpr(x), x, false)
|
||||
+
|
||||
+/*
|
||||
* This is needed in functions which generate the stack canary, see
|
||||
* arch/x86/kernel/smpboot.c::start_secondary() for an example.
|
||||
*/
|
||||
@ -0,0 +1,99 @@
|
||||
From da6b353786997c0ffa67127355ad1d54ed3324c2 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Thu, 23 Jan 2025 18:27:07 +0100
|
||||
Subject: [PATCH] pwm: Ensure callbacks exist before calling them
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
If one of the waveform functions is called for a chip that only supports
|
||||
.apply(), we want that an error code is returned and not a NULL pointer
|
||||
exception.
|
||||
|
||||
Fixes: 6c5126c6406d ("pwm: Provide new consumer API functions for waveforms")
|
||||
Cc: stable@vger.kernel.org
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Tested-by: Trevor Gamblin <tgamblin@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/20250123172709.391349-2-u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 13 +++++++++++--
|
||||
include/linux/pwm.h | 17 +++++++++++++++++
|
||||
2 files changed, 28 insertions(+), 2 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -242,6 +242,9 @@ int pwm_round_waveform_might_sleep(struc
|
||||
|
||||
BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
+ if (!pwmchip_supports_waveform(chip))
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
if (!pwm_wf_valid(wf))
|
||||
return -EINVAL;
|
||||
|
||||
@@ -294,6 +297,9 @@ int pwm_get_waveform_might_sleep(struct
|
||||
|
||||
BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
+ if (!pwmchip_supports_waveform(chip) || !ops->read_waveform)
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
guard(pwmchip)(chip);
|
||||
|
||||
if (!chip->operational)
|
||||
@@ -320,6 +326,9 @@ static int __pwm_set_waveform(struct pwm
|
||||
|
||||
BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
+ if (!pwmchip_supports_waveform(chip))
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
if (!pwm_wf_valid(wf))
|
||||
return -EINVAL;
|
||||
|
||||
@@ -592,7 +601,7 @@ static int __pwm_apply(struct pwm_device
|
||||
state->usage_power == pwm->state.usage_power)
|
||||
return 0;
|
||||
|
||||
- if (ops->write_waveform) {
|
||||
+ if (pwmchip_supports_waveform(chip)) {
|
||||
struct pwm_waveform wf;
|
||||
char wfhw[WFHWSIZE];
|
||||
|
||||
@@ -728,7 +737,7 @@ static int pwm_get_state_hw(struct pwm_d
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
int ret = -EOPNOTSUPP;
|
||||
|
||||
- if (ops->read_waveform) {
|
||||
+ if (pwmchip_supports_waveform(chip) && ops->read_waveform) {
|
||||
char wfhw[WFHWSIZE];
|
||||
struct pwm_waveform wf;
|
||||
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -342,6 +342,23 @@ struct pwm_chip {
|
||||
struct pwm_device pwms[] __counted_by(npwm);
|
||||
};
|
||||
|
||||
+/**
|
||||
+ * pwmchip_supports_waveform() - checks if the given chip supports waveform callbacks
|
||||
+ * @chip: The pwm_chip to test
|
||||
+ *
|
||||
+ * Returns true iff the pwm chip support the waveform functions like
|
||||
+ * pwm_set_waveform_might_sleep() and pwm_round_waveform_might_sleep()
|
||||
+ */
|
||||
+static inline bool pwmchip_supports_waveform(struct pwm_chip *chip)
|
||||
+{
|
||||
+ /*
|
||||
+ * only check for .write_waveform(). If that is available,
|
||||
+ * .round_waveform_tohw() and .round_waveform_fromhw() asserted to be
|
||||
+ * available, too, in pwmchip_add().
|
||||
+ */
|
||||
+ return chip->ops->write_waveform != NULL;
|
||||
+}
|
||||
+
|
||||
static inline struct device *pwmchip_parent(const struct pwm_chip *chip)
|
||||
{
|
||||
return chip->dev.parent;
|
||||
@ -0,0 +1,61 @@
|
||||
From 895fe4537cc8586f51abb5c66524efaa42c29883 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Thu, 6 Feb 2025 13:06:25 +0100
|
||||
Subject: [PATCH] pwm: Add upgrade path to #pwm-cells = <3> for users of
|
||||
of_pwm_single_xlate()
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
The PWM chip on PXA only has a single output. Back when the device tree
|
||||
binding was defined it was considered a good idea to not pass the PWM
|
||||
line index as is done for all other PWM types as it would be always zero
|
||||
anyhow and so doesn't add any value.
|
||||
|
||||
However for consistency reasons it is nice when all PWMs use the same
|
||||
binding. For that reason let of_pwm_single_xlate() (i.e. the function
|
||||
that implements the PXA behaviour) behave in the same way as
|
||||
of_pwm_xlate_with_flags() for 3 (or more) parameters. With that in
|
||||
place, the pxa-pwm binding can be updated to #pwm-cells = <3> without
|
||||
breaking old device trees that stick to #pwm-cells = <1>.
|
||||
|
||||
Reviewed-by: Herve Codina <herve.codina@bootlin.com>
|
||||
Tested-by: Duje Mihanović <duje.mihanovic@skole.hr>
|
||||
Reviewed-by: Daniel Mack <daniel@zonque.org>
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/b33a84d3f073880e94fc303cd32ebe095eb5ce46.1738842938.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 16 ++++++++++++++++
|
||||
1 file changed, 16 insertions(+)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -988,11 +988,27 @@ of_pwm_xlate_with_flags(struct pwm_chip
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(of_pwm_xlate_with_flags);
|
||||
|
||||
+/*
|
||||
+ * This callback is used for PXA PWM chips that only have a single PWM line.
|
||||
+ * For such chips you could argue that passing the line number (i.e. the first
|
||||
+ * parameter in the common case) is useless as it's always zero. So compared to
|
||||
+ * the default xlate function of_pwm_xlate_with_flags() the first parameter is
|
||||
+ * the default period and the second are flags.
|
||||
+ *
|
||||
+ * Note that if #pwm-cells = <3>, the semantic is the same as for
|
||||
+ * of_pwm_xlate_with_flags() to allow converting the affected driver to
|
||||
+ * #pwm-cells = <3> without breaking the legacy binding.
|
||||
+ *
|
||||
+ * Don't use for new drivers.
|
||||
+ */
|
||||
struct pwm_device *
|
||||
of_pwm_single_xlate(struct pwm_chip *chip, const struct of_phandle_args *args)
|
||||
{
|
||||
struct pwm_device *pwm;
|
||||
|
||||
+ if (args->args_count >= 3)
|
||||
+ return of_pwm_xlate_with_flags(chip, args);
|
||||
+
|
||||
pwm = pwm_request_from_chip(chip, 0, NULL);
|
||||
if (IS_ERR(pwm))
|
||||
return pwm;
|
||||
@ -0,0 +1,71 @@
|
||||
From 00e53d0f4baedd72196b65f00698b2a5a537dc2b Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Sat, 5 Apr 2025 11:27:12 +0200
|
||||
Subject: [PATCH] pwm: Let pwm_set_waveform() succeed even if lowlevel driver
|
||||
rounded up
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Waveform parameters are supposed to be rounded down to the next value
|
||||
possible for the hardware. However when a requested value is too small,
|
||||
.round_waveform_tohw() is supposed to pick the next bigger value and
|
||||
return 1. Let pwm_set_waveform() behave in the same way.
|
||||
|
||||
This creates consistency between pwm_set_waveform_might_sleep() with
|
||||
exact=false and pwm_round_waveform_might_sleep() +
|
||||
pwm_set_waveform_might_sleep() with exact=true.
|
||||
|
||||
The PWM_DEBUG rounding check has to be adapted to only trigger if no
|
||||
uprounding happend.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Tested-by: Trevor Gamblin <tgamblin@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/353dc6ae31be815e41fd3df89c257127ca0d1a09.1743844730.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 13 +++++++------
|
||||
1 file changed, 7 insertions(+), 6 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -322,7 +322,7 @@ static int __pwm_set_waveform(struct pwm
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
char wfhw[WFHWSIZE];
|
||||
struct pwm_waveform wf_rounded;
|
||||
- int err;
|
||||
+ int err, ret_tohw;
|
||||
|
||||
BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
@@ -332,16 +332,16 @@ static int __pwm_set_waveform(struct pwm
|
||||
if (!pwm_wf_valid(wf))
|
||||
return -EINVAL;
|
||||
|
||||
- err = __pwm_round_waveform_tohw(chip, pwm, wf, &wfhw);
|
||||
- if (err)
|
||||
- return err;
|
||||
+ ret_tohw = __pwm_round_waveform_tohw(chip, pwm, wf, &wfhw);
|
||||
+ if (ret_tohw < 0)
|
||||
+ return ret_tohw;
|
||||
|
||||
if ((IS_ENABLED(CONFIG_PWM_DEBUG) || exact) && wf->period_length_ns) {
|
||||
err = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf_rounded);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
- if (IS_ENABLED(CONFIG_PWM_DEBUG) && !pwm_check_rounding(wf, &wf_rounded))
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && ret_tohw == 0 && !pwm_check_rounding(wf, &wf_rounded))
|
||||
dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
@@ -382,7 +382,8 @@ static int __pwm_set_waveform(struct pwm
|
||||
wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns,
|
||||
wf_set.duty_length_ns, wf_set.period_length_ns, wf_set.duty_offset_ns);
|
||||
}
|
||||
- return 0;
|
||||
+
|
||||
+ return ret_tohw;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -0,0 +1,54 @@
|
||||
From 96d20cfd16e779923153f7347b0bef6b3c7606ce Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Sat, 5 Apr 2025 11:27:17 +0200
|
||||
Subject: [PATCH] pwm: Do stricter return value checking for
|
||||
.round_waveform_tohw()
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
The .round_waveform_tohw() is supposed to return 0 if the request could
|
||||
be rounded down to match the hardware capabilities and return 1 if
|
||||
rounding down wasn't possible.
|
||||
|
||||
Expand the PWM_DEBUG check to not only assert proper downrounding if 0
|
||||
was returned but also check that it was actually rounded up when the
|
||||
callback signalled uprounding.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/dfb824ae37f99df068c752d48cbd163c044a74fb.1743844730.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 12 ++++++------
|
||||
1 file changed, 6 insertions(+), 6 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -270,10 +270,10 @@ int pwm_round_waveform_might_sleep(struc
|
||||
wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns, ret_tohw);
|
||||
|
||||
if (IS_ENABLED(CONFIG_PWM_DEBUG) &&
|
||||
- ret_tohw == 0 && !pwm_check_rounding(&wf_req, wf))
|
||||
- dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ (ret_tohw == 0) != pwm_check_rounding(&wf_req, wf))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu], ret: %d\n",
|
||||
wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns,
|
||||
- wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns);
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns, ret_tohw);
|
||||
|
||||
return ret_tohw;
|
||||
}
|
||||
@@ -341,10 +341,10 @@ static int __pwm_set_waveform(struct pwm
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
- if (IS_ENABLED(CONFIG_PWM_DEBUG) && ret_tohw == 0 && !pwm_check_rounding(wf, &wf_rounded))
|
||||
- dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && (ret_tohw == 0) != pwm_check_rounding(wf, &wf_rounded))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu], ret: %d\n",
|
||||
wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
- wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns, ret_tohw);
|
||||
|
||||
if (exact && pwmwfcmp(wf, &wf_rounded)) {
|
||||
dev_dbg(&chip->dev, "Requested no rounding, but %llu/%llu [+%llu] -> %llu/%llu [+%llu]\n",
|
||||
@ -0,0 +1,38 @@
|
||||
From e463b05d10da12b13d03f41a407e2ad043af158f Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Tue, 8 Apr 2025 16:23:54 +0200
|
||||
Subject: [PATCH] pwm: Better document return value of
|
||||
pwm_round_waveform_might_sleep()
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Better explain how pwm_round_waveform_might_sleep() (and so the
|
||||
respective lowlevel driver callback) is supposed to round and the
|
||||
meaning of the return value.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/db84abf1e82e4498fc0e7c318d2673771d0039fe.1744120697.git.ukleinek@kernel.org
|
||||
[ukleinek: Fix a rst formatting issue reported by Stephen Rothwell]
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 8 ++++++--
|
||||
1 file changed, 6 insertions(+), 2 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -229,8 +229,12 @@ static int __pwm_write_waveform(struct p
|
||||
* these two calls and the waveform determined by
|
||||
* pwm_round_waveform_might_sleep() cannot be implemented any more.
|
||||
*
|
||||
- * Returns 0 on success, 1 if there is no valid hardware configuration matching
|
||||
- * the input waveform under the PWM rounding rules or a negative errno.
|
||||
+ * Usually all values passed in @wf are rounded down to the nearest possible
|
||||
+ * value (in the order period_length_ns, duty_length_ns and then
|
||||
+ * duty_offset_ns). Only if this isn't possible, a value might grow.
|
||||
+ *
|
||||
+ * Returns 0 on success, 1 if at least one value had to be rounded up or a
|
||||
+ * negative errno.
|
||||
*/
|
||||
int pwm_round_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
{
|
||||
@ -0,0 +1,157 @@
|
||||
From 7f8ce4d88b42fcbd3350370ec4d02e00979fc5a9 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Thu, 17 Apr 2025 20:16:11 +0200
|
||||
Subject: [PATCH] pwm: Fix various formatting issues in kernel-doc
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Add Return and (where interesting) Context sections, fix some formatting
|
||||
and drop documenting the internal function __pwm_apply().
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/20250417181611.2693599-2-u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 42 +++++++++++++++++++++++++++---------------
|
||||
include/linux/pwm.h | 8 +++++---
|
||||
2 files changed, 32 insertions(+), 18 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -216,14 +216,14 @@ static int __pwm_write_waveform(struct p
|
||||
*
|
||||
* Typically a given waveform cannot be implemented exactly by hardware, e.g.
|
||||
* because hardware only supports coarse period resolution or no duty_offset.
|
||||
- * This function returns the actually implemented waveform if you pass wf to
|
||||
- * pwm_set_waveform_might_sleep now.
|
||||
+ * This function returns the actually implemented waveform if you pass @wf to
|
||||
+ * pwm_set_waveform_might_sleep() now.
|
||||
*
|
||||
* Note however that the world doesn't stop turning when you call it, so when
|
||||
- * doing
|
||||
+ * doing::
|
||||
*
|
||||
- * pwm_round_waveform_might_sleep(mypwm, &wf);
|
||||
- * pwm_set_waveform_might_sleep(mypwm, &wf, true);
|
||||
+ * pwm_round_waveform_might_sleep(mypwm, &wf);
|
||||
+ * pwm_set_waveform_might_sleep(mypwm, &wf, true);
|
||||
*
|
||||
* the latter might fail, e.g. because an input clock changed its rate between
|
||||
* these two calls and the waveform determined by
|
||||
@@ -233,8 +233,9 @@ static int __pwm_write_waveform(struct p
|
||||
* value (in the order period_length_ns, duty_length_ns and then
|
||||
* duty_offset_ns). Only if this isn't possible, a value might grow.
|
||||
*
|
||||
- * Returns 0 on success, 1 if at least one value had to be rounded up or a
|
||||
+ * Returns: 0 on success, 1 if at least one value had to be rounded up or a
|
||||
* negative errno.
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_round_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
{
|
||||
@@ -291,6 +292,9 @@ EXPORT_SYMBOL_GPL(pwm_round_waveform_mig
|
||||
*
|
||||
* Stores the current configuration of the PWM in @wf. Note this is the
|
||||
* equivalent of pwm_get_state_hw() (and not pwm_get_state()) for pwm_waveform.
|
||||
+ *
|
||||
+ * Returns: 0 on success or a negative errno
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_get_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
{
|
||||
@@ -399,13 +403,17 @@ static int __pwm_set_waveform(struct pwm
|
||||
*
|
||||
* Typically a requested waveform cannot be implemented exactly, e.g. because
|
||||
* you requested .period_length_ns = 100 ns, but the hardware can only set
|
||||
- * periods that are a multiple of 8.5 ns. With that hardware passing exact =
|
||||
+ * periods that are a multiple of 8.5 ns. With that hardware passing @exact =
|
||||
* true results in pwm_set_waveform_might_sleep() failing and returning 1. If
|
||||
- * exact = false you get a period of 93.5 ns (i.e. the biggest period not bigger
|
||||
+ * @exact = false you get a period of 93.5 ns (i.e. the biggest period not bigger
|
||||
* than the requested value).
|
||||
- * Note that even with exact = true, some rounding by less than 1 is
|
||||
+ * Note that even with @exact = true, some rounding by less than 1 ns is
|
||||
* possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
- * exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
+ * @exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
+ *
|
||||
+ * Returns: 0 on success, 1 if was rounded up (if !@exact) or no perfect match was
|
||||
+ * possible (if @exact), or a negative errno
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_set_waveform_might_sleep(struct pwm_device *pwm,
|
||||
const struct pwm_waveform *wf, bool exact)
|
||||
@@ -565,11 +573,6 @@ static bool pwm_state_valid(const struct
|
||||
return true;
|
||||
}
|
||||
|
||||
-/**
|
||||
- * __pwm_apply() - atomically apply a new state to a PWM device
|
||||
- * @pwm: PWM device
|
||||
- * @state: new state to apply
|
||||
- */
|
||||
static int __pwm_apply(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
struct pwm_chip *chip;
|
||||
@@ -678,6 +681,9 @@ static int __pwm_apply(struct pwm_device
|
||||
* Cannot be used in atomic context.
|
||||
* @pwm: PWM device
|
||||
* @state: new state to apply
|
||||
+ *
|
||||
+ * Returns: 0 on success, or a negative errno
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_apply_might_sleep(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
@@ -719,6 +725,9 @@ EXPORT_SYMBOL_GPL(pwm_apply_might_sleep)
|
||||
* Not all PWM devices support this function, check with pwm_might_sleep().
|
||||
* @pwm: PWM device
|
||||
* @state: new state to apply
|
||||
+ *
|
||||
+ * Returns: 0 on success, or a negative errno
|
||||
+ * Context: Any
|
||||
*/
|
||||
int pwm_apply_atomic(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
@@ -778,6 +787,9 @@ static int pwm_get_state_hw(struct pwm_d
|
||||
* This function will adjust the PWM config to the PWM arguments provided
|
||||
* by the DT or PWM lookup table. This is particularly useful to adapt
|
||||
* the bootloader config to the Linux one.
|
||||
+ *
|
||||
+ * Returns: 0 on success or a negative error code on failure.
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_adjust_config(struct pwm_device *pwm)
|
||||
{
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -218,6 +218,8 @@ static inline void pwm_init_state(const
|
||||
*
|
||||
* pwm_get_state(pwm, &state);
|
||||
* duty = pwm_get_relative_duty_cycle(&state, 100);
|
||||
+ *
|
||||
+ * Returns: rounded relative duty cycle multiplied by @scale
|
||||
*/
|
||||
static inline unsigned int
|
||||
pwm_get_relative_duty_cycle(const struct pwm_state *state, unsigned int scale)
|
||||
@@ -244,8 +246,8 @@ pwm_get_relative_duty_cycle(const struct
|
||||
* pwm_set_relative_duty_cycle(&state, 50, 100);
|
||||
* pwm_apply_might_sleep(pwm, &state);
|
||||
*
|
||||
- * This functions returns -EINVAL if @duty_cycle and/or @scale are
|
||||
- * inconsistent (@scale == 0 or @duty_cycle > @scale).
|
||||
+ * Returns: 0 on success or ``-EINVAL`` if @duty_cycle and/or @scale are
|
||||
+ * inconsistent (@scale == 0 or @duty_cycle > @scale)
|
||||
*/
|
||||
static inline int
|
||||
pwm_set_relative_duty_cycle(struct pwm_state *state, unsigned int duty_cycle,
|
||||
@@ -346,7 +348,7 @@ struct pwm_chip {
|
||||
* pwmchip_supports_waveform() - checks if the given chip supports waveform callbacks
|
||||
* @chip: The pwm_chip to test
|
||||
*
|
||||
- * Returns true iff the pwm chip support the waveform functions like
|
||||
+ * Returns: true iff the pwm chip support the waveform functions like
|
||||
* pwm_set_waveform_might_sleep() and pwm_round_waveform_might_sleep()
|
||||
*/
|
||||
static inline bool pwmchip_supports_waveform(struct pwm_chip *chip)
|
||||
@ -0,0 +1,64 @@
|
||||
From e866834c8baabc33b431902beeeb0c94dfbc1024 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Wed, 30 Apr 2025 13:55:58 +0200
|
||||
Subject: [PATCH] pwm: Let pwm_set_waveform_might_sleep() fail for exact but
|
||||
impossible requests
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Up to now pwm_set_waveform_might_sleep() returned 1 for exact requests
|
||||
that couldn't be served exactly. In contrast to
|
||||
pwm_round_waveform_might_sleep() and pwm_set_waveform_might_sleep() with
|
||||
exact = false this is an error condition. So simplify handling for
|
||||
callers of pwm_set_waveform_might_sleep() by returning -EDOM instead of
|
||||
1 in this case.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/20538a46719584dafd8a1395c886780a97dcdf79.1746010245.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 21 ++++++++++++++++-----
|
||||
1 file changed, 16 insertions(+), 5 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -404,15 +404,16 @@ static int __pwm_set_waveform(struct pwm
|
||||
* Typically a requested waveform cannot be implemented exactly, e.g. because
|
||||
* you requested .period_length_ns = 100 ns, but the hardware can only set
|
||||
* periods that are a multiple of 8.5 ns. With that hardware passing @exact =
|
||||
- * true results in pwm_set_waveform_might_sleep() failing and returning 1. If
|
||||
- * @exact = false you get a period of 93.5 ns (i.e. the biggest period not bigger
|
||||
- * than the requested value).
|
||||
+ * true results in pwm_set_waveform_might_sleep() failing and returning -EDOM.
|
||||
+ * If @exact = false you get a period of 93.5 ns (i.e. the biggest period not
|
||||
+ * bigger than the requested value).
|
||||
* Note that even with @exact = true, some rounding by less than 1 ns is
|
||||
* possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
* @exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
*
|
||||
- * Returns: 0 on success, 1 if was rounded up (if !@exact) or no perfect match was
|
||||
- * possible (if @exact), or a negative errno
|
||||
+ * Returns: 0 on success, 1 if was rounded up (if !@exact), -EDOM if setting
|
||||
+ * failed due to the exact waveform not being possible (if @exact), or a
|
||||
+ * different negative errno on failure.
|
||||
* Context: May sleep.
|
||||
*/
|
||||
int pwm_set_waveform_might_sleep(struct pwm_device *pwm,
|
||||
@@ -440,6 +441,16 @@ int pwm_set_waveform_might_sleep(struct
|
||||
err = __pwm_set_waveform(pwm, wf, exact);
|
||||
}
|
||||
|
||||
+ /*
|
||||
+ * map err == 1 to -EDOM for exact requests. Also make sure that -EDOM is
|
||||
+ * only returned in exactly that case. Note that __pwm_set_waveform()
|
||||
+ * should never return -EDOM which justifies the unlikely().
|
||||
+ */
|
||||
+ if (unlikely(err == -EDOM))
|
||||
+ err = -EINVAL;
|
||||
+ else if (exact && err == 1)
|
||||
+ err = -EDOM;
|
||||
+
|
||||
return err;
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(pwm_set_waveform_might_sleep);
|
||||
@ -0,0 +1,58 @@
|
||||
From 164c4ac754abaf9643815d09001cc7d81042d624 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Wed, 30 Apr 2025 13:55:59 +0200
|
||||
Subject: [PATCH] pwm: Let pwm_set_waveform_might_sleep() return 0 instead of 1
|
||||
after rounding up
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
While telling the caller of pwm_set_waveform_might_sleep() if the
|
||||
request was completed by rounding down only or (some) rounding up gives
|
||||
additional information, it makes usage this function needlessly hard and
|
||||
the additional information is not used. A prove for that is that
|
||||
currently both users of this function just pass the returned value up to
|
||||
their caller even though a positive value isn't intended there.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/528cc3bbd9e35dea8646b1bcc0fbfe6c498bb4ed.1746010245.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 14 ++++++++------
|
||||
1 file changed, 8 insertions(+), 6 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -411,9 +411,8 @@ static int __pwm_set_waveform(struct pwm
|
||||
* possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
* @exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
*
|
||||
- * Returns: 0 on success, 1 if was rounded up (if !@exact), -EDOM if setting
|
||||
- * failed due to the exact waveform not being possible (if @exact), or a
|
||||
- * different negative errno on failure.
|
||||
+ * Returns: 0 on success, -EDOM if setting failed due to the exact waveform not
|
||||
+ * being possible (if @exact), or a different negative errno on failure.
|
||||
* Context: May sleep.
|
||||
*/
|
||||
int pwm_set_waveform_might_sleep(struct pwm_device *pwm,
|
||||
@@ -442,14 +441,17 @@ int pwm_set_waveform_might_sleep(struct
|
||||
}
|
||||
|
||||
/*
|
||||
- * map err == 1 to -EDOM for exact requests. Also make sure that -EDOM is
|
||||
- * only returned in exactly that case. Note that __pwm_set_waveform()
|
||||
- * should never return -EDOM which justifies the unlikely().
|
||||
+ * map err == 1 to -EDOM for exact requests and 0 for !exact ones. Also
|
||||
+ * make sure that -EDOM is only returned in exactly that case. Note that
|
||||
+ * __pwm_set_waveform() should never return -EDOM which justifies the
|
||||
+ * unlikely().
|
||||
*/
|
||||
if (unlikely(err == -EDOM))
|
||||
err = -EINVAL;
|
||||
else if (exact && err == 1)
|
||||
err = -EDOM;
|
||||
+ else if (err == 1)
|
||||
+ err = 0;
|
||||
|
||||
return err;
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
From d041b76ac9fb9e60e7cdb0265ed9d8b6058a88bf Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Wed, 30 Apr 2025 13:56:00 +0200
|
||||
Subject: [PATCH] pwm: Formally describe the procedure used to pick a hardware
|
||||
waveform setting
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This serves as specification for both, PWM consumers and the respective
|
||||
callback for lowlevel drivers.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/d2916bfa70274961ded26b07ab6998c36b90e69a.1746010245.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 24 +++++++++++++++++++++++-
|
||||
1 file changed, 23 insertions(+), 1 deletion(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -231,7 +231,9 @@ static int __pwm_write_waveform(struct p
|
||||
*
|
||||
* Usually all values passed in @wf are rounded down to the nearest possible
|
||||
* value (in the order period_length_ns, duty_length_ns and then
|
||||
- * duty_offset_ns). Only if this isn't possible, a value might grow.
|
||||
+ * duty_offset_ns). Only if this isn't possible, a value might grow. See the
|
||||
+ * documentation for pwm_set_waveform_might_sleep() for a more formal
|
||||
+ * description.
|
||||
*
|
||||
* Returns: 0 on success, 1 if at least one value had to be rounded up or a
|
||||
* negative errno.
|
||||
@@ -411,6 +413,26 @@ static int __pwm_set_waveform(struct pwm
|
||||
* possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
* @exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
*
|
||||
+ * Let C be the set of possible hardware configurations for a given PWM device,
|
||||
+ * consisting of tuples (p, d, o) where p is the period length, d is the duty
|
||||
+ * length and o the duty offset.
|
||||
+ *
|
||||
+ * The following algorithm is implemented to pick the hardware setting
|
||||
+ * (p, d, o) ∈ C for a given request (p', d', o') with @exact = false::
|
||||
+ *
|
||||
+ * p = max( { ṗ | ∃ ḋ, ȯ : (ṗ, ḋ, ȯ) ∈ C ∧ ṗ ≤ p' } ∪ { min({ ṗ | ∃ ḋ, ȯ : (ṗ, ḋ, ȯ) ∈ C }) })
|
||||
+ * d = max( { ḋ | ∃ ȯ : (p, ḋ, ȯ) ∈ C ∧ ḋ ≤ d' } ∪ { min({ ḋ | ∃ ȯ : (p, ḋ, ȯ) ∈ C }) })
|
||||
+ * o = max( { ȯ | (p, d, ȯ) ∈ C ∧ ȯ ≤ o' } ∪ { min({ ȯ | (p, d, ȯ) ∈ C }) })
|
||||
+ *
|
||||
+ * In words: The chosen period length is the maximal possible period length not
|
||||
+ * bigger than the requested period length and if that doesn't exist, the
|
||||
+ * minimal period length. The chosen duty length is the maximal possible duty
|
||||
+ * length that is compatible with the chosen period length and isn't bigger than
|
||||
+ * the requested duty length. Again if such a value doesn't exist, the minimal
|
||||
+ * duty length compatible with the chosen period is picked. After that the duty
|
||||
+ * offset compatible with the chosen period and duty length is chosen in the
|
||||
+ * same way.
|
||||
+ *
|
||||
* Returns: 0 on success, -EDOM if setting failed due to the exact waveform not
|
||||
* being possible (if @exact), or a different negative errno on failure.
|
||||
* Context: May sleep.
|
||||
@ -0,0 +1,877 @@
|
||||
From git@z Thu Jan 1 00:00:00 1970
|
||||
Subject: [PATCH v3 2/5] mfd: Add Rockchip mfpwm driver
|
||||
From: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
Date: Mon, 27 Oct 2025 18:11:57 +0100
|
||||
Message-Id: <20251027-rk3576-pwm-v3-2-654a5cb1e3f8@collabora.com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
With the Rockchip RK3576, the PWM IP used by Rockchip has changed
|
||||
substantially. Looking at both the downstream pwm-rockchip driver as
|
||||
well as the mainline pwm-rockchip driver made it clear that with all its
|
||||
additional features and its differences from previous IP revisions, it
|
||||
is best supported in a new driver.
|
||||
|
||||
This brings us to the question as to what such a new driver should be.
|
||||
To me, it soon became clear that it should actually be several new
|
||||
drivers, most prominently when Uwe Kleine-König let me know that I
|
||||
should not implement the pwm subsystem's capture callback, but instead
|
||||
write a counter driver for this functionality.
|
||||
|
||||
Combined with the other as-of-yet unimplemented functionality of this
|
||||
new IP, it became apparent that it needs to be spread across several
|
||||
subsystems.
|
||||
|
||||
For this reason, we add a new MFD core driver, called mfpwm (short for
|
||||
"Multi-function PWM"). This "parent" driver makes sure that only one
|
||||
device function driver is using the device at a time, and is in charge
|
||||
of registering the MFD cell devices for the individual device functions
|
||||
offered by the device.
|
||||
|
||||
An acquire/release pattern is used to guarantee that device function
|
||||
drivers don't step on each other's toes.
|
||||
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
MAINTAINERS | 2 +
|
||||
drivers/mfd/Kconfig | 15 ++
|
||||
drivers/mfd/Makefile | 1 +
|
||||
drivers/mfd/rockchip-mfpwm.c | 340 +++++++++++++++++++++++++++
|
||||
include/linux/mfd/rockchip-mfpwm.h | 454 +++++++++++++++++++++++++++++++++++++
|
||||
5 files changed, 812 insertions(+)
|
||||
|
||||
--- a/drivers/mfd/Kconfig
|
||||
+++ b/drivers/mfd/Kconfig
|
||||
@@ -1244,6 +1244,21 @@ config MFD_RC5T583
|
||||
Additional drivers must be enabled in order to use the
|
||||
different functionality of the device.
|
||||
|
||||
+config MFD_ROCKCHIP_MFPWM
|
||||
+ tristate "Rockchip multi-function PWM controller"
|
||||
+ depends on OF
|
||||
+ depends on HAS_IOMEM
|
||||
+ depends on COMMON_CLK
|
||||
+ select MFD_CORE
|
||||
+ help
|
||||
+ Some Rockchip SoCs, such as the RK3576, use a PWM controller that has
|
||||
+ several different functions, such as generating PWM waveforms but also
|
||||
+ counting waveforms.
|
||||
+
|
||||
+ This driver manages the overall device, and selects between different
|
||||
+ functionalities at runtime as needed. Drivers for them are implemented
|
||||
+ in their respective subsystems.
|
||||
+
|
||||
config MFD_RK8XX
|
||||
tristate
|
||||
select MFD_CORE
|
||||
--- a/drivers/mfd/Makefile
|
||||
+++ b/drivers/mfd/Makefile
|
||||
@@ -227,6 +227,7 @@ obj-$(CONFIG_MFD_PALMAS) += palmas.o
|
||||
obj-$(CONFIG_MFD_VIPERBOARD) += viperboard.o
|
||||
obj-$(CONFIG_MFD_NTXEC) += ntxec.o
|
||||
obj-$(CONFIG_MFD_RC5T583) += rc5t583.o rc5t583-irq.o
|
||||
+obj-$(CONFIG_MFD_ROCKCHIP_MFPWM) += rockchip-mfpwm.o
|
||||
obj-$(CONFIG_MFD_RK8XX) += rk8xx-core.o
|
||||
obj-$(CONFIG_MFD_RK8XX_I2C) += rk8xx-i2c.o
|
||||
obj-$(CONFIG_MFD_RK8XX_SPI) += rk8xx-spi.o
|
||||
--- /dev/null
|
||||
+++ b/drivers/mfd/rockchip-mfpwm.c
|
||||
@@ -0,0 +1,340 @@
|
||||
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
+ *
|
||||
+ * A driver to manage all the different functionalities exposed by Rockchip's
|
||||
+ * PWMv4 hardware.
|
||||
+ *
|
||||
+ * This driver is chiefly focused on guaranteeing non-concurrent operation
|
||||
+ * between the different device functions, as well as setting the clocks.
|
||||
+ * It registers the device function platform devices, e.g. PWM output or
|
||||
+ * PWM capture.
|
||||
+ *
|
||||
+ * Authors:
|
||||
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ */
|
||||
+
|
||||
+#include <linux/array_size.h>
|
||||
+#include <linux/clk.h>
|
||||
+#include <linux/clk-provider.h>
|
||||
+#include <linux/mfd/core.h>
|
||||
+#include <linux/mfd/rockchip-mfpwm.h>
|
||||
+#include <linux/module.h>
|
||||
+#include <linux/of.h>
|
||||
+#include <linux/overflow.h>
|
||||
+#include <linux/platform_device.h>
|
||||
+#include <linux/spinlock.h>
|
||||
+
|
||||
+/**
|
||||
+ * struct rockchip_mfpwm - private mfpwm driver instance state struct
|
||||
+ * @pdev: pointer to this instance's &struct platform_device
|
||||
+ * @base: pointer to the memory mapped registers of this device
|
||||
+ * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from
|
||||
+ * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from
|
||||
+ * @rc_clk: pointer to the RC oscillator the PWM signal may be derived from
|
||||
+ * @chosen_clk: a clk-mux of pwm_clk, osc_clk and rc_clk
|
||||
+ * @pclk: pointer to the APB bus clock needed for mmio register access
|
||||
+ * @active_func: pointer to the currently active device function, or %NULL if no
|
||||
+ * device function is currently actively using any of the shared
|
||||
+ * resources. May only be checked/modified with @state_lock held.
|
||||
+ * @acquire_cnt: number of times @active_func has currently mfpwm_acquire()'d
|
||||
+ * it. Must only be checked or modified while holding @state_lock.
|
||||
+ * @state_lock: this lock is held while either the active device function, the
|
||||
+ * enable register, or the chosen clock is being changed.
|
||||
+ * @irq: the IRQ number of this device
|
||||
+ */
|
||||
+struct rockchip_mfpwm {
|
||||
+ struct platform_device *pdev;
|
||||
+ void __iomem *base;
|
||||
+ struct clk *pwm_clk;
|
||||
+ struct clk *osc_clk;
|
||||
+ struct clk *rc_clk;
|
||||
+ struct clk *chosen_clk;
|
||||
+ struct clk *pclk;
|
||||
+ struct rockchip_mfpwm_func *active_func;
|
||||
+ unsigned int acquire_cnt;
|
||||
+ spinlock_t state_lock;
|
||||
+ int irq;
|
||||
+};
|
||||
+
|
||||
+static atomic_t subdev_id = ATOMIC_INIT(0);
|
||||
+
|
||||
+static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev)
|
||||
+{
|
||||
+ return platform_get_drvdata(pdev);
|
||||
+}
|
||||
+
|
||||
+static int mfpwm_check_pwmf(const struct rockchip_mfpwm_func *pwmf,
|
||||
+ const char *fname)
|
||||
+{
|
||||
+ struct device *dev = &pwmf->parent->pdev->dev;
|
||||
+
|
||||
+ if (IS_ERR_OR_NULL(pwmf)) {
|
||||
+ dev_warn(dev, "called %s with an erroneous handle, no effect\n",
|
||||
+ fname);
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+
|
||||
+ if (IS_ERR_OR_NULL(pwmf->parent)) {
|
||||
+ dev_warn(dev, "called %s with an erroneous mfpwm_func parent, no effect\n",
|
||||
+ fname);
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+__attribute__((nonnull))
|
||||
+static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
|
||||
+ unsigned int cnt;
|
||||
+
|
||||
+ if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
|
||||
+ return -EBUSY;
|
||||
+
|
||||
+ if (!mfpwm->active_func)
|
||||
+ mfpwm->active_func = pwmf;
|
||||
+
|
||||
+ if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
|
||||
+ mfpwm->acquire_cnt = cnt;
|
||||
+ } else {
|
||||
+ dev_warn(&mfpwm->pdev->dev, "prevented acquire counter overflow in %s\n",
|
||||
+ __func__);
|
||||
+ return -EOVERFLOW;
|
||||
+ }
|
||||
+
|
||||
+ dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
|
||||
+ pwmf->id, mfpwm->acquire_cnt);
|
||||
+
|
||||
+ return clk_enable(mfpwm->pclk);
|
||||
+}
|
||||
+
|
||||
+int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm;
|
||||
+ unsigned long flags;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ mfpwm = pwmf->parent;
|
||||
+ dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id);
|
||||
+
|
||||
+ if (!spin_trylock_irqsave(&mfpwm->state_lock, flags))
|
||||
+ return -EBUSY;
|
||||
+
|
||||
+ ret = mfpwm_do_acquire(pwmf);
|
||||
+
|
||||
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, ROCKCHIP_MFPWM);
|
||||
+
|
||||
+__attribute__((nonnull))
|
||||
+static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
|
||||
+
|
||||
+ if (!mfpwm->active_func)
|
||||
+ return;
|
||||
+
|
||||
+ if (mfpwm->active_func->id != pwmf->id)
|
||||
+ return;
|
||||
+
|
||||
+ /*
|
||||
+ * No need to check_sub_overflow here, !mfpwm->active_func above catches
|
||||
+ * this type of problem already.
|
||||
+ */
|
||||
+ mfpwm->acquire_cnt--;
|
||||
+
|
||||
+ if (!mfpwm->acquire_cnt)
|
||||
+ mfpwm->active_func = NULL;
|
||||
+
|
||||
+ clk_disable(mfpwm->pclk);
|
||||
+}
|
||||
+
|
||||
+void mfpwm_release(const struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm;
|
||||
+ unsigned long flags;
|
||||
+
|
||||
+ if (mfpwm_check_pwmf(pwmf, "mfpwm_release"))
|
||||
+ return;
|
||||
+
|
||||
+ mfpwm = pwmf->parent;
|
||||
+
|
||||
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
|
||||
+ mfpwm_do_release(pwmf);
|
||||
+ dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n",
|
||||
+ pwmf->id, mfpwm->acquire_cnt);
|
||||
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
|
||||
+}
|
||||
+EXPORT_SYMBOL_NS_GPL(mfpwm_release, ROCKCHIP_MFPWM);
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_register_subdev - register a single mfpwm_func
|
||||
+ * @mfpwm: pointer to the parent &struct rockchip_mfpwm
|
||||
+ * @name: sub-device name string
|
||||
+ *
|
||||
+ * Allocate a single &struct mfpwm_func, fill its members with appropriate data,
|
||||
+ * and register a new mfd cell.
|
||||
+ *
|
||||
+ * Returns: 0 on success, negative errno on error
|
||||
+ */
|
||||
+static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm,
|
||||
+ const char *name)
|
||||
+{
|
||||
+ struct rockchip_mfpwm_func *func;
|
||||
+ struct mfd_cell cell = {};
|
||||
+
|
||||
+ func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL);
|
||||
+ if (IS_ERR(func))
|
||||
+ return PTR_ERR(func);
|
||||
+ func->irq = mfpwm->irq;
|
||||
+ func->parent = mfpwm;
|
||||
+ func->id = atomic_inc_return(&subdev_id);
|
||||
+ func->base = mfpwm->base;
|
||||
+ func->core = mfpwm->chosen_clk;
|
||||
+ cell.name = name;
|
||||
+ cell.platform_data = func;
|
||||
+ cell.pdata_size = sizeof(*func);
|
||||
+ // cell.ignore_resource_conflicts = true;
|
||||
+ // cell.resources = mfpwm->pdev->resource;
|
||||
+ // cell.num_resources = mfpwm->pdev->num_resources;
|
||||
+
|
||||
+ return devm_mfd_add_devices(&mfpwm->pdev->dev, func->id, &cell, 1, NULL,
|
||||
+ 0, NULL);
|
||||
+}
|
||||
+
|
||||
+static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm)
|
||||
+{
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = mfpwm_register_subdev(mfpwm, "pwm-rockchip-v4");
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-capture");
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_mfpwm_probe(struct platform_device *pdev)
|
||||
+{
|
||||
+ struct device *dev = &pdev->dev;
|
||||
+ struct rockchip_mfpwm *mfpwm;
|
||||
+ char *clk_mux_name;
|
||||
+ const char *mux_p_names[3];
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ mfpwm = devm_kzalloc(&pdev->dev, sizeof(*mfpwm), GFP_KERNEL);
|
||||
+ if (IS_ERR(mfpwm))
|
||||
+ return PTR_ERR(mfpwm);
|
||||
+
|
||||
+ mfpwm->pdev = pdev;
|
||||
+
|
||||
+ spin_lock_init(&mfpwm->state_lock);
|
||||
+
|
||||
+ mfpwm->base = devm_platform_ioremap_resource(pdev, 0);
|
||||
+ if (IS_ERR(mfpwm->base))
|
||||
+ return dev_err_probe(dev, PTR_ERR(mfpwm->base),
|
||||
+ "failed to ioremap address\n");
|
||||
+
|
||||
+ mfpwm->pclk = devm_clk_get_prepared(dev, "pclk");
|
||||
+ if (IS_ERR(mfpwm->pclk))
|
||||
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pclk),
|
||||
+ "couldn't get and prepare 'pclk' clock\n");
|
||||
+
|
||||
+ mfpwm->irq = platform_get_irq(pdev, 0);
|
||||
+ if (mfpwm->irq < 0)
|
||||
+ return dev_err_probe(dev, mfpwm->irq, "couldn't get irq 0\n");
|
||||
+
|
||||
+ mfpwm->pwm_clk = devm_clk_get_prepared(dev, "pwm");
|
||||
+ if (IS_ERR(mfpwm->pwm_clk))
|
||||
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk),
|
||||
+ "couldn't get and prepare 'pwm' clock\n");
|
||||
+
|
||||
+ mfpwm->osc_clk = devm_clk_get_prepared(dev, "osc");
|
||||
+ if (IS_ERR(mfpwm->osc_clk))
|
||||
+ return dev_err_probe(dev, PTR_ERR(mfpwm->osc_clk),
|
||||
+ "couldn't get and prepare 'osc' clock\n");
|
||||
+
|
||||
+ mfpwm->rc_clk = devm_clk_get_prepared(dev, "rc");
|
||||
+ if (IS_ERR(mfpwm->rc_clk))
|
||||
+ return dev_err_probe(dev, PTR_ERR(mfpwm->rc_clk),
|
||||
+ "couldn't get and prepare 'rc' clock\n");
|
||||
+
|
||||
+ clk_mux_name = devm_kasprintf(dev, GFP_KERNEL, "%s_chosen", dev_name(dev));
|
||||
+ if (!clk_mux_name)
|
||||
+ return -ENOMEM;
|
||||
+
|
||||
+ mux_p_names[0] = __clk_get_name(mfpwm->pwm_clk);
|
||||
+ mux_p_names[1] = __clk_get_name(mfpwm->osc_clk);
|
||||
+ mux_p_names[2] = __clk_get_name(mfpwm->rc_clk);
|
||||
+ mfpwm->chosen_clk = clk_register_mux(dev, clk_mux_name, mux_p_names,
|
||||
+ ARRAY_SIZE(mux_p_names),
|
||||
+ CLK_SET_RATE_PARENT,
|
||||
+ mfpwm->base + PWMV4_REG_CLK_CTRL,
|
||||
+ PWMV4_CLK_SRC_SHIFT, PWMV4_CLK_SRC_WIDTH,
|
||||
+ CLK_MUX_HIWORD_MASK, NULL);
|
||||
+ ret = clk_prepare(mfpwm->chosen_clk);
|
||||
+ if (ret) {
|
||||
+ dev_err(dev, "failed to prepare PWM clock mux: %pe\n",
|
||||
+ ERR_PTR(ret));
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ platform_set_drvdata(pdev, mfpwm);
|
||||
+
|
||||
+ ret = mfpwm_register_subdevs(mfpwm);
|
||||
+ if (ret) {
|
||||
+ dev_err(dev, "failed to register sub-devices: %pe\n",
|
||||
+ ERR_PTR(ret));
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static void rockchip_mfpwm_remove(struct platform_device *pdev)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm = to_rockchip_mfpwm(pdev);
|
||||
+ unsigned long flags;
|
||||
+
|
||||
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
|
||||
+
|
||||
+ if (mfpwm->chosen_clk) {
|
||||
+ clk_unprepare(mfpwm->chosen_clk);
|
||||
+ clk_unregister_mux(mfpwm->chosen_clk);
|
||||
+ }
|
||||
+
|
||||
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
|
||||
+}
|
||||
+
|
||||
+static const struct of_device_id rockchip_mfpwm_of_match[] = {
|
||||
+ {
|
||||
+ .compatible = "rockchip,rk3576-pwm",
|
||||
+ },
|
||||
+ { /* sentinel */ }
|
||||
+};
|
||||
+MODULE_DEVICE_TABLE(of, rockchip_mfpwm_of_match);
|
||||
+
|
||||
+static struct platform_driver rockchip_mfpwm_driver = {
|
||||
+ .driver = {
|
||||
+ .name = KBUILD_MODNAME,
|
||||
+ .of_match_table = rockchip_mfpwm_of_match,
|
||||
+ },
|
||||
+ .probe = rockchip_mfpwm_probe,
|
||||
+ .remove = rockchip_mfpwm_remove,
|
||||
+};
|
||||
+module_platform_driver(rockchip_mfpwm_driver);
|
||||
+
|
||||
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
|
||||
+MODULE_DESCRIPTION("Rockchip MFPWM Driver");
|
||||
+MODULE_LICENSE("GPL");
|
||||
--- /dev/null
|
||||
+++ b/include/linux/mfd/rockchip-mfpwm.h
|
||||
@@ -0,0 +1,454 @@
|
||||
+/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
+ *
|
||||
+ * Common header file for all the Rockchip Multi-function PWM controller
|
||||
+ * drivers that are spread across subsystems.
|
||||
+ *
|
||||
+ * Authors:
|
||||
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ */
|
||||
+
|
||||
+#ifndef __SOC_ROCKCHIP_MFPWM_H__
|
||||
+#define __SOC_ROCKCHIP_MFPWM_H__
|
||||
+
|
||||
+#include <linux/bits.h>
|
||||
+#include <linux/clk.h>
|
||||
+#include <linux/hw_bitfield.h>
|
||||
+#include <linux/io.h>
|
||||
+#include <linux/spinlock.h>
|
||||
+
|
||||
+struct rockchip_mfpwm;
|
||||
+
|
||||
+/**
|
||||
+ * struct rockchip_mfpwm_func - struct representing a single function driver
|
||||
+ *
|
||||
+ * @id: unique id for this function driver instance
|
||||
+ * @base: pointer to start of MMIO registers
|
||||
+ * @parent: a pointer to the parent mfpwm struct
|
||||
+ * @irq: the shared IRQ gotten from the parent mfpwm device
|
||||
+ * @core: a pointer to the clk mux that drives this channel's PWM
|
||||
+ */
|
||||
+struct rockchip_mfpwm_func {
|
||||
+ int id;
|
||||
+ void __iomem *base;
|
||||
+ struct rockchip_mfpwm *parent;
|
||||
+ int irq;
|
||||
+ struct clk *core;
|
||||
+};
|
||||
+
|
||||
+/*
|
||||
+ * PWMV4 Register Definitions
|
||||
+ * --------------------------
|
||||
+ *
|
||||
+ * Attributes:
|
||||
+ * RW - Read-Write
|
||||
+ * RO - Read-Only
|
||||
+ * WO - Write-Only
|
||||
+ * W1T - Write high, Self-clearing
|
||||
+ * W1C - Write high to clear interrupt
|
||||
+ *
|
||||
+ * Bit ranges to be understood with Verilog-like semantics,
|
||||
+ * e.g. [03:00] is 4 bits: 0, 1, 2 and 3.
|
||||
+ *
|
||||
+ * All registers must be accessed with 32-bit width accesses only
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_VERSION 0x000
|
||||
+/*
|
||||
+ * VERSION Register Description
|
||||
+ * [31:24] RO | Hardware Major Version
|
||||
+ * [23:16] RO | Hardware Minor Version
|
||||
+ * [15:15] RO | Reserved
|
||||
+ * [14:14] RO | Hardware supports biphasic counters
|
||||
+ * [13:13] RO | Hardware supports filters
|
||||
+ * [12:12] RO | Hardware supports waveform generation
|
||||
+ * [11:11] RO | Hardware supports counter
|
||||
+ * [10:10] RO | Hardware supports frequency metering
|
||||
+ * [09:09] RO | Hardware supports power key functionality
|
||||
+ * [08:08] RO | Hardware supports infrared transmissions
|
||||
+ * [07:04] RO | Channel index of this instance
|
||||
+ * [03:00] RO | Number of channels the base instance supports
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_ENABLE 0x004
|
||||
+/*
|
||||
+ * ENABLE Register Description
|
||||
+ * [31:16] WO | Write Enable Mask for the lower half of the register
|
||||
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
|
||||
+ * the same write operation
|
||||
+ * [15:06] RO | Reserved
|
||||
+ * [05:05] RW | PWM Channel Counter Read Enable, 1 = enabled
|
||||
+ */
|
||||
+#define PWMV4_CHN_CNT_RD_EN(v) FIELD_PREP_WM16(BIT(5), (v))
|
||||
+/*
|
||||
+ * [04:04] W1T | PWM Globally Joined Control Enable
|
||||
+ * 1 = this PWM channel will be enabled by a global pwm enable
|
||||
+ * bit instead of the PWM Enable bit.
|
||||
+ */
|
||||
+#define PWMV4_GLOBAL_CTRL_EN(v) FIELD_PREP_WM16(BIT(4), (v))
|
||||
+/*
|
||||
+ * [03:03] RW | Force Clock Enable
|
||||
+ * 0 = disabled, if the PWM channel is inactive then so is the
|
||||
+ * clock prescale module
|
||||
+ */
|
||||
+#define PWMV4_FORCE_CLK_EN(v) FIELD_PREP_WM16(BIT(3), (v))
|
||||
+/*
|
||||
+ * [02:02] W1T | PWM Control Update Enable
|
||||
+ * 1 = enabled, commits modifications of _CTRL, _PERIOD, _DUTY and
|
||||
+ * _OFFSET registers once 1 is written to it
|
||||
+ */
|
||||
+#define PWMV4_CTRL_UPDATE_EN FIELD_PREP_WM16_CONST(BIT(2), 1)
|
||||
+/*
|
||||
+ * [01:01] RW | PWM Enable, 1 = enabled
|
||||
+ * If in one-shot mode, clears after end of operation
|
||||
+ */
|
||||
+#define PWMV4_EN_MASK BIT(1)
|
||||
+#define PWMV4_EN(v) FIELD_PREP_WM16(PWMV4_EN_MASK, \
|
||||
+ ((v) ? 1 : 0))
|
||||
+/*
|
||||
+ * [00:00] RW | PWM Clock Enable, 1 = enabled
|
||||
+ * If in one-shot mode, clears after end of operation
|
||||
+ */
|
||||
+#define PWMV4_CLK_EN_MASK BIT(0)
|
||||
+#define PWMV4_CLK_EN(v) FIELD_PREP_WM16(PWMV4_CLK_EN_MASK, \
|
||||
+ ((v) ? 1 : 0))
|
||||
+#define PWMV4_EN_BOTH_MASK (PWMV4_EN_MASK | PWMV4_CLK_EN_MASK)
|
||||
+static inline __pure bool rockchip_pwm_v4_is_enabled(unsigned int val)
|
||||
+{
|
||||
+ return (val & PWMV4_EN_BOTH_MASK);
|
||||
+}
|
||||
+
|
||||
+#define PWMV4_REG_CLK_CTRL 0x008
|
||||
+/*
|
||||
+ * CLK_CTRL Register Description
|
||||
+ * [31:16] WO | Write Enable Mask for the lower half of the register
|
||||
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
|
||||
+ * the same write operation
|
||||
+ * [15:15] RW | Clock Global Selection
|
||||
+ * 0 = current channel scale clock
|
||||
+ * 1 = global channel scale clock
|
||||
+ */
|
||||
+#define PWMV4_CLK_GLOBAL(v) FIELD_PREP_WM16(BIT(15), (v))
|
||||
+/*
|
||||
+ * [14:13] RW | Clock Source Selection
|
||||
+ * 0 = Clock from PLL, frequency can be configured
|
||||
+ * 1 = Clock from crystal oscillator, frequency is fixed
|
||||
+ * 2 = Clock from RC oscillator, frequency is fixed
|
||||
+ * 3 = Reserved
|
||||
+ * NOTE: The purpose for this clock-mux-outside-CRU construct is
|
||||
+ * to let the SoC go into a sleep state with the PWM
|
||||
+ * hardware still having a clock signal for IR input, which
|
||||
+ * can then wake up the SoC.
|
||||
+ */
|
||||
+#define PWMV4_CLK_SRC_PLL 0x0U
|
||||
+#define PWMV4_CLK_SRC_CRYSTAL 0x1U
|
||||
+#define PWMV4_CLK_SRC_RC 0x2U
|
||||
+#define PWMV4_CLK_SRC_SHIFT 13
|
||||
+#define PWMV4_CLK_SRC_WIDTH 2
|
||||
+/*
|
||||
+ * [12:04] RW | Scale Factor to apply to pre-scaled clock
|
||||
+ * 1 <= v <= 256, v means clock divided by 2*v
|
||||
+ */
|
||||
+#define PWMV4_CLK_SCALE_F(v) FIELD_PREP_WM16(GENMASK(12, 4), (v))
|
||||
+/*
|
||||
+ * [03:03] RO | Reserved
|
||||
+ * [02:00] RW | Prescale Factor
|
||||
+ * v here means the input clock is divided by pow(2, v)
|
||||
+ */
|
||||
+#define PWMV4_CLK_PRESCALE_F(v) FIELD_PREP_WM16(GENMASK(2, 0), (v))
|
||||
+
|
||||
+#define PWMV4_REG_CTRL 0x00C
|
||||
+/*
|
||||
+ * CTRL Register Description
|
||||
+ * [31:16] WO | Write Enable Mask for the lower half of the register
|
||||
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
|
||||
+ * the same write operation
|
||||
+ * [15:09] RO | Reserved
|
||||
+ * [08:06] RW | PWM Input Channel Selection
|
||||
+ * By default, the channel selects its own input, but writing v
|
||||
+ * here selects PWM input from channel v instead.
|
||||
+ */
|
||||
+#define PWMV4_CTRL_IN_SEL(v) FIELD_PREP_WM16(GENMASK(8, 6), (v))
|
||||
+/* [05:05] RW | Aligned Mode, 0 = Valid, 1 = Invalid */
|
||||
+#define PWMV4_CTRL_UNALIGNED(v) FIELD_PREP_WM16(BIT(5), (v))
|
||||
+/* [04:04] RW | Output Mode, 0 = Left Aligned, 1 = Centre Aligned */
|
||||
+#define PWMV4_LEFT_ALIGNED 0x0U
|
||||
+#define PWMV4_CENTRE_ALIGNED 0x1U
|
||||
+#define PWMV4_CTRL_OUT_MODE(v) FIELD_PREP_WM16(BIT(4), (v))
|
||||
+/*
|
||||
+ * [03:03] RW | Inactive Polarity for when the channel is either disabled or
|
||||
+ * has completed outputting the entire waveform in one-shot mode.
|
||||
+ * 0 = Negative, 1 = Positive
|
||||
+ */
|
||||
+#define PWMV4_POLARITY_N 0x0U
|
||||
+#define PWMV4_POLARITY_P 0x1U
|
||||
+#define PWMV4_INACTIVE_POL(v) FIELD_PREP_WM16(BIT(3), (v))
|
||||
+/*
|
||||
+ * [02:02] RW | Duty Cycle Polarity to use at the start of the waveform.
|
||||
+ * 0 = Negative, 1 = Positive
|
||||
+ */
|
||||
+#define PWMV4_DUTY_POL_SHIFT 2
|
||||
+#define PWMV4_DUTY_POL_MASK BIT(PWMV4_DUTY_POL_SHIFT)
|
||||
+#define PWMV4_DUTY_POL(v) FIELD_PREP_WM16(PWMV4_DUTY_POL_MASK, \
|
||||
+ (v))
|
||||
+/*
|
||||
+ * [01:00] RW | PWM Mode
|
||||
+ * 0 = One-shot mode, PWM generates waveform RPT times
|
||||
+ * 1 = Continuous mode
|
||||
+ * 2 = Capture mode, PWM measures cycles of input waveform
|
||||
+ * 3 = Reserved
|
||||
+ */
|
||||
+#define PWMV4_MODE_ONESHOT 0x0U
|
||||
+#define PWMV4_MODE_CONT 0x1U
|
||||
+#define PWMV4_MODE_CAPTURE 0x2U
|
||||
+#define PWMV4_MODE_MASK GENMASK(1, 0)
|
||||
+#define PWMV4_MODE(v) FIELD_PREP_WM16(PWMV4_MODE_MASK, (v))
|
||||
+#define PWMV4_CTRL_COM_FLAGS (PWMV4_INACTIVE_POL(PWMV4_POLARITY_N) | \
|
||||
+ PWMV4_DUTY_POL(PWMV4_POLARITY_P) | \
|
||||
+ PWMV4_CTRL_OUT_MODE(PWMV4_LEFT_ALIGNED) | \
|
||||
+ PWMV4_CTRL_UNALIGNED(true))
|
||||
+#define PWMV4_CTRL_CONT_FLAGS (PWMV4_MODE(PWMV4_MODE_CONT) | \
|
||||
+ PWMV4_CTRL_COM_FLAGS)
|
||||
+#define PWMV4_CTRL_CAP_FLAGS (PWMV4_MODE(PWMV4_MODE_CAPTURE) | \
|
||||
+ PWMV4_CTRL_COM_FLAGS)
|
||||
+
|
||||
+#define PWMV4_REG_PERIOD 0x010
|
||||
+/*
|
||||
+ * PERIOD Register Description
|
||||
+ * [31:00] RW | Period of the output waveform
|
||||
+ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_DUTY 0x014
|
||||
+/*
|
||||
+ * DUTY Register Description
|
||||
+ * [31:00] RW | Duty cycle of the output waveform
|
||||
+ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_OFFSET 0x018
|
||||
+/*
|
||||
+ * OFFSET Register Description
|
||||
+ * [31:00] RW | Offset of the output waveform, based on the PWM clock
|
||||
+ * Constraints: 0 <= v <= (PERIOD - DUTY)
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_RPT 0x01C
|
||||
+/*
|
||||
+ * RPT Register Description
|
||||
+ * [31:16] RW | Second dimensional of the effective number of waveform
|
||||
+ * repetitions. Increases by one every first dimensional times.
|
||||
+ * Value `n` means `n + 1` repetitions. The final number of
|
||||
+ * repetitions of the waveform in one-shot mode is:
|
||||
+ * `(first_dimensional + 1) * (second_dimensional + 1)`
|
||||
+ * [15:00] RW | First dimensional of the effective number of waveform
|
||||
+ * repetitions. Value `n` means `n + 1` repetitions.
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_FILTER_CTRL 0x020
|
||||
+/*
|
||||
+ * FILTER_CTRL Register Description
|
||||
+ * [31:16] WO | Write Enable Mask for the lower half of the register
|
||||
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
|
||||
+ * the same write operation
|
||||
+ * [15:10] RO | Reserved
|
||||
+ * [09:04] RW | Filter window number
|
||||
+ * [03:01] RO | Reserved
|
||||
+ * [00:00] RW | Filter Enable, 0 = disabled, 1 = enabled
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_CNT 0x024
|
||||
+/*
|
||||
+ * CNT Register Description
|
||||
+ * [31:00] RO | Current value of the PWM Channel 0 counter in pwm clock cycles,
|
||||
+ * 0 <= v <= 2^32-1
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_ENABLE_DELAY 0x028
|
||||
+/*
|
||||
+ * ENABLE_DELAY Register Description
|
||||
+ * [31:16] RO | Reserved
|
||||
+ * [15:00] RW | PWM enable delay, in an unknown unit but probably cycles
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_HPC 0x02C
|
||||
+/*
|
||||
+ * HPC Register Description
|
||||
+ * [31:00] RW | Number of effective high polarity cycles of the input waveform
|
||||
+ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_LPC 0x030
|
||||
+/*
|
||||
+ * LPC Register Description
|
||||
+ * [31:00] RW | Number of effective low polarity cycles of the input waveform
|
||||
+ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_BIPHASIC_CNT_CTRL0 0x040
|
||||
+/*
|
||||
+ * BIPHASIC_CNT_CTRL0 Register Description
|
||||
+ * [31:16] WO | Write Enable Mask for the lower half of the register
|
||||
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
|
||||
+ * the same write operation
|
||||
+ * [15:10] RO | Reserved
|
||||
+ * [09:09] RW | Biphasic Counter Phase Edge Selection for mode 0,
|
||||
+ * 0 = rising edge (posedge), 1 = falling edge (negedge)
|
||||
+ * [08:08] RW | Biphasic Counter Clock force enable, 1 = force enable
|
||||
+ * [07:07] W1T | Synchronous Enable
|
||||
+ * [06:06] W1T | Mode Switch
|
||||
+ * 0 = Normal Mode, 1 = Switch timer clock and measured clock
|
||||
+ * Constraints: "Biphasic Counter Mode" must be 0 if this is 1
|
||||
+ * [05:03] RW | Biphasic Counter Mode
|
||||
+ * 0x0 = Mode 0, 0x1 = Mode 1, 0x2 = Mode 2, 0x3 = Mode 3,
|
||||
+ * 0x4 = Mode 4, 0x5 = Reserved
|
||||
+ * [02:02] RW | Biphasic Counter Clock Selection
|
||||
+ * 0 = clock is from PLL and frequency can be configured
|
||||
+ * 1 = clock is from crystal oscillator and frequency is fixed
|
||||
+ * [01:01] RW | Biphasic Counter Continuous Mode
|
||||
+ * [00:00] W1T | Biphasic Counter Enable
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_BIPHASIC_CNT_CTRL1 0x044
|
||||
+/*
|
||||
+ * BIPHASIC_CNT_CTRL1 Register Description
|
||||
+ * [31:16] WO | Write Enable Mask for the lower half of the register
|
||||
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
|
||||
+ * the same write operation
|
||||
+ * [15:11] RO | Reserved
|
||||
+ * [10:04] RW | Biphasic Counter Filter Window Number
|
||||
+ * [03:01] RO | Reserved
|
||||
+ * [00:00] RW | Biphasic Counter Filter Enable
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_BIPHASIC_CNT_TIMER 0x048
|
||||
+/*
|
||||
+ * BIPHASIC_CNT_TIMER Register Description
|
||||
+ * [31:00] RW | Biphasic Counter Timer Value, in number of biphasic counter
|
||||
+ * timer clock cycles
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_BIPHASIC_CNT_RES 0x04C
|
||||
+/*
|
||||
+ * BIPHASIC_CNT_RES Register Description
|
||||
+ * [31:00] RO | Biphasic Counter Result Value
|
||||
+ * Constraints: Can only be read after INTSTS[9] is asserted
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_BIPHASIC_CNT_RES_S 0x050
|
||||
+/*
|
||||
+ * BIPHASIC_CNT_RES_S Register Description
|
||||
+ * [31:00] RO | Biphasic Counter Result Value with synchronised processing
|
||||
+ * Can be read in real-time if BIPHASIC_CNT_CTRL0[7] was set to 1
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_INTSTS 0x070
|
||||
+/*
|
||||
+ * INTSTS Register Description
|
||||
+ * [31:10] RO | Reserved
|
||||
+ * [09:09] W1C | Biphasic Counter Interrupt Status, 1 = interrupt asserted
|
||||
+ * [08:08] W1C | Waveform Middle Interrupt Status, 1 = interrupt asserted
|
||||
+ * [07:07] W1C | Waveform Max Interrupt Status, 1 = interrupt asserted
|
||||
+ * [06:06] W1C | IR Transmission End Interrupt Status, 1 = interrupt asserted
|
||||
+ * [05:05] W1C | Power Key Match Interrupt Status, 1 = interrupt asserted
|
||||
+ * [04:04] W1C | Frequency Meter Interrupt Status, 1 = interrupt asserted
|
||||
+ * [03:03] W1C | Reload Interrupt Status, 1 = interrupt asserted
|
||||
+ * [02:02] W1C | Oneshot End Interrupt Status, 1 = interrupt asserted
|
||||
+ * [01:01] W1C | HPC Capture Interrupt Status, 1 = interrupt asserted
|
||||
+ * [00:00] W1C | LPC Capture Interrupt Status, 1 = interrupt asserted
|
||||
+ */
|
||||
+#define PWMV4_INT_LPC BIT(0)
|
||||
+#define PWMV4_INT_HPC BIT(1)
|
||||
+#define PWMV4_INT_LPC_W(v) FIELD_PREP_WM16(PWMV4_INT_LPC, \
|
||||
+ ((v) ? 1 : 0))
|
||||
+#define PWMV4_INT_HPC_W(v) FIELD_PREP_WM16(PWMV4_INT_HPC, \
|
||||
+ ((v) ? 1 : 0))
|
||||
+
|
||||
+#define PWMV4_REG_INT_EN 0x074
|
||||
+/*
|
||||
+ * INT_EN Register Description
|
||||
+ * [31:16] WO | Write Enable Mask for the lower half of the register
|
||||
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
|
||||
+ * the same write operation
|
||||
+ * [15:10] RO | Reserved
|
||||
+ * [09:09] RW | Biphasic Counter Interrupt Enable, 1 = enabled
|
||||
+ * [08:08] W1C | Waveform Middle Interrupt Enable, 1 = enabled
|
||||
+ * [07:07] W1C | Waveform Max Interrupt Enable, 1 = enabled
|
||||
+ * [06:06] W1C | IR Transmission End Interrupt Enable, 1 = enabled
|
||||
+ * [05:05] W1C | Power Key Match Interrupt Enable, 1 = enabled
|
||||
+ * [04:04] W1C | Frequency Meter Interrupt Enable, 1 = enabled
|
||||
+ * [03:03] W1C | Reload Interrupt Enable, 1 = enabled
|
||||
+ * [02:02] W1C | Oneshot End Interrupt Enable, 1 = enabled
|
||||
+ * [01:01] W1C | HPC Capture Interrupt Enable, 1 = enabled
|
||||
+ * [00:00] W1C | LPC Capture Interrupt Enable, 1 = enabled
|
||||
+ */
|
||||
+
|
||||
+#define PWMV4_REG_INT_MASK 0x078
|
||||
+/*
|
||||
+ * INT_MASK Register Description
|
||||
+ * [31:16] WO | Write Enable Mask for the lower half of the register
|
||||
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
|
||||
+ * the same write operation
|
||||
+ * [15:10] RO | Reserved
|
||||
+ * [09:09] RW | Biphasic Counter Interrupt Masked, 1 = masked
|
||||
+ * [08:08] W1C | Waveform Middle Interrupt Masked, 1 = masked
|
||||
+ * [07:07] W1C | Waveform Max Interrupt Masked, 1 = masked
|
||||
+ * [06:06] W1C | IR Transmission End Interrupt Masked, 1 = masked
|
||||
+ * [05:05] W1C | Power Key Match Interrupt Masked, 1 = masked
|
||||
+ * [04:04] W1C | Frequency Meter Interrupt Masked, 1 = masked
|
||||
+ * [03:03] W1C | Reload Interrupt Masked, 1 = masked
|
||||
+ * [02:02] W1C | Oneshot End Interrupt Masked, 1 = masked
|
||||
+ * [01:01] W1C | HPC Capture Interrupt Masked, 1 = masked
|
||||
+ * [00:00] W1C | LPC Capture Interrupt Masked, 1 = masked
|
||||
+ */
|
||||
+
|
||||
+static inline u32 mfpwm_reg_read(void __iomem *base, u32 reg)
|
||||
+{
|
||||
+ return readl(base + reg);
|
||||
+}
|
||||
+
|
||||
+static inline void mfpwm_reg_write(void __iomem *base, u32 reg, u32 val)
|
||||
+{
|
||||
+ writel(val, base + reg);
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_acquire - try becoming the active mfpwm function device
|
||||
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
|
||||
+ *
|
||||
+ * mfpwm device "function" drivers must call this function before doing anything
|
||||
+ * that either modifies or relies on the parent device's state, such as clocks,
|
||||
+ * enabling/disabling outputs, modifying shared regs etc.
|
||||
+ *
|
||||
+ * The return statues should always be checked.
|
||||
+ *
|
||||
+ * All mfpwm_acquire() calls must be balanced with corresponding mfpwm_release()
|
||||
+ * calls once the device is no longer making changes that affect other devices,
|
||||
+ * or stops producing user-visible effects that depend on the current device
|
||||
+ * state being kept as-is. (e.g. after the PWM output signal is stopped)
|
||||
+ *
|
||||
+ * The same device function may mfpwm_acquire() multiple times while it already
|
||||
+ * is active, i.e. it is re-entrant, though it needs to balance this with the
|
||||
+ * same number of mfpwm_release() calls.
|
||||
+ *
|
||||
+ * Context: This function does not sleep.
|
||||
+ *
|
||||
+ * Return:
|
||||
+ * * %0 - success
|
||||
+ * * %-EBUSY - a different device function is active
|
||||
+ * * %-EOVERFLOW - the acquire counter is at its maximum
|
||||
+ */
|
||||
+extern int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf);
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_release - drop usage of active mfpwm device function by 1
|
||||
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
|
||||
+ *
|
||||
+ * This is the balancing call to mfpwm_acquire(). If no users of the device
|
||||
+ * function remain, set the mfpwm device to have no active device function,
|
||||
+ * allowing other device functions to claim it.
|
||||
+ */
|
||||
+extern void mfpwm_release(const struct rockchip_mfpwm_func *pwmf);
|
||||
+
|
||||
+#endif /* __SOC_ROCKCHIP_MFPWM_H__ */
|
||||
@ -0,0 +1,416 @@
|
||||
From git@z Thu Jan 1 00:00:00 1970
|
||||
Subject: [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver
|
||||
From: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
Date: Mon, 27 Oct 2025 18:11:58 +0100
|
||||
Message-Id: <20251027-rk3576-pwm-v3-3-654a5cb1e3f8@collabora.com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
The Rockchip RK3576 brings with it a new PWM IP, in downstream code
|
||||
referred to as "v4". This new IP is different enough from the previous
|
||||
Rockchip IP that I felt it necessary to add a new driver for it, instead
|
||||
of shoehorning it in the old one.
|
||||
|
||||
Add this new driver, based on the PWM core's waveform APIs. Its platform
|
||||
device is registered by the parent mfpwm driver, from which it also
|
||||
receives a little platform data struct, so that mfpwm can guarantee that
|
||||
all the platform device drivers spread across different subsystems for
|
||||
this specific hardware IP do not interfere with each other.
|
||||
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
MAINTAINERS | 1 +
|
||||
drivers/pwm/Kconfig | 13 ++
|
||||
drivers/pwm/Makefile | 1 +
|
||||
drivers/pwm/pwm-rockchip-v4.c | 353 ++++++++++++++++++++++++++++++++++++++++++
|
||||
4 files changed, 368 insertions(+)
|
||||
|
||||
--- a/drivers/pwm/Kconfig
|
||||
+++ b/drivers/pwm/Kconfig
|
||||
@@ -551,6 +551,19 @@ config PWM_RZ_MTU3
|
||||
To compile this driver as a module, choose M here: the module
|
||||
will be called pwm-rz-mtu3.
|
||||
|
||||
+config PWM_ROCKCHIP_V4
|
||||
+ tristate "Rockchip PWM v4 support"
|
||||
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
|
||||
+ depends on HAS_IOMEM
|
||||
+ depends on MFD_ROCKCHIP_MFPWM
|
||||
+ help
|
||||
+ Generic PWM framework driver for the PWM controller found on
|
||||
+ later Rockchip SoCs such as the RK3576.
|
||||
+
|
||||
+ Uses the Rockchip Multi-function PWM controller driver infrastructure
|
||||
+ to guarantee fearlessly concurrent operation with other functions of
|
||||
+ the same device implemented by drivers in other subsystems.
|
||||
+
|
||||
config PWM_SAMSUNG
|
||||
tristate "Samsung PWM support"
|
||||
depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST
|
||||
--- a/drivers/pwm/Makefile
|
||||
+++ b/drivers/pwm/Makefile
|
||||
@@ -49,6 +49,7 @@ obj-$(CONFIG_PWM_RASPBERRYPI_POE) += pwm
|
||||
obj-$(CONFIG_PWM_RCAR) += pwm-rcar.o
|
||||
obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
|
||||
obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
|
||||
+obj-$(CONFIG_PWM_ROCKCHIP_V4) += pwm-rockchip-v4.o
|
||||
obj-$(CONFIG_PWM_RZ_MTU3) += pwm-rz-mtu3.o
|
||||
obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
|
||||
obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
|
||||
--- /dev/null
|
||||
+++ b/drivers/pwm/pwm-rockchip-v4.c
|
||||
@@ -0,0 +1,353 @@
|
||||
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
+ *
|
||||
+ * A Pulse-Width-Modulation (PWM) generator driver for the generators found in
|
||||
+ * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". Uses
|
||||
+ * the MFPWM infrastructure to guarantee exclusive use over the device without
|
||||
+ * other functions of the device from different drivers interfering with its
|
||||
+ * operation while it's active.
|
||||
+ *
|
||||
+ * Technical Reference Manual: Chapter 31 of the RK3506 TRM Part 1, a SoC which
|
||||
+ * uses the same PWM hardware and has a publicly available TRM.
|
||||
+ * https://opensource.rock-chips.com/images/3/36/Rockchip_RK3506_TRM_Part_1_V1.2-20250811.pdf
|
||||
+ *
|
||||
+ * Authors:
|
||||
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ *
|
||||
+ * Limitations:
|
||||
+ * - When the output is disabled, it will end abruptly without letting the
|
||||
+ * current period complete.
|
||||
+ * TODO: This can be fixed in the driver in the future by having the enable-
|
||||
+ * to-disable transition switch to oneshot mode with one repetition,
|
||||
+ * and then disable the pwmclk and release mfpwm when the oneshot
|
||||
+ * complete interrupt fires.
|
||||
+ * - When the output is disabled, the pin will remain driven to whatever state
|
||||
+ * it last had.
|
||||
+ * - Adjustments to the duty cycle will only take effect during the next period.
|
||||
+ * - Adjustments to the period length will only take effect during the next
|
||||
+ * period.
|
||||
+ * - offset should be between 0 and (period - duty)
|
||||
+ */
|
||||
+
|
||||
+#include <linux/math64.h>
|
||||
+#include <linux/mfd/rockchip-mfpwm.h>
|
||||
+#include <linux/platform_device.h>
|
||||
+#include <linux/pwm.h>
|
||||
+
|
||||
+struct rockchip_pwm_v4 {
|
||||
+ struct rockchip_mfpwm_func *pwmf;
|
||||
+ struct pwm_chip chip;
|
||||
+};
|
||||
+
|
||||
+struct rockchip_pwm_v4_wf {
|
||||
+ u32 period;
|
||||
+ u32 duty;
|
||||
+ u32 offset;
|
||||
+ u8 enable;
|
||||
+};
|
||||
+
|
||||
+static inline struct rockchip_pwm_v4 *to_rockchip_pwm_v4(struct pwm_chip *chip)
|
||||
+{
|
||||
+ return pwmchip_get_drvdata(chip);
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * rockchip_pwm_v4_round_single - convert a PWM parameter to hardware
|
||||
+ * @rate: clock rate of the PWM clock, as per clk_get_rate
|
||||
+ * @in_val: parameter in nanoseconds to convert
|
||||
+ *
|
||||
+ * Returns the rounded value, saturating at U32_MAX if too large
|
||||
+ */
|
||||
+static u32 rockchip_pwm_v4_round_single(unsigned long rate, u64 in_val)
|
||||
+{
|
||||
+ u64 tmp;
|
||||
+
|
||||
+ tmp = mul_u64_u64_div_u64(rate, in_val, NSEC_PER_SEC);
|
||||
+ if (tmp > U32_MAX)
|
||||
+ tmp = U32_MAX;
|
||||
+
|
||||
+ return (u32)tmp;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * rockchip_pwm_v4_round_params - convert PWM parameters to hardware
|
||||
+ * @rate: PWM clock rate to do the calculations at
|
||||
+ * @duty: PWM duty cycle in nanoseconds
|
||||
+ * @period: PWM period in nanoseconds
|
||||
+ * @offset: PWM offset in nanoseconds
|
||||
+ * @out_duty: pointer to where the rounded duty value should be stored
|
||||
+ * @out_period: pointer to where the rounded period value should be stored
|
||||
+ * @out_offset: pointer to where the rounded offset value should be stored
|
||||
+ *
|
||||
+ * Convert nanosecond-based duty/period/offset parameters to the PWM hardware's
|
||||
+ * native rounded representation in number of cycles at clock rate @rate. Should
|
||||
+ * any of the input parameters be out of range for the hardware, the
|
||||
+ * corresponding output parameter is the maximum permissible value for said
|
||||
+ * parameter with considerations to the others.
|
||||
+ */
|
||||
+static void rockchip_pwm_v4_round_params(unsigned long rate, u64 duty,
|
||||
+ u64 period, u64 offset, u32 *out_duty,
|
||||
+ u32 *out_period, u32 *out_offset)
|
||||
+{
|
||||
+ *out_period = rockchip_pwm_v4_round_single(rate, period);
|
||||
+
|
||||
+ *out_duty = rockchip_pwm_v4_round_single(rate, duty);
|
||||
+
|
||||
+ /* As per TRM, PWM_OFFSET: "The value ranges from 0 to (period-duty)" */
|
||||
+ *out_offset = rockchip_pwm_v4_round_single(rate, offset);
|
||||
+ if (*out_offset > (*out_period - *out_duty))
|
||||
+ *out_offset = *out_period - *out_duty;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_round_wf_tohw(struct pwm_chip *chip,
|
||||
+ struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf,
|
||||
+ void *_wfhw)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ unsigned long rate;
|
||||
+
|
||||
+ rate = clk_get_rate(pc->pwmf->core);
|
||||
+
|
||||
+ rockchip_pwm_v4_round_params(rate, wf->duty_length_ns,
|
||||
+ wf->period_length_ns, wf->duty_offset_ns,
|
||||
+ &wfhw->duty, &wfhw->period, &wfhw->offset);
|
||||
+
|
||||
+ if (wf->period_length_ns > 0)
|
||||
+ wfhw->enable = PWMV4_EN_BOTH_MASK;
|
||||
+ else
|
||||
+ wfhw->enable = 0;
|
||||
+
|
||||
+ dev_dbg(&chip->dev,
|
||||
+ "tohw: duty %llu -> %u, period %llu -> %u, offset %llu -> %u, rate %lu\n",
|
||||
+ wf->duty_length_ns, wfhw->duty, wf->period_length_ns,
|
||||
+ wfhw->period, wf->duty_offset_ns, wfhw->offset, rate);
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_round_wf_fromhw(struct pwm_chip *chip,
|
||||
+ struct pwm_device *pwm,
|
||||
+ const void *_wfhw,
|
||||
+ struct pwm_waveform *wf)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ unsigned long rate;
|
||||
+
|
||||
+ rate = clk_get_rate(pc->pwmf->core);
|
||||
+
|
||||
+ if (rockchip_pwm_v4_is_enabled(wfhw->enable)) {
|
||||
+ if (!rate)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ wf->period_length_ns = (u64)wfhw->period * NSEC_PER_SEC / rate;
|
||||
+ wf->duty_length_ns = (u64)wfhw->duty * NSEC_PER_SEC / rate;
|
||||
+ wf->duty_offset_ns = (u64)wfhw->offset * NSEC_PER_SEC / rate;
|
||||
+ } else {
|
||||
+ wf->period_length_ns = 0;
|
||||
+ wf->duty_length_ns = 0;
|
||||
+ wf->duty_offset_ns = 0;
|
||||
+ }
|
||||
+
|
||||
+ dev_dbg(&chip->dev,
|
||||
+ "fromhw: duty %u -> %llu, period %u -> %llu, offset %u -> %llu, rate %lu\n",
|
||||
+ wfhw->duty, wf->duty_length_ns, wfhw->period,
|
||||
+ wf->period_length_ns, wfhw->offset, wf->duty_offset_ns, rate);
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ void *_wfhw)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ wfhw->period = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_PERIOD);
|
||||
+ wfhw->duty = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_DUTY);
|
||||
+ wfhw->offset = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_OFFSET);
|
||||
+ wfhw->enable = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_ENABLE) & PWMV4_EN_BOTH_MASK;
|
||||
+
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_write_wf(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const void *_wfhw)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ bool was_enabled = false;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ was_enabled = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
|
||||
+ PWMV4_REG_ENABLE));
|
||||
+
|
||||
+ /*
|
||||
+ * "But Nicolas", you ask with valid concerns, "why would you enable the
|
||||
+ * PWM before setting all the parameter registers?"
|
||||
+ *
|
||||
+ * Excellent question, Mr. Reader M. Strawman! The RK3576 TRM Part 1
|
||||
+ * Section 34.6.3 specifies that this is the intended order of writes.
|
||||
+ * Doing the PWM_EN and PWM_CLK_EN writes after the params but before
|
||||
+ * the CTRL_UPDATE_EN, or even after the CTRL_UPDATE_EN, results in
|
||||
+ * erratic behaviour where repeated turning on and off of the PWM may
|
||||
+ * not turn it off under all circumstances. This is also why we don't
|
||||
+ * use relaxed writes; it's not worth the footgun.
|
||||
+ */
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ FIELD_PREP_WM16(PWMV4_EN_BOTH_MASK, wfhw->enable));
|
||||
+
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_PERIOD, wfhw->period);
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_DUTY, wfhw->duty);
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_OFFSET, wfhw->offset);
|
||||
+
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL, PWMV4_CTRL_CONT_FLAGS);
|
||||
+
|
||||
+ /* Commit new configuration to hardware output. */
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_CTRL_UPDATE_EN);
|
||||
+
|
||||
+ if (rockchip_pwm_v4_is_enabled(wfhw->enable)) {
|
||||
+ if (!was_enabled) {
|
||||
+ dev_dbg(&chip->dev, "Enabling PWM output\n");
|
||||
+ ret = clk_enable(pc->pwmf->core);
|
||||
+ if (ret)
|
||||
+ goto err_mfpwm_release;
|
||||
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
|
||||
+ if (ret) {
|
||||
+ clk_disable(pc->pwmf->core);
|
||||
+ goto err_mfpwm_release;
|
||||
+ }
|
||||
+
|
||||
+ /*
|
||||
+ * Output should be on now, acquire device to guarantee
|
||||
+ * exclusion with other device functions while it's on.
|
||||
+ */
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ goto err_mfpwm_release;
|
||||
+ }
|
||||
+ } else if (was_enabled) {
|
||||
+ dev_dbg(&chip->dev, "Disabling PWM output\n");
|
||||
+ clk_rate_exclusive_put(pc->pwmf->core);
|
||||
+ clk_disable(pc->pwmf->core);
|
||||
+ /* Output is off now, extra release to balance extra acquire */
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ }
|
||||
+
|
||||
+err_mfpwm_release:
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static const struct pwm_ops rockchip_pwm_v4_ops = {
|
||||
+ .sizeof_wfhw = sizeof(struct rockchip_pwm_v4_wf),
|
||||
+ .round_waveform_tohw = rockchip_pwm_v4_round_wf_tohw,
|
||||
+ .round_waveform_fromhw = rockchip_pwm_v4_round_wf_fromhw,
|
||||
+ .read_waveform = rockchip_pwm_v4_read_wf,
|
||||
+ .write_waveform = rockchip_pwm_v4_write_wf,
|
||||
+};
|
||||
+
|
||||
+static bool rockchip_pwm_v4_on_and_continuous(struct rockchip_pwm_v4 *pc)
|
||||
+{
|
||||
+ bool en;
|
||||
+ u32 val;
|
||||
+
|
||||
+ en = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
|
||||
+ PWMV4_REG_ENABLE));
|
||||
+ val = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_CTRL);
|
||||
+
|
||||
+ return en && ((val & PWMV4_MODE_MASK) == PWMV4_MODE_CONT);
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_probe(struct platform_device *pdev)
|
||||
+{
|
||||
+ struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
|
||||
+ struct rockchip_pwm_v4 *pc;
|
||||
+ struct pwm_chip *chip;
|
||||
+ struct device *dev = &pdev->dev;
|
||||
+ int ret;
|
||||
+
|
||||
+ /*
|
||||
+ * For referencing the PWM in the DT to work, we need the parent MFD
|
||||
+ * device's OF node. Code shamelessly adapted from `drivers/pci/of.c`'s
|
||||
+ * pci_set_of_node(), which does this for bus reasons.
|
||||
+ */
|
||||
+ dev->parent->of_node_reused = true;
|
||||
+ device_set_node(dev,
|
||||
+ of_fwnode_handle(no_free_ptr(dev->parent->of_node)));
|
||||
+
|
||||
+ chip = devm_pwmchip_alloc(dev, 1, sizeof(*pc));
|
||||
+ if (IS_ERR(chip))
|
||||
+ return PTR_ERR(chip);
|
||||
+
|
||||
+ pc = to_rockchip_pwm_v4(chip);
|
||||
+ pc->pwmf = pwmf;
|
||||
+
|
||||
+ ret = mfpwm_acquire(pwmf);
|
||||
+ if (ret == -EBUSY)
|
||||
+ dev_warn(dev, "PWM hardware already in use, can't check initial state\n");
|
||||
+ else if (ret < 0)
|
||||
+ return dev_err_probe(dev, ret, "Couldn't acquire mfpwm in probe\n");
|
||||
+
|
||||
+ if (!rockchip_pwm_v4_on_and_continuous(pc))
|
||||
+ mfpwm_release(pwmf);
|
||||
+ else {
|
||||
+ dev_dbg(dev, "PWM was already on at probe time\n");
|
||||
+ ret = clk_enable(pwmf->core);
|
||||
+ if (ret)
|
||||
+ return dev_err_probe(dev, ret, "Enabling pwm clock failed\n");
|
||||
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
|
||||
+ if (ret) {
|
||||
+ clk_disable(pwmf->core);
|
||||
+ return dev_err_probe(dev, ret, "Protecting pwm clock failed\n");
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ platform_set_drvdata(pdev, chip);
|
||||
+
|
||||
+ chip->ops = &rockchip_pwm_v4_ops;
|
||||
+
|
||||
+ ret = devm_pwmchip_add(dev, chip);
|
||||
+ if (ret)
|
||||
+ return dev_err_probe(dev, ret, "Failed to add PWM chip\n");
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static const struct platform_device_id rockchip_pwm_v4_ids[] = {
|
||||
+ { .name = "pwm-rockchip-v4", },
|
||||
+ { /* sentinel */ }
|
||||
+};
|
||||
+MODULE_DEVICE_TABLE(platform, rockchip_pwm_v4_ids);
|
||||
+
|
||||
+static struct platform_driver rockchip_pwm_v4_driver = {
|
||||
+ .probe = rockchip_pwm_v4_probe,
|
||||
+ .driver = {
|
||||
+ .name = "pwm-rockchip-v4",
|
||||
+ },
|
||||
+ .id_table = rockchip_pwm_v4_ids,
|
||||
+};
|
||||
+module_platform_driver(rockchip_pwm_v4_driver);
|
||||
+
|
||||
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
|
||||
+MODULE_DESCRIPTION("Rockchip PWMv4 Driver");
|
||||
+MODULE_LICENSE("GPL");
|
||||
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
|
||||
+MODULE_ALIAS("platform:pwm-rockchip-v4");
|
||||
@ -0,0 +1,365 @@
|
||||
From git@z Thu Jan 1 00:00:00 1970
|
||||
Subject: [PATCH v3 4/5] counter: Add rockchip-pwm-capture driver
|
||||
From: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
Date: Mon, 27 Oct 2025 18:11:59 +0100
|
||||
Message-Id: <20251027-rk3576-pwm-v3-4-654a5cb1e3f8@collabora.com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Among many other things, Rockchip's new PWMv4 IP in the RK3576 supports
|
||||
PWM capture functionality.
|
||||
|
||||
Add a basic driver for this that works to expose HPC/LPC counts and
|
||||
state change events to userspace through the counter framework. It's
|
||||
quite basic, but works well enough to demonstrate the device function
|
||||
exclusion stuff that mfpwm does, in order to eventually support all the
|
||||
functions of this device in drivers within their appropriate subsystems,
|
||||
without them interfering with each other.
|
||||
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
MAINTAINERS | 1 +
|
||||
drivers/counter/Kconfig | 13 ++
|
||||
drivers/counter/Makefile | 1 +
|
||||
drivers/counter/rockchip-pwm-capture.c | 306 +++++++++++++++++++++++++++++++++
|
||||
4 files changed, 321 insertions(+)
|
||||
|
||||
--- a/drivers/counter/Kconfig
|
||||
+++ b/drivers/counter/Kconfig
|
||||
@@ -90,6 +90,19 @@ config MICROCHIP_TCB_CAPTURE
|
||||
To compile this driver as a module, choose M here: the
|
||||
module will be called microchip-tcb-capture.
|
||||
|
||||
+config ROCKCHIP_PWM_CAPTURE
|
||||
+ tristate "Rockchip PWM Counter Capture driver"
|
||||
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
|
||||
+ depends on MFD_ROCKCHIP_MFPWM
|
||||
+ depends on HAS_IOMEM
|
||||
+ help
|
||||
+ Generic counter framework driver for the multi-function PWM on
|
||||
+ Rockchip SoCs such as the RK3576.
|
||||
+
|
||||
+ Uses the Rockchip Multi-function PWM controller driver infrastructure
|
||||
+ to guarantee exclusive operation with other functions of the same
|
||||
+ device implemented by drivers in other subsystems.
|
||||
+
|
||||
config RZ_MTU3_CNT
|
||||
tristate "Renesas RZ/G2L MTU3a counter driver"
|
||||
depends on RZ_MTU3
|
||||
--- a/drivers/counter/Makefile
|
||||
+++ b/drivers/counter/Makefile
|
||||
@@ -17,3 +17,4 @@ obj-$(CONFIG_FTM_QUADDEC) += ftm-quaddec
|
||||
obj-$(CONFIG_MICROCHIP_TCB_CAPTURE) += microchip-tcb-capture.o
|
||||
obj-$(CONFIG_INTEL_QEP) += intel-qep.o
|
||||
obj-$(CONFIG_TI_ECAP_CAPTURE) += ti-ecap-capture.o
|
||||
+obj-$(CONFIG_ROCKCHIP_PWM_CAPTURE) += rockchip-pwm-capture.o
|
||||
--- /dev/null
|
||||
+++ b/drivers/counter/rockchip-pwm-capture.c
|
||||
@@ -0,0 +1,306 @@
|
||||
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
+ *
|
||||
+ * A counter driver for the Pulse-Width-Modulation (PWM) hardware found on
|
||||
+ * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". It
|
||||
+ * allows for measuring the high cycles and low cycles of a PWM signal through
|
||||
+ * the generic counter framework, while guaranteeing exclusive use over the
|
||||
+ * MFPWM device while the counter is enabled.
|
||||
+ *
|
||||
+ * Authors:
|
||||
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ */
|
||||
+
|
||||
+#include <linux/cleanup.h>
|
||||
+#include <linux/counter.h>
|
||||
+#include <linux/devm-helpers.h>
|
||||
+#include <linux/interrupt.h>
|
||||
+#include <linux/mfd/rockchip-mfpwm.h>
|
||||
+#include <linux/mod_devicetable.h>
|
||||
+#include <linux/of.h>
|
||||
+#include <linux/platform_device.h>
|
||||
+#include <linux/spinlock.h>
|
||||
+
|
||||
+#define RKPWMC_INT_MASK (PWMV4_INT_LPC | PWMV4_INT_HPC)
|
||||
+
|
||||
+struct rockchip_pwm_capture {
|
||||
+ struct rockchip_mfpwm_func *pwmf;
|
||||
+ struct counter_device *counter;
|
||||
+ bool is_enabled;
|
||||
+ spinlock_t enable_lock;
|
||||
+};
|
||||
+
|
||||
+/*
|
||||
+ * Channel 0 receives a state changed notification whenever the LPC interrupt
|
||||
+ * fires.
|
||||
+ *
|
||||
+ * Channel 1 receives a state changed notification whenever the HPC interrupt
|
||||
+ * fires.
|
||||
+ */
|
||||
+static struct counter_signal rkpwmc_signals[] = {
|
||||
+ {
|
||||
+ .id = 0,
|
||||
+ .name = "Channel 0"
|
||||
+ },
|
||||
+ {
|
||||
+ .id = 1,
|
||||
+ .name = "Channel 1"
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+static const enum counter_synapse_action rkpwmc_hpc_lpc_actions[] = {
|
||||
+ COUNTER_SYNAPSE_ACTION_BOTH_EDGES,
|
||||
+
|
||||
+};
|
||||
+
|
||||
+static struct counter_synapse rkpwmc_pwm_synapses[] = {
|
||||
+ {
|
||||
+ .actions_list = rkpwmc_hpc_lpc_actions,
|
||||
+ .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions),
|
||||
+ .signal = &rkpwmc_signals[0]
|
||||
+ },
|
||||
+ {
|
||||
+ .actions_list = rkpwmc_hpc_lpc_actions,
|
||||
+ .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions),
|
||||
+ .signal = &rkpwmc_signals[1]
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+static const enum counter_function rkpwmc_functions[] = {
|
||||
+ COUNTER_FUNCTION_INCREASE,
|
||||
+};
|
||||
+
|
||||
+static int rkpwmc_enable_read(struct counter_device *counter,
|
||||
+ struct counter_count *count,
|
||||
+ u8 *enable)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
|
||||
+
|
||||
+ guard(spinlock)(&pc->enable_lock);
|
||||
+
|
||||
+ *enable = pc->is_enabled;
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rkpwmc_enable_write(struct counter_device *counter,
|
||||
+ struct counter_count *count,
|
||||
+ u8 enable)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
|
||||
+ int ret;
|
||||
+
|
||||
+ guard(spinlock)(&pc->enable_lock);
|
||||
+
|
||||
+ if (!!enable != pc->is_enabled) {
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ if (enable) {
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(false));
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL,
|
||||
+ PWMV4_CTRL_CAP_FLAGS);
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN,
|
||||
+ PWMV4_INT_LPC_W(true) |
|
||||
+ PWMV4_INT_HPC_W(true));
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(true) | PWMV4_CLK_EN(true));
|
||||
+
|
||||
+ ret = clk_enable(pc->pwmf->core);
|
||||
+ if (ret)
|
||||
+ goto err_release;
|
||||
+
|
||||
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
|
||||
+ if (ret)
|
||||
+ goto err_disable_pwm_clk;
|
||||
+
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ goto err_unprotect_pwm_clk;
|
||||
+
|
||||
+ pc->is_enabled = true;
|
||||
+ } else {
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN,
|
||||
+ PWMV4_INT_LPC_W(false) |
|
||||
+ PWMV4_INT_HPC_W(false));
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(false) | PWMV4_CLK_EN(false));
|
||||
+ clk_rate_exclusive_put(pc->pwmf->core);
|
||||
+ clk_disable(pc->pwmf->core);
|
||||
+ pc->is_enabled = false;
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ }
|
||||
+
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ }
|
||||
+
|
||||
+ return 0;
|
||||
+
|
||||
+err_unprotect_pwm_clk:
|
||||
+ clk_rate_exclusive_put(pc->pwmf->core);
|
||||
+err_disable_pwm_clk:
|
||||
+ clk_disable(pc->pwmf->core);
|
||||
+err_release:
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static struct counter_comp rkpwmc_ext[] = {
|
||||
+ COUNTER_COMP_ENABLE(rkpwmc_enable_read, rkpwmc_enable_write),
|
||||
+};
|
||||
+
|
||||
+enum rkpwmc_count_id {
|
||||
+ COUNT_LPC = 0,
|
||||
+ COUNT_HPC = 1,
|
||||
+};
|
||||
+
|
||||
+static struct counter_count rkpwmc_counts[] = {
|
||||
+ {
|
||||
+ .id = COUNT_LPC,
|
||||
+ .name = "PWM core clock cycles during which the signal was low",
|
||||
+ .functions_list = rkpwmc_functions,
|
||||
+ .num_functions = ARRAY_SIZE(rkpwmc_functions),
|
||||
+ .synapses = rkpwmc_pwm_synapses,
|
||||
+ .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
|
||||
+ .ext = rkpwmc_ext,
|
||||
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
|
||||
+ },
|
||||
+ {
|
||||
+ .id = COUNT_HPC,
|
||||
+ .name = "PWM core clock cycles during which the signal was high",
|
||||
+ .functions_list = rkpwmc_functions,
|
||||
+ .num_functions = ARRAY_SIZE(rkpwmc_functions),
|
||||
+ .synapses = rkpwmc_pwm_synapses,
|
||||
+ .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
|
||||
+ .ext = rkpwmc_ext,
|
||||
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+static int rkpwmc_count_read(struct counter_device *counter,
|
||||
+ struct counter_count *count, u64 *value)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
|
||||
+
|
||||
+ guard(spinlock)(&pc->enable_lock);
|
||||
+
|
||||
+ if (!pc->is_enabled) {
|
||||
+ *value = 0;
|
||||
+ return 0;
|
||||
+ }
|
||||
+
|
||||
+ switch (count->id) {
|
||||
+ case COUNT_LPC:
|
||||
+ *value = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_LPC);
|
||||
+ return 0;
|
||||
+ case COUNT_HPC:
|
||||
+ *value = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_HPC);
|
||||
+ return 0;
|
||||
+ default:
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static const struct counter_ops rkpwmc_ops = {
|
||||
+ .count_read = rkpwmc_count_read,
|
||||
+};
|
||||
+
|
||||
+static irqreturn_t rkpwmc_irq_handler(int irq, void *data)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = data;
|
||||
+ u32 intsts;
|
||||
+ u32 clr = 0;
|
||||
+
|
||||
+ intsts = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_INTSTS);
|
||||
+
|
||||
+ if (!(intsts & RKPWMC_INT_MASK))
|
||||
+ return IRQ_NONE;
|
||||
+
|
||||
+ if (intsts & PWMV4_INT_LPC) {
|
||||
+ clr |= PWMV4_INT_LPC;
|
||||
+ counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 0);
|
||||
+ }
|
||||
+
|
||||
+ if (intsts & PWMV4_INT_HPC) {
|
||||
+ clr |= PWMV4_INT_HPC;
|
||||
+ counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 1);
|
||||
+ }
|
||||
+
|
||||
+ if (clr)
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INTSTS, clr);
|
||||
+
|
||||
+ /* If other interrupt status bits are set, they're not for this driver */
|
||||
+ if (intsts != clr)
|
||||
+ return IRQ_NONE;
|
||||
+
|
||||
+ return IRQ_HANDLED;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_capture_probe(struct platform_device *pdev)
|
||||
+{
|
||||
+ struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
|
||||
+ struct rockchip_pwm_capture *pc;
|
||||
+ struct counter_device *counter;
|
||||
+ int ret;
|
||||
+
|
||||
+ /* Set our (still unset) OF node to the parent MFD device's OF node */
|
||||
+ pdev->dev.parent->of_node_reused = true;
|
||||
+ device_set_node(&pdev->dev,
|
||||
+ of_fwnode_handle(no_free_ptr(pdev->dev.parent->of_node)));
|
||||
+
|
||||
+ counter = devm_counter_alloc(&pdev->dev, sizeof(*pc));
|
||||
+ if (IS_ERR(counter))
|
||||
+ return PTR_ERR(counter);
|
||||
+
|
||||
+ pc = counter_priv(counter);
|
||||
+ pc->pwmf = pwmf;
|
||||
+ spin_lock_init(&pc->enable_lock);
|
||||
+
|
||||
+ platform_set_drvdata(pdev, pc);
|
||||
+
|
||||
+ ret = devm_request_irq(&pdev->dev, pwmf->irq, rkpwmc_irq_handler,
|
||||
+ IRQF_SHARED, pdev->name, pc);
|
||||
+ if (ret)
|
||||
+ return dev_err_probe(&pdev->dev, ret, "Failed requesting IRQ\n");
|
||||
+
|
||||
+ counter->name = pdev->name;
|
||||
+ counter->signals = rkpwmc_signals;
|
||||
+ counter->num_signals = ARRAY_SIZE(rkpwmc_signals);
|
||||
+ counter->ops = &rkpwmc_ops;
|
||||
+ counter->counts = rkpwmc_counts;
|
||||
+ counter->num_counts = ARRAY_SIZE(rkpwmc_counts);
|
||||
+
|
||||
+ pc->counter = counter;
|
||||
+
|
||||
+ ret = devm_counter_add(&pdev->dev, counter);
|
||||
+ if (ret < 0)
|
||||
+ return dev_err_probe(&pdev->dev, ret, "Failed to add counter\n");
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static const struct platform_device_id rockchip_pwm_capture_id_table[] = {
|
||||
+ { .name = "rockchip-pwm-capture", },
|
||||
+ { /* sentinel */ },
|
||||
+};
|
||||
+MODULE_DEVICE_TABLE(platform, rockchip_pwm_capture_id_table);
|
||||
+
|
||||
+static struct platform_driver rockchip_pwm_capture_driver = {
|
||||
+ .probe = rockchip_pwm_capture_probe,
|
||||
+ .id_table = rockchip_pwm_capture_id_table,
|
||||
+ .driver = {
|
||||
+ .name = "rockchip-pwm-capture",
|
||||
+ },
|
||||
+};
|
||||
+module_platform_driver(rockchip_pwm_capture_driver);
|
||||
+
|
||||
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
|
||||
+MODULE_DESCRIPTION("Rockchip PWM Counter Capture Driver");
|
||||
+MODULE_LICENSE("GPL");
|
||||
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
|
||||
+MODULE_IMPORT_NS("COUNTER");
|
||||
+MODULE_ALIAS("platform:rockchip-pwm-capture");
|
||||
@ -0,0 +1,243 @@
|
||||
From git@z Thu Jan 1 00:00:00 1970
|
||||
Subject: [PATCH v3 5/5] arm64: dts: rockchip: add PWM nodes to RK3576 SoC
|
||||
dtsi
|
||||
From: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
Date: Mon, 27 Oct 2025 18:12:00 +0100
|
||||
Message-Id: <20251027-rk3576-pwm-v3-5-654a5cb1e3f8@collabora.com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
The RK3576 SoC features three distinct PWM controllers, with variable
|
||||
numbers of channels. Add each channel as a separate node to the SoC's
|
||||
device tree, as they don't really overlap in register ranges.
|
||||
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
arch/arm64/boot/dts/rockchip/rk3576.dtsi | 208 +++++++++++++++++++++++++++++++
|
||||
1 file changed, 208 insertions(+)
|
||||
|
||||
--- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi
|
||||
+++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi
|
||||
@@ -1042,6 +1042,32 @@
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
+ pwm0_2ch_0: pwm@27330000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x27330000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>,
|
||||
+ <&cru CLK_PMU1PWM_OSC>, <&cru CLK_PMU1PWM_RC>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm0m0_ch0>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm0_2ch_1: pwm@27331000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x27331000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>,
|
||||
+ <&cru CLK_PMU1PWM_OSC>, <&cru CLK_PMU1PWM_RC>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm0m0_ch1>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
pmu: power-management@27380000 {
|
||||
compatible = "rockchip,rk3576-pmu", "syscon", "simple-mfd";
|
||||
reg = <0x0 0x27380000 0x0 0x800>;
|
||||
@@ -2492,6 +2518,188 @@
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
+ pwm1_6ch_0: pwm@2add0000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2add0000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
|
||||
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm1m0_ch0>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm1_6ch_1: pwm@2add1000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2add1000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
|
||||
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm1m0_ch1>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm1_6ch_2: pwm@2add2000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2add2000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
|
||||
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 104 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm1m0_ch2>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm1_6ch_3: pwm@2add3000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2add3000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
|
||||
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 105 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm1m0_ch3>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm1_6ch_4: pwm@2add4000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2add4000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
|
||||
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 106 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm1m0_ch4>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm1_6ch_5: pwm@2add5000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2add5000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
|
||||
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 107 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm1m0_ch5>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm2_8ch_0: pwm@2ade0000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade0000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
|
||||
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 108 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm2m0_ch0>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm2_8ch_1: pwm@2ade1000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade1000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
|
||||
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 109 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm2m0_ch1>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm2_8ch_2: pwm@2ade2000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade2000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
|
||||
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 110 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm2m0_ch2>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm2_8ch_3: pwm@2ade3000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade3000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
|
||||
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 111 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm2m0_ch3>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm2_8ch_4: pwm@2ade4000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade4000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
|
||||
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 112 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm2m0_ch4>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm2_8ch_5: pwm@2ade5000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade5000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
|
||||
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 113 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm2m0_ch5>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm2_8ch_6: pwm@2ade6000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade6000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
|
||||
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 114 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm2m0_ch6>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ pwm2_8ch_7: pwm@2ade7000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade7000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
|
||||
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
|
||||
+ clock-names = "pwm", "pclk", "osc", "rc";
|
||||
+ interrupts = <GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ pinctrl-names = "default";
|
||||
+ pinctrl-0 = <&pwm2m0_ch7>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
saradc: adc@2ae00000 {
|
||||
compatible = "rockchip,rk3576-saradc", "rockchip,rk3588-saradc";
|
||||
reg = <0x0 0x2ae00000 0x0 0x10000>;
|
||||
@ -1,6 +1,6 @@
|
||||
--- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi
|
||||
+++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi
|
||||
@@ -1927,11 +1927,13 @@
|
||||
@@ -1953,11 +1953,13 @@
|
||||
};
|
||||
|
||||
rng: rng@2a410000 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user