rockchip: backport pending PWMv4 support

Used by RK3576 SoCs.

Signed-off-by: Tianling Shen <cnsztl@immortalwrt.org>
This commit is contained in:
Tianling Shen 2025-12-11 14:41:27 +08:00
parent 2151ced240
commit f2c5abcda3
No known key found for this signature in database
GPG Key ID: 6850B6345C862176
26 changed files with 4399 additions and 5 deletions

View File

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

View File

@ -29,7 +29,7 @@ Signed-off-by: David S. Miller <davem@davemloft.net>
--- a/MAINTAINERS --- a/MAINTAINERS
+++ b/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> M: SkyLake Huang <SkyLake.Huang@mediatek.com>
L: netdev@vger.kernel.org L: netdev@vger.kernel.org
S: Maintained S: Maintained

View File

@ -24,7 +24,7 @@ Signed-off-by: David S. Miller <davem@davemloft.net>
--- a/MAINTAINERS --- a/MAINTAINERS
+++ b/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 L: netdev@vger.kernel.org
S: Maintained S: Maintained
F: drivers/net/phy/mediatek/mtk-ge-soc.c F: drivers/net/phy/mediatek/mtk-ge-soc.c

View File

@ -36,7 +36,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
--- a/MAINTAINERS --- a/MAINTAINERS
+++ b/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: Documentation/filesystems/ubifs.rst
F: fs/ubifs/ F: fs/ubifs/

View File

@ -19,7 +19,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
--- a/MAINTAINERS --- a/MAINTAINERS
+++ b/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 L: netdev@vger.kernel.org
S: Maintained S: Maintained
F: drivers/net/pcs/pcs-mtk-lynxi.c F: drivers/net/pcs/pcs-mtk-lynxi.c

View File

@ -413,6 +413,7 @@ CONFIG_MFD_CORE=y
CONFIG_MFD_RK8XX=y CONFIG_MFD_RK8XX=y
CONFIG_MFD_RK8XX_I2C=y CONFIG_MFD_RK8XX_I2C=y
CONFIG_MFD_RK8XX_SPI=y CONFIG_MFD_RK8XX_SPI=y
CONFIG_MFD_ROCKCHIP_MFPWM=y
CONFIG_MFD_SYSCON=y CONFIG_MFD_SYSCON=y
CONFIG_MIGRATION=y CONFIG_MIGRATION=y
CONFIG_MMC=y CONFIG_MMC=y
@ -571,6 +572,7 @@ CONFIG_PTP_1588_CLOCK=y
CONFIG_PTP_1588_CLOCK_OPTIONAL=y CONFIG_PTP_1588_CLOCK_OPTIONAL=y
CONFIG_PWM=y CONFIG_PWM=y
CONFIG_PWM_ROCKCHIP=y CONFIG_PWM_ROCKCHIP=y
CONFIG_PWM_ROCKCHIP_V4=y
# CONFIG_QFMT_V2 is not set # CONFIG_QFMT_V2 is not set
CONFIG_QUEUED_RWLOCKS=y CONFIG_QUEUED_RWLOCKS=y
CONFIG_QUEUED_SPINLOCKS=y CONFIG_QUEUED_SPINLOCKS=y
@ -615,6 +617,7 @@ CONFIG_ROCKCHIP_MBOX=y
CONFIG_ROCKCHIP_MFPWM=y CONFIG_ROCKCHIP_MFPWM=y
CONFIG_ROCKCHIP_PHY=y CONFIG_ROCKCHIP_PHY=y
CONFIG_ROCKCHIP_PM_DOMAINS=y CONFIG_ROCKCHIP_PM_DOMAINS=y
CONFIG_ROCKCHIP_PWM_CAPTURE=y
# CONFIG_ROCKCHIP_SARADC is not set # CONFIG_ROCKCHIP_SARADC is not set
CONFIG_ROCKCHIP_THERMAL=y CONFIG_ROCKCHIP_THERMAL=y
CONFIG_ROCKCHIP_TIMER=y CONFIG_ROCKCHIP_TIMER=y

View File

@ -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);
};

View File

@ -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,

View File

@ -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);

View File

@ -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)
);

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}
/**

View File

@ -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",

View File

@ -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)
{

View File

@ -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)

View File

@ -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);

View File

@ -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;
}

View File

@ -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.

View File

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

View File

@ -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");

View File

@ -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");

View File

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

View File

@ -1,6 +1,6 @@
--- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi --- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi
+++ b/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 { rng: rng@2a410000 {