mirror of
https://github.com/coolsnowwolf/lede.git
synced 2025-04-19 14:13:30 +00:00
rockchip: add pwmv4 support for rk3576 soc in kernel 6.12
This commit is contained in:
parent
2f12c63bd0
commit
b9c1373a04
@ -0,0 +1,237 @@
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
---
|
||||
drivers/pwm/core.c | 95 +++++++++++++++++++++++++++++++++++++++++----
|
||||
include/linux/pwm.h | 13 +++++++
|
||||
2 files changed, 100 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/drivers/pwm/core.c b/drivers/pwm/core.c
|
||||
index 6e752e148b98..b97e2ea0691d 100644
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -31,6 +31,24 @@ static DEFINE_MUTEX(pwm_lock);
|
||||
|
||||
static DEFINE_IDR(pwm_chips);
|
||||
|
||||
+static void pwmchip_lock(struct pwm_chip *chip)
|
||||
+{
|
||||
+ if (chip->atomic)
|
||||
+ spin_lock(&chip->atomic_lock);
|
||||
+ else
|
||||
+ mutex_lock(&chip->nonatomic_lock);
|
||||
+}
|
||||
+
|
||||
+static void pwmchip_unlock(struct pwm_chip *chip)
|
||||
+{
|
||||
+ if (chip->atomic)
|
||||
+ spin_unlock(&chip->atomic_lock);
|
||||
+ else
|
||||
+ mutex_unlock(&chip->nonatomic_lock);
|
||||
+}
|
||||
+
|
||||
+DEFINE_GUARD(pwmchip, struct pwm_chip *, pwmchip_lock(_T), pwmchip_unlock(_T))
|
||||
+
|
||||
static void pwm_apply_debug(struct pwm_device *pwm,
|
||||
const struct pwm_state *state)
|
||||
{
|
||||
@@ -220,6 +238,7 @@ static int __pwm_apply(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
int pwm_apply_might_sleep(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
int err;
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
|
||||
/*
|
||||
* Some lowlevel driver's implementations of .apply() make use of
|
||||
@@ -230,7 +249,12 @@ int pwm_apply_might_sleep(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
*/
|
||||
might_sleep();
|
||||
|
||||
- if (IS_ENABLED(CONFIG_PWM_DEBUG) && pwm->chip->atomic) {
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && chip->atomic) {
|
||||
/*
|
||||
* Catch any drivers that have been marked as atomic but
|
||||
* that will sleep anyway.
|
||||
@@ -254,9 +278,16 @@ EXPORT_SYMBOL_GPL(pwm_apply_might_sleep);
|
||||
*/
|
||||
int pwm_apply_atomic(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
- WARN_ONCE(!pwm->chip->atomic,
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+
|
||||
+ WARN_ONCE(!chip->atomic,
|
||||
"sleeping PWM driver used in atomic context\n");
|
||||
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
return __pwm_apply(pwm, state);
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(pwm_apply_atomic);
|
||||
@@ -336,6 +367,11 @@ static int pwm_capture(struct pwm_device *pwm, struct pwm_capture *result,
|
||||
|
||||
guard(mutex)(&pwm_lock);
|
||||
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
return ops->capture(chip, pwm, result, timeout);
|
||||
}
|
||||
|
||||
@@ -368,6 +404,14 @@ static int pwm_device_request(struct pwm_device *pwm, const char *label)
|
||||
if (test_bit(PWMF_REQUESTED, &pwm->flags))
|
||||
return -EBUSY;
|
||||
|
||||
+ /*
|
||||
+ * This function is called while holding pwm_lock. As .operational only
|
||||
+ * changes while holding this lock, checking it here without holding the
|
||||
+ * chip lock is fine.
|
||||
+ */
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
if (!try_module_get(chip->owner))
|
||||
return -ENODEV;
|
||||
|
||||
@@ -396,7 +440,9 @@ static int pwm_device_request(struct pwm_device *pwm, const char *label)
|
||||
*/
|
||||
struct pwm_state state = { 0, };
|
||||
|
||||
- err = ops->get_state(chip, pwm, &state);
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ err = ops->get_state(chip, pwm, &state);
|
||||
+
|
||||
trace_pwm_get(pwm, &state, err);
|
||||
|
||||
if (!err)
|
||||
@@ -1020,6 +1066,7 @@ struct pwm_chip *pwmchip_alloc(struct device *parent, unsigned int npwm, size_t
|
||||
|
||||
chip->npwm = npwm;
|
||||
chip->uses_pwmchip_alloc = true;
|
||||
+ chip->operational = false;
|
||||
|
||||
pwmchip_dev = &chip->dev;
|
||||
device_initialize(pwmchip_dev);
|
||||
@@ -1125,6 +1172,11 @@ int __pwmchip_add(struct pwm_chip *chip, struct module *owner)
|
||||
|
||||
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);
|
||||
@@ -1138,6 +1190,9 @@ int __pwmchip_add(struct pwm_chip *chip, struct module *owner)
|
||||
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;
|
||||
@@ -1145,6 +1200,9 @@ int __pwmchip_add(struct pwm_chip *chip, struct module *owner)
|
||||
return 0;
|
||||
|
||||
err_device_add:
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = false;
|
||||
+
|
||||
if (IS_ENABLED(CONFIG_OF))
|
||||
of_pwmchip_remove(chip);
|
||||
|
||||
@@ -1164,11 +1222,27 @@ void pwmchip_remove(struct pwm_chip *chip)
|
||||
{
|
||||
pwmchip_sysfs_unexport(chip);
|
||||
|
||||
- if (IS_ENABLED(CONFIG_OF))
|
||||
- of_pwmchip_remove(chip);
|
||||
+ scoped_guard(mutex, &pwm_lock) {
|
||||
+ unsigned int i;
|
||||
+
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ chip->operational = false;
|
||||
+
|
||||
+ for (i = 0; i < chip->npwm; ++i) {
|
||||
+ struct pwm_device *pwm = &chip->pwms[i];
|
||||
+
|
||||
+ if (test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
+ dev_alert(&chip->dev, "Freeing requested PWM #%u\n", i);
|
||||
+ if (pwm->chip->ops->free)
|
||||
+ pwm->chip->ops->free(pwm->chip, pwm);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_OF))
|
||||
+ of_pwmchip_remove(chip);
|
||||
|
||||
- scoped_guard(mutex, &pwm_lock)
|
||||
idr_remove(&pwm_chips, chip->id);
|
||||
+ }
|
||||
|
||||
device_del(&chip->dev);
|
||||
}
|
||||
@@ -1538,12 +1612,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.
|
||||
+ */
|
||||
+ if (chip->operational && !test_and_clear_bit(PWMF_REQUESTED, &pwm->flags)) {
|
||||
pr_warn("PWM device already freed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
- if (chip->ops->free)
|
||||
+ if (chip->operational && chip->ops->free)
|
||||
pwm->chip->ops->free(pwm->chip, pwm);
|
||||
|
||||
pwm->label = NULL;
|
||||
diff --git a/include/linux/pwm.h b/include/linux/pwm.h
|
||||
index 8acd60b53f58..464054a45e57 100644
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -275,6 +275,9 @@ struct pwm_ops {
|
||||
* @of_xlate: request a PWM device given a device tree PWM specifier
|
||||
* @atomic: can the driver's ->apply() be called in atomic context
|
||||
* @uses_pwmchip_alloc: signals if pwmchip_allow was used to allocate this chip
|
||||
+ * @operational: signals if the chip can be used (or is already deregistered)
|
||||
+ * @nonatomic_lock: mutex for nonatomic chips
|
||||
+ * @atomic_lock: mutex for atomic chips
|
||||
* @pwms: array of PWM devices allocated by the framework
|
||||
*/
|
||||
struct pwm_chip {
|
||||
@@ -290,6 +293,16 @@ struct pwm_chip {
|
||||
|
||||
/* only used internally by the PWM framework */
|
||||
bool uses_pwmchip_alloc;
|
||||
+ bool operational;
|
||||
+ union {
|
||||
+ /*
|
||||
+ * depending on the chip being atomic or not either the mutex or
|
||||
+ * the spinlock is used. It protects .operational and
|
||||
+ * synchronizes calls to the .ops->apply and .ops->get_state()
|
||||
+ */
|
||||
+ struct mutex nonatomic_lock;
|
||||
+ struct spinlock atomic_lock;
|
||||
+ };
|
||||
struct pwm_device pwms[] __counted_by(npwm);
|
||||
};
|
||||
|
||||
--
|
||||
2.43.0
|
@ -0,0 +1,412 @@
|
||||
From 17e40c25158f2505cbcdeda96624afcbab4af368 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:57:58 +0200
|
||||
Subject: [PATCH] pwm: New abstraction for PWM waveforms
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Up to now the configuration of a PWM setting is described exclusively by
|
||||
a struct pwm_state which contains information about period, duty_cycle,
|
||||
polarity and if the PWM is enabled. (There is another member usage_power
|
||||
which doesn't completely fit into pwm_state, I ignore it here for
|
||||
simplicity.)
|
||||
|
||||
Instead of a polarity the new abstraction has a member duty_offset_ns
|
||||
that defines when the rising edge happens after the period start. This
|
||||
is more general, as with a pwm_state the rising edge can only happen at
|
||||
the period's start or such that the falling edge is at the end of the
|
||||
period (i.e. duty_offset_ns == 0 or duty_offset_ns == period_length_ns -
|
||||
duty_length_ns).
|
||||
|
||||
A disabled PWM is modeled by .period_length_ns = 0. In my eyes this is a
|
||||
nice usage of that otherwise unusable setting, as it doesn't define
|
||||
anything about the future which matches the fact that consumers should
|
||||
consider the state of the output as undefined and it's just there to say
|
||||
"No further requirements about the output, you can save some power.".
|
||||
|
||||
Further I renamed period and duty_cycle to period_length_ns and
|
||||
duty_length_ns. In the past there was confusion from time to time about
|
||||
duty_cycle being measured in nanoseconds because people expected a
|
||||
percentage of period instead. With "length_ns" as suffix the semantic
|
||||
should be more obvious to people unfamiliar with the pwm subsystem.
|
||||
period is renamed to period_length_ns for consistency.
|
||||
|
||||
The API for consumers doesn't change yet, but lowlevel drivers can
|
||||
implement callbacks that work with pwm_waveforms instead of pwm_states.
|
||||
A new thing about these callbacks is that the calculation of hardware
|
||||
settings needed to implement a certain waveform is separated from
|
||||
actually writing these settings. The motivation for that is that this
|
||||
allows a consumer to query the hardware capabilities without actually
|
||||
modifying the hardware state.
|
||||
|
||||
The rounding rules that are expected to be implemented in the
|
||||
round_waveform_tohw() are: First pick the biggest possible period not
|
||||
bigger than wf->period_length_ns. For that period pick the biggest
|
||||
possible duty setting not bigger than wf->duty_length_ns. Third pick the
|
||||
biggest possible offset not bigger than wf->duty_offset_ns. If the
|
||||
requested period is too small for the hardware, it's expected that a
|
||||
setting with the minimal period and duty_length_ns = duty_offset_ns = 0
|
||||
is returned and this fact is signaled by a return value of 1.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Tested-by: Trevor Gamblin <tgamblin@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/df0faa33bf9e7c9e2e5eab8d31bbf61e861bd401.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
[ukleinek: Update pwm_check_rounding() to return bool instead of int.]
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 234 ++++++++++++++++++++++++++++++++++++++++----
|
||||
include/linux/pwm.h | 36 +++++++
|
||||
2 files changed, 249 insertions(+), 21 deletions(-)
|
||||
|
||||
diff --git a/drivers/pwm/core.c b/drivers/pwm/core.c
|
||||
index 5a095eb46b544f..bbe7bfdb154927 100644
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -49,6 +49,102 @@ static void pwmchip_unlock(struct pwm_chip *chip)
|
||||
|
||||
DEFINE_GUARD(pwmchip, struct pwm_chip *, pwmchip_lock(_T), pwmchip_unlock(_T))
|
||||
|
||||
+static void pwm_wf2state(const struct pwm_waveform *wf, struct pwm_state *state)
|
||||
+{
|
||||
+ if (wf->period_length_ns) {
|
||||
+ if (wf->duty_length_ns + wf->duty_offset_ns < wf->period_length_ns)
|
||||
+ *state = (struct pwm_state){
|
||||
+ .enabled = true,
|
||||
+ .polarity = PWM_POLARITY_NORMAL,
|
||||
+ .period = wf->period_length_ns,
|
||||
+ .duty_cycle = wf->duty_length_ns,
|
||||
+ };
|
||||
+ else
|
||||
+ *state = (struct pwm_state){
|
||||
+ .enabled = true,
|
||||
+ .polarity = PWM_POLARITY_INVERSED,
|
||||
+ .period = wf->period_length_ns,
|
||||
+ .duty_cycle = wf->period_length_ns - wf->duty_length_ns,
|
||||
+ };
|
||||
+ } else {
|
||||
+ *state = (struct pwm_state){
|
||||
+ .enabled = false,
|
||||
+ };
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static void pwm_state2wf(const struct pwm_state *state, struct pwm_waveform *wf)
|
||||
+{
|
||||
+ if (state->enabled) {
|
||||
+ if (state->polarity == PWM_POLARITY_NORMAL)
|
||||
+ *wf = (struct pwm_waveform){
|
||||
+ .period_length_ns = state->period,
|
||||
+ .duty_length_ns = state->duty_cycle,
|
||||
+ .duty_offset_ns = 0,
|
||||
+ };
|
||||
+ else
|
||||
+ *wf = (struct pwm_waveform){
|
||||
+ .period_length_ns = state->period,
|
||||
+ .duty_length_ns = state->period - state->duty_cycle,
|
||||
+ .duty_offset_ns = state->duty_cycle,
|
||||
+ };
|
||||
+ } else {
|
||||
+ *wf = (struct pwm_waveform){
|
||||
+ .period_length_ns = 0,
|
||||
+ };
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static bool pwm_check_rounding(const struct pwm_waveform *wf,
|
||||
+ const struct pwm_waveform *wf_rounded)
|
||||
+{
|
||||
+ if (!wf->period_length_ns)
|
||||
+ return true;
|
||||
+
|
||||
+ if (wf->period_length_ns < wf_rounded->period_length_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ if (wf->duty_length_ns < wf_rounded->duty_length_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ if (wf->duty_offset_ns < wf_rounded->duty_offset_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+static int __pwm_round_waveform_tohw(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf, void *wfhw)
|
||||
+{
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+
|
||||
+ return ops->round_waveform_tohw(chip, pwm, wf, wfhw);
|
||||
+}
|
||||
+
|
||||
+static int __pwm_round_waveform_fromhw(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const void *wfhw, struct pwm_waveform *wf)
|
||||
+{
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+
|
||||
+ return ops->round_waveform_fromhw(chip, pwm, wfhw, wf);
|
||||
+}
|
||||
+
|
||||
+static int __pwm_read_waveform(struct pwm_chip *chip, struct pwm_device *pwm, void *wfhw)
|
||||
+{
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+
|
||||
+ return ops->read_waveform(chip, pwm, wfhw);
|
||||
+}
|
||||
+
|
||||
+static int __pwm_write_waveform(struct pwm_chip *chip, struct pwm_device *pwm, const void *wfhw)
|
||||
+{
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+
|
||||
+ return ops->write_waveform(chip, pwm, wfhw);
|
||||
+}
|
||||
+
|
||||
+#define WFHWSIZE 20
|
||||
+
|
||||
static void pwm_apply_debug(struct pwm_device *pwm,
|
||||
const struct pwm_state *state)
|
||||
{
|
||||
@@ -182,6 +278,7 @@ static bool pwm_state_valid(const struct pwm_state *state)
|
||||
static int __pwm_apply(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
{
|
||||
struct pwm_chip *chip;
|
||||
+ const struct pwm_ops *ops;
|
||||
int err;
|
||||
|
||||
if (!pwm || !state)
|
||||
@@ -205,6 +302,7 @@ static int __pwm_apply(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
}
|
||||
|
||||
chip = pwm->chip;
|
||||
+ ops = chip->ops;
|
||||
|
||||
if (state->period == pwm->state.period &&
|
||||
state->duty_cycle == pwm->state.duty_cycle &&
|
||||
@@ -213,18 +311,69 @@ static int __pwm_apply(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
state->usage_power == pwm->state.usage_power)
|
||||
return 0;
|
||||
|
||||
- err = chip->ops->apply(chip, pwm, state);
|
||||
- trace_pwm_apply(pwm, state, err);
|
||||
- if (err)
|
||||
- return err;
|
||||
+ if (ops->write_waveform) {
|
||||
+ struct pwm_waveform wf;
|
||||
+ char wfhw[WFHWSIZE];
|
||||
|
||||
- pwm->state = *state;
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
|
||||
- /*
|
||||
- * only do this after pwm->state was applied as some
|
||||
- * implementations of .get_state depend on this
|
||||
- */
|
||||
- pwm_apply_debug(pwm, state);
|
||||
+ pwm_state2wf(state, &wf);
|
||||
+
|
||||
+ /*
|
||||
+ * The rounding is wrong here for states with inverted polarity.
|
||||
+ * While .apply() rounds down duty_cycle (which represents the
|
||||
+ * time from the start of the period to the inner edge),
|
||||
+ * .round_waveform_tohw() rounds down the time the PWM is high.
|
||||
+ * Can be fixed if the need arises, until reported otherwise
|
||||
+ * let's assume that consumers don't care.
|
||||
+ */
|
||||
+
|
||||
+ err = __pwm_round_waveform_tohw(chip, pwm, &wf, &wfhw);
|
||||
+ if (err) {
|
||||
+ if (err > 0)
|
||||
+ /*
|
||||
+ * This signals an invalid request, typically
|
||||
+ * the requested period (or duty_offset) is
|
||||
+ * smaller than possible with the hardware.
|
||||
+ */
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ return err;
|
||||
+ }
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG)) {
|
||||
+ struct pwm_waveform wf_rounded;
|
||||
+
|
||||
+ err = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf_rounded);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ if (!pwm_check_rounding(&wf, &wf_rounded))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ wf.duty_length_ns, wf.period_length_ns, wf.duty_offset_ns,
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
+ }
|
||||
+
|
||||
+ err = __pwm_write_waveform(chip, pwm, &wfhw);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ pwm->state = *state;
|
||||
+
|
||||
+ } else {
|
||||
+ err = ops->apply(chip, pwm, state);
|
||||
+ trace_pwm_apply(pwm, state, err);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ pwm->state = *state;
|
||||
+
|
||||
+ /*
|
||||
+ * only do this after pwm->state was applied as some
|
||||
+ * implementations of .get_state() depend on this
|
||||
+ */
|
||||
+ pwm_apply_debug(pwm, state);
|
||||
+ }
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -292,6 +441,41 @@ int pwm_apply_atomic(struct pwm_device *pwm, const struct pwm_state *state)
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(pwm_apply_atomic);
|
||||
|
||||
+static int pwm_get_state_hw(struct pwm_device *pwm, struct pwm_state *state)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+ int ret = -EOPNOTSUPP;
|
||||
+
|
||||
+ if (ops->read_waveform) {
|
||||
+ char wfhw[WFHWSIZE];
|
||||
+ struct pwm_waveform wf;
|
||||
+
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
+
|
||||
+ scoped_guard(pwmchip, chip) {
|
||||
+
|
||||
+ ret = __pwm_read_waveform(chip, pwm, &wfhw);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ ret = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ pwm_wf2state(&wf, state);
|
||||
+
|
||||
+ } else if (ops->get_state) {
|
||||
+ scoped_guard(pwmchip, chip)
|
||||
+ ret = ops->get_state(chip, pwm, state);
|
||||
+
|
||||
+ trace_pwm_get(pwm, state, ret);
|
||||
+ }
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* pwm_adjust_config() - adjust the current PWM config to the PWM arguments
|
||||
* @pwm: PWM device
|
||||
@@ -435,7 +619,7 @@ static int pwm_device_request(struct pwm_device *pwm, const char *label)
|
||||
}
|
||||
}
|
||||
|
||||
- if (ops->get_state) {
|
||||
+ if (ops->read_waveform || ops->get_state) {
|
||||
/*
|
||||
* Zero-initialize state because most drivers are unaware of
|
||||
* .usage_power. The other members of state are supposed to be
|
||||
@@ -445,11 +629,7 @@ static int pwm_device_request(struct pwm_device *pwm, const char *label)
|
||||
*/
|
||||
struct pwm_state state = { 0, };
|
||||
|
||||
- scoped_guard(pwmchip, chip)
|
||||
- err = ops->get_state(chip, pwm, &state);
|
||||
-
|
||||
- trace_pwm_get(pwm, &state, err);
|
||||
-
|
||||
+ err = pwm_get_state_hw(pwm, &state);
|
||||
if (!err)
|
||||
pwm->state = state;
|
||||
|
||||
@@ -1136,12 +1316,24 @@ static bool pwm_ops_check(const struct pwm_chip *chip)
|
||||
{
|
||||
const struct pwm_ops *ops = chip->ops;
|
||||
|
||||
- if (!ops->apply)
|
||||
- return false;
|
||||
+ if (ops->write_waveform) {
|
||||
+ if (!ops->round_waveform_tohw ||
|
||||
+ !ops->round_waveform_fromhw ||
|
||||
+ !ops->write_waveform)
|
||||
+ return false;
|
||||
|
||||
- if (IS_ENABLED(CONFIG_PWM_DEBUG) && !ops->get_state)
|
||||
- dev_warn(pwmchip_parent(chip),
|
||||
- "Please implement the .get_state() callback\n");
|
||||
+ if (WFHWSIZE < ops->sizeof_wfhw) {
|
||||
+ dev_warn(pwmchip_parent(chip), "WFHWSIZE < %zu\n", ops->sizeof_wfhw);
|
||||
+ return false;
|
||||
+ }
|
||||
+ } else {
|
||||
+ if (!ops->apply)
|
||||
+ return false;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && !ops->get_state)
|
||||
+ dev_warn(pwmchip_parent(chip),
|
||||
+ "Please implement the .get_state() callback\n");
|
||||
+ }
|
||||
|
||||
return true;
|
||||
}
|
||||
diff --git a/include/linux/pwm.h b/include/linux/pwm.h
|
||||
index 3ea73e075abe87..d8cfe1c9b19d83 100644
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -49,6 +49,31 @@ enum {
|
||||
PWMF_EXPORTED = 1,
|
||||
};
|
||||
|
||||
+/**
|
||||
+ * struct pwm_waveform - description of a PWM waveform
|
||||
+ * @period_length_ns: PWM period
|
||||
+ * @duty_length_ns: PWM duty cycle
|
||||
+ * @duty_offset_ns: offset of the rising edge from the period's start
|
||||
+ *
|
||||
+ * This is a representation of a PWM waveform alternative to struct pwm_state
|
||||
+ * below. It's more expressive than struct pwm_state as it contains a
|
||||
+ * duty_offset_ns and so can represent offsets other than zero (with .polarity =
|
||||
+ * PWM_POLARITY_NORMAL) and period - duty_cycle (.polarity =
|
||||
+ * PWM_POLARITY_INVERSED).
|
||||
+ *
|
||||
+ * Note there is no explicit bool for enabled. A "disabled" PWM is represented
|
||||
+ * by .period_length_ns = 0. Note further that the behaviour of a "disabled" PWM
|
||||
+ * is undefined. Depending on the hardware's capabilities it might drive the
|
||||
+ * active or inactive level, go high-z or even continue to toggle.
|
||||
+ *
|
||||
+ * The unit for all three members is nanoseconds.
|
||||
+ */
|
||||
+struct pwm_waveform {
|
||||
+ u64 period_length_ns;
|
||||
+ u64 duty_length_ns;
|
||||
+ u64 duty_offset_ns;
|
||||
+};
|
||||
+
|
||||
/*
|
||||
* struct pwm_state - state of a PWM channel
|
||||
* @period: PWM period (in nanoseconds)
|
||||
@@ -259,6 +284,17 @@ struct pwm_ops {
|
||||
void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
|
||||
int (*capture)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
struct pwm_capture *result, unsigned long timeout);
|
||||
+
|
||||
+ size_t sizeof_wfhw;
|
||||
+ int (*round_waveform_tohw)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf, void *wfhw);
|
||||
+ int (*round_waveform_fromhw)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const void *wfhw, struct pwm_waveform *wf);
|
||||
+ int (*read_waveform)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ void *wfhw);
|
||||
+ int (*write_waveform)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const void *wfhw);
|
||||
+
|
||||
int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
const struct pwm_state *state);
|
||||
int (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
|
@ -0,0 +1,327 @@
|
||||
From 6c5126c6406d1c31e91f5b925c621c1c785366be Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@baylibre.com>
|
||||
Date: Fri, 20 Sep 2024 10:57:59 +0200
|
||||
Subject: [PATCH] pwm: Provide new consumer API functions for waveforms
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Provide API functions for consumers to work with waveforms.
|
||||
|
||||
Note that one relevant difference between pwm_get_state() and
|
||||
pwm_get_waveform*() is that the latter yields the actually configured
|
||||
hardware state, while the former yields the last state passed to
|
||||
pwm_apply*() and so doesn't account for hardware specific rounding.
|
||||
|
||||
Signed-off-by: Uwe Kleine-König <u.kleine-koenig@baylibre.com>
|
||||
Tested-by: Trevor Gamblin <tgamblin@baylibre.com>
|
||||
Link: https://lore.kernel.org/r/6c97d27682853f603e18e9196043886dd671845d.1726819463.git.u.kleine-koenig@baylibre.com
|
||||
Signed-off-by: Uwe Kleine-König <ukleinek@kernel.org>
|
||||
---
|
||||
drivers/pwm/core.c | 261 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
include/linux/pwm.h | 6 +-
|
||||
2 files changed, 266 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/drivers/pwm/core.c b/drivers/pwm/core.c
|
||||
index bbe7bfdb154927..038f17dd275798 100644
|
||||
--- a/drivers/pwm/core.c
|
||||
+++ b/drivers/pwm/core.c
|
||||
@@ -49,6 +49,30 @@ static void pwmchip_unlock(struct pwm_chip *chip)
|
||||
|
||||
DEFINE_GUARD(pwmchip, struct pwm_chip *, pwmchip_lock(_T), pwmchip_unlock(_T))
|
||||
|
||||
+static bool pwm_wf_valid(const struct pwm_waveform *wf)
|
||||
+{
|
||||
+ /*
|
||||
+ * For now restrict waveforms to period_length_ns <= S64_MAX to provide
|
||||
+ * some space for future extensions. One possibility is to simplify
|
||||
+ * representing waveforms with inverted polarity using negative values
|
||||
+ * somehow.
|
||||
+ */
|
||||
+ if (wf->period_length_ns > S64_MAX)
|
||||
+ return false;
|
||||
+
|
||||
+ if (wf->duty_length_ns > wf->period_length_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ /*
|
||||
+ * .duty_offset_ns is supposed to be smaller than .period_length_ns, apart
|
||||
+ * from the corner case .duty_offset_ns == 0 && .period_length_ns == 0.
|
||||
+ */
|
||||
+ if (wf->duty_offset_ns && wf->duty_offset_ns >= wf->period_length_ns)
|
||||
+ return false;
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
static void pwm_wf2state(const struct pwm_waveform *wf, struct pwm_state *state)
|
||||
{
|
||||
if (wf->period_length_ns) {
|
||||
@@ -95,6 +119,29 @@ static void pwm_state2wf(const struct pwm_state *state, struct pwm_waveform *wf)
|
||||
}
|
||||
}
|
||||
|
||||
+static int pwmwfcmp(const struct pwm_waveform *a, const struct pwm_waveform *b)
|
||||
+{
|
||||
+ if (a->period_length_ns > b->period_length_ns)
|
||||
+ return 1;
|
||||
+
|
||||
+ if (a->period_length_ns < b->period_length_ns)
|
||||
+ return -1;
|
||||
+
|
||||
+ if (a->duty_length_ns > b->duty_length_ns)
|
||||
+ return 1;
|
||||
+
|
||||
+ if (a->duty_length_ns < b->duty_length_ns)
|
||||
+ return -1;
|
||||
+
|
||||
+ if (a->duty_offset_ns > b->duty_offset_ns)
|
||||
+ return 1;
|
||||
+
|
||||
+ if (a->duty_offset_ns < b->duty_offset_ns)
|
||||
+ return -1;
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
static bool pwm_check_rounding(const struct pwm_waveform *wf,
|
||||
const struct pwm_waveform *wf_rounded)
|
||||
{
|
||||
@@ -145,6 +192,220 @@ static int __pwm_write_waveform(struct pwm_chip *chip, struct pwm_device *pwm, c
|
||||
|
||||
#define WFHWSIZE 20
|
||||
|
||||
+/**
|
||||
+ * pwm_round_waveform_might_sleep - Query hardware capabilities
|
||||
+ * Cannot be used in atomic context.
|
||||
+ * @pwm: PWM device
|
||||
+ * @wf: waveform to round and output parameter
|
||||
+ *
|
||||
+ * Typically a given waveform cannot be implemented exactly by hardware, e.g.
|
||||
+ * because hardware only supports coarse period resolution or no duty_offset.
|
||||
+ * This function returns the actually implemented waveform if you pass wf to
|
||||
+ * pwm_set_waveform_might_sleep now.
|
||||
+ *
|
||||
+ * Note however that the world doesn't stop turning when you call it, so when
|
||||
+ * doing
|
||||
+ *
|
||||
+ * pwm_round_waveform_might_sleep(mypwm, &wf);
|
||||
+ * pwm_set_waveform_might_sleep(mypwm, &wf, true);
|
||||
+ *
|
||||
+ * the latter might fail, e.g. because an input clock changed its rate between
|
||||
+ * these two calls and the waveform determined by
|
||||
+ * pwm_round_waveform_might_sleep() cannot be implemented any more.
|
||||
+ *
|
||||
+ * Returns 0 on success, 1 if there is no valid hardware configuration matching
|
||||
+ * the input waveform under the PWM rounding rules or a negative errno.
|
||||
+ */
|
||||
+int pwm_round_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+ struct pwm_waveform wf_req = *wf;
|
||||
+ char wfhw[WFHWSIZE];
|
||||
+ int ret_tohw, ret_fromhw;
|
||||
+
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
+
|
||||
+ if (!pwm_wf_valid(wf))
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
+ ret_tohw = __pwm_round_waveform_tohw(chip, pwm, wf, wfhw);
|
||||
+ if (ret_tohw < 0)
|
||||
+ return ret_tohw;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && ret_tohw > 1)
|
||||
+ dev_err(&chip->dev, "Unexpected return value from __pwm_round_waveform_tohw: requested %llu/%llu [+%llu], return value %d\n",
|
||||
+ wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns, ret_tohw);
|
||||
+
|
||||
+ ret_fromhw = __pwm_round_waveform_fromhw(chip, pwm, wfhw, wf);
|
||||
+ if (ret_fromhw < 0)
|
||||
+ return ret_fromhw;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && ret_fromhw > 0)
|
||||
+ dev_err(&chip->dev, "Unexpected return value from __pwm_round_waveform_fromhw: requested %llu/%llu [+%llu], return value %d\n",
|
||||
+ wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns, ret_tohw);
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) &&
|
||||
+ ret_tohw == 0 && !pwm_check_rounding(&wf_req, wf))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ wf_req.duty_length_ns, wf_req.period_length_ns, wf_req.duty_offset_ns,
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns);
|
||||
+
|
||||
+ return ret_tohw;
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(pwm_round_waveform_might_sleep);
|
||||
+
|
||||
+/**
|
||||
+ * pwm_get_waveform_might_sleep - Query hardware about current configuration
|
||||
+ * Cannot be used in atomic context.
|
||||
+ * @pwm: PWM device
|
||||
+ * @wf: output parameter
|
||||
+ *
|
||||
+ * Stores the current configuration of the PWM in @wf. Note this is the
|
||||
+ * equivalent of pwm_get_state_hw() (and not pwm_get_state()) for pwm_waveform.
|
||||
+ */
|
||||
+int pwm_get_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+ char wfhw[WFHWSIZE];
|
||||
+ int err;
|
||||
+
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
+
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
+ err = __pwm_read_waveform(chip, pwm, &wfhw);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ return __pwm_round_waveform_fromhw(chip, pwm, &wfhw, wf);
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(pwm_get_waveform_might_sleep);
|
||||
+
|
||||
+/* Called with the pwmchip lock held */
|
||||
+static int __pwm_set_waveform(struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf,
|
||||
+ bool exact)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ const struct pwm_ops *ops = chip->ops;
|
||||
+ char wfhw[WFHWSIZE];
|
||||
+ struct pwm_waveform wf_rounded;
|
||||
+ int err;
|
||||
+
|
||||
+ BUG_ON(WFHWSIZE < ops->sizeof_wfhw);
|
||||
+
|
||||
+ if (!pwm_wf_valid(wf))
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ err = __pwm_round_waveform_tohw(chip, pwm, wf, &wfhw);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ if ((IS_ENABLED(CONFIG_PWM_DEBUG) || exact) && wf->period_length_ns) {
|
||||
+ err = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf_rounded);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && !pwm_check_rounding(wf, &wf_rounded))
|
||||
+ dev_err(&chip->dev, "Wrong rounding: requested %llu/%llu [+%llu], result %llu/%llu [+%llu]\n",
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
+
|
||||
+ if (exact && pwmwfcmp(wf, &wf_rounded)) {
|
||||
+ dev_dbg(&chip->dev, "Requested no rounding, but %llu/%llu [+%llu] -> %llu/%llu [+%llu]\n",
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns);
|
||||
+
|
||||
+ return 1;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ err = __pwm_write_waveform(chip, pwm, &wfhw);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ /* update .state */
|
||||
+ pwm_wf2state(wf, &pwm->state);
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && ops->read_waveform && wf->period_length_ns) {
|
||||
+ struct pwm_waveform wf_set;
|
||||
+
|
||||
+ err = __pwm_read_waveform(chip, pwm, &wfhw);
|
||||
+ if (err)
|
||||
+ /* maybe ignore? */
|
||||
+ return err;
|
||||
+
|
||||
+ err = __pwm_round_waveform_fromhw(chip, pwm, &wfhw, &wf_set);
|
||||
+ if (err)
|
||||
+ /* maybe ignore? */
|
||||
+ return err;
|
||||
+
|
||||
+ if (pwmwfcmp(&wf_set, &wf_rounded) != 0)
|
||||
+ dev_err(&chip->dev,
|
||||
+ "Unexpected setting: requested %llu/%llu [+%llu], expected %llu/%llu [+%llu], set %llu/%llu [+%llu]\n",
|
||||
+ wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns,
|
||||
+ wf_rounded.duty_length_ns, wf_rounded.period_length_ns, wf_rounded.duty_offset_ns,
|
||||
+ wf_set.duty_length_ns, wf_set.period_length_ns, wf_set.duty_offset_ns);
|
||||
+ }
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * pwm_set_waveform_might_sleep - Apply a new waveform
|
||||
+ * Cannot be used in atomic context.
|
||||
+ * @pwm: PWM device
|
||||
+ * @wf: The waveform to apply
|
||||
+ * @exact: If true no rounding is allowed
|
||||
+ *
|
||||
+ * Typically a requested waveform cannot be implemented exactly, e.g. because
|
||||
+ * you requested .period_length_ns = 100 ns, but the hardware can only set
|
||||
+ * periods that are a multiple of 8.5 ns. With that hardware passing exact =
|
||||
+ * true results in pwm_set_waveform_might_sleep() failing and returning 1. If
|
||||
+ * exact = false you get a period of 93.5 ns (i.e. the biggest period not bigger
|
||||
+ * than the requested value).
|
||||
+ * Note that even with exact = true, some rounding by less than 1 is
|
||||
+ * possible/needed. In the above example requesting .period_length_ns = 94 and
|
||||
+ * exact = true, you get the hardware configured with period = 93.5 ns.
|
||||
+ */
|
||||
+int pwm_set_waveform_might_sleep(struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf, bool exact)
|
||||
+{
|
||||
+ struct pwm_chip *chip = pwm->chip;
|
||||
+ int err;
|
||||
+
|
||||
+ might_sleep();
|
||||
+
|
||||
+ guard(pwmchip)(chip);
|
||||
+
|
||||
+ if (!chip->operational)
|
||||
+ return -ENODEV;
|
||||
+
|
||||
+ if (IS_ENABLED(CONFIG_PWM_DEBUG) && chip->atomic) {
|
||||
+ /*
|
||||
+ * Catch any drivers that have been marked as atomic but
|
||||
+ * that will sleep anyway.
|
||||
+ */
|
||||
+ non_block_start();
|
||||
+ err = __pwm_set_waveform(pwm, wf, exact);
|
||||
+ non_block_end();
|
||||
+ } else {
|
||||
+ err = __pwm_set_waveform(pwm, wf, exact);
|
||||
+ }
|
||||
+
|
||||
+ return err;
|
||||
+}
|
||||
+EXPORT_SYMBOL_GPL(pwm_set_waveform_might_sleep);
|
||||
+
|
||||
static void pwm_apply_debug(struct pwm_device *pwm,
|
||||
const struct pwm_state *state)
|
||||
{
|
||||
diff --git a/include/linux/pwm.h b/include/linux/pwm.h
|
||||
index d8cfe1c9b19d83..c3d9ddeafa65e1 100644
|
||||
--- a/include/linux/pwm.h
|
||||
+++ b/include/linux/pwm.h
|
||||
@@ -358,7 +358,11 @@ static inline void pwmchip_set_drvdata(struct pwm_chip *chip, void *data)
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_PWM)
|
||||
-/* PWM user APIs */
|
||||
+
|
||||
+/* PWM consumer APIs */
|
||||
+int pwm_round_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf);
|
||||
+int pwm_get_waveform_might_sleep(struct pwm_device *pwm, struct pwm_waveform *wf);
|
||||
+int pwm_set_waveform_might_sleep(struct pwm_device *pwm, const struct pwm_waveform *wf, bool exact);
|
||||
int pwm_apply_might_sleep(struct pwm_device *pwm, const struct pwm_state *state);
|
||||
int pwm_apply_atomic(struct pwm_device *pwm, const struct pwm_state *state);
|
||||
int pwm_adjust_config(struct pwm_device *pwm);
|
@ -0,0 +1,21 @@
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
Documentation/devicetree/bindings/pinctrl/rockchip,pinctrl.yaml | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/Documentation/devicetree/bindings/pinctrl/rockchip,pinctrl.yaml b/Documentation/devicetree/bindings/pinctrl/rockchip,pinctrl.yaml
|
||||
index 960758dc417f7405010fab067bfbf6f5c4704179..125af766b99297dc229db158846daea974dda28e 100644
|
||||
--- a/Documentation/devicetree/bindings/pinctrl/rockchip,pinctrl.yaml
|
||||
+++ b/Documentation/devicetree/bindings/pinctrl/rockchip,pinctrl.yaml
|
||||
@@ -135,7 +135,7 @@ additionalProperties:
|
||||
description:
|
||||
Pin bank index.
|
||||
- minimum: 0
|
||||
- maximum: 13
|
||||
+ maximum: 14
|
||||
description:
|
||||
Mux 0 means GPIO and mux 1 to N means
|
||||
the specific device function.
|
||||
|
||||
--
|
||||
2.49.0
|
@ -0,0 +1,127 @@
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
.../bindings/pwm/rockchip,rk3576-pwm.yaml | 94 ++++++++++++++++++++++
|
||||
MAINTAINERS | 7 ++
|
||||
2 files changed, 101 insertions(+)
|
||||
|
||||
diff --git a/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..143d4df5df8fa89d508faca5ddf67603fb7cb3f5
|
||||
--- /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>;
|
||||
+ };
|
||||
+ };
|
||||
diff --git a/MAINTAINERS b/MAINTAINERS
|
||||
index 96b82704950184bd71623ff41fc4df31e4c7fe87..407179d2a90dd49800f2bb5770a1280c5afebb5a 100644
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -20885,6 +20885,13 @@ F: Documentation/userspace-api/media/v4l/metafmt-rkisp1.rst
|
||||
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>
|
||||
|
||||
--
|
||||
2.49.0
|
@ -0,0 +1,90 @@
|
||||
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
---
|
||||
include/soc/rockchip/utils.h | 76 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 76 insertions(+)
|
||||
|
||||
diff --git a/include/soc/rockchip/utils.h b/include/soc/rockchip/utils.h
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..3349069e75ff51ebd7a22089af796feafd227ffb
|
||||
--- /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__ */
|
||||
|
||||
--
|
||||
2.49.0
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,401 @@
|
||||
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(+)
|
||||
|
||||
diff --git a/MAINTAINERS b/MAINTAINERS
|
||||
index e6a9347be1e7889089e1d9e655cb23c2d8399b40..3ddd245fd4ad8d9ed2e762910a7a1f6436f93e34 100644
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -20891,6 +20891,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
|
||||
|
||||
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
|
||||
index 4731d5b90d7edcc61138e4a5bf7e98906953ece4..242039f62ab091cea337bf27ef310bcf696b6ed0 100644
|
||||
--- 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.
|
||||
|
||||
+config PWM_ROCKCHIP_V4
|
||||
+ tristate "Rockchip PWM v4 support"
|
||||
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
|
||||
+ depends on ROCKCHIP_MFPWM
|
||||
+ depends on HAS_IOMEM
|
||||
+ help
|
||||
+ Generic PWM framework driver for the PWM controller found on
|
||||
+ later Rockchip SoCs such as the RK3576.
|
||||
+
|
||||
+ Uses the Rockchip Multi-function PWM controller driver infrastructure
|
||||
+ to guarantee fearlessly concurrent operation with other functions of
|
||||
+ the same device implemented by drivers in other subsystems.
|
||||
+
|
||||
config PWM_RZ_MTU3
|
||||
tristate "Renesas RZ/G2L MTU3a PWM Timer support"
|
||||
depends on RZ_MTU3
|
||||
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
|
||||
index 539e0def3f82fcb866ab83a0346a15f7efdd7127..b5aca7ff58ac83f844581df526624617025291de 100644
|
||||
--- a/drivers/pwm/Makefile
|
||||
+++ b/drivers/pwm/Makefile
|
||||
@@ -49,6 +49,7 @@ obj-$(CONFIG_PWM_RASPBERRYPI_POE) += pwm-raspberrypi-poe.o
|
||||
obj-$(CONFIG_PWM_RCAR) += pwm-rcar.o
|
||||
obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
|
||||
obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
|
||||
+obj-$(CONFIG_PWM_ROCKCHIP_V4) += pwm-rockchip-v4.o
|
||||
obj-$(CONFIG_PWM_RZ_MTU3) += pwm-rz-mtu3.o
|
||||
obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
|
||||
obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
|
||||
diff --git a/drivers/pwm/pwm-rockchip-v4.c b/drivers/pwm/pwm-rockchip-v4.c
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..980b27454ef9b930bef0496ca528533cf419fa0e
|
||||
--- /dev/null
|
||||
+++ b/drivers/pwm/pwm-rockchip-v4.c
|
||||
@@ -0,0 +1,336 @@
|
||||
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+/*
|
||||
+ * Copyright (c) 2025 Collabora Ltd.
|
||||
+ *
|
||||
+ * A Pulse-Width-Modulation (PWM) generator driver for the generators found in
|
||||
+ * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". Uses
|
||||
+ * the MFPWM infrastructure to guarantee exclusive use over the device without
|
||||
+ * other functions of the device from different drivers interfering with its
|
||||
+ * operation while it's active.
|
||||
+ *
|
||||
+ * Authors:
|
||||
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ */
|
||||
+
|
||||
+#include <linux/platform_device.h>
|
||||
+#include <linux/pwm.h>
|
||||
+#include <soc/rockchip/mfpwm.h>
|
||||
+
|
||||
+struct rockchip_pwm_v4 {
|
||||
+ struct rockchip_mfpwm_func *pwmf;
|
||||
+ struct pwm_chip chip;
|
||||
+};
|
||||
+
|
||||
+struct rockchip_pwm_v4_wf {
|
||||
+ u32 period;
|
||||
+ u32 duty;
|
||||
+ u32 offset;
|
||||
+ u8 enable;
|
||||
+};
|
||||
+
|
||||
+static inline struct rockchip_pwm_v4 *to_rockchip_pwm_v4(struct pwm_chip *chip)
|
||||
+{
|
||||
+ return pwmchip_get_drvdata(chip);
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * rockchip_pwm_v4_round_single - convert a PWM parameter to hardware
|
||||
+ * @rate: clock rate of the PWM clock, as per clk_get_rate
|
||||
+ * @in_val: parameter in nanoseconds to convert
|
||||
+ * @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
|
||||
+ */
|
||||
+static int rockchip_pwm_v4_round_single(unsigned long rate, u64 in_val,
|
||||
+ u32 *out_val)
|
||||
+{
|
||||
+ u64 tmp;
|
||||
+
|
||||
+ if (!out_val)
|
||||
+ return 0;
|
||||
+
|
||||
+ tmp = mult_frac(rate, in_val, NSEC_PER_SEC);
|
||||
+ if (tmp > U32_MAX)
|
||||
+ return -EOVERFLOW;
|
||||
+
|
||||
+ *out_val = tmp;
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * rockchip_pwm_v4_round_params - convert PWM parameters to hardware
|
||||
+ * @rate: PWM clock rate to do the calculations at
|
||||
+ * @duty: PWM duty cycle in nanoseconds
|
||||
+ * @period: PWM period in nanoseconds
|
||||
+ * @offset: PWM offset in nanoseconds
|
||||
+ * @out_duty: pointer to where the rounded duty value should be stored
|
||||
+ * 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
|
||||
+ */
|
||||
+static int 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;
|
||||
+
|
||||
+ ret = rockchip_pwm_v4_round_single(rate, duty, out_duty);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ 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;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_round_wf_tohw(struct pwm_chip *chip,
|
||||
+ struct pwm_device *pwm,
|
||||
+ const struct pwm_waveform *wf,
|
||||
+ void *_wfhw)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ unsigned long rate;
|
||||
+ 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 = 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);
|
||||
+
|
||||
+ 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);
|
||||
+
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_round_wf_fromhw(struct pwm_chip *chip,
|
||||
+ struct pwm_device *pwm,
|
||||
+ const void *_wfhw,
|
||||
+ struct pwm_waveform *wf)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ unsigned long rate;
|
||||
+ 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 = mfpwm_clk_get_rate(pc->pwmf->parent);
|
||||
+
|
||||
+ /* Let's avoid a cool division-by-zero if the clock's busted. */
|
||||
+ if (!rate) {
|
||||
+ ret = -EINVAL;
|
||||
+ goto out_mfpwm_release;
|
||||
+ }
|
||||
+
|
||||
+ wf->duty_length_ns = mult_frac(wfhw->duty, NSEC_PER_SEC, 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;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ void *_wfhw)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ wfhw->period = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_PERIOD);
|
||||
+ wfhw->duty = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_DUTY);
|
||||
+ wfhw->offset = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_OFFSET);
|
||||
+ wfhw->enable = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_ENABLE) & PWMV4_EN_BOTH_MASK;
|
||||
+
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rockchip_pwm_v4_write_wf(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||
+ const void *_wfhw)
|
||||
+{
|
||||
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
|
||||
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
|
||||
+ bool was_enabled = false;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ was_enabled = pwmv4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
|
||||
+ PWMV4_REG_ENABLE));
|
||||
+
|
||||
+ /*
|
||||
+ * "But Nicolas", you ask with valid concerns, "why would you enable the
|
||||
+ * PWM before setting all the parameter registers?"
|
||||
+ *
|
||||
+ * Excellent question, Mr. Reader M. Strawman! The RK3576 TRM Part 1
|
||||
+ * Section 34.6.3 specifies that this is the intended order of writes.
|
||||
+ * Doing the PWM_EN and PWM_CLK_EN writes after the params but before
|
||||
+ * the CTRL_UPDATE_EN, or even after the CTRL_UPDATE_EN, results in
|
||||
+ * erratic behaviour where repeated turning on and off of the PWM may
|
||||
+ * not turn it off under all circumstances. This is also why we don't
|
||||
+ * use relaxed writes; it's not worth the footgun.
|
||||
+ */
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ REG_UPDATE_WE(wfhw->enable, 0, 1));
|
||||
+
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_PERIOD, wfhw->period);
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_DUTY, wfhw->duty);
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_OFFSET, wfhw->offset);
|
||||
+
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL, PWMV4_CTRL_CONT_FLAGS);
|
||||
+
|
||||
+ /* Commit new configuration to hardware output. */
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_CTRL_UPDATE_EN(1));
|
||||
+
|
||||
+ if (pwmv4_is_enabled(wfhw->enable)) {
|
||||
+ if (!was_enabled) {
|
||||
+ dev_dbg(&chip->dev, "enabling PWM output\n");
|
||||
+ ret = mfpwm_pwmclk_enable(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ goto err_mfpwm_release;
|
||||
+
|
||||
+ /*
|
||||
+ * Output should be on now, acquire device to guarantee
|
||||
+ * exclusion with other device functions while it's on.
|
||||
+ */
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ goto err_mfpwm_release;
|
||||
+ }
|
||||
+ } else if (was_enabled) {
|
||||
+ dev_dbg(&chip->dev, "disabling PWM output\n");
|
||||
+ mfpwm_pwmclk_disable(pc->pwmf);
|
||||
+ /* Output is off now, extra release to balance extra acquire */
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ }
|
||||
+
|
||||
+err_mfpwm_release:
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+
|
||||
+ 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,
|
||||
+ .round_waveform_fromhw = rockchip_pwm_v4_round_wf_fromhw,
|
||||
+ .read_waveform = rockchip_pwm_v4_read_wf,
|
||||
+ .write_waveform = rockchip_pwm_v4_write_wf,
|
||||
+};
|
||||
+
|
||||
+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;
|
||||
+ int ret;
|
||||
+
|
||||
+ chip = devm_pwmchip_alloc(&pdev->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);
|
||||
+
|
||||
+ chip->ops = &rockchip_pwm_v4_ops;
|
||||
+ chip->atomic = true;
|
||||
+
|
||||
+ ret = pwmchip_add(chip);
|
||||
+ if (ret)
|
||||
+ return dev_err_probe(&pdev->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 */ }
|
||||
+};
|
||||
+MODULE_DEVICE_TABLE(platform, rockchip_pwm_v4_ids);
|
||||
+
|
||||
+static struct platform_driver rockchip_pwm_v4_driver = {
|
||||
+ .probe = rockchip_pwm_v4_probe,
|
||||
+ .remove = rockchip_pwm_v4_remove,
|
||||
+ .driver = {
|
||||
+ .name = "pwm-rockchip-v4",
|
||||
+ },
|
||||
+ .id_table = rockchip_pwm_v4_ids,
|
||||
+};
|
||||
+module_platform_driver(rockchip_pwm_v4_driver);
|
||||
+
|
||||
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
|
||||
+MODULE_DESCRIPTION("Rockchip PWMv4 Driver");
|
||||
+MODULE_LICENSE("GPL");
|
||||
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
|
||||
|
||||
--
|
||||
2.49.0
|
@ -0,0 +1,403 @@
|
||||
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(+)
|
||||
|
||||
diff --git a/MAINTAINERS b/MAINTAINERS
|
||||
index 3ddd245fd4ad8d9ed2e762910a7a1f6436f93e34..e5d26256d05a04a9642371cf3dbb4dd0c1c34e68 100644
|
||||
--- a/MAINTAINERS
|
||||
+++ b/MAINTAINERS
|
||||
@@ -20891,6 +20891,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
|
||||
diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig
|
||||
index d30d22dfe57741b145a45632b6325d5f9680590e..01b4f5c326478c73b518041830ee0d65b37f6833 100644
|
||||
--- a/drivers/counter/Kconfig
|
||||
+++ b/drivers/counter/Kconfig
|
||||
@@ -90,6 +90,19 @@ config MICROCHIP_TCB_CAPTURE
|
||||
To compile this driver as a module, choose M here: the
|
||||
module will be called microchip-tcb-capture.
|
||||
|
||||
+config ROCKCHIP_PWM_CAPTURE
|
||||
+ tristate "Rockchip PWM Counter Capture driver"
|
||||
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
|
||||
+ depends on ROCKCHIP_MFPWM
|
||||
+ depends on HAS_IOMEM
|
||||
+ help
|
||||
+ Generic counter framework driver for the multi-function PWM on
|
||||
+ Rockchip SoCs such as the RK3576.
|
||||
+
|
||||
+ Uses the Rockchip Multi-function PWM controller driver infrastructure
|
||||
+ to guarantee exclusive operation with other functions of the same
|
||||
+ device implemented by drivers in other subsystems.
|
||||
+
|
||||
config RZ_MTU3_CNT
|
||||
tristate "Renesas RZ/G2L MTU3a counter driver"
|
||||
depends on RZ_MTU3
|
||||
diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile
|
||||
index fa3c1d08f7068835aa912aa13bc92bcfd44d16fb..2bfcfc2c584bd174a9885064746a98f15b204aec 100644
|
||||
--- a/drivers/counter/Makefile
|
||||
+++ b/drivers/counter/Makefile
|
||||
@@ -17,3 +17,4 @@ obj-$(CONFIG_FTM_QUADDEC) += ftm-quaddec.o
|
||||
obj-$(CONFIG_MICROCHIP_TCB_CAPTURE) += microchip-tcb-capture.o
|
||||
obj-$(CONFIG_INTEL_QEP) += intel-qep.o
|
||||
obj-$(CONFIG_TI_ECAP_CAPTURE) += ti-ecap-capture.o
|
||||
+obj-$(CONFIG_ROCKCHIP_PWM_CAPTURE) += rockchip-pwm-capture.o
|
||||
diff --git a/drivers/counter/rockchip-pwm-capture.c b/drivers/counter/rockchip-pwm-capture.c
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..b2bfa2c6e04dfa0410fa0d7ef1c395217e4a9db2
|
||||
--- /dev/null
|
||||
+++ b/drivers/counter/rockchip-pwm-capture.c
|
||||
@@ -0,0 +1,341 @@
|
||||
+// 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.
|
||||
+ *
|
||||
+ * Authors:
|
||||
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
|
||||
+ */
|
||||
+
|
||||
+#include <linux/cleanup.h>
|
||||
+#include <linux/counter.h>
|
||||
+#include <linux/devm-helpers.h>
|
||||
+#include <linux/interrupt.h>
|
||||
+#include <linux/math.h>
|
||||
+#include <linux/mod_devicetable.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;
|
||||
+ bool is_enabled;
|
||||
+ spinlock_t enable_lock;
|
||||
+ atomic_t lpc;
|
||||
+ atomic_t hpc;
|
||||
+ atomic_t captures_left;
|
||||
+ struct delayed_work clear_capture;
|
||||
+};
|
||||
+
|
||||
+static struct counter_signal rkpwmc_signals[] = {
|
||||
+ {
|
||||
+ .id = 0,
|
||||
+ .name = "Channel 1"
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+static const enum counter_synapse_action rkpwmc_hpc_lpc_actions[] = {
|
||||
+ COUNTER_SYNAPSE_ACTION_BOTH_EDGES,
|
||||
+
|
||||
+};
|
||||
+
|
||||
+static struct counter_synapse rkpwmc_pwm_synapses[] = {
|
||||
+ {
|
||||
+ .actions_list = rkpwmc_hpc_lpc_actions,
|
||||
+ .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions),
|
||||
+ .signal = &rkpwmc_signals[0]
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+static const enum counter_function rkpwmc_functions[] = {
|
||||
+ COUNTER_FUNCTION_INCREASE,
|
||||
+};
|
||||
+
|
||||
+static int rkpwmc_enable_read(struct counter_device *counter,
|
||||
+ struct counter_count *count,
|
||||
+ u8 *enable)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
|
||||
+
|
||||
+ guard(spinlock)(&pc->enable_lock);
|
||||
+
|
||||
+ *enable = pc->is_enabled;
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int rkpwmc_enable_write(struct counter_device *counter,
|
||||
+ struct counter_count *count,
|
||||
+ u8 enable)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
|
||||
+ int ret;
|
||||
+
|
||||
+ guard(spinlock)(&pc->enable_lock);
|
||||
+
|
||||
+ if (!!enable != pc->is_enabled) {
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ if (enable) {
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(false));
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL,
|
||||
+ PWMV4_CTRL_CAP_FLAGS);
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN,
|
||||
+ PWMV4_INT_LPC_W(true) |
|
||||
+ PWMV4_INT_HPC_W(true));
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(true) | PWMV4_CLK_EN(true));
|
||||
+
|
||||
+ ret = mfpwm_pwmclk_enable(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ goto err_release;
|
||||
+
|
||||
+ ret = mfpwm_acquire(pc->pwmf);
|
||||
+ if (ret)
|
||||
+ goto err_disable_pwm_clk;
|
||||
+
|
||||
+ atomic_set(&pc->captures_left, 4);
|
||||
+ pc->is_enabled = true;
|
||||
+ } else {
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN,
|
||||
+ PWMV4_INT_LPC_W(false) |
|
||||
+ PWMV4_INT_HPC_W(false));
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
|
||||
+ PWMV4_EN(false) | PWMV4_CLK_EN(false));
|
||||
+ /*
|
||||
+ * 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);
|
||||
+ pc->is_enabled = false;
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ }
|
||||
+
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+ }
|
||||
+
|
||||
+ return 0;
|
||||
+
|
||||
+err_disable_pwm_clk:
|
||||
+ mfpwm_pwmclk_disable(pc->pwmf);
|
||||
+err_release:
|
||||
+ mfpwm_release(pc->pwmf);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static struct counter_comp rkpwmc_ext[] = {
|
||||
+ COUNTER_COMP_ENABLE(rkpwmc_enable_read, rkpwmc_enable_write),
|
||||
+};
|
||||
+
|
||||
+enum rkpwmc_count_id {
|
||||
+ COUNT_PERIOD = 0,
|
||||
+ COUNT_DUTY = 1,
|
||||
+};
|
||||
+
|
||||
+static struct counter_count rkpwmc_counts[] = {
|
||||
+ {
|
||||
+ .id = COUNT_PERIOD,
|
||||
+ .name = "Period in nanoseconds",
|
||||
+ .functions_list = rkpwmc_functions,
|
||||
+ .num_functions = ARRAY_SIZE(rkpwmc_functions),
|
||||
+ .synapses = rkpwmc_pwm_synapses,
|
||||
+ .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
|
||||
+ .ext = rkpwmc_ext,
|
||||
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
|
||||
+ },
|
||||
+ {
|
||||
+ .id = COUNT_DUTY,
|
||||
+ .name = "Duty cycle in nanoseconds",
|
||||
+ .functions_list = rkpwmc_functions,
|
||||
+ .num_functions = ARRAY_SIZE(rkpwmc_functions),
|
||||
+ .synapses = rkpwmc_pwm_synapses,
|
||||
+ .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
|
||||
+ .ext = rkpwmc_ext,
|
||||
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+static int rkpwmc_count_read(struct counter_device *counter,
|
||||
+ struct counter_count *count, u64 *value)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
|
||||
+ unsigned long rate;
|
||||
+ u64 period;
|
||||
+ u64 lpc;
|
||||
+ u64 hpc;
|
||||
+ int ret = 0;
|
||||
+
|
||||
+ if (count->id != COUNT_PERIOD && count->id != COUNT_DUTY)
|
||||
+ 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 = {
|
||||
+ .count_read = rkpwmc_count_read,
|
||||
+};
|
||||
+
|
||||
+static irqreturn_t rkpwmc_irq_handler(int irq, void *data)
|
||||
+{
|
||||
+ struct rockchip_pwm_capture *pc = data;
|
||||
+ u32 intsts;
|
||||
+ u32 clr = 0;
|
||||
+ u32 lpc;
|
||||
+ u32 hpc;
|
||||
+
|
||||
+ intsts = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_INTSTS);
|
||||
+
|
||||
+ if (!(intsts & RKPWMC_INT_MASK))
|
||||
+ return IRQ_NONE;
|
||||
+
|
||||
+ if (intsts & PWMV4_INT_LPC) {
|
||||
+ clr |= PWMV4_INT_LPC;
|
||||
+ atomic_dec_if_positive(&pc->captures_left);
|
||||
+ }
|
||||
+
|
||||
+ 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);
|
||||
+ }
|
||||
+
|
||||
+ if (clr)
|
||||
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INTSTS, clr);
|
||||
+
|
||||
+ 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);
|
||||
+ struct rockchip_pwm_capture *pc;
|
||||
+ struct counter_device *counter;
|
||||
+ int ret;
|
||||
+
|
||||
+ counter = devm_counter_alloc(&pdev->dev, sizeof(*pc));
|
||||
+ if (IS_ERR(counter))
|
||||
+ return PTR_ERR(counter);
|
||||
+
|
||||
+ pc = counter_priv(counter);
|
||||
+ pc->pwmf = pwmf;
|
||||
+ spin_lock_init(&pc->enable_lock);
|
||||
+
|
||||
+ platform_set_drvdata(pdev, pc);
|
||||
+
|
||||
+ ret = devm_request_irq(&pdev->dev, pwmf->irq, rkpwmc_irq_handler,
|
||||
+ IRQF_SHARED, pdev->name, pc);
|
||||
+ if (ret)
|
||||
+ return dev_err_probe(&pdev->dev, ret, "Failed requesting IRQ\n");
|
||||
+
|
||||
+ 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);
|
||||
+ counter->ops = &rkpwmc_ops;
|
||||
+ counter->counts = rkpwmc_counts;
|
||||
+ counter->num_counts = ARRAY_SIZE(rkpwmc_counts);
|
||||
+
|
||||
+ ret = devm_counter_add(&pdev->dev, counter);
|
||||
+ if (ret < 0)
|
||||
+ return dev_err_probe(&pdev->dev, ret, "Failed to add counter\n");
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static 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 */ },
|
||||
+};
|
||||
+MODULE_DEVICE_TABLE(platform, rockchip_pwm_capture_id_table);
|
||||
+
|
||||
+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",
|
||||
+ },
|
||||
+};
|
||||
+module_platform_driver(rockchip_pwm_capture_driver);
|
||||
+
|
||||
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
|
||||
+MODULE_DESCRIPTION("Rockchip PWM Counter Capture Driver");
|
||||
+MODULE_LICENSE("GPL");
|
||||
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
|
||||
+MODULE_IMPORT_NS("COUNTER");
|
||||
|
||||
--
|
||||
2.49.0
|
@ -0,0 +1,13 @@
|
||||
diff --git a/drivers/pwm/pwm-rockchip-v4.c b/drivers/pwm/pwm-rockchip-v4.c
|
||||
index 980b27454..3bc3d4979 100644
|
||||
--- 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 platform_device *pdev)
|
||||
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