mirror of
https://github.com/Heleguo/lede.git
synced 2025-12-16 10:51:12 +00:00
rockchip: backport pending PWMv4 support for 6.12
This commit is contained in:
parent
5ac3610aaf
commit
2cac64c0bc
@ -404,6 +404,7 @@ CONFIG_MFD_CORE=y
|
||||
CONFIG_MFD_RK8XX=y
|
||||
CONFIG_MFD_RK8XX_I2C=y
|
||||
CONFIG_MFD_RK8XX_SPI=y
|
||||
CONFIG_MFD_ROCKCHIP_MFPWM=y
|
||||
CONFIG_MFD_SYSCON=y
|
||||
CONFIG_MIGRATION=y
|
||||
CONFIG_MMC=y
|
||||
@ -587,6 +588,7 @@ CONFIG_ROCKCHIP_MBOX=y
|
||||
CONFIG_ROCKCHIP_MFPWM=y
|
||||
CONFIG_ROCKCHIP_PHY=y
|
||||
CONFIG_ROCKCHIP_PM_DOMAINS=y
|
||||
CONFIG_ROCKCHIP_PWM_CAPTURE=y
|
||||
CONFIG_ROCKCHIP_THERMAL=y
|
||||
CONFIG_ROCKCHIP_TIMER=y
|
||||
CONFIG_RODATA_FULL_DEFAULT_ENABLED=y
|
||||
|
||||
@ -1,8 +1,35 @@
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
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 | 95 +++++++++++++++++++++++++++++++++++++++++----
|
||||
include/linux/pwm.h | 13 +++++++
|
||||
2 files changed, 100 insertions(+), 8 deletions(-)
|
||||
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
|
||||
@ -71,8 +98,15 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
return __pwm_apply(pwm, state);
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(pwm_apply_atomic);
|
||||
@@ -340,6 +371,11 @@ static int pwm_capture(struct pwm_device
|
||||
@@ -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);
|
||||
@ -83,7 +117,7 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
return ops->capture(chip, pwm, result, timeout);
|
||||
}
|
||||
|
||||
@@ -372,6 +408,14 @@ static int pwm_device_request(struct pwm
|
||||
@@ -372,6 +413,14 @@ static int pwm_device_request(struct pwm
|
||||
if (test_bit(PWMF_REQUESTED, &pwm->flags))
|
||||
return -EBUSY;
|
||||
|
||||
@ -98,7 +132,7 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
if (!try_module_get(chip->owner))
|
||||
return -ENODEV;
|
||||
|
||||
@@ -400,7 +444,9 @@ err_get_device:
|
||||
@@ -400,7 +449,9 @@ err_get_device:
|
||||
*/
|
||||
struct pwm_state state = { 0, };
|
||||
|
||||
@ -109,7 +143,7 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
trace_pwm_get(pwm, &state, err);
|
||||
|
||||
if (!err)
|
||||
@@ -1024,6 +1070,7 @@ struct pwm_chip *pwmchip_alloc(struct de
|
||||
@@ -1024,6 +1075,7 @@ struct pwm_chip *pwmchip_alloc(struct de
|
||||
|
||||
chip->npwm = npwm;
|
||||
chip->uses_pwmchip_alloc = true;
|
||||
@ -117,7 +151,7 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
|
||||
pwmchip_dev = &chip->dev;
|
||||
device_initialize(pwmchip_dev);
|
||||
@@ -1129,6 +1176,11 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
@@ -1129,6 +1181,11 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
|
||||
chip->owner = owner;
|
||||
|
||||
@ -129,7 +163,7 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
guard(mutex)(&pwm_lock);
|
||||
|
||||
ret = idr_alloc(&pwm_chips, chip, 0, 0, GFP_KERNEL);
|
||||
@@ -1142,6 +1194,9 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
@@ -1142,6 +1199,9 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
if (IS_ENABLED(CONFIG_OF))
|
||||
of_pwmchip_add(chip);
|
||||
|
||||
@ -139,7 +173,7 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
ret = device_add(&chip->dev);
|
||||
if (ret)
|
||||
goto err_device_add;
|
||||
@@ -1149,6 +1204,9 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
@@ -1149,6 +1209,9 @@ int __pwmchip_add(struct pwm_chip *chip,
|
||||
return 0;
|
||||
|
||||
err_device_add:
|
||||
@ -149,7 +183,7 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
if (IS_ENABLED(CONFIG_OF))
|
||||
of_pwmchip_remove(chip);
|
||||
|
||||
@@ -1168,11 +1226,27 @@ void pwmchip_remove(struct pwm_chip *chi
|
||||
@@ -1168,11 +1231,27 @@ void pwmchip_remove(struct pwm_chip *chi
|
||||
{
|
||||
pwmchip_sysfs_unexport(chip);
|
||||
|
||||
@ -165,7 +199,7 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
+ struct pwm_device *pwm = &chip->pwms[i];
|
||||
+
|
||||
+ if (test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
+ dev_alert(&chip->dev, "Freeing requested PWM #%u\n", i);
|
||||
+ dev_warn(&chip->dev, "Freeing requested PWM #%u\n", i);
|
||||
+ if (pwm->chip->ops->free)
|
||||
+ pwm->chip->ops->free(pwm->chip, pwm);
|
||||
+ }
|
||||
@ -180,15 +214,15 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
|
||||
device_del(&chip->dev);
|
||||
}
|
||||
@@ -1542,12 +1616,17 @@ void pwm_put(struct pwm_device *pwm)
|
||||
@@ -1542,12 +1621,17 @@ void pwm_put(struct pwm_device *pwm)
|
||||
|
||||
guard(mutex)(&pwm_lock);
|
||||
|
||||
- if (!test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
+ /*
|
||||
+ * If the chip isn't operational, PWMF_REQUESTED was already cleared. So
|
||||
+ * don't warn in this case. This can only happen if a consumer called
|
||||
+ * pwm_put() twice.
|
||||
+ * 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");
|
||||
@ -221,10 +255,10 @@ Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
+ /*
|
||||
+ * depending on the chip being atomic or not either the mutex or
|
||||
+ * the spinlock is used. It protects .operational and
|
||||
+ * synchronizes calls to the .ops->apply and .ops->get_state()
|
||||
+ * synchronizes the callbacks in .ops
|
||||
+ */
|
||||
+ struct mutex nonatomic_lock;
|
||||
+ struct spinlock atomic_lock;
|
||||
+ spinlock_t atomic_lock;
|
||||
+ };
|
||||
struct pwm_device pwms[] __counted_by(npwm);
|
||||
};
|
||||
@ -49,11 +49,11 @@ 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>
|
||||
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>
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 234 ++++++++++++++++++++++++++++++++++++++++----
|
||||
include/linux/pwm.h | 36 +++++++
|
||||
@ -302,7 +302,7 @@ Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
/**
|
||||
* pwm_adjust_config() - adjust the current PWM config to the PWM arguments
|
||||
* @pwm: PWM device
|
||||
@@ -434,7 +618,7 @@ err_get_device:
|
||||
@@ -439,7 +623,7 @@ err_get_device:
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,7 +311,7 @@ Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
/*
|
||||
* Zero-initialize state because most drivers are unaware of
|
||||
* .usage_power. The other members of state are supposed to be
|
||||
@@ -444,11 +628,7 @@ err_get_device:
|
||||
@@ -449,11 +633,7 @@ err_get_device:
|
||||
*/
|
||||
struct pwm_state state = { 0, };
|
||||
|
||||
@ -324,7 +324,7 @@ Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
if (!err)
|
||||
pwm->state = state;
|
||||
|
||||
@@ -1135,12 +1315,24 @@ static bool pwm_ops_check(const struct p
|
||||
@@ -1140,12 +1320,24 @@ static bool pwm_ops_check(const struct p
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
|
||||
@ -13,10 +13,10 @@ 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>
|
||||
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>
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 261 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
include/linux/pwm.h | 6 +-
|
||||
@ -0,0 +1,236 @@
|
||||
From 1afd01db1a76cdd1d96696e3790d66c79621784c Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:58:00 +0200
|
||||
Subject: [PATCH] pwm: Add tracing for waveform callbacks
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This adds trace events for the recently introduced waveform callbacks.
|
||||
With the introduction of some helper macros consistency among the
|
||||
different events is ensured.
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/1d71879b0de3bf01459c7a9d0f040d43eb5ace56.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 24 +++++--
|
||||
include/trace/events/pwm.h | 134 ++++++++++++++++++++++++++++++++++---
|
||||
2 files changed, 146 insertions(+), 12 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -164,30 +164,46 @@ static int __pwm_round_waveform_tohw(str
|
||||
const struct pwm_waveform *wf, void *wfhw)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret;
|
||||
|
||||
- return ops->round_waveform_tohw(chip, pwm, wf, wfhw);
|
||||
+ ret = ops->round_waveform_tohw(chip, pwm, wf, wfhw);
|
||||
+ trace_pwm_round_waveform_tohw(pwm, wf, wfhw, ret);
|
||||
+
|
||||
+ return ret;
|
||||
}
|
||||
|
||||
static int __pwm_round_waveform_fromhw(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
const void *wfhw, struct pwm_waveform *wf)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = ops->round_waveform_fromhw(chip, pwm, wfhw, wf);
|
||||
+ trace_pwm_round_waveform_fromhw(pwm, wfhw, wf, ret);
|
||||
|
||||
- return ops->round_waveform_fromhw(chip, pwm, wfhw, wf);
|
||||
+ return ret;
|
||||
}
|
||||
|
||||
static int __pwm_read_waveform(struct pwm_chip *chip, struct pwm_device *pwm, void *wfhw)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret;
|
||||
|
||||
- return ops->read_waveform(chip, pwm, wfhw);
|
||||
+ ret = ops->read_waveform(chip, pwm, wfhw);
|
||||
+ trace_pwm_read_waveform(pwm, wfhw, ret);
|
||||
+
|
||||
+ return ret;
|
||||
}
|
||||
|
||||
static int __pwm_write_waveform(struct pwm_chip *chip, struct pwm_device *pwm, const void *wfhw)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = ops->write_waveform(chip, pwm, wfhw);
|
||||
+ trace_pwm_write_waveform(pwm, wfhw, ret);
|
||||
|
||||
- return ops->write_waveform(chip, pwm, wfhw);
|
||||
+ return ret;
|
||||
}
|
||||
|
||||
#define WFHWSIZE 20
|
||||
--- a/include/trace/events/pwm.h
|
||||
+++ b/include/trace/events/pwm.h
|
||||
@@ -8,15 +8,135 @@
|
||||
#include <linux/pwm.h>
|
||||
#include <linux/tracepoint.h>
|
||||
|
||||
+#define TP_PROTO_pwm(args...) \
|
||||
+ TP_PROTO(struct pwm_device *pwm, args)
|
||||
+
|
||||
+#define TP_ARGS_pwm(args...) \
|
||||
+ TP_ARGS(pwm, args)
|
||||
+
|
||||
+#define TP_STRUCT__entry_pwm(args...) \
|
||||
+ TP_STRUCT__entry( \
|
||||
+ __field(unsigned int, chipid) \
|
||||
+ __field(unsigned int, hwpwm) \
|
||||
+ args)
|
||||
+
|
||||
+#define TP_fast_assign_pwm(args...) \
|
||||
+ TP_fast_assign( \
|
||||
+ __entry->chipid = pwm->chip->id; \
|
||||
+ __entry->hwpwm = pwm->hwpwm; \
|
||||
+ args)
|
||||
+
|
||||
+#define TP_printk_pwm(fmt, args...) \
|
||||
+ TP_printk("pwmchip%u.%u: " fmt, __entry->chipid, __entry->hwpwm, args)
|
||||
+
|
||||
+#define __field_pwmwf(wf) \
|
||||
+ __field(u64, wf ## _period_length_ns) \
|
||||
+ __field(u64, wf ## _duty_length_ns) \
|
||||
+ __field(u64, wf ## _duty_offset_ns) \
|
||||
+
|
||||
+#define fast_assign_pwmwf(wf) \
|
||||
+ __entry->wf ## _period_length_ns = wf->period_length_ns; \
|
||||
+ __entry->wf ## _duty_length_ns = wf->duty_length_ns; \
|
||||
+ __entry->wf ## _duty_offset_ns = wf->duty_offset_ns
|
||||
+
|
||||
+#define printk_pwmwf_format(wf) \
|
||||
+ "%lld/%lld [+%lld]"
|
||||
+
|
||||
+#define printk_pwmwf_formatargs(wf) \
|
||||
+ __entry->wf ## _duty_length_ns, __entry->wf ## _period_length_ns, __entry->wf ## _duty_offset_ns
|
||||
+
|
||||
+TRACE_EVENT(pwm_round_waveform_tohw,
|
||||
+
|
||||
+ TP_PROTO_pwm(const struct pwm_waveform *wf, void *wfhw, int err),
|
||||
+
|
||||
+ TP_ARGS_pwm(wf, wfhw, err),
|
||||
+
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
+ __field_pwmwf(wf)
|
||||
+ __field(void *, wfhw)
|
||||
+ __field(int, err)
|
||||
+ ),
|
||||
+
|
||||
+ TP_fast_assign_pwm(
|
||||
+ fast_assign_pwmwf(wf);
|
||||
+ __entry->wfhw = wfhw;
|
||||
+ __entry->err = err;
|
||||
+ ),
|
||||
+
|
||||
+ TP_printk_pwm(printk_pwmwf_format(wf) " > %p err=%d",
|
||||
+ printk_pwmwf_formatargs(wf), __entry->wfhw, __entry->err)
|
||||
+);
|
||||
+
|
||||
+TRACE_EVENT(pwm_round_waveform_fromhw,
|
||||
+
|
||||
+ TP_PROTO_pwm(const void *wfhw, struct pwm_waveform *wf, int err),
|
||||
+
|
||||
+ TP_ARGS_pwm(wfhw, wf, err),
|
||||
+
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
+ __field(const void *, wfhw)
|
||||
+ __field_pwmwf(wf)
|
||||
+ __field(int, err)
|
||||
+ ),
|
||||
+
|
||||
+ TP_fast_assign_pwm(
|
||||
+ __entry->wfhw = wfhw;
|
||||
+ fast_assign_pwmwf(wf);
|
||||
+ __entry->err = err;
|
||||
+ ),
|
||||
+
|
||||
+ TP_printk_pwm("%p > " printk_pwmwf_format(wf) " err=%d",
|
||||
+ __entry->wfhw, printk_pwmwf_formatargs(wf), __entry->err)
|
||||
+);
|
||||
+
|
||||
+TRACE_EVENT(pwm_read_waveform,
|
||||
+
|
||||
+ TP_PROTO_pwm(void *wfhw, int err),
|
||||
+
|
||||
+ TP_ARGS_pwm(wfhw, err),
|
||||
+
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
+ __field(void *, wfhw)
|
||||
+ __field(int, err)
|
||||
+ ),
|
||||
+
|
||||
+ TP_fast_assign_pwm(
|
||||
+ __entry->wfhw = wfhw;
|
||||
+ __entry->err = err;
|
||||
+ ),
|
||||
+
|
||||
+ TP_printk_pwm("%p err=%d",
|
||||
+ __entry->wfhw, __entry->err)
|
||||
+);
|
||||
+
|
||||
+TRACE_EVENT(pwm_write_waveform,
|
||||
+
|
||||
+ TP_PROTO_pwm(const void *wfhw, int err),
|
||||
+
|
||||
+ TP_ARGS_pwm(wfhw, err),
|
||||
+
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
+ __field(const void *, wfhw)
|
||||
+ __field(int, err)
|
||||
+ ),
|
||||
+
|
||||
+ TP_fast_assign_pwm(
|
||||
+ __entry->wfhw = wfhw;
|
||||
+ __entry->err = err;
|
||||
+ ),
|
||||
+
|
||||
+ TP_printk_pwm("%p err=%d",
|
||||
+ __entry->wfhw, __entry->err)
|
||||
+);
|
||||
+
|
||||
+
|
||||
DECLARE_EVENT_CLASS(pwm,
|
||||
|
||||
TP_PROTO(struct pwm_device *pwm, const struct pwm_state *state, int err),
|
||||
|
||||
TP_ARGS(pwm, state, err),
|
||||
|
||||
- TP_STRUCT__entry(
|
||||
- __field(unsigned int, chipid)
|
||||
- __field(unsigned int, hwpwm)
|
||||
+ TP_STRUCT__entry_pwm(
|
||||
__field(u64, period)
|
||||
__field(u64, duty_cycle)
|
||||
__field(enum pwm_polarity, polarity)
|
||||
@@ -24,9 +144,7 @@ DECLARE_EVENT_CLASS(pwm,
|
||||
__field(int, err)
|
||||
),
|
||||
|
||||
- TP_fast_assign(
|
||||
- __entry->chipid = pwm->chip->id;
|
||||
- __entry->hwpwm = pwm->hwpwm;
|
||||
+ TP_fast_assign_pwm(
|
||||
__entry->period = state->period;
|
||||
__entry->duty_cycle = state->duty_cycle;
|
||||
__entry->polarity = state->polarity;
|
||||
@@ -34,8 +152,8 @@ DECLARE_EVENT_CLASS(pwm,
|
||||
__entry->err = err;
|
||||
),
|
||||
|
||||
- TP_printk("pwmchip%u.%u: period=%llu duty_cycle=%llu polarity=%d enabled=%d err=%d",
|
||||
- __entry->chipid, __entry->hwpwm, __entry->period, __entry->duty_cycle,
|
||||
+ TP_printk_pwm("period=%llu duty_cycle=%llu polarity=%d enabled=%d err=%d",
|
||||
+ __entry->period, __entry->duty_cycle,
|
||||
__entry->polarity, __entry->enabled, __entry->err)
|
||||
|
||||
);
|
||||
@ -0,0 +1,367 @@
|
||||
From 65406de2b0d059d44472ad6f3f88a9b4a9894833 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:58:03 +0200
|
||||
Subject: [PATCH] pwm: Reorder symbols in core.c
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This moves pwm_get() and friends above the functions handling
|
||||
registration of pwmchips. The motivation is that character device
|
||||
support needs pwm_get() and pwm_put() and so ideally is defined below
|
||||
these and when a pwmchip is registered this registers the character
|
||||
device. So the natural order is
|
||||
|
||||
pwm_get() and friend
|
||||
pwm character device symbols
|
||||
pwm_chip functions
|
||||
|
||||
. The advantage of having these in their natural order is that static
|
||||
functions don't need to be forward declared.
|
||||
|
||||
Note that the diff that git produces for this change some functions are
|
||||
moved down instead. This is technically equivalent, but not how this
|
||||
change was created.
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/193b3d933294da34e020650bff93b778de46b1c5.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 312 ++++++++++++++++++++++-----------------------
|
||||
1 file changed, 156 insertions(+), 156 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -1619,132 +1619,6 @@ static bool pwm_ops_check(const struct p
|
||||
return true;
|
||||
}
|
||||
|
||||
-/**
|
||||
- * __pwmchip_add() - register a new PWM chip
|
||||
- * @chip: the PWM chip to add
|
||||
- * @owner: reference to the module providing the chip.
|
||||
- *
|
||||
- * Register a new PWM chip. @owner is supposed to be THIS_MODULE, use the
|
||||
- * pwmchip_add wrapper to do this right.
|
||||
- *
|
||||
- * Returns: 0 on success or a negative error code on failure.
|
||||
- */
|
||||
-int __pwmchip_add(struct pwm_chip *chip, struct module *owner)
|
||||
-{
|
||||
- int ret;
|
||||
-
|
||||
- if (!chip || !pwmchip_parent(chip) || !chip->ops || !chip->npwm)
|
||||
- return -EINVAL;
|
||||
-
|
||||
- /*
|
||||
- * a struct pwm_chip must be allocated using (devm_)pwmchip_alloc,
|
||||
- * otherwise the embedded struct device might disappear too early
|
||||
- * resulting in memory corruption.
|
||||
- * Catch drivers that were not converted appropriately.
|
||||
- */
|
||||
- if (!chip->uses_pwmchip_alloc)
|
||||
- return -EINVAL;
|
||||
-
|
||||
- if (!pwm_ops_check(chip))
|
||||
- return -EINVAL;
|
||||
-
|
||||
- chip->owner = owner;
|
||||
-
|
||||
- if (chip->atomic)
|
||||
- spin_lock_init(&chip->atomic_lock);
|
||||
- else
|
||||
- mutex_init(&chip->nonatomic_lock);
|
||||
-
|
||||
- guard(mutex)(&pwm_lock);
|
||||
-
|
||||
- ret = idr_alloc(&pwm_chips, chip, 0, 0, GFP_KERNEL);
|
||||
- if (ret < 0)
|
||||
- return ret;
|
||||
-
|
||||
- chip->id = ret;
|
||||
-
|
||||
- dev_set_name(&chip->dev, "pwmchip%u", chip->id);
|
||||
-
|
||||
- if (IS_ENABLED(CONFIG_OF))
|
||||
- of_pwmchip_add(chip);
|
||||
-
|
||||
- scoped_guard(pwmchip, chip)
|
||||
- chip->operational = true;
|
||||
-
|
||||
- ret = device_add(&chip->dev);
|
||||
- if (ret)
|
||||
- goto err_device_add;
|
||||
-
|
||||
- return 0;
|
||||
-
|
||||
-err_device_add:
|
||||
- scoped_guard(pwmchip, chip)
|
||||
- chip->operational = false;
|
||||
-
|
||||
- if (IS_ENABLED(CONFIG_OF))
|
||||
- of_pwmchip_remove(chip);
|
||||
-
|
||||
- idr_remove(&pwm_chips, chip->id);
|
||||
-
|
||||
- return ret;
|
||||
-}
|
||||
-EXPORT_SYMBOL_GPL(__pwmchip_add);
|
||||
-
|
||||
-/**
|
||||
- * pwmchip_remove() - remove a PWM chip
|
||||
- * @chip: the PWM chip to remove
|
||||
- *
|
||||
- * Removes a PWM chip.
|
||||
- */
|
||||
-void pwmchip_remove(struct pwm_chip *chip)
|
||||
-{
|
||||
- pwmchip_sysfs_unexport(chip);
|
||||
-
|
||||
- scoped_guard(mutex, &pwm_lock) {
|
||||
- unsigned int i;
|
||||
-
|
||||
- scoped_guard(pwmchip, chip)
|
||||
- chip->operational = false;
|
||||
-
|
||||
- for (i = 0; i < chip->npwm; ++i) {
|
||||
- struct pwm_device *pwm = &chip->pwms[i];
|
||||
-
|
||||
- if (test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
- dev_warn(&chip->dev, "Freeing requested PWM #%u\n", i);
|
||||
- if (pwm->chip->ops->free)
|
||||
- pwm->chip->ops->free(pwm->chip, pwm);
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- if (IS_ENABLED(CONFIG_OF))
|
||||
- of_pwmchip_remove(chip);
|
||||
-
|
||||
- idr_remove(&pwm_chips, chip->id);
|
||||
- }
|
||||
-
|
||||
- device_del(&chip->dev);
|
||||
-}
|
||||
-EXPORT_SYMBOL_GPL(pwmchip_remove);
|
||||
-
|
||||
-static void devm_pwmchip_remove(void *data)
|
||||
-{
|
||||
- struct pwm_chip *chip = data;
|
||||
-
|
||||
- pwmchip_remove(chip);
|
||||
-}
|
||||
-
|
||||
-int __devm_pwmchip_add(struct device *dev, struct pwm_chip *chip, struct module *owner)
|
||||
-{
|
||||
- int ret;
|
||||
-
|
||||
- ret = __pwmchip_add(chip, owner);
|
||||
- if (ret)
|
||||
- return ret;
|
||||
-
|
||||
- return devm_add_action_or_reset(dev, devm_pwmchip_remove, chip);
|
||||
-}
|
||||
-EXPORT_SYMBOL_GPL(__devm_pwmchip_add);
|
||||
-
|
||||
static struct device_link *pwm_device_link_add(struct device *dev,
|
||||
struct pwm_device *pwm)
|
||||
{
|
||||
@@ -1923,36 +1797,6 @@ static DEFINE_MUTEX(pwm_lookup_lock);
|
||||
static LIST_HEAD(pwm_lookup_list);
|
||||
|
||||
/**
|
||||
- * pwm_add_table() - register PWM device consumers
|
||||
- * @table: array of consumers to register
|
||||
- * @num: number of consumers in table
|
||||
- */
|
||||
-void pwm_add_table(struct pwm_lookup *table, size_t num)
|
||||
-{
|
||||
- guard(mutex)(&pwm_lookup_lock);
|
||||
-
|
||||
- while (num--) {
|
||||
- list_add_tail(&table->list, &pwm_lookup_list);
|
||||
- table++;
|
||||
- }
|
||||
-}
|
||||
-
|
||||
-/**
|
||||
- * pwm_remove_table() - unregister PWM device consumers
|
||||
- * @table: array of consumers to unregister
|
||||
- * @num: number of consumers in table
|
||||
- */
|
||||
-void pwm_remove_table(struct pwm_lookup *table, size_t num)
|
||||
-{
|
||||
- guard(mutex)(&pwm_lookup_lock);
|
||||
-
|
||||
- while (num--) {
|
||||
- list_del(&table->list);
|
||||
- table++;
|
||||
- }
|
||||
-}
|
||||
-
|
||||
-/**
|
||||
* pwm_get() - look up and request a PWM device
|
||||
* @dev: device for PWM consumer
|
||||
* @con_id: consumer name
|
||||
@@ -2178,6 +2022,162 @@ struct pwm_device *devm_fwnode_pwm_get(s
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(devm_fwnode_pwm_get);
|
||||
|
||||
+/**
|
||||
+ * __pwmchip_add() - register a new PWM chip
|
||||
+ * @chip: the PWM chip to add
|
||||
+ * @owner: reference to the module providing the chip.
|
||||
+ *
|
||||
+ * Register a new PWM chip. @owner is supposed to be THIS_MODULE, use the
|
||||
+ * pwmchip_add wrapper to do this right.
|
||||
+ *
|
||||
+ * Returns: 0 on success or a negative error code on failure.
|
||||
+ */
|
||||
+int __pwmchip_add(struct pwm_chip *chip, struct module *owner)
|
||||
+{
|
||||
+ int ret;
|
||||
+
|
||||
+ if (!chip || !pwmchip_parent(chip) || !chip->ops || !chip->npwm)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ /*
|
||||
+ * a struct pwm_chip must be allocated using (devm_)pwmchip_alloc,
|
||||
+ * otherwise the embedded struct device might disappear too early
|
||||
+ * resulting in memory corruption.
|
||||
+ * Catch drivers that were not converted appropriately.
|
||||
+ */
|
||||
+ if (!chip->uses_pwmchip_alloc)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ if (!pwm_ops_check(chip))
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ chip->owner = owner;
|
||||
+
|
||||
+ if (chip->atomic)
|
||||
+ spin_lock_init(&chip->atomic_lock);
|
||||
+ else
|
||||
+ mutex_init(&chip->nonatomic_lock);
|
||||
+
|
||||
+ guard(mutex)(&pwm_lock);
|
||||
+
|
||||
+ ret = idr_alloc(&pwm_chips, chip, 0, 0, GFP_KERNEL);
|
||||
+ if (ret < 0)
|
||||
+ return ret;
|
||||
+
|
||||
+ chip->id = ret;
|
||||
+
|
||||
+ dev_set_name(&chip->dev, "pwmchip%u", chip->id);
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_OF))
|
||||
+ of_pwmchip_add(chip);
|
||||
+
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = true;
|
||||
+
|
||||
+ ret = device_add(&chip->dev);
|
||||
+ if (ret)
|
||||
+ goto err_device_add;
|
||||
+
|
||||
+ return 0;
|
||||
+
|
||||
+err_device_add:
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = false;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_OF))
|
||||
+ of_pwmchip_remove(chip);
|
||||
+
|
||||
+ idr_remove(&pwm_chips, chip->id);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(__pwmchip_add);
|
||||
+
|
||||
+/**
|
||||
+ * pwmchip_remove() - remove a PWM chip
|
||||
+ * @chip: the PWM chip to remove
|
||||
+ *
|
||||
+ * Removes a PWM chip.
|
||||
+ */
|
||||
+void pwmchip_remove(struct pwm_chip *chip)
|
||||
+{
|
||||
+ pwmchip_sysfs_unexport(chip);
|
||||
+
|
||||
+ scoped_guard(mutex, &pwm_lock) {
|
||||
+ unsigned int i;
|
||||
+
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = false;
|
||||
+
|
||||
+ for (i = 0; i < chip->npwm; ++i) {
|
||||
+ struct pwm_device *pwm = &chip->pwms[i];
|
||||
+
|
||||
+ if (test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
+ dev_warn(&chip->dev, "Freeing requested PWM #%u\n", i);
|
||||
+ if (pwm->chip->ops->free)
|
||||
+ pwm->chip->ops->free(pwm->chip, pwm);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_OF))
|
||||
+ of_pwmchip_remove(chip);
|
||||
+
|
||||
+ idr_remove(&pwm_chips, chip->id);
|
||||
+ }
|
||||
+
|
||||
+ device_del(&chip->dev);
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(pwmchip_remove);
|
||||
+
|
||||
+static void devm_pwmchip_remove(void *data)
|
||||
+{
|
||||
+ struct pwm_chip *chip = data;
|
||||
+
|
||||
+ pwmchip_remove(chip);
|
||||
+}
|
||||
+
|
||||
+int __devm_pwmchip_add(struct device *dev, struct pwm_chip *chip, struct module *owner)
|
||||
+{
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = __pwmchip_add(chip, owner);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ return devm_add_action_or_reset(dev, devm_pwmchip_remove, chip);
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(__devm_pwmchip_add);
|
||||
+
|
||||
+/**
|
||||
+ * pwm_add_table() - register PWM device consumers
|
||||
+ * @table: array of consumers to register
|
||||
+ * @num: number of consumers in table
|
||||
+ */
|
||||
+void pwm_add_table(struct pwm_lookup *table, size_t num)
|
||||
+{
|
||||
+ guard(mutex)(&pwm_lookup_lock);
|
||||
+
|
||||
+ while (num--) {
|
||||
+ list_add_tail(&table->list, &pwm_lookup_list);
|
||||
+ table++;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * pwm_remove_table() - unregister PWM device consumers
|
||||
+ * @table: array of consumers to unregister
|
||||
+ * @num: number of consumers in table
|
||||
+ */
|
||||
+void pwm_remove_table(struct pwm_lookup *table, size_t num)
|
||||
+{
|
||||
+ guard(mutex)(&pwm_lookup_lock);
|
||||
+
|
||||
+ while (num--) {
|
||||
+ list_del(&table->list);
|
||||
+ table++;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
static void pwm_dbg_show(struct pwm_chip *chip, struct seq_file *s)
|
||||
{
|
||||
unsigned int i;
|
||||
@ -0,0 +1,117 @@
|
||||
From 4f3d1be4c2f8a22470f3625cbc778ba2e2130def Mon Sep 17 00:00:00 2001
|
||||
From: Vincent Mailhol <mailhol.vincent@wanadoo.fr>
|
||||
Date: Thu, 14 Nov 2024 02:18:32 +0900
|
||||
Subject: [PATCH] compiler.h: add const_true()
|
||||
|
||||
__builtin_constant_p() is known for not always being able to produce
|
||||
constant expression [1] which led to the introduction of
|
||||
__is_constexpr() [2]. Because of its dependency on
|
||||
__builtin_constant_p(), statically_true() suffers from the same
|
||||
issues.
|
||||
|
||||
For example:
|
||||
|
||||
void foo(int a)
|
||||
{
|
||||
/* fail on GCC */
|
||||
BUILD_BUG_ON_ZERO(statically_true(a));
|
||||
|
||||
/* fail on both clang and GCC */
|
||||
static char arr[statically_true(a) ? 1 : 2];
|
||||
}
|
||||
|
||||
For the same reasons why __is_constexpr() was created to cover
|
||||
__builtin_constant_p() edge cases, __is_constexpr() can be used to
|
||||
resolve statically_true() limitations.
|
||||
|
||||
Note that, somehow, GCC is not always able to fold this:
|
||||
|
||||
__is_constexpr(x) && (x)
|
||||
|
||||
It is OK in BUILD_BUG_ON_ZERO() but not in array declarations nor in
|
||||
static_assert():
|
||||
|
||||
void bar(int a)
|
||||
{
|
||||
/* success */
|
||||
BUILD_BUG_ON_ZERO(__is_constexpr(a) && (a));
|
||||
|
||||
/* fail on GCC */
|
||||
static char arr[__is_constexpr(a) && (a) ? 1 : 2];
|
||||
|
||||
/* fail on GCC */
|
||||
static_assert(__is_constexpr(a) && (a));
|
||||
}
|
||||
|
||||
Encapsulating the expression in a __builtin_choose_expr() switch
|
||||
resolves all these failed tests.
|
||||
|
||||
Define a new const_true() macro which, by making use of the
|
||||
__builtin_choose_expr() and __is_constexpr(x) combo, always produces a
|
||||
constant expression.
|
||||
|
||||
It should be noted that statically_true() is the only one able to fold
|
||||
tautological expressions in which at least one on the operands is not a
|
||||
constant expression. For example:
|
||||
|
||||
statically_true(true || var)
|
||||
statically_true(var == var)
|
||||
statically_true(var * 0 + 1)
|
||||
statically_true(!(var * 8 % 4))
|
||||
|
||||
always evaluates to true, whereas all of these would be false under
|
||||
const_true() if var is not a constant expression [3].
|
||||
|
||||
For this reason, usage of const_true() should be the exception.
|
||||
Reflect in the documentation that const_true() is less powerful and
|
||||
that statically_true() is the overall preferred solution.
|
||||
|
||||
[1] __builtin_constant_p cannot resolve to const when optimizing
|
||||
Link: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=19449
|
||||
|
||||
[2] commit 3c8ba0d61d04 ("kernel.h: Retain constant expression output for max()/min()")
|
||||
Link: https://git.kernel.org/torvalds/c/3c8ba0d61d04
|
||||
|
||||
[3] https://godbolt.org/z/c61PMxqbK
|
||||
|
||||
CC: Linus Torvalds <torvalds@linux-foundation.org>
|
||||
CC: Rasmus Villemoes <linux@rasmusvillemoes.dk>
|
||||
CC: Luc Van Oostenryck <luc.vanoostenryck@gmail.com>
|
||||
Reviewed-by: Yury Norov <yury.norov@gmail.com>,
|
||||
Signed-off-by: Vincent Mailhol <mailhol.vincent@wanadoo.fr>
|
||||
Signed-off-by: Yury Norov <yury.norov@gmail.com>
|
||||
---
|
||||
include/linux/compiler.h | 22 ++++++++++++++++++++++
|
||||
1 file changed, 22 insertions(+)
|
||||
|
||||
--- a/include/linux/compiler.h
|
||||
+++ b/include/linux/compiler.h
|
||||
@@ -303,6 +303,28 @@ static inline void *offset_to_ptr(const
|
||||
#define statically_true(x) (__builtin_constant_p(x) && (x))
|
||||
|
||||
/*
|
||||
+ * Similar to statically_true() but produces a constant expression
|
||||
+ *
|
||||
+ * To be used in conjunction with macros, such as BUILD_BUG_ON_ZERO(),
|
||||
+ * which require their input to be a constant expression and for which
|
||||
+ * statically_true() would otherwise fail.
|
||||
+ *
|
||||
+ * This is a trade-off: const_true() requires all its operands to be
|
||||
+ * compile time constants. Else, it would always returns false even on
|
||||
+ * the most trivial cases like:
|
||||
+ *
|
||||
+ * true || non_const_var
|
||||
+ *
|
||||
+ * On the opposite, statically_true() is able to fold more complex
|
||||
+ * tautologies and will return true on expressions such as:
|
||||
+ *
|
||||
+ * !(non_const_var * 8 % 4)
|
||||
+ *
|
||||
+ * For the general case, statically_true() is better.
|
||||
+ */
|
||||
+#define const_true(x) __builtin_choose_expr(__is_constexpr(x), x, false)
|
||||
+
|
||||
+/*
|
||||
* This is needed in functions which generate the stack canary, see
|
||||
* arch/x86/kernel/smpboot.c::start_secondary() for an example.
|
||||
*/
|
||||
@ -0,0 +1,99 @@
|
||||
From da6b353786997c0ffa67127355ad1d54ed3324c2 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Thu, 23 Jan 2025 18:27:07 +0100
|
||||
Subject: [PATCH] pwm: Ensure callbacks exist before calling them
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
If one of the waveform functions is called for a chip that only supports
|
||||
.apply(), we want that an error code is returned and not a NULL pointer
|
||||
exception.
|
||||
|
||||
Fixes: 6c5126c6406d ("pwm: Provide new consumer API functions for waveforms")
|
||||
Cc: stable@vger.kernel.org
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Tested-by: Trevor Gamblin <tgamblin@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/20250123172709.391349-2-u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 13 +++++++++++--
|
||||
include/linux/pwm.h | 17 +++++++++++++++++
|
||||
2 files changed, 28 insertions(+), 2 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -242,6 +242,9 @@ int pwm_round_waveform_might_sleep(struc
|
||||
|
||||
BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
+ if (!pwmchip_supports_waveform(chip))
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
if (!pwm_wf_valid(wf))
|
||||
return -EINVAL;
|
||||
|
||||
@@ -294,6 +297,9 @@ int pwm_get_waveform_might_sleep(struct
|
||||
|
||||
BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
+ if (!pwmchip_supports_waveform(chip) || !ops->read_waveform)
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
guard(pwmchip)(chip);
|
||||
|
||||
if (!chip->operational)
|
||||
@@ -320,6 +326,9 @@ static int __pwm_set_waveform(struct pwm
|
||||
|
||||
BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
+ if (!pwmchip_supports_waveform(chip))
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
if (!pwm_wf_valid(wf))
|
||||
return -EINVAL;
|
||||
|
||||
@@ -592,7 +601,7 @@ static int __pwm_apply(struct pwm_device
|
||||
state->usage_power == pwm->state.usage_power)
|
||||
return 0;
|
||||
|
||||
- if (ops->write_waveform) {
|
||||
+ if (pwmchip_supports_waveform(chip)) {
|
||||
struct pwm_waveform wf;
|
||||
char wfhw[WFHWSIZE];
|
||||
|
||||
@@ -728,7 +737,7 @@ static int pwm_get_state_hw(struct pwm_d
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
int ret = -EOPNOTSUPP;
|
||||
|
||||
- if (ops->read_waveform) {
|
||||
+ if (pwmchip_supports_waveform(chip) && ops->read_waveform) {
|
||||
char wfhw[WFHWSIZE];
|
||||
struct pwm_waveform wf;
|
||||
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -342,6 +342,23 @@ struct pwm_chip {
|
||||
struct pwm_device pwms[] __counted_by(npwm);
|
||||
};
|
||||
|
||||
+/**
|
||||
+ * pwmchip_supports_waveform() - checks if the given chip supports waveform callbacks
|
||||
+ * @chip: The pwm_chip to test
|
||||
+ *
|
||||
+ * Returns true iff the pwm chip support the waveform functions like
|
||||
+ * pwm_set_waveform_might_sleep() and pwm_round_waveform_might_sleep()
|
||||
+ */
|
||||
+static inline bool pwmchip_supports_waveform(struct pwm_chip *chip)
|
||||
+{
|
||||
+ /*
|
||||
+ * only check for .write_waveform(). If that is available,
|
||||
+ * .round_waveform_tohw() and .round_waveform_fromhw() asserted to be
|
||||
+ * available, too, in pwmchip_add().
|
||||
+ */
|
||||
+ return chip->ops->write_waveform != NULL;
|
||||
+}
|
||||
+
|
||||
static inline struct device *pwmchip_parent(const struct pwm_chip *chip)
|
||||
{
|
||||
return chip->dev.parent;
|
||||
@ -0,0 +1,61 @@
|
||||
From 895fe4537cc8586f51abb5c66524efaa42c29883 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Thu, 6 Feb 2025 13:06:25 +0100
|
||||
Subject: [PATCH] pwm: Add upgrade path to #pwm-cells = <3> for users of
|
||||
of_pwm_single_xlate()
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
The PWM chip on PXA only has a single output. Back when the device tree
|
||||
binding was defined it was considered a good idea to not pass the PWM
|
||||
line index as is done for all other PWM types as it would be always zero
|
||||
anyhow and so doesn't add any value.
|
||||
|
||||
However for consistency reasons it is nice when all PWMs use the same
|
||||
binding. For that reason let of_pwm_single_xlate() (i.e. the function
|
||||
that implements the PXA behaviour) behave in the same way as
|
||||
of_pwm_xlate_with_flags() for 3 (or more) parameters. With that in
|
||||
place, the pxa-pwm binding can be updated to #pwm-cells = <3> without
|
||||
breaking old device trees that stick to #pwm-cells = <1>.
|
||||
|
||||
Reviewed-by: Herve Codina <herve.codina@bootlin.com>
|
||||
Tested-by: Duje Mihanovi? <duje.mihanovic@skole.hr>
|
||||
Reviewed-by: Daniel Mack <daniel@zonque.org>
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/b33a84d3f073880e94fc303cd32ebe095eb5ce46.1738842938.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 16 ++++++++++++++++
|
||||
1 file changed, 16 insertions(+)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -988,11 +988,27 @@ of_pwm_xlate_with_flags(struct pwm_chip
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(of_pwm_xlate_with_flags);
|
||||
|
||||
+/*
|
||||
+ * This callback is used for PXA PWM chips that only have a single PWM line.
|
||||
+ * For such chips you could argue that passing the line number (i.e. the first
|
||||
+ * parameter in the common case) is useless as it's always zero. So compared to
|
||||
+ * the default xlate function of_pwm_xlate_with_flags() the first parameter is
|
||||
+ * the default period and the second are flags.
|
||||
+ *
|
||||
+ * Note that if #pwm-cells = <3>, the semantic is the same as for
|
||||
+ * of_pwm_xlate_with_flags() to allow converting the affected driver to
|
||||
+ * #pwm-cells = <3> without breaking the legacy binding.
|
||||
+ *
|
||||
+ * Don't use for new drivers.
|
||||
+ */
|
||||
struct pwm_device *
|
||||
of_pwm_single_xlate(struct pwm_chip *chip, const struct of_phandle_args *args)
|
||||
{
|
||||
struct pwm_device *pwm;
|
||||
|
||||
+ if (args->args_count >= 3)
|
||||
+ return of_pwm_xlate_with_flags(chip, args);
|
||||
+
|
||||
pwm = pwm_request_from_chip(chip, 0, NULL);
|
||||
if (IS_ERR(pwm))
|
||||
return pwm;
|
||||
@ -0,0 +1,71 @@
|
||||
From 00e53d0f4baedd72196b65f00698b2a5a537dc2b Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Sat, 5 Apr 2025 11:27:12 +0200
|
||||
Subject: [PATCH] pwm: Let pwm_set_waveform() succeed even if lowlevel driver
|
||||
rounded up
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Waveform parameters are supposed to be rounded down to the next value
|
||||
possible for the hardware. However when a requested value is too small,
|
||||
.round_waveform_tohw() is supposed to pick the next bigger value and
|
||||
return 1. Let pwm_set_waveform() behave in the same way.
|
||||
|
||||
This creates consistency between pwm_set_waveform_might_sleep() with
|
||||
exact=false and pwm_round_waveform_might_sleep() +
|
||||
pwm_set_waveform_might_sleep() with exact=true.
|
||||
|
||||
The PWM_DEBUG rounding check has to be adapted to only trigger if no
|
||||
uprounding happend.
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Tested-by: Trevor Gamblin <tgamblin@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/353dc6ae31be815e41fd3df89c257127ca0d1a09.1743844730.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 13 +++++++------
|
||||
1 file changed, 7 insertions(+), 6 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -322,7 +322,7 @@ static int __pwm_set_waveform(struct pwm
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
char wfhw[WFHWSIZE];
|
||||
struct pwm_waveform wf_rounded;
|
||||
- int err;
|
||||
+ int err, ret_tohw;
|
||||
|
||||
BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
@@ -332,16 +332,16 @@ static int __pwm_set_waveform(struct pwm
|
||||
if (!pwm_wf_valid(wf))
|
||||
return -EINVAL;
|
||||
|
||||
- err = __pwm_round_waveform_tohw(chip, pwm, wf, &wfhw);
|
||||
- if (err)
|
||||
- return err;
|
||||
+ ret_tohw = __pwm_round_waveform_tohw(chip, pwm, wf, &wfhw);
|
||||
+ if (ret_tohw < 0)
|
||||
+ return ret_tohw;
|
||||
|
||||
if ((IS_ENABLED(CONFIG_PWM_DEBUG) || exact) && wf->period_length_ns) {
|
||||
err = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf_rounded);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
- if (IS_ENABLED(CONFIG_PWM_DEBUG) && !pwm_check_rounding(wf, &wf_rounded))
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && ret_tohw == 0 && !pwm_check_rounding(wf, &wf_rounded))
|
||||
dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
@@ -382,7 +382,8 @@ static int __pwm_set_waveform(struct pwm
|
||||
wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns,
|
||||
wf_set.duty_length_ns, wf_set.period_length_ns, wf_set.duty_offset_ns);
|
||||
}
|
||||
- return 0;
|
||||
+
|
||||
+ return ret_tohw;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -0,0 +1,54 @@
|
||||
From 96d20cfd16e779923153f7347b0bef6b3c7606ce Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Sat, 5 Apr 2025 11:27:17 +0200
|
||||
Subject: [PATCH] pwm: Do stricter return value checking for
|
||||
.round_waveform_tohw()
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
The .round_waveform_tohw() is supposed to return 0 if the request could
|
||||
be rounded down to match the hardware capabilities and return 1 if
|
||||
rounding down wasn't possible.
|
||||
|
||||
Expand the PWM_DEBUG check to not only assert proper downrounding if 0
|
||||
was returned but also check that it was actually rounded up when the
|
||||
callback signalled uprounding.
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/dfb824ae37f99df068c752d48cbd163c044a74fb.1743844730.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 12 ++++++------
|
||||
1 file changed, 6 insertions(+), 6 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -270,10 +270,10 @@ int pwm_round_waveform_might_sleep(struc
|
||||
wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns, ret_tohw);
|
||||
|
||||
if (IS_ENABLED(CONFIG_PWM_DEBUG) &&
|
||||
- ret_tohw == 0 && !pwm_check_rounding(&wf_req, wf))
|
||||
- dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ (ret_tohw == 0) != pwm_check_rounding(&wf_req, wf))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu], ret: %d\n",
|
||||
wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns,
|
||||
- wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns);
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns, ret_tohw);
|
||||
|
||||
return ret_tohw;
|
||||
}
|
||||
@@ -341,10 +341,10 @@ static int __pwm_set_waveform(struct pwm
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
- if (IS_ENABLED(CONFIG_PWM_DEBUG) && ret_tohw == 0 && !pwm_check_rounding(wf, &wf_rounded))
|
||||
- dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && (ret_tohw == 0) != pwm_check_rounding(wf, &wf_rounded))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu], ret: %d\n",
|
||||
wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
- wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns, ret_tohw);
|
||||
|
||||
if (exact && pwmwfcmp(wf, &wf_rounded)) {
|
||||
dev_dbg(&chip->dev, "Requested no rounding, but %llu/%llu [+%llu] -> %llu/%llu [+%llu]\n",
|
||||
@ -0,0 +1,38 @@
|
||||
From e463b05d10da12b13d03f41a407e2ad043af158f Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Tue, 8 Apr 2025 16:23:54 +0200
|
||||
Subject: [PATCH] pwm: Better document return value of
|
||||
pwm_round_waveform_might_sleep()
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Better explain how pwm_round_waveform_might_sleep() (and so the
|
||||
respective lowlevel driver callback) is supposed to round and the
|
||||
meaning of the return value.
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/db84abf1e82e4498fc0e7c318d2673771d0039fe.1744120697.git.ukleinek@kernel.org
|
||||
[ukleinek: Fix a rst formatting issue reported by Stephen Rothwell]
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 8 ++++++--
|
||||
1 file changed, 6 insertions(+), 2 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -229,8 +229,12 @@ static int __pwm_write_waveform(struct p
|
||||
* these two calls and the waveform determined by
|
||||
* pwm_round_waveform_might_sleep() cannot be implemented any more.
|
||||
*
|
||||
- * Returns 0 on success, 1 if there is no valid hardware configuration matching
|
||||
- * the input waveform under the PWM rounding rules or a negative errno.
|
||||
+ * Usually all values passed in @wf are rounded down to the nearest possible
|
||||
+ * value (in the order period_length_ns, duty_length_ns and then
|
||||
+ * duty_offset_ns). Only if this isn't possible, a value might grow.
|
||||
+ *
|
||||
+ * Returns 0 on success, 1 if at least one value had to be rounded up or a
|
||||
+ * negative errno.
|
||||
*/
|
||||
int pwm_round_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
{
|
||||
@ -0,0 +1,157 @@
|
||||
From 7f8ce4d88b42fcbd3350370ec4d02e00979fc5a9 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Thu, 17 Apr 2025 20:16:11 +0200
|
||||
Subject: [PATCH] pwm: Fix various formatting issues in kernel-doc
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Add Return and (where interesting) Context sections, fix some formatting
|
||||
and drop documenting the internal function __pwm_apply().
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/20250417181611.2693599-2-u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 42 +++++++++++++++++++++++++++---------------
|
||||
include/linux/pwm.h | 8 +++++---
|
||||
2 files changed, 32 insertions(+), 18 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -216,14 +216,14 @@ static int __pwm_write_waveform(struct p
|
||||
*
|
||||
* Typically a given waveform cannot be implemented exactly by hardware, e.g.
|
||||
* because hardware only supports coarse period resolution or no duty_offset.
|
||||
- * This function returns the actually implemented waveform if you pass wf to
|
||||
- * pwm_set_waveform_might_sleep now.
|
||||
+ * This function returns the actually implemented waveform if you pass @wf to
|
||||
+ * pwm_set_waveform_might_sleep() now.
|
||||
*
|
||||
* Note however that the world doesn't stop turning when you call it, so when
|
||||
- * doing
|
||||
+ * doing::
|
||||
*
|
||||
- * pwm_round_waveform_might_sleep(mypwm, &wf);
|
||||
- * pwm_set_waveform_might_sleep(mypwm, &wf, true);
|
||||
+ * pwm_round_waveform_might_sleep(mypwm, &wf);
|
||||
+ * pwm_set_waveform_might_sleep(mypwm, &wf, true);
|
||||
*
|
||||
* the latter might fail, e.g. because an input clock changed its rate between
|
||||
* these two calls and the waveform determined by
|
||||
@@ -233,8 +233,9 @@ static int __pwm_write_waveform(struct p
|
||||
* value (in the order period_length_ns, duty_length_ns and then
|
||||
* duty_offset_ns). Only if this isn't possible, a value might grow.
|
||||
*
|
||||
- * Returns 0 on success, 1 if at least one value had to be rounded up or a
|
||||
+ * Returns: 0 on success, 1 if at least one value had to be rounded up or a
|
||||
* negative errno.
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_round_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
{
|
||||
@@ -291,6 +292,9 @@ EXPORT_SYMBOL_GPL(pwm_round_waveform_mig
|
||||
*
|
||||
* Stores the current configuration of the PWM in @wf. Note this is the
|
||||
* equivalent of pwm_get_state_hw() (and not pwm_get_state()) for pwm_waveform.
|
||||
+ *
|
||||
+ * Returns: 0 on success or a negative errno
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_get_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
{
|
||||
@@ -399,13 +403,17 @@ static int __pwm_set_waveform(struct pwm
|
||||
*
|
||||
* Typically a requested waveform cannot be implemented exactly, e.g. because
|
||||
* you requested .period_length_ns = 100 ns, but the hardware can only set
|
||||
- * periods that are a multiple of 8.5 ns. With that hardware passing exact =
|
||||
+ * periods that are a multiple of 8.5 ns. With that hardware passing @exact =
|
||||
* true results in pwm_set_waveform_might_sleep() failing and returning 1. If
|
||||
- * exact = false you get a period of 93.5 ns (i.e. the biggest period not bigger
|
||||
+ * @exact = false you get a period of 93.5 ns (i.e. the biggest period not bigger
|
||||
* than the requested value).
|
||||
- * Note that even with exact = true, some rounding by less than 1 is
|
||||
+ * Note that even with @exact = true, some rounding by less than 1 ns is
|
||||
* possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
- * exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
+ * @exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
+ *
|
||||
+ * Returns: 0 on success, 1 if was rounded up (if !@exact) or no perfect match was
|
||||
+ * possible (if @exact), or a negative errno
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_set_waveform_might_sleep(struct pwm_device *pwm,
|
||||
const struct pwm_waveform *wf, bool exact)
|
||||
@@ -565,11 +573,6 @@ static bool pwm_state_valid(const struct
|
||||
return true;
|
||||
}
|
||||
|
||||
-/**
|
||||
- * __pwm_apply() - atomically apply a new state to a PWM device
|
||||
- * @pwm: PWM device
|
||||
- * @state: new state to apply
|
||||
- */
|
||||
static int __pwm_apply(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
struct pwm_chip *chip;
|
||||
@@ -678,6 +681,9 @@ static int __pwm_apply(struct pwm_device
|
||||
* Cannot be used in atomic context.
|
||||
* @pwm: PWM device
|
||||
* @state: new state to apply
|
||||
+ *
|
||||
+ * Returns: 0 on success, or a negative errno
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_apply_might_sleep(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
@@ -719,6 +725,9 @@ EXPORT_SYMBOL_GPL(pwm_apply_might_sleep)
|
||||
* Not all PWM devices support this function, check with pwm_might_sleep().
|
||||
* @pwm: PWM device
|
||||
* @state: new state to apply
|
||||
+ *
|
||||
+ * Returns: 0 on success, or a negative errno
|
||||
+ * Context: Any
|
||||
*/
|
||||
int pwm_apply_atomic(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
@@ -778,6 +787,9 @@ static int pwm_get_state_hw(struct pwm_d
|
||||
* This function will adjust the PWM config to the PWM arguments provided
|
||||
* by the DT or PWM lookup table. This is particularly useful to adapt
|
||||
* the bootloader config to the Linux one.
|
||||
+ *
|
||||
+ * Returns: 0 on success or a negative error code on failure.
|
||||
+ * Context: May sleep.
|
||||
*/
|
||||
int pwm_adjust_config(struct pwm_device *pwm)
|
||||
{
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -218,6 +218,8 @@ static inline void pwm_init_state(const
|
||||
*
|
||||
* pwm_get_state(pwm, &state);
|
||||
* duty = pwm_get_relative_duty_cycle(&state, 100);
|
||||
+ *
|
||||
+ * Returns: rounded relative duty cycle multiplied by @scale
|
||||
*/
|
||||
static inline unsigned int
|
||||
pwm_get_relative_duty_cycle(const struct pwm_state *state, unsigned int scale)
|
||||
@@ -244,8 +246,8 @@ pwm_get_relative_duty_cycle(const struct
|
||||
* pwm_set_relative_duty_cycle(&state, 50, 100);
|
||||
* pwm_apply_might_sleep(pwm, &state);
|
||||
*
|
||||
- * This functions returns -EINVAL if @duty_cycle and/or @scale are
|
||||
- * inconsistent (@scale == 0 or @duty_cycle > @scale).
|
||||
+ * Returns: 0 on success or ``-EINVAL`` if @duty_cycle and/or @scale are
|
||||
+ * inconsistent (@scale == 0 or @duty_cycle > @scale)
|
||||
*/
|
||||
static inline int
|
||||
pwm_set_relative_duty_cycle(struct pwm_state *state, unsigned int duty_cycle,
|
||||
@@ -346,7 +348,7 @@ struct pwm_chip {
|
||||
* pwmchip_supports_waveform() - checks if the given chip supports waveform callbacks
|
||||
* @chip: The pwm_chip to test
|
||||
*
|
||||
- * Returns true iff the pwm chip support the waveform functions like
|
||||
+ * Returns: true iff the pwm chip support the waveform functions like
|
||||
* pwm_set_waveform_might_sleep() and pwm_round_waveform_might_sleep()
|
||||
*/
|
||||
static inline bool pwmchip_supports_waveform(struct pwm_chip *chip)
|
||||
@ -0,0 +1,64 @@
|
||||
From e866834c8baabc33b431902beeeb0c94dfbc1024 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Wed, 30 Apr 2025 13:55:58 +0200
|
||||
Subject: [PATCH] pwm: Let pwm_set_waveform_might_sleep() fail for exact but
|
||||
impossible requests
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Up to now pwm_set_waveform_might_sleep() returned 1 for exact requests
|
||||
that couldn't be served exactly. In contrast to
|
||||
pwm_round_waveform_might_sleep() and pwm_set_waveform_might_sleep() with
|
||||
exact = false this is an error condition. So simplify handling for
|
||||
callers of pwm_set_waveform_might_sleep() by returning -EDOM instead of
|
||||
1 in this case.
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/20538a46719584dafd8a1395c886780a97dcdf79.1746010245.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 21 ++++++++++++++++-----
|
||||
1 file changed, 16 insertions(+), 5 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -404,15 +404,16 @@ static int __pwm_set_waveform(struct pwm
|
||||
* Typically a requested waveform cannot be implemented exactly, e.g. because
|
||||
* you requested .period_length_ns = 100 ns, but the hardware can only set
|
||||
* periods that are a multiple of 8.5 ns. With that hardware passing @exact =
|
||||
- * true results in pwm_set_waveform_might_sleep() failing and returning 1. If
|
||||
- * @exact = false you get a period of 93.5 ns (i.e. the biggest period not bigger
|
||||
- * than the requested value).
|
||||
+ * true results in pwm_set_waveform_might_sleep() failing and returning -EDOM.
|
||||
+ * If @exact = false you get a period of 93.5 ns (i.e. the biggest period not
|
||||
+ * bigger than the requested value).
|
||||
* Note that even with @exact = true, some rounding by less than 1 ns is
|
||||
* possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
* @exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
*
|
||||
- * Returns: 0 on success, 1 if was rounded up (if !@exact) or no perfect match was
|
||||
- * possible (if @exact), or a negative errno
|
||||
+ * Returns: 0 on success, 1 if was rounded up (if !@exact), -EDOM if setting
|
||||
+ * failed due to the exact waveform not being possible (if @exact), or a
|
||||
+ * different negative errno on failure.
|
||||
* Context: May sleep.
|
||||
*/
|
||||
int pwm_set_waveform_might_sleep(struct pwm_device *pwm,
|
||||
@@ -440,6 +441,16 @@ int pwm_set_waveform_might_sleep(struct
|
||||
err = __pwm_set_waveform(pwm, wf, exact);
|
||||
}
|
||||
|
||||
+ /*
|
||||
+ * map err == 1 to -EDOM for exact requests. Also make sure that -EDOM is
|
||||
+ * only returned in exactly that case. Note that __pwm_set_waveform()
|
||||
+ * should never return -EDOM which justifies the unlikely().
|
||||
+ */
|
||||
+ if (unlikely(err == -EDOM))
|
||||
+ err = -EINVAL;
|
||||
+ else if (exact && err == 1)
|
||||
+ err = -EDOM;
|
||||
+
|
||||
return err;
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(pwm_set_waveform_might_sleep);
|
||||
@ -0,0 +1,58 @@
|
||||
From 164c4ac754abaf9643815d09001cc7d81042d624 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Wed, 30 Apr 2025 13:55:59 +0200
|
||||
Subject: [PATCH] pwm: Let pwm_set_waveform_might_sleep() return 0 instead of 1
|
||||
after rounding up
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
While telling the caller of pwm_set_waveform_might_sleep() if the
|
||||
request was completed by rounding down only or (some) rounding up gives
|
||||
additional information, it makes usage this function needlessly hard and
|
||||
the additional information is not used. A prove for that is that
|
||||
currently both users of this function just pass the returned value up to
|
||||
their caller even though a positive value isn't intended there.
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/528cc3bbd9e35dea8646b1bcc0fbfe6c498bb4ed.1746010245.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 14 ++++++++------
|
||||
1 file changed, 8 insertions(+), 6 deletions(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -411,9 +411,8 @@ static int __pwm_set_waveform(struct pwm
|
||||
* possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
* @exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
*
|
||||
- * Returns: 0 on success, 1 if was rounded up (if !@exact), -EDOM if setting
|
||||
- * failed due to the exact waveform not being possible (if @exact), or a
|
||||
- * different negative errno on failure.
|
||||
+ * Returns: 0 on success, -EDOM if setting failed due to the exact waveform not
|
||||
+ * being possible (if @exact), or a different negative errno on failure.
|
||||
* Context: May sleep.
|
||||
*/
|
||||
int pwm_set_waveform_might_sleep(struct pwm_device *pwm,
|
||||
@@ -442,14 +441,17 @@ int pwm_set_waveform_might_sleep(struct
|
||||
}
|
||||
|
||||
/*
|
||||
- * map err == 1 to -EDOM for exact requests. Also make sure that -EDOM is
|
||||
- * only returned in exactly that case. Note that __pwm_set_waveform()
|
||||
- * should never return -EDOM which justifies the unlikely().
|
||||
+ * map err == 1 to -EDOM for exact requests and 0 for !exact ones. Also
|
||||
+ * make sure that -EDOM is only returned in exactly that case. Note that
|
||||
+ * __pwm_set_waveform() should never return -EDOM which justifies the
|
||||
+ * unlikely().
|
||||
*/
|
||||
if (unlikely(err == -EDOM))
|
||||
err = -EINVAL;
|
||||
else if (exact && err == 1)
|
||||
err = -EDOM;
|
||||
+ else if (err == 1)
|
||||
+ err = 0;
|
||||
|
||||
return err;
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
From d041b76ac9fb9e60e7cdb0265ed9d8b6058a88bf Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Wed, 30 Apr 2025 13:56:00 +0200
|
||||
Subject: [PATCH] pwm: Formally describe the procedure used to pick a hardware
|
||||
waveform setting
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This serves as specification for both, PWM consumers and the respective
|
||||
callback for lowlevel drivers.
|
||||
|
||||
Signed-off-by: Uwe Kleine-K?nig <u.kleine-koenig@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/d2916bfa70274961ded26b07ab6998c36b90e69a.1746010245.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-K?nig <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 24 +++++++++++++++++++++++-
|
||||
1 file changed, 23 insertions(+), 1 deletion(-)
|
||||
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -231,7 +231,9 @@ static int __pwm_write_waveform(struct p
|
||||
*
|
||||
* Usually all values passed in @wf are rounded down to the nearest possible
|
||||
* value (in the order period_length_ns, duty_length_ns and then
|
||||
- * duty_offset_ns). Only if this isn't possible, a value might grow.
|
||||
+ * duty_offset_ns). Only if this isn't possible, a value might grow. See the
|
||||
+ * documentation for pwm_set_waveform_might_sleep() for a more formal
|
||||
+ * description.
|
||||
*
|
||||
* Returns: 0 on success, 1 if at least one value had to be rounded up or a
|
||||
* negative errno.
|
||||
@@ -411,6 +413,26 @@ static int __pwm_set_waveform(struct pwm
|
||||
* possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
* @exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
*
|
||||
+ * Let C be the set of possible hardware configurations for a given PWM device,
|
||||
+ * consisting of tuples (p, d, o) where p is the period length, d is the duty
|
||||
+ * length and o the duty offset.
|
||||
+ *
|
||||
+ * The following algorithm is implemented to pick the hardware setting
|
||||
+ * (p, d, o) ( C for a given request (p', d', o') with @exact = false::
|
||||
+ *
|
||||
+ * p = max( { ? | ? ?, ? : (?, ?, ?) ( C … ? + p' } “ { min({ ? | ? ?, ? : (?, ?, ?) ( C }) })
|
||||
+ * d = max( { ? | ? ? : (p, ?, ?) ( C … ? + d' } “ { min({ ? | ? ? : (p, ?, ?) ( C }) })
|
||||
+ * o = max( { ? | (p, d, ?) ( C … ? + o' } “ { min({ ? | (p, d, ?) ( C }) })
|
||||
+ *
|
||||
+ * In words: The chosen period length is the maximal possible period length not
|
||||
+ * bigger than the requested period length and if that doesn't exist, the
|
||||
+ * minimal period length. The chosen duty length is the maximal possible duty
|
||||
+ * length that is compatible with the chosen period length and isn't bigger than
|
||||
+ * the requested duty length. Again if such a value doesn't exist, the minimal
|
||||
+ * duty length compatible with the chosen period is picked. After that the duty
|
||||
+ * offset compatible with the chosen period and duty length is chosen in the
|
||||
+ * same way.
|
||||
+ *
|
||||
* Returns: 0 on success, -EDOM if setting failed due to the exact waveform not
|
||||
* being possible (if @exact), or a different negative errno on failure.
|
||||
* Context: May sleep.
|
||||
@ -0,0 +1,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 */
|
||||
@ -1,53 +1,83 @@
|
||||
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/soc/rockchip/Kconfig | 13 +
|
||||
drivers/soc/rockchip/Makefile | 1 +
|
||||
drivers/soc/rockchip/mfpwm.c | 608 ++++++++++++++++++++++++++++++++++++++++++
|
||||
include/soc/rockchip/mfpwm.h | 505 +++++++++++++++++++++++++++++++++++
|
||||
5 files changed, 1129 insertions(+)
|
||||
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/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -19962,6 +19962,8 @@ L: linux-rockchip@lists.infradead.org
|
||||
L: linux-pwm@vger.kernel.org
|
||||
S: Maintained
|
||||
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
|
||||
+F: drivers/soc/rockchip/mfpwm.c
|
||||
+F: include/soc/rockchip/mfpwm.h
|
||||
--- 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.
|
||||
|
||||
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
|
||||
M: Daniel Golle <daniel@makrotopia.org>
|
||||
--- a/drivers/soc/rockchip/Kconfig
|
||||
+++ b/drivers/soc/rockchip/Kconfig
|
||||
@@ -30,4 +30,17 @@ config ROCKCHIP_DTPM
|
||||
on this platform. That will create all the power capping capable
|
||||
devices.
|
||||
|
||||
+config ROCKCHIP_MFPWM
|
||||
+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, with drivers for them
|
||||
+ implemented in their respective subsystems.
|
||||
+ functionalities at runtime as needed. Drivers for them are implemented
|
||||
+ in their respective subsystems.
|
||||
+
|
||||
endif
|
||||
--- a/drivers/soc/rockchip/Makefile
|
||||
+++ b/drivers/soc/rockchip/Makefile
|
||||
@@ -5,3 +5,4 @@
|
||||
obj-$(CONFIG_ROCKCHIP_GRF) += grf.o
|
||||
obj-$(CONFIG_ROCKCHIP_IODOMAIN) += io-domain.o
|
||||
obj-$(CONFIG_ROCKCHIP_DTPM) += dtpm.o
|
||||
+obj-$(CONFIG_ROCKCHIP_MFPWM) += mfpwm.o
|
||||
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/soc/rockchip/mfpwm.c
|
||||
@@ -0,0 +1,608 @@
|
||||
+++ b/drivers/mfd/rockchip-mfpwm.c
|
||||
@@ -0,0 +1,340 @@
|
||||
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
@ -64,13 +94,16 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * 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>
|
||||
+#include <soc/rockchip/mfpwm.h>
|
||||
+
|
||||
+/**
|
||||
+ * struct rockchip_mfpwm - private mfpwm driver instance state struct
|
||||
@ -78,20 +111,14 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * @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
|
||||
+ * @chosen_clk: is one of either @pwm_clk or @osc_clk, depending on choice.
|
||||
+ * May only be swapped out while holding @state_lock.
|
||||
+ * @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
|
||||
+ * @pwm_dev: pointer to the &struct platform_device of the pwm output driver
|
||||
+ * @counter_dev: pointer to the &struct platform_device of the counter driver
|
||||
+ * @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.
|
||||
+ * 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.
|
||||
+ * @pwmclk_enable_cnt: number of times @active_func has enabled the pwmclk sans
|
||||
+ * disabling it. Must only be checked or modified while
|
||||
+ * holding @state_lock. Only exists to fix a splat on mfpwm
|
||||
+ * driver remove.
|
||||
+ * @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
|
||||
@ -101,13 +128,11 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ void __iomem *base;
|
||||
+ struct clk *pwm_clk;
|
||||
+ struct clk *osc_clk;
|
||||
+ struct clk *rc_clk;
|
||||
+ struct clk *chosen_clk;
|
||||
+ struct clk *pclk;
|
||||
+ struct platform_device *pwm_dev;
|
||||
+ struct platform_device *counter_dev;
|
||||
+ struct rockchip_mfpwm_func *active_func;
|
||||
+ unsigned int acquire_cnt;
|
||||
+ unsigned int pwmclk_enable_cnt;
|
||||
+ spinlock_t state_lock;
|
||||
+ int irq;
|
||||
+};
|
||||
@ -119,27 +144,20 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ return platform_get_drvdata(pdev);
|
||||
+}
|
||||
+
|
||||
+unsigned long mfpwm_clk_get_rate(struct rockchip_mfpwm *mfpwm)
|
||||
+{
|
||||
+ if (!mfpwm || !mfpwm->chosen_clk)
|
||||
+ return 0;
|
||||
+
|
||||
+ return clk_get_rate(mfpwm->chosen_clk);
|
||||
+}
|
||||
+EXPORT_SYMBOL_NS_GPL(mfpwm_clk_get_rate, ROCKCHIP_MFPWM);
|
||||
+
|
||||
+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)) {
|
||||
+ WARN(1, "called %s with an erroneous handle, no effect\n",
|
||||
+ fname);
|
||||
+ dev_warn(dev, "called %s with an erroneous handle, no effect\n",
|
||||
+ fname);
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+
|
||||
+ if (IS_ERR_OR_NULL(pwmf->parent)) {
|
||||
+ WARN(1, "called %s with an erroneous mfpwm_func parent, no effect\n",
|
||||
+ fname);
|
||||
+ dev_warn(dev, "called %s with an erroneous mfpwm_func parent, no effect\n",
|
||||
+ fname);
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+
|
||||
@ -147,56 +165,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+}
|
||||
+
|
||||
+__attribute__((nonnull))
|
||||
+static bool mfpwm_pwmf_is_active_pwmf(const struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ if (pwmf->parent->active_func) {
|
||||
+ if (pwmf->parent->active_func->id == pwmf->id)
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+int mfpwm_pwmclk_enable(struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ unsigned long flags;
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = mfpwm_check_pwmf(pwmf, "mfpwm_pwmclk_enable");
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ spin_lock_irqsave(&pwmf->parent->state_lock, flags);
|
||||
+ if (mfpwm_pwmf_is_active_pwmf(pwmf)) {
|
||||
+ ret = clk_enable(pwmf->parent->chosen_clk);
|
||||
+ pwmf->parent->pwmclk_enable_cnt++;
|
||||
+ } else {
|
||||
+ ret = -EBUSY;
|
||||
+ }
|
||||
+
|
||||
+ spin_unlock_irqrestore(&pwmf->parent->state_lock, flags);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+EXPORT_SYMBOL_NS_GPL(mfpwm_pwmclk_enable, ROCKCHIP_MFPWM);
|
||||
+
|
||||
+void mfpwm_pwmclk_disable(struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ unsigned long flags;
|
||||
+
|
||||
+ if (mfpwm_check_pwmf(pwmf, "mfpwm_pwmclk_enable"))
|
||||
+ return;
|
||||
+
|
||||
+ spin_lock_irqsave(&pwmf->parent->state_lock, flags);
|
||||
+ if (mfpwm_pwmf_is_active_pwmf(pwmf)) {
|
||||
+ clk_disable(pwmf->parent->chosen_clk);
|
||||
+ pwmf->parent->pwmclk_enable_cnt--;
|
||||
+ }
|
||||
+ spin_unlock_irqrestore(&pwmf->parent->state_lock, flags);
|
||||
+}
|
||||
+EXPORT_SYMBOL_NS_GPL(mfpwm_pwmclk_disable, ROCKCHIP_MFPWM);
|
||||
+
|
||||
+__attribute__((nonnull))
|
||||
+static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
|
||||
@ -211,7 +179,8 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
|
||||
+ mfpwm->acquire_cnt = cnt;
|
||||
+ } else {
|
||||
+ WARN(1, "prevented acquire counter overflow in %s\n", __func__);
|
||||
+ dev_warn(&mfpwm->pdev->dev, "prevented acquire counter overflow in %s\n",
|
||||
+ __func__);
|
||||
+ return -EOVERFLOW;
|
||||
+ }
|
||||
+
|
||||
@ -286,63 +255,21 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+}
|
||||
+EXPORT_SYMBOL_NS_GPL(mfpwm_release, ROCKCHIP_MFPWM);
|
||||
+
|
||||
+void mfpwm_remove_func(struct rockchip_mfpwm_func *pwmf)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm;
|
||||
+ unsigned long flags;
|
||||
+
|
||||
+ if (mfpwm_check_pwmf(pwmf, "mfpwm_remove_func"))
|
||||
+ return;
|
||||
+
|
||||
+ mfpwm = pwmf->parent;
|
||||
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
|
||||
+
|
||||
+ if (mfpwm_pwmf_is_active_pwmf(pwmf)) {
|
||||
+ dev_dbg(&mfpwm->pdev->dev, "removing active function %d\n",
|
||||
+ pwmf->id);
|
||||
+
|
||||
+ while (mfpwm->acquire_cnt > 0)
|
||||
+ mfpwm_do_release(pwmf);
|
||||
+ for (; mfpwm->pwmclk_enable_cnt > 0; mfpwm->pwmclk_enable_cnt--)
|
||||
+ clk_disable(mfpwm->chosen_clk);
|
||||
+
|
||||
+ mfpwm_reg_write(mfpwm->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(false) | PWMV4_CLK_EN(false));
|
||||
+ }
|
||||
+
|
||||
+ if (mfpwm->pwm_dev && mfpwm->pwm_dev->id == pwmf->id) {
|
||||
+ dev_dbg(&mfpwm->pdev->dev, "clearing pwm_dev pointer\n");
|
||||
+ mfpwm->pwm_dev = NULL;
|
||||
+ } else if (mfpwm->counter_dev && mfpwm->counter_dev->id == pwmf->id) {
|
||||
+ dev_dbg(&mfpwm->pdev->dev, "clearing counter_dev pointer\n");
|
||||
+ mfpwm->counter_dev = NULL;
|
||||
+ } else {
|
||||
+ WARN(1, "trying to remove an unknown mfpwm device function");
|
||||
+ }
|
||||
+
|
||||
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
|
||||
+}
|
||||
+EXPORT_SYMBOL_NS_GPL(mfpwm_remove_func, ROCKCHIP_MFPWM);
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_register_subdev - register a single mfpwm_func
|
||||
+ * @mfpwm: pointer to the parent &struct rockchip_mfpwm
|
||||
+ * @target: pointer to where the &struct platform_device pointer should be
|
||||
+ * stored, usually a member of @mfpwm
|
||||
+ * @name: sub-device name string
|
||||
+ *
|
||||
+ * Allocate a single &struct mfpwm_func, fill its members with appropriate data,
|
||||
+ * and register a new platform device, saving its pointer to @target. The
|
||||
+ * allocation is devres tracked, so will be automatically freed on mfpwm remove.
|
||||
+ * and register a new mfd cell.
|
||||
+ *
|
||||
+ * Returns: 0 on success, negative errno on error
|
||||
+ */
|
||||
+static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm,
|
||||
+ struct platform_device **target,
|
||||
+ const char *name)
|
||||
+{
|
||||
+ struct rockchip_mfpwm_func *func;
|
||||
+ struct platform_device *child;
|
||||
+ struct mfd_cell cell = {};
|
||||
+
|
||||
+ func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL);
|
||||
+ if (IS_ERR(func))
|
||||
@ -351,226 +278,39 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ func->parent = mfpwm;
|
||||
+ func->id = atomic_inc_return(&subdev_id);
|
||||
+ func->base = mfpwm->base;
|
||||
+ child = platform_device_register_data(&mfpwm->pdev->dev, name, func->id,
|
||||
+ func, sizeof(*func));
|
||||
+ 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;
|
||||
+
|
||||
+ if (IS_ERR(child))
|
||||
+ return PTR_ERR(child);
|
||||
+
|
||||
+ *target = child;
|
||||
+
|
||||
+ return 0;
|
||||
+ 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, &mfpwm->pwm_dev, "pwm-rockchip-v4");
|
||||
+ ret = mfpwm_register_subdev(mfpwm, "pwm-rockchip-v4");
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ ret = mfpwm_register_subdev(mfpwm, &mfpwm->counter_dev,
|
||||
+ "rockchip-pwm-capture");
|
||||
+ ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-capture");
|
||||
+ if (ret)
|
||||
+ goto err_unreg_pwm_dev;
|
||||
+ return ret;
|
||||
+
|
||||
+ return 0;
|
||||
+
|
||||
+err_unreg_pwm_dev:
|
||||
+ platform_device_unregister(mfpwm->pwm_dev);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_get_clk_src - read the currently selected clock source
|
||||
+ * @mfpwm: pointer to the driver's private &struct rockchip_mfpwm instance
|
||||
+ *
|
||||
+ * Read the device register to extract the currently selected clock source,
|
||||
+ * and return it.
|
||||
+ *
|
||||
+ * Returns:
|
||||
+ * * the numeric clock source ID on success, 0 <= id <= 2
|
||||
+ * * negative errno on error
|
||||
+ */
|
||||
+static int mfpwm_get_clk_src(struct rockchip_mfpwm *mfpwm)
|
||||
+{
|
||||
+ u32 val;
|
||||
+
|
||||
+ clk_enable(mfpwm->pclk);
|
||||
+ val = mfpwm_reg_read(mfpwm->base, PWMV4_REG_CLK_CTRL);
|
||||
+ clk_disable(mfpwm->pclk);
|
||||
+
|
||||
+ return (val & PWMV4_CLK_SRC_MASK) >> PWMV4_CLK_SRC_SHIFT;
|
||||
+}
|
||||
+
|
||||
+static int mfpwm_choose_clk(struct rockchip_mfpwm *mfpwm)
|
||||
+{
|
||||
+ int ret;
|
||||
+
|
||||
+ ret = mfpwm_get_clk_src(mfpwm);
|
||||
+ if (ret < 0) {
|
||||
+ dev_err(&mfpwm->pdev->dev, "couldn't get current clock source: %pe\n",
|
||||
+ ERR_PTR(ret));
|
||||
+ return ret;
|
||||
+ }
|
||||
+ if (ret == PWMV4_CLK_SRC_CRYSTAL) {
|
||||
+ if (mfpwm->osc_clk) {
|
||||
+ mfpwm->chosen_clk = mfpwm->osc_clk;
|
||||
+ } else {
|
||||
+ dev_warn(&mfpwm->pdev->dev, "initial state wanted 'osc' as clock source, but it's unavailable. Defaulting to 'pwm'.\n");
|
||||
+ mfpwm->chosen_clk = mfpwm->pwm_clk;
|
||||
+ }
|
||||
+ } else {
|
||||
+ mfpwm->chosen_clk = mfpwm->pwm_clk;
|
||||
+ }
|
||||
+
|
||||
+ return clk_rate_exclusive_get(mfpwm->chosen_clk);
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_switch_clk_src - switch between PWM clock sources
|
||||
+ * @mfpwm: pointer to &struct rockchip_mfpwm driver data
|
||||
+ * @clk_src: one of either %PWMV4_CLK_SRC_CRYSTAL or %PWMV4_CLK_SRC_PLL
|
||||
+ *
|
||||
+ * Switch between clock sources, ``_exclusive_put``ing the old rate,
|
||||
+ * ``clk_rate_exclusive_get``ing the new one, writing the registers and
|
||||
+ * swapping out the &struct_rockchip_mfpwm->chosen_clk.
|
||||
+ *
|
||||
+ * Returns:
|
||||
+ * * %0 - Success
|
||||
+ * * %-EINVAL - A wrong @clk_src was given or it is unavailable
|
||||
+ * * %-EBUSY - Device is currently in use, try again later
|
||||
+ */
|
||||
+__attribute__((nonnull))
|
||||
+static int mfpwm_switch_clk_src(struct rockchip_mfpwm *mfpwm,
|
||||
+ unsigned int clk_src)
|
||||
+{
|
||||
+ struct clk *prev;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ scoped_cond_guard(spinlock_try, return -EBUSY, &mfpwm->state_lock) {
|
||||
+ /* Don't fiddle with any of this stuff if the PWM is on */
|
||||
+ if (mfpwm->active_func)
|
||||
+ return -EBUSY;
|
||||
+
|
||||
+ prev = mfpwm->chosen_clk;
|
||||
+ ret = mfpwm_get_clk_src(mfpwm);
|
||||
+ if (ret < 0)
|
||||
+ return ret;
|
||||
+ if (ret == clk_src)
|
||||
+ return 0;
|
||||
+
|
||||
+ switch (clk_src) {
|
||||
+ case PWMV4_CLK_SRC_PLL:
|
||||
+ mfpwm->chosen_clk = mfpwm->pwm_clk;
|
||||
+ break;
|
||||
+ case PWMV4_CLK_SRC_CRYSTAL:
|
||||
+ if (!mfpwm->osc_clk)
|
||||
+ return -EINVAL;
|
||||
+ mfpwm->chosen_clk = mfpwm->osc_clk;
|
||||
+ break;
|
||||
+ default:
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+
|
||||
+ clk_enable(mfpwm->pclk);
|
||||
+
|
||||
+ mfpwm_reg_write(mfpwm->base, PWMV4_REG_CLK_CTRL,
|
||||
+ PWMV4_CLK_SRC(clk_src));
|
||||
+ clk_rate_exclusive_get(mfpwm->chosen_clk);
|
||||
+ if (prev)
|
||||
+ clk_rate_exclusive_put(prev);
|
||||
+
|
||||
+ clk_disable(mfpwm->pclk);
|
||||
+ }
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static ssize_t chosen_clock_show(struct device *dev,
|
||||
+ struct device_attribute *attr, char *buf)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
|
||||
+ unsigned long clk_src = 0;
|
||||
+
|
||||
+ /*
|
||||
+ * Why the weird indirection here? I have the suspicion that if we
|
||||
+ * emitted to sysfs with the lock still held, then a nefarious program
|
||||
+ * could hog the lock by somehow forcing a full buffer condition and
|
||||
+ * then refusing to read from it. Don't know whether that's feasible
|
||||
+ * to achieve in reality, but I don't want to find out the hard way
|
||||
+ * either.
|
||||
+ */
|
||||
+ scoped_guard(spinlock, &mfpwm->state_lock) {
|
||||
+ if (mfpwm->chosen_clk == mfpwm->pwm_clk)
|
||||
+ clk_src = PWMV4_CLK_SRC_PLL;
|
||||
+ else if (mfpwm->osc_clk && mfpwm->chosen_clk == mfpwm->osc_clk)
|
||||
+ clk_src = PWMV4_CLK_SRC_CRYSTAL;
|
||||
+ else
|
||||
+ return -ENODEV;
|
||||
+ }
|
||||
+
|
||||
+ if (clk_src == PWMV4_CLK_SRC_PLL)
|
||||
+ return sysfs_emit(buf, "pll\n");
|
||||
+ else if (clk_src == PWMV4_CLK_SRC_CRYSTAL)
|
||||
+ return sysfs_emit(buf, "crystal\n");
|
||||
+
|
||||
+ return -ENODEV;
|
||||
+}
|
||||
+
|
||||
+static ssize_t chosen_clock_store(struct device *dev,
|
||||
+ struct device_attribute *attr,
|
||||
+ const char *buf, size_t count)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
|
||||
+ int ret;
|
||||
+
|
||||
+ if (sysfs_streq(buf, "pll")) {
|
||||
+ ret = mfpwm_switch_clk_src(mfpwm, PWMV4_CLK_SRC_PLL);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+ return count;
|
||||
+ } else if (sysfs_streq(buf, "crystal")) {
|
||||
+ ret = mfpwm_switch_clk_src(mfpwm, PWMV4_CLK_SRC_CRYSTAL);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+ return count;
|
||||
+ } else {
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static DEVICE_ATTR_RW(chosen_clock);
|
||||
+
|
||||
+static ssize_t available_clocks_show(struct device *dev,
|
||||
+ struct device_attribute *attr, char *buf)
|
||||
+{
|
||||
+ struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
|
||||
+ ssize_t size = 0;
|
||||
+
|
||||
+ size += sysfs_emit_at(buf, size, "pll\n");
|
||||
+ if (mfpwm->osc_clk)
|
||||
+ size += sysfs_emit_at(buf, size, "crystal\n");
|
||||
+
|
||||
+ return size;
|
||||
+}
|
||||
+
|
||||
+static DEVICE_ATTR_RO(available_clocks);
|
||||
+
|
||||
+static struct attribute *mfpwm_attrs[] = {
|
||||
+ &dev_attr_available_clocks.attr,
|
||||
+ &dev_attr_chosen_clock.attr,
|
||||
+ NULL,
|
||||
+};
|
||||
+
|
||||
+ATTRIBUTE_GROUPS(mfpwm);
|
||||
+
|
||||
+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);
|
||||
@ -600,14 +340,35 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk),
|
||||
+ "couldn't get and prepare 'pwm' clock\n");
|
||||
+
|
||||
+ mfpwm->osc_clk = devm_clk_get_optional_prepared(dev, "osc");
|
||||
+ 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");
|
||||
+
|
||||
+ ret = mfpwm_choose_clk(mfpwm);
|
||||
+ if (ret)
|
||||
+ 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);
|
||||
+
|
||||
@ -628,8 +389,10 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+
|
||||
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
|
||||
+
|
||||
+ if (mfpwm->chosen_clk)
|
||||
+ clk_rate_exclusive_put(mfpwm->chosen_clk);
|
||||
+ if (mfpwm->chosen_clk) {
|
||||
+ clk_unprepare(mfpwm->chosen_clk);
|
||||
+ clk_unregister_mux(mfpwm->chosen_clk);
|
||||
+ }
|
||||
+
|
||||
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
|
||||
+}
|
||||
@ -646,7 +409,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ .driver = {
|
||||
+ .name = KBUILD_MODNAME,
|
||||
+ .of_match_table = rockchip_mfpwm_of_match,
|
||||
+ .dev_groups = mfpwm_groups,
|
||||
+ },
|
||||
+ .probe = rockchip_mfpwm_probe,
|
||||
+ .remove = rockchip_mfpwm_remove,
|
||||
@ -657,8 +419,8 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+MODULE_DESCRIPTION("Rockchip MFPWM Driver");
|
||||
+MODULE_LICENSE("GPL");
|
||||
--- /dev/null
|
||||
+++ b/include/soc/rockchip/mfpwm.h
|
||||
@@ -0,0 +1,505 @@
|
||||
+++ b/include/linux/mfd/rockchip-mfpwm.h
|
||||
@@ -0,0 +1,454 @@
|
||||
+/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
@ -673,10 +435,11 @@ Signed-off-by: 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>
|
||||
+#include <soc/rockchip/utils.h>
|
||||
+
|
||||
+struct rockchip_mfpwm;
|
||||
+
|
||||
@ -687,12 +450,14 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * @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;
|
||||
+};
|
||||
+
|
||||
+/*
|
||||
@ -716,7 +481,7 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+/*
|
||||
+ * VERSION Register Description
|
||||
+ * [31:24] RO | Hardware Major Version
|
||||
+ * [23:16] RO | Hardware Minor Version docArrive
|
||||
+ * [23:16] RO | Hardware Minor Version
|
||||
+ * [15:15] RO | Reserved
|
||||
+ * [14:14] RO | Hardware supports biphasic counters
|
||||
+ * [13:13] RO | Hardware supports filters
|
||||
@ -728,25 +493,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * [07:04] RO | Channel index of this instance
|
||||
+ * [03:00] RO | Number of channels the base instance supports
|
||||
+ */
|
||||
+static inline __pure u32 pwmv4_ver_chn_num(u32 val)
|
||||
+{
|
||||
+ return (val & GENMASK(3, 0));
|
||||
+}
|
||||
+
|
||||
+static inline __pure u32 pwmv4_ver_chn_idx(u32 val)
|
||||
+{
|
||||
+ return (val & GENMASK(7, 4)) >> 4;
|
||||
+}
|
||||
+
|
||||
+static inline __pure u32 pwmv4_ver_major(u32 val)
|
||||
+{
|
||||
+ return (val & GENMASK(31, 24)) >> 24;
|
||||
+}
|
||||
+
|
||||
+static inline __pure u32 pwmv4_ver_minor(u32 val)
|
||||
+{
|
||||
+ return (val & GENMASK(23, 16)) >> 16;
|
||||
+}
|
||||
+
|
||||
+#define PWMV4_REG_ENABLE 0x004
|
||||
+/*
|
||||
@ -757,40 +503,41 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * [15:06] RO | Reserved
|
||||
+ * [05:05] RW | PWM Channel Counter Read Enable, 1 = enabled
|
||||
+ */
|
||||
+#define PWMV4_CHN_CNT_RD_EN(v) REG_UPDATE_BIT_WE((v), 5)
|
||||
+#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) REG_UPDATE_BIT_WE((v), 4)
|
||||
+#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) REG_UPDATE_BIT_WE((v), 3)
|
||||
+#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(v) REG_UPDATE_BIT_WE((v), 2)
|
||||
+#define PWMV4_CTRL_UPDATE_EN_MASK BIT(2)
|
||||
+#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(v) REG_UPDATE_BIT_WE((v), 1)
|
||||
+#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(v) REG_UPDATE_BIT_WE((v), 0)
|
||||
+#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 pwmv4_is_enabled(unsigned int val)
|
||||
+static inline __pure bool rockchip_pwm_v4_is_enabled(unsigned int val)
|
||||
+{
|
||||
+ return (val & PWMV4_EN_BOTH_MASK);
|
||||
+}
|
||||
@ -805,38 +552,34 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * 0 = current channel scale clock
|
||||
+ * 1 = global channel scale clock
|
||||
+ */
|
||||
+#define PWMV4_CLK_GLOBAL(v) REG_UPDATE_BIT_WE((v), 15)
|
||||
+#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: This reg is of questionable usefulness on RK3576, as it
|
||||
+ * just muxes between 100m_50m_24m and 24m directly. The
|
||||
+ * only use-case I can come up with is if you must use 100m
|
||||
+ * or 50m PWM on one channel but absolutely require to use
|
||||
+ * the lower rate 24m clock on another channel on the same
|
||||
+ * chip, which doesn't seem like a realistic use case. I
|
||||
+ * suspect that's why downstream doesn't use it.
|
||||
+ * 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(v) REG_UPDATE_WE((v), 13, 14)
|
||||
+#define PWMV4_CLK_SRC_SHIFT 13
|
||||
+#define PWMV4_CLK_SRC_MASK GENMASK(14, 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) REG_UPDATE_WE((v), 4, 12)
|
||||
+#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) REG_UPDATE_WE((v), 0, 2)
|
||||
+#define PWMV4_CLK_PRESCALE_F(v) FIELD_PREP_WM16(GENMASK(2, 0), (v))
|
||||
+
|
||||
+#define PWMV4_REG_CTRL 0x00C
|
||||
+/*
|
||||
@ -849,13 +592,13 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * 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) REG_UPDATE_WE((v), 6, 8)
|
||||
+#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) REG_UPDATE_BIT_WE((v), 5)
|
||||
+#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) REG_UPDATE_BIT_WE((v), 4)
|
||||
+#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.
|
||||
@ -863,14 +606,15 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ */
|
||||
+#define PWMV4_POLARITY_N 0x0U
|
||||
+#define PWMV4_POLARITY_P 0x1U
|
||||
+#define PWMV4_INACTIVE_POL(v) REG_UPDATE_BIT_WE((v), 3)
|
||||
+#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(v) REG_UPDATE_BIT_WE((v), 2)
|
||||
+#define PWMV4_DUTY_POL_MASK BIT(2)
|
||||
+#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
|
||||
@ -881,7 +625,8 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+#define PWMV4_MODE_ONESHOT 0x0U
|
||||
+#define PWMV4_MODE_CONT 0x1U
|
||||
+#define PWMV4_MODE_CAPTURE 0x2U
|
||||
+#define PWMV4_MODE(v) REG_UPDATE_WE((v), 0, 1)
|
||||
+#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) | \
|
||||
@ -1038,8 +783,10 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ */
|
||||
+#define PWMV4_INT_LPC BIT(0)
|
||||
+#define PWMV4_INT_HPC BIT(1)
|
||||
+#define PWMV4_INT_LPC_W(v) REG_UPDATE_BIT_WE(((v) ? 1 : 0), 0)
|
||||
+#define PWMV4_INT_HPC_W(v) REG_UPDATE_BIT_WE(((v) ? 1 : 0), 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
|
||||
+/*
|
||||
@ -1090,14 +837,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_clk_get_rate - get the currently used clock's rate, in Hz
|
||||
+ * @mfpwm: pointer to the &struct rockchip_mfpwm instance
|
||||
+ *
|
||||
+ * Returns: %0 on error, clock rate in Hz on success
|
||||
+ */
|
||||
+unsigned long mfpwm_clk_get_rate(struct rockchip_mfpwm *mfpwm);
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_acquire - try becoming the active mfpwm function device
|
||||
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
|
||||
+ *
|
||||
@ -1123,7 +862,7 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * * %-EBUSY - a different device function is active
|
||||
+ * * %-EOVERFLOW - the acquire counter is at its maximum
|
||||
+ */
|
||||
+int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf);
|
||||
+extern int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf);
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_release - drop usage of active mfpwm device function by 1
|
||||
@ -1133,34 +872,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * function remain, set the mfpwm device to have no active device function,
|
||||
+ * allowing other device functions to claim it.
|
||||
+ */
|
||||
+void mfpwm_release(const struct rockchip_mfpwm_func *pwmf);
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_pwmclk_enable - enable the pwm clock the signal and timing is based on
|
||||
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
|
||||
+ *
|
||||
+ * Context: must be the active device function to call this
|
||||
+ *
|
||||
+ * Returns: 0 on success, negative errno on error.
|
||||
+ */
|
||||
+int mfpwm_pwmclk_enable(struct rockchip_mfpwm_func *pwmf);
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_pwmclk_disable - disable the pwm clock the signal and timing is based on
|
||||
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
|
||||
+ *
|
||||
+ * Context: must be the active device function to call this
|
||||
+ */
|
||||
+void mfpwm_pwmclk_disable(struct rockchip_mfpwm_func *pwmf);
|
||||
+
|
||||
+/**
|
||||
+ * mfpwm_remove_func - remove a device function driver from the mfpwm
|
||||
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
|
||||
+ *
|
||||
+ * If the device function driver described by @pwmf is the currently active
|
||||
+ * device function, then it'll have its mfpwm_acquires and its pwmclk_enables
|
||||
+ * balanced and be removed as the active device function driver.
|
||||
+ */
|
||||
+void mfpwm_remove_func(struct rockchip_mfpwm_func *pwmf);
|
||||
+extern void mfpwm_release(const struct rockchip_mfpwm_func *pwmf);
|
||||
+
|
||||
+#endif /* __SOC_ROCKCHIP_MFPWM_H__ */
|
||||
@ -1,32 +1,42 @@
|
||||
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 | 336 ++++++++++++++++++++++++++++++++++++++++++
|
||||
4 files changed, 351 insertions(+)
|
||||
drivers/pwm/pwm-rockchip-v4.c | 353 ++++++++++++++++++++++++++++++++++++++++++
|
||||
4 files changed, 368 insertions(+)
|
||||
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -19962,6 +19962,7 @@ L: linux-rockchip@lists.infradead.org
|
||||
L: linux-pwm@vger.kernel.org
|
||||
S: Maintained
|
||||
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
|
||||
+F: drivers/pwm/pwm-rockchip-v4.c
|
||||
F: drivers/soc/rockchip/mfpwm.c
|
||||
F: include/soc/rockchip/mfpwm.h
|
||||
|
||||
--- a/drivers/pwm/Kconfig
|
||||
+++ b/drivers/pwm/Kconfig
|
||||
@@ -540,6 +540,19 @@ config PWM_ROCKCHIP
|
||||
Generic PWM framework driver for the PWM controller found on
|
||||
Rockchip SoCs.
|
||||
@@ -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 ROCKCHIP_MFPWM
|
||||
+ 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.
|
||||
@ -35,9 +45,9 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ to guarantee fearlessly concurrent operation with other functions of
|
||||
+ the same device implemented by drivers in other subsystems.
|
||||
+
|
||||
config PWM_RZ_MTU3
|
||||
tristate "Renesas RZ/G2L MTU3a PWM Timer support"
|
||||
depends on RZ_MTU3
|
||||
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
|
||||
@ -50,7 +60,7 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
|
||||
--- /dev/null
|
||||
+++ b/drivers/pwm/pwm-rockchip-v4.c
|
||||
@@ -0,0 +1,336 @@
|
||||
@@ -0,0 +1,353 @@
|
||||
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
@ -61,13 +71,32 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * 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>
|
||||
+#include <soc/rockchip/mfpwm.h>
|
||||
+
|
||||
+struct rockchip_pwm_v4 {
|
||||
+ struct rockchip_mfpwm_func *pwmf;
|
||||
@ -90,29 +119,18 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * 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
|
||||
+ * @out_val: pointer to location where converted result should be stored.
|
||||
+ *
|
||||
+ * If @out_val is %NULL, no calculation is performed.
|
||||
+ *
|
||||
+ * Return:
|
||||
+ * * %0 - Success
|
||||
+ * * %-EOVERFLOW - Result too large for target type
|
||||
+ * Returns the rounded value, saturating at U32_MAX if too large
|
||||
+ */
|
||||
+static int rockchip_pwm_v4_round_single(unsigned long rate, u64 in_val,
|
||||
+ u32 *out_val)
|
||||
+static u32 rockchip_pwm_v4_round_single(unsigned long rate, u64 in_val)
|
||||
+{
|
||||
+ u64 tmp;
|
||||
+
|
||||
+ if (!out_val)
|
||||
+ return 0;
|
||||
+
|
||||
+ tmp = mult_frac(rate, in_val, NSEC_PER_SEC);
|
||||
+ tmp = mul_u64_u64_div_u64(rate, in_val, NSEC_PER_SEC);
|
||||
+ if (tmp > U32_MAX)
|
||||
+ return -EOVERFLOW;
|
||||
+ tmp = U32_MAX;
|
||||
+
|
||||
+ *out_val = tmp;
|
||||
+
|
||||
+ return 0;
|
||||
+ return (u32)tmp;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
@ -122,42 +140,27 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * @period: PWM period in nanoseconds
|
||||
+ * @offset: PWM offset in nanoseconds
|
||||
+ * @out_duty: pointer to where the rounded duty value should be stored
|
||||
+ * if NULL, don't calculate or store it
|
||||
+ * @out_period: pointer to where the rounded period value should be stored
|
||||
+ * if NULL, don't calculate or store it
|
||||
+ * @out_offset: pointer to where the rounded offset value should be stored
|
||||
+ * if NULL, don't calculate or store it
|
||||
+ *
|
||||
+ * Convert nanosecond-based duty/period/offset parameters to the PWM hardware's
|
||||
+ * native rounded representation in number of cycles at clock rate @rate. If an
|
||||
+ * out_ parameter is a NULL pointer, the corresponding parameter will not be
|
||||
+ * calculated or stored. Should an overflow error occur for any of the
|
||||
+ * parameters, assume the data at all the out_ locations is invalid and may not
|
||||
+ * even have been touched at all.
|
||||
+ *
|
||||
+ * Return:
|
||||
+ * * %0 - Success
|
||||
+ * * %-EOVERFLOW - One of the results is too large for the PWM hardware
|
||||
+ * 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 int rockchip_pwm_v4_round_params(unsigned long rate, u64 duty,
|
||||
+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)
|
||||
+{
|
||||
+ int ret;
|
||||
+ *out_period = rockchip_pwm_v4_round_single(rate, period);
|
||||
+
|
||||
+ ret = rockchip_pwm_v4_round_single(rate, duty, out_duty);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+ *out_duty = rockchip_pwm_v4_round_single(rate, duty);
|
||||
+
|
||||
+ ret = rockchip_pwm_v4_round_single(rate, period, out_period);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ ret = rockchip_pwm_v4_round_single(rate, offset, out_offset);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ return 0;
|
||||
+ /* 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,
|
||||
@ -168,30 +171,24 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ unsigned long rate;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ /* We do not want chosen_clk to change out from under us here */
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+ rate = clk_get_rate(pc->pwmf->core);
|
||||
+
|
||||
+ rate = mfpwm_clk_get_rate(pc->pwmf->parent);
|
||||
+
|
||||
+ ret = rockchip_pwm_v4_round_params(rate, wf->duty_length_ns,
|
||||
+ wf->period_length_ns,
|
||||
+ wf->duty_offset_ns, &wfhw->duty,
|
||||
+ &wfhw->period, &wfhw->offset);
|
||||
+ 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 = %u, period = %u, offset = %u, rate %lu\n",
|
||||
+ wfhw->duty, wfhw->period, wfhw->offset, rate);
|
||||
+ 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);
|
||||
+
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ return ret;
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_round_wf_fromhw(struct pwm_chip *chip,
|
||||
@ -202,37 +199,28 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ unsigned long rate;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ /* We do not want chosen_clk to change out from under us here */
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+ rate = clk_get_rate(pc->pwmf->core);
|
||||
+
|
||||
+ rate = mfpwm_clk_get_rate(pc->pwmf->parent);
|
||||
+ if (rockchip_pwm_v4_is_enabled(wfhw->enable)) {
|
||||
+ if (!rate)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ /* Let's avoid a cool division-by-zero if the clock's busted. */
|
||||
+ if (!rate) {
|
||||
+ ret = -EINVAL;
|
||||
+ goto out_mfpwm_release;
|
||||
+ 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;
|
||||
+ }
|
||||
+
|
||||
+ wf->duty_length_ns = mult_frac(wfhw->duty, NSEC_PER_SEC, rate);
|
||||
+ 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);
|
||||
+
|
||||
+ if (pwmv4_is_enabled(wfhw->enable))
|
||||
+ wf->period_length_ns = mult_frac(wfhw->period, NSEC_PER_SEC,
|
||||
+ rate);
|
||||
+ else
|
||||
+ wf->period_length_ns = 0;
|
||||
+
|
||||
+ wf->duty_offset_ns = mult_frac(wfhw->offset, NSEC_PER_SEC, rate);
|
||||
+
|
||||
+ dev_dbg(&chip->dev, "fromhw: duty = %llu, period = %llu, offset = %llu\n",
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns);
|
||||
+
|
||||
+out_mfpwm_release:
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ return ret;
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
@ -269,8 +257,8 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ was_enabled = pwmv4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
|
||||
+ PWMV4_REG_ENABLE));
|
||||
+ 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
|
||||
@ -285,7 +273,7 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ * use relaxed writes; it's not worth the footgun.
|
||||
+ */
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ REG_UPDATE_WE(wfhw->enable, 0, 1));
|
||||
+ 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);
|
||||
@ -295,14 +283,19 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+
|
||||
+ /* Commit new configuration to hardware output. */
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_CTRL_UPDATE_EN(1));
|
||||
+ PWMV4_CTRL_UPDATE_EN);
|
||||
+
|
||||
+ if (pwmv4_is_enabled(wfhw->enable)) {
|
||||
+ if (rockchip_pwm_v4_is_enabled(wfhw->enable)) {
|
||||
+ if (!was_enabled) {
|
||||
+ dev_dbg(&chip->dev, "enabling PWM output\n");
|
||||
+ ret = mfpwm_pwmclk_enable(pc->pwmf);
|
||||
+ 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
|
||||
@ -313,8 +306,9 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ goto err_mfpwm_release;
|
||||
+ }
|
||||
+ } else if (was_enabled) {
|
||||
+ dev_dbg(&chip->dev, "disabling PWM output\n");
|
||||
+ mfpwm_pwmclk_disable(pc->pwmf);
|
||||
+ 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);
|
||||
+ }
|
||||
@ -325,7 +319,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+/* We state the PWM chip is atomic, so none of these functions should sleep. */
|
||||
+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,
|
||||
@ -334,39 +327,73 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ .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;
|
||||
+
|
||||
+ chip = devm_pwmchip_alloc(&pdev->dev, 1, sizeof(*pc));
|
||||
+ /*
|
||||
+ * 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;
|
||||
+
|
||||
+ platform_set_drvdata(pdev, pc);
|
||||
+ 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;
|
||||
+ chip->atomic = true;
|
||||
+
|
||||
+ ret = pwmchip_add(chip);
|
||||
+ ret = devm_pwmchip_add(dev, chip);
|
||||
+ if (ret)
|
||||
+ return dev_err_probe(&pdev->dev, ret, "failed to add PWM chip\n");
|
||||
+ return dev_err_probe(dev, ret, "Failed to add PWM chip\n");
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static void rockchip_pwm_v4_remove(struct platform_device *pdev)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = platform_get_drvdata(pdev);
|
||||
+
|
||||
+ mfpwm_remove_func(pc->pwmf);
|
||||
+}
|
||||
+
|
||||
+static const struct platform_device_id rockchip_pwm_v4_ids[] = {
|
||||
+ { .name = "pwm-rockchip-v4", },
|
||||
+ { /* sentinel */ }
|
||||
@ -375,7 +402,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+
|
||||
+static struct platform_driver rockchip_pwm_v4_driver = {
|
||||
+ .probe = rockchip_pwm_v4_probe,
|
||||
+ .remove = rockchip_pwm_v4_remove,
|
||||
+ .driver = {
|
||||
+ .name = "pwm-rockchip-v4",
|
||||
+ },
|
||||
@ -387,3 +413,4 @@ Signed-off-by: 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");
|
||||
@ -1,21 +1,30 @@
|
||||
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 | 341 +++++++++++++++++++++++++++++++++
|
||||
4 files changed, 356 insertions(+)
|
||||
drivers/counter/rockchip-pwm-capture.c | 306 +++++++++++++++++++++++++++++++++
|
||||
4 files changed, 321 insertions(+)
|
||||
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -19962,6 +19962,7 @@ L: linux-rockchip@lists.infradead.org
|
||||
L: linux-pwm@vger.kernel.org
|
||||
S: Maintained
|
||||
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
|
||||
+F: drivers/counter/rockchip-pwm-capture.c
|
||||
F: drivers/pwm/pwm-rockchip-v4.c
|
||||
F: drivers/soc/rockchip/mfpwm.c
|
||||
F: include/soc/rockchip/mfpwm.h
|
||||
--- a/drivers/counter/Kconfig
|
||||
+++ b/drivers/counter/Kconfig
|
||||
@@ -90,6 +90,19 @@ config MICROCHIP_TCB_CAPTURE
|
||||
@ -25,7 +34,7 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+config ROCKCHIP_PWM_CAPTURE
|
||||
+ tristate "Rockchip PWM Counter Capture driver"
|
||||
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
|
||||
+ depends on ROCKCHIP_MFPWM
|
||||
+ depends on MFD_ROCKCHIP_MFPWM
|
||||
+ depends on HAS_IOMEM
|
||||
+ help
|
||||
+ Generic counter framework driver for the multi-function PWM on
|
||||
@ -47,16 +56,16 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+obj-$(CONFIG_ROCKCHIP_PWM_CAPTURE) += rockchip-pwm-capture.o
|
||||
--- /dev/null
|
||||
+++ b/drivers/counter/rockchip-pwm-capture.c
|
||||
@@ -0,0 +1,341 @@
|
||||
@@ -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 period and duty cycle in nanoseconds through the
|
||||
+ * generic counter framework, while guaranteeing exclusive use over the MFPWM
|
||||
+ * device while the counter is enabled.
|
||||
+ * 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>
|
||||
@ -66,35 +75,35 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+#include <linux/counter.h>
|
||||
+#include <linux/devm-helpers.h>
|
||||
+#include <linux/interrupt.h>
|
||||
+#include <linux/math.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>
|
||||
+#include <linux/workqueue.h>
|
||||
+#include <soc/rockchip/mfpwm.h>
|
||||
+
|
||||
+#define RKPWMC_INT_MASK (PWMV4_INT_LPC | PWMV4_INT_HPC)
|
||||
+/*
|
||||
+ * amount of jiffies between no PWM signal change and us deciding PWM is off.
|
||||
+ * PWM signals with a period longer than this won't be detected by us, which is
|
||||
+ * the trade-off we make to have a faster response time when a signal is turned
|
||||
+ * off.
|
||||
+ */
|
||||
+#define RKPWMC_CLEAR_DELAY (max(msecs_to_jiffies(100), 1))
|
||||
+
|
||||
+struct rockchip_pwm_capture {
|
||||
+ struct rockchip_mfpwm_func *pwmf;
|
||||
+ struct counter_device *counter;
|
||||
+ bool is_enabled;
|
||||
+ spinlock_t enable_lock;
|
||||
+ atomic_t lpc;
|
||||
+ atomic_t hpc;
|
||||
+ atomic_t captures_left;
|
||||
+ struct delayed_work clear_capture;
|
||||
+};
|
||||
+
|
||||
+/*
|
||||
+ * 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"
|
||||
+ },
|
||||
+};
|
||||
@ -110,6 +119,11 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ .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[] = {
|
||||
@ -154,15 +168,18 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(true) | PWMV4_CLK_EN(true));
|
||||
+
|
||||
+ ret = mfpwm_pwmclk_enable(pc->pwmf);
|
||||
+ ret = clk_enable(pc->pwmf->core);
|
||||
+ if (ret)
|
||||
+ goto err_release;
|
||||
+
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
|
||||
+ if (ret)
|
||||
+ goto err_disable_pwm_clk;
|
||||
+
|
||||
+ atomic_set(&pc->captures_left, 4);
|
||||
+ 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,
|
||||
@ -170,14 +187,8 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ PWMV4_INT_HPC_W(false));
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(false) | PWMV4_CLK_EN(false));
|
||||
+ /*
|
||||
+ * Do not use cancel_delayed_work here unless you want
|
||||
+ * to cause the interrupt handler, which may still be
|
||||
+ * running at this point, to stall. Similarly, don't do
|
||||
+ * flush_delayed_work since it may sleep.
|
||||
+ */
|
||||
+ mod_delayed_work(system_bh_wq, &pc->clear_capture, 0);
|
||||
+ mfpwm_pwmclk_disable(pc->pwmf);
|
||||
+ clk_rate_exclusive_put(pc->pwmf->core);
|
||||
+ clk_disable(pc->pwmf->core);
|
||||
+ pc->is_enabled = false;
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ }
|
||||
@ -187,8 +198,10 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+
|
||||
+ return 0;
|
||||
+
|
||||
+err_unprotect_pwm_clk:
|
||||
+ clk_rate_exclusive_put(pc->pwmf->core);
|
||||
+err_disable_pwm_clk:
|
||||
+ mfpwm_pwmclk_disable(pc->pwmf);
|
||||
+ clk_disable(pc->pwmf->core);
|
||||
+err_release:
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+
|
||||
@ -200,14 +213,14 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+};
|
||||
+
|
||||
+enum rkpwmc_count_id {
|
||||
+ COUNT_PERIOD = 0,
|
||||
+ COUNT_DUTY = 1,
|
||||
+ COUNT_LPC = 0,
|
||||
+ COUNT_HPC = 1,
|
||||
+};
|
||||
+
|
||||
+static struct counter_count rkpwmc_counts[] = {
|
||||
+ {
|
||||
+ .id = COUNT_PERIOD,
|
||||
+ .name = "Period in nanoseconds",
|
||||
+ .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,
|
||||
@ -216,8 +229,8 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
|
||||
+ },
|
||||
+ {
|
||||
+ .id = COUNT_DUTY,
|
||||
+ .name = "Duty cycle in nanoseconds",
|
||||
+ .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,
|
||||
@ -231,39 +244,24 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ struct counter_count *count, u64 *value)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
|
||||
+ unsigned long rate;
|
||||
+ u64 period;
|
||||
+ u64 lpc;
|
||||
+ u64 hpc;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ if (count->id != COUNT_PERIOD && count->id != COUNT_DUTY)
|
||||
+ 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;
|
||||
+
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ rate = mfpwm_clk_get_rate(pc->pwmf->parent);
|
||||
+ if (!rate) {
|
||||
+ ret = -EINVAL;
|
||||
+ goto out_release;
|
||||
+ }
|
||||
+
|
||||
+ hpc = (u32) atomic_read(&pc->hpc);
|
||||
+
|
||||
+ if (count->id == COUNT_PERIOD) {
|
||||
+ lpc = (u32) atomic_read(&pc->lpc);
|
||||
+ period = mult_frac((hpc + lpc), NSEC_PER_SEC, rate);
|
||||
+ *value = period;
|
||||
+ } else {
|
||||
+ *value = mult_frac(hpc, NSEC_PER_SEC, rate);
|
||||
+ }
|
||||
+
|
||||
+out_release:
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static const struct counter_ops rkpwmc_ops = {
|
||||
@ -275,8 +273,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ struct rockchip_pwm_capture *pc = data;
|
||||
+ u32 intsts;
|
||||
+ u32 clr = 0;
|
||||
+ u32 lpc;
|
||||
+ u32 hpc;
|
||||
+
|
||||
+ intsts = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_INTSTS);
|
||||
+
|
||||
@ -285,43 +281,24 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+
|
||||
+ if (intsts & PWMV4_INT_LPC) {
|
||||
+ clr |= PWMV4_INT_LPC;
|
||||
+ atomic_dec_if_positive(&pc->captures_left);
|
||||
+ counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 0);
|
||||
+ }
|
||||
+
|
||||
+ if (intsts & PWMV4_INT_HPC) {
|
||||
+ clr |= PWMV4_INT_HPC;
|
||||
+ atomic_dec_if_positive(&pc->captures_left);
|
||||
+ }
|
||||
+
|
||||
+ /* After 4 interrupts, reset to 4 captures left and read the regs */
|
||||
+ if (!atomic_cmpxchg(&pc->captures_left, 0, 4)) {
|
||||
+ lpc = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_LPC);
|
||||
+ hpc = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_HPC);
|
||||
+
|
||||
+ atomic_set(&pc->lpc, lpc);
|
||||
+ atomic_set(&pc->hpc, hpc);
|
||||
+ mod_delayed_work(system_bh_wq, &pc->clear_capture,
|
||||
+ RKPWMC_CLEAR_DELAY);
|
||||
+ counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 1);
|
||||
+ }
|
||||
+
|
||||
+ if (clr)
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INTSTS, clr);
|
||||
+
|
||||
+ if (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 void rkpwmc_clear_capture_worker(struct work_struct *work)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = container_of(
|
||||
+ work, struct rockchip_pwm_capture, clear_capture.work);
|
||||
+
|
||||
+ atomic_set(&pc->hpc, 0);
|
||||
+ atomic_set(&pc->lpc, 0);
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_capture_probe(struct platform_device *pdev)
|
||||
+{
|
||||
+ struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
|
||||
@ -329,6 +306,11 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ 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);
|
||||
@ -344,9 +326,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ if (ret)
|
||||
+ return dev_err_probe(&pdev->dev, ret, "Failed requesting IRQ\n");
|
||||
+
|
||||
+ ret = devm_delayed_work_autocancel(&pdev->dev, &pc->clear_capture,
|
||||
+ rkpwmc_clear_capture_worker);
|
||||
+
|
||||
+ counter->name = pdev->name;
|
||||
+ counter->signals = rkpwmc_signals;
|
||||
+ counter->num_signals = ARRAY_SIZE(rkpwmc_signals);
|
||||
@ -354,6 +333,8 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ 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");
|
||||
@ -361,13 +342,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static void rockchip_pwm_capture_remove(struct platform_device *pdev)
|
||||
+{
|
||||
+ struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
|
||||
+
|
||||
+ mfpwm_remove_func(pwmf);
|
||||
+}
|
||||
+
|
||||
+static const struct platform_device_id rockchip_pwm_capture_id_table[] = {
|
||||
+ { .name = "rockchip-pwm-capture", },
|
||||
+ { /* sentinel */ },
|
||||
@ -376,7 +350,6 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+
|
||||
+static struct platform_driver rockchip_pwm_capture_driver = {
|
||||
+ .probe = rockchip_pwm_capture_probe,
|
||||
+ .remove = rockchip_pwm_capture_remove,
|
||||
+ .id_table = rockchip_pwm_capture_id_table,
|
||||
+ .driver = {
|
||||
+ .name = "rockchip-pwm-capture",
|
||||
@ -389,3 +362,4 @@ Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+MODULE_LICENSE("GPL");
|
||||
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
|
||||
+MODULE_IMPORT_NS("COUNTER");
|
||||
+MODULE_ALIAS("platform:rockchip-pwm-capture");
|
||||
@ -1,119 +0,0 @@
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
.../bindings/pwm/rockchip,rk3576-pwm.yaml | 94 ++++++++++++++++++++++
|
||||
MAINTAINERS | 7 ++
|
||||
2 files changed, 101 insertions(+)
|
||||
|
||||
--- /dev/null
|
||||
+++ b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
|
||||
@@ -0,0 +1,94 @@
|
||||
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
|
||||
+%YAML 1.2
|
||||
+---
|
||||
+$id: http://devicetree.org/schemas/pwm/rockchip,rk3576-pwm.yaml#
|
||||
+$schema: http://devicetree.org/meta-schemas/core.yaml#
|
||||
+
|
||||
+title: Rockchip PWMv4 controller
|
||||
+
|
||||
+maintainers:
|
||||
+ - Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+
|
||||
+description: |
|
||||
+ The Rockchip PWMv4 controller is a PWM controller found on several Rockchip
|
||||
+ SoCs, such as the RK3576.
|
||||
+
|
||||
+ It supports both generating and capturing PWM signals.
|
||||
+
|
||||
+allOf:
|
||||
+ - $ref: pwm.yaml#
|
||||
+
|
||||
+properties:
|
||||
+ compatible:
|
||||
+ items:
|
||||
+ - const: rockchip,rk3576-pwm
|
||||
+
|
||||
+ reg:
|
||||
+ maxItems: 1
|
||||
+
|
||||
+ clocks:
|
||||
+ minItems: 2
|
||||
+ items:
|
||||
+ - description: Used to derive the PWM signal.
|
||||
+ - description: Used as the APB bus clock.
|
||||
+ - description: Used as an added alternative to derive the PWM signal.
|
||||
+
|
||||
+ clock-names:
|
||||
+ minItems: 2
|
||||
+ items:
|
||||
+ - const: pwm
|
||||
+ - const: pclk
|
||||
+ - const: osc
|
||||
+
|
||||
+ interrupts:
|
||||
+ maxItems: 1
|
||||
+
|
||||
+ "#pwm-cells":
|
||||
+ const: 3
|
||||
+
|
||||
+required:
|
||||
+ - compatible
|
||||
+ - reg
|
||||
+ - clocks
|
||||
+ - clock-names
|
||||
+ - interrupts
|
||||
+
|
||||
+additionalProperties: false
|
||||
+
|
||||
+examples:
|
||||
+ - |
|
||||
+ #include <dt-bindings/clock/rockchip,rk3576-cru.h>
|
||||
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
|
||||
+ #include <dt-bindings/interrupt-controller/irq.h>
|
||||
+
|
||||
+ soc {
|
||||
+ #address-cells = <2>;
|
||||
+ #size-cells = <2>;
|
||||
+
|
||||
+ pwm@2add0000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2add0000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, <&cru CLK_OSC_PWM1>;
|
||||
+ clock-names = "pwm", "pclk", "osc";
|
||||
+ interrupts = <GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ };
|
||||
+ };
|
||||
+ - |
|
||||
+ #include <dt-bindings/clock/rockchip,rk3576-cru.h>
|
||||
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
|
||||
+ #include <dt-bindings/interrupt-controller/irq.h>
|
||||
+
|
||||
+ soc {
|
||||
+ #address-cells = <2>;
|
||||
+ #size-cells = <2>;
|
||||
+
|
||||
+ pwm@2ade3000 {
|
||||
+ compatible = "rockchip,rk3576-pwm";
|
||||
+ reg = <0x0 0x2ade3000 0x0 0x1000>;
|
||||
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>;
|
||||
+ clock-names = "pwm", "pclk";
|
||||
+ interrupts = <GIC_SPI 111 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ #pwm-cells = <3>;
|
||||
+ };
|
||||
+ };
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -19956,6 +19956,13 @@ F: Documentation/userspace-api/media/v4l
|
||||
F: drivers/media/platform/rockchip/rkisp1
|
||||
F: include/uapi/linux/rkisp1-config.h
|
||||
|
||||
+ROCKCHIP MFPWM
|
||||
+M: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+L: linux-rockchip@lists.infradead.org
|
||||
+L: linux-pwm@vger.kernel.org
|
||||
+S: Maintained
|
||||
+F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
|
||||
+
|
||||
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
|
||||
M: Daniel Golle <daniel@makrotopia.org>
|
||||
M: Aurelien Jarno <aurelien@aurel32.net>
|
||||
@ -1,84 +0,0 @@
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
include/soc/rockchip/utils.h | 76 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 76 insertions(+)
|
||||
|
||||
--- /dev/null
|
||||
+++ b/include/soc/rockchip/utils.h
|
||||
@@ -0,0 +1,76 @@
|
||||
+/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
+ *
|
||||
+ * Utility types, inline functions, and macros that are used across several
|
||||
+ * Rockchip-specific drivers.
|
||||
+ *
|
||||
+ * Authors:
|
||||
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ */
|
||||
+
|
||||
+#ifndef __SOC_ROCKCHIP_UTILS_H__
|
||||
+#define __SOC_ROCKCHIP_UTILS_H__
|
||||
+
|
||||
+#include <linux/bits.h>
|
||||
+#include <linux/build_bug.h>
|
||||
+#include <linux/limits.h>
|
||||
+
|
||||
+/*
|
||||
+ * Incoming macro basilisks, stare directly at them at your own peril.
|
||||
+ * As a gentle reminder to help with code comprehension: BUILD_BUG_ON_ZERO
|
||||
+ * is confusingly named; it's a version of BUILD_BUG_ON that evaluates to zero
|
||||
+ * if it does not trigger, i.e. the assertion within the macro still checks
|
||||
+ * for a truthy value, not zero.
|
||||
+ */
|
||||
+
|
||||
+/**
|
||||
+ * REG_UPDATE_WE - generate a register write value with a write-enable mask
|
||||
+ * @_val: unshifted value we wish to update between @_low and @_high
|
||||
+ * @_low: index of the low bit of the bit range we want to update
|
||||
+ * @_high: index of the high bit of the bit range we want to update
|
||||
+ *
|
||||
+ * This macro statically generates a value consisting of @_val shifted to the
|
||||
+ * left by @_low, and a write-enable mask in the upper 16 bits of the value
|
||||
+ * that sets bit ``i << 16`` to ``1`` if bit ``i`` is within the @_low to @_high
|
||||
+ * range. Only up to bit (@_high - @_low) of @_val is used for safety, i.e.
|
||||
+ * trying to write a value that doesn't fit in the specified range will simply
|
||||
+ * truncate it.
|
||||
+ *
|
||||
+ * This is useful for some hardware, like some of Rockchip's registers, where
|
||||
+ * a 32-bit width register is divided into a value low half, and a write enable
|
||||
+ * high half. Bits in the low half are only update if the corresponding bit in
|
||||
+ * the high half is ``1``, allowing for lock-free atomic updates of a register.
|
||||
+ *
|
||||
+ * This macro replaces the venerable ``HIWORD_UPDATE``, which is copied and
|
||||
+ * pasted in slightly different forms across many different Rockchip drivers.
|
||||
+ * Before switching drivers to use it, familiarise yourself with the semantics
|
||||
+ * of your specific ``HIWORD_UPDATE`` compared to this function-like macro's
|
||||
+ * semantics.
|
||||
+ *
|
||||
+ * Return: the value, shifted into place, with the required write-enable bits
|
||||
+ */
|
||||
+#define REG_UPDATE_WE(_val, _low, _high) ( \
|
||||
+ BUILD_BUG_ON_ZERO(const_true((_low) > (_high))) + \
|
||||
+ BUILD_BUG_ON_ZERO(const_true((_high) > 15)) + \
|
||||
+ BUILD_BUG_ON_ZERO(const_true((_low) < 0)) + \
|
||||
+ BUILD_BUG_ON_ZERO(const_true((u64) (_val) > U16_MAX)) + \
|
||||
+ ((_val & GENMASK((_high) - (_low), 0)) << (_low) | \
|
||||
+ (GENMASK((_high), (_low)) << 16)))
|
||||
+
|
||||
+/**
|
||||
+ * REG_UPDATE_BIT_WE - update a bit with a write-enable mask
|
||||
+ * @__val: new value of the bit, either ``0`` 0r ``1``
|
||||
+ * @__bit: bit index to modify, 0 <= @__bit < 16.
|
||||
+ *
|
||||
+ * This is like REG_UPDATE_WE() but only modifies a single bit, thereby making
|
||||
+ * invocation easier by avoiding having to pass a repeated value.
|
||||
+ *
|
||||
+ * Return: a value with bit @__bit set to @__val and @__bit << 16 set to ``1``
|
||||
+ */
|
||||
+#define REG_UPDATE_BIT_WE(__val, __bit) ( \
|
||||
+ BUILD_BUG_ON_ZERO(const_true((__val) > 1)) + \
|
||||
+ BUILD_BUG_ON_ZERO(const_true((__val) < 0)) + \
|
||||
+ REG_UPDATE_WE((__val), (__bit), (__bit)))
|
||||
+
|
||||
+#endif /* __SOC_ROCKCHIP_UTILS_H__ */
|
||||
@ -1,11 +0,0 @@
|
||||
--- a/drivers/pwm/pwm-rockchip-v4.c
|
||||
+++ b/drivers/pwm/pwm-rockchip-v4.c
|
||||
@@ -292,6 +292,8 @@ static int rockchip_pwm_v4_probe(struct
|
||||
if (IS_ERR(chip))
|
||||
return PTR_ERR(chip);
|
||||
|
||||
+ device_set_of_node_from_dev(&pdev->dev, pdev->dev.parent);
|
||||
+
|
||||
pc = to_rockchip_pwm_v4(chip);
|
||||
pc->pwmf = pwmf;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user