From b9c1373a040405b55cec5ed366ce02427f16b0eb Mon Sep 17 00:00:00 2001 From: coolsnowwolf Date: Fri, 18 Apr 2025 22:46:06 +0800 Subject: [PATCH] rockchip: add pwmv4 support for rk3576 soc in kernel 6.12 --- .../340-01-pwm-add-more-locking.patch | 237 ++++ ...wm-new-abstraction-for-pwm-waveforms.patch | 412 ++++++ ...consumer-API-functions-for-waveforms.patch | 327 +++++ ...rease-mas-amount-of-device-functions.patch | 21 + ...-new-binding-for-rockchip-rk3576-pwm.patch | 127 ++ ...der-for-things-shared-across-drivers.patch | 90 ++ ...341-04-soc-rockchip-add-mfpwm-driver.patch | 1181 +++++++++++++++++ ...341-05-pwm-add-rockchip-pwmv4-driver.patch | 401 ++++++ ...nter-add-rockchip-pwm-capture-driver.patch | 403 ++++++ ...4-add-device-set-of-node-from-parent.patch | 13 + 10 files changed, 3212 insertions(+) create mode 100644 target/linux/rockchip/patches-6.12/340-01-pwm-add-more-locking.patch create mode 100644 target/linux/rockchip/patches-6.12/340-02-pwm-new-abstraction-for-pwm-waveforms.patch create mode 100644 target/linux/rockchip/patches-6.12/340-03-pwm-provide-new-consumer-API-functions-for-waveforms.patch create mode 100644 target/linux/rockchip/patches-6.12/341-01-dt-bindings-pinctrl-rockchip-increase-mas-amount-of-device-functions.patch create mode 100644 target/linux/rockchip/patches-6.12/341-02-dt-bindings-pwm-add-a-new-binding-for-rockchip-rk3576-pwm.patch create mode 100644 target/linux/rockchip/patches-6.12/341-03-soc-rockchip-add-utils-header-for-things-shared-across-drivers.patch create mode 100644 target/linux/rockchip/patches-6.12/341-04-soc-rockchip-add-mfpwm-driver.patch create mode 100644 target/linux/rockchip/patches-6.12/341-05-pwm-add-rockchip-pwmv4-driver.patch create mode 100644 target/linux/rockchip/patches-6.12/341-06-counter-add-rockchip-pwm-capture-driver.patch create mode 100644 target/linux/rockchip/patches-6.12/341-07-pwm-pwmv4-add-device-set-of-node-from-parent.patch diff --git a/target/linux/rockchip/patches-6.12/340-01-pwm-add-more-locking.patch b/target/linux/rockchip/patches-6.12/340-01-pwm-add-more-locking.patch new file mode 100644 index 000000000..cb76d814e --- /dev/null +++ b/target/linux/rockchip/patches-6.12/340-01-pwm-add-more-locking.patch @@ -0,0 +1,237 @@ +Signed-off-by: Uwe Kleine-König +--- + 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 diff --git a/target/linux/rockchip/patches-6.12/340-02-pwm-new-abstraction-for-pwm-waveforms.patch b/target/linux/rockchip/patches-6.12/340-02-pwm-new-abstraction-for-pwm-waveforms.patch new file mode 100644 index 000000000..2aed97080 --- /dev/null +++ b/target/linux/rockchip/patches-6.12/340-02-pwm-new-abstraction-for-pwm-waveforms.patch @@ -0,0 +1,412 @@ +From 17e40c25158f2505cbcdeda96624afcbab4af368 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= +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 +Tested-by: Trevor Gamblin +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 +--- + 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, diff --git a/target/linux/rockchip/patches-6.12/340-03-pwm-provide-new-consumer-API-functions-for-waveforms.patch b/target/linux/rockchip/patches-6.12/340-03-pwm-provide-new-consumer-API-functions-for-waveforms.patch new file mode 100644 index 000000000..9efb3a6db --- /dev/null +++ b/target/linux/rockchip/patches-6.12/340-03-pwm-provide-new-consumer-API-functions-for-waveforms.patch @@ -0,0 +1,327 @@ +From 6c5126c6406d1c31e91f5b925c621c1c785366be Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= +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 +Tested-by: Trevor Gamblin +Link: https://lore.kernel.org/r/6c97d27682853f603e18e9196043886dd671845d.1726819463.git.u.kleine-koenig@baylibre.com +Signed-off-by: Uwe Kleine-König +--- + 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); diff --git a/target/linux/rockchip/patches-6.12/341-01-dt-bindings-pinctrl-rockchip-increase-mas-amount-of-device-functions.patch b/target/linux/rockchip/patches-6.12/341-01-dt-bindings-pinctrl-rockchip-increase-mas-amount-of-device-functions.patch new file mode 100644 index 000000000..562b48e69 --- /dev/null +++ b/target/linux/rockchip/patches-6.12/341-01-dt-bindings-pinctrl-rockchip-increase-mas-amount-of-device-functions.patch @@ -0,0 +1,21 @@ +Signed-off-by: Nicolas Frattaroli +--- + 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 diff --git a/target/linux/rockchip/patches-6.12/341-02-dt-bindings-pwm-add-a-new-binding-for-rockchip-rk3576-pwm.patch b/target/linux/rockchip/patches-6.12/341-02-dt-bindings-pwm-add-a-new-binding-for-rockchip-rk3576-pwm.patch new file mode 100644 index 000000000..332b358b1 --- /dev/null +++ b/target/linux/rockchip/patches-6.12/341-02-dt-bindings-pwm-add-a-new-binding-for-rockchip-rk3576-pwm.patch @@ -0,0 +1,127 @@ +Signed-off-by: Nicolas Frattaroli +--- + .../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 ++ ++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 ++ #include ++ #include ++ ++ 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 = ; ++ #pwm-cells = <3>; ++ }; ++ }; ++ - | ++ #include ++ #include ++ #include ++ ++ 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 = ; ++ #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 ++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 + M: Aurelien Jarno + +-- +2.49.0 diff --git a/target/linux/rockchip/patches-6.12/341-03-soc-rockchip-add-utils-header-for-things-shared-across-drivers.patch b/target/linux/rockchip/patches-6.12/341-03-soc-rockchip-add-utils-header-for-things-shared-across-drivers.patch new file mode 100644 index 000000000..83ad5b87f --- /dev/null +++ b/target/linux/rockchip/patches-6.12/341-03-soc-rockchip-add-utils-header-for-things-shared-across-drivers.patch @@ -0,0 +1,90 @@ +Signed-off-by: Nicolas Frattaroli +--- + 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 ++ */ ++ ++#ifndef __SOC_ROCKCHIP_UTILS_H__ ++#define __SOC_ROCKCHIP_UTILS_H__ ++ ++#include ++#include ++#include ++ ++/* ++ * 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 diff --git a/target/linux/rockchip/patches-6.12/341-04-soc-rockchip-add-mfpwm-driver.patch b/target/linux/rockchip/patches-6.12/341-04-soc-rockchip-add-mfpwm-driver.patch new file mode 100644 index 000000000..405323e03 --- /dev/null +++ b/target/linux/rockchip/patches-6.12/341-04-soc-rockchip-add-mfpwm-driver.patch @@ -0,0 +1,1181 @@ +Signed-off-by: Nicolas Frattaroli +--- + 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(+) + +diff --git a/MAINTAINERS b/MAINTAINERS +index 407179d2a90dd49800f2bb5770a1280c5afebb5a..e6a9347be1e7889089e1d9e655cb23c2d8399b40 100644 +--- a/MAINTAINERS ++++ b/MAINTAINERS +@@ -20891,6 +20891,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 + + ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT + M: Daniel Golle +diff --git a/drivers/soc/rockchip/Kconfig b/drivers/soc/rockchip/Kconfig +index 785f60c6f3ad1a09f517e69a69726a8178bed168..4e1e4926c514a5a2c4d4caf8cf9809a098badc7d 100644 +--- 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 ++ tristate "Rockchip multi-function PWM controller" ++ depends on OF ++ depends on HAS_IOMEM ++ 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. ++ + endif +diff --git a/drivers/soc/rockchip/Makefile b/drivers/soc/rockchip/Makefile +index 23d414433c8c58557effc214337ec8e6ff17a461..ba12dbd01ac794910d9407c268e89071cd2b3139 100644 +--- 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 +diff --git a/drivers/soc/rockchip/mfpwm.c b/drivers/soc/rockchip/mfpwm.c +new file mode 100644 +index 0000000000000000000000000000000000000000..9331c530f0581573e2b74f62a6622b8625c5b2c5 +--- /dev/null ++++ b/drivers/soc/rockchip/mfpwm.c +@@ -0,0 +1,608 @@ ++// SPDX-License-Identifier: GPL-2.0-or-later ++/* ++ * Copyright (c) 2025 Collabora Ltd. ++ * ++ * A driver to manage all the different functionalities exposed by Rockchip's ++ * PWMv4 hardware. ++ * ++ * This driver is chiefly focused on guaranteeing non-concurrent operation ++ * between the different device functions, as well as setting the clocks. ++ * It registers the device function platform devices, e.g. PWM output or ++ * PWM capture. ++ * ++ * Authors: ++ * Nicolas Frattaroli ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++/** ++ * struct rockchip_mfpwm - private mfpwm driver instance state struct ++ * @pdev: pointer to this instance's &struct platform_device ++ * @base: pointer to the memory mapped registers of this device ++ * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from ++ * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from ++ * @chosen_clk: is one of either @pwm_clk or @osc_clk, depending on choice. ++ * May only be swapped out while holding @state_lock. ++ * @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. ++ * @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 ++ */ ++struct rockchip_mfpwm { ++ struct platform_device *pdev; ++ void __iomem *base; ++ struct clk *pwm_clk; ++ struct clk *osc_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; ++}; ++ ++static atomic_t subdev_id = ATOMIC_INIT(0); ++ ++static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev) ++{ ++ return platform_get_drvdata(pdev); ++} ++ ++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) ++{ ++ if (IS_ERR_OR_NULL(pwmf)) { ++ WARN(1, "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); ++ return -EINVAL; ++ } ++ ++ return 0; ++} ++ ++__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; ++ unsigned int cnt; ++ ++ if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id) ++ return -EBUSY; ++ ++ if (!mfpwm->active_func) ++ mfpwm->active_func = pwmf; ++ ++ if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) { ++ mfpwm->acquire_cnt = cnt; ++ } else { ++ WARN(1, "prevented acquire counter overflow in %s\n", __func__); ++ return -EOVERFLOW; ++ } ++ ++ dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n", ++ pwmf->id, mfpwm->acquire_cnt); ++ ++ return clk_enable(mfpwm->pclk); ++} ++ ++int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf) ++{ ++ struct rockchip_mfpwm *mfpwm; ++ unsigned long flags; ++ int ret = 0; ++ ++ ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire"); ++ if (ret) ++ return ret; ++ ++ mfpwm = pwmf->parent; ++ dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id); ++ ++ if (!spin_trylock_irqsave(&mfpwm->state_lock, flags)) ++ return -EBUSY; ++ ++ ret = mfpwm_do_acquire(pwmf); ++ ++ spin_unlock_irqrestore(&mfpwm->state_lock, flags); ++ ++ return ret; ++} ++EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, ROCKCHIP_MFPWM); ++ ++__attribute__((nonnull)) ++static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf) ++{ ++ struct rockchip_mfpwm *mfpwm = pwmf->parent; ++ ++ if (!mfpwm->active_func) ++ return; ++ ++ if (mfpwm->active_func->id != pwmf->id) ++ return; ++ ++ /* ++ * No need to check_sub_overflow here, !mfpwm->active_func above catches ++ * this type of problem already. ++ */ ++ mfpwm->acquire_cnt--; ++ ++ if (!mfpwm->acquire_cnt) ++ mfpwm->active_func = NULL; ++ ++ clk_disable(mfpwm->pclk); ++} ++ ++void mfpwm_release(const struct rockchip_mfpwm_func *pwmf) ++{ ++ struct rockchip_mfpwm *mfpwm; ++ unsigned long flags; ++ ++ if (mfpwm_check_pwmf(pwmf, "mfpwm_release")) ++ return; ++ ++ mfpwm = pwmf->parent; ++ ++ spin_lock_irqsave(&mfpwm->state_lock, flags); ++ mfpwm_do_release(pwmf); ++ dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n", ++ pwmf->id, mfpwm->acquire_cnt); ++ spin_unlock_irqrestore(&mfpwm->state_lock, flags); ++} ++EXPORT_SYMBOL_NS_GPL(mfpwm_release, ROCKCHIP_MFPWM); ++ ++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. ++ * ++ * 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; ++ ++ func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL); ++ if (IS_ERR(func)) ++ return PTR_ERR(func); ++ func->irq = mfpwm->irq; ++ func->parent = mfpwm; ++ func->id = atomic_inc_return(&subdev_id); ++ func->base = mfpwm->base; ++ child = platform_device_register_data(&mfpwm->pdev->dev, name, func->id, ++ func, sizeof(*func)); ++ ++ if (IS_ERR(child)) ++ return PTR_ERR(child); ++ ++ *target = child; ++ ++ return 0; ++} ++ ++static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm) ++{ ++ int ret; ++ ++ ret = mfpwm_register_subdev(mfpwm, &mfpwm->pwm_dev, "pwm-rockchip-v4"); ++ if (ret) ++ return ret; ++ ++ ret = mfpwm_register_subdev(mfpwm, &mfpwm->counter_dev, ++ "rockchip-pwm-capture"); ++ if (ret) ++ goto err_unreg_pwm_dev; ++ ++ 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; ++ int ret = 0; ++ ++ mfpwm = devm_kzalloc(&pdev->dev, sizeof(*mfpwm), GFP_KERNEL); ++ if (IS_ERR(mfpwm)) ++ return PTR_ERR(mfpwm); ++ ++ mfpwm->pdev = pdev; ++ ++ spin_lock_init(&mfpwm->state_lock); ++ ++ mfpwm->base = devm_platform_ioremap_resource(pdev, 0); ++ if (IS_ERR(mfpwm->base)) ++ return dev_err_probe(dev, PTR_ERR(mfpwm->base), ++ "failed to ioremap address\n"); ++ ++ mfpwm->pclk = devm_clk_get_prepared(dev, "pclk"); ++ if (IS_ERR(mfpwm->pclk)) ++ return dev_err_probe(dev, PTR_ERR(mfpwm->pclk), ++ "couldn't get and prepare 'pclk' clock\n"); ++ ++ mfpwm->irq = platform_get_irq(pdev, 0); ++ if (mfpwm->irq < 0) ++ return dev_err_probe(dev, mfpwm->irq, "couldn't get irq 0\n"); ++ ++ mfpwm->pwm_clk = devm_clk_get_prepared(dev, "pwm"); ++ if (IS_ERR(mfpwm->pwm_clk)) ++ return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk), ++ "couldn't get and prepare 'pwm' clock\n"); ++ ++ mfpwm->osc_clk = devm_clk_get_optional_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) ++ return ret; ++ ++ platform_set_drvdata(pdev, mfpwm); ++ ++ ret = mfpwm_register_subdevs(mfpwm); ++ if (ret) { ++ dev_err(dev, "failed to register sub-devices: %pe\n", ++ ERR_PTR(ret)); ++ return ret; ++ } ++ ++ return ret; ++} ++ ++static void rockchip_mfpwm_remove(struct platform_device *pdev) ++{ ++ struct rockchip_mfpwm *mfpwm = to_rockchip_mfpwm(pdev); ++ unsigned long flags; ++ ++ spin_lock_irqsave(&mfpwm->state_lock, flags); ++ ++ if (mfpwm->chosen_clk) ++ clk_rate_exclusive_put(mfpwm->chosen_clk); ++ ++ spin_unlock_irqrestore(&mfpwm->state_lock, flags); ++} ++ ++static const struct of_device_id rockchip_mfpwm_of_match[] = { ++ { ++ .compatible = "rockchip,rk3576-pwm", ++ }, ++ { /* sentinel */ } ++}; ++MODULE_DEVICE_TABLE(of, rockchip_mfpwm_of_match); ++ ++static struct platform_driver rockchip_mfpwm_driver = { ++ .driver = { ++ .name = KBUILD_MODNAME, ++ .of_match_table = rockchip_mfpwm_of_match, ++ .dev_groups = mfpwm_groups, ++ }, ++ .probe = rockchip_mfpwm_probe, ++ .remove = rockchip_mfpwm_remove, ++}; ++module_platform_driver(rockchip_mfpwm_driver); ++ ++MODULE_AUTHOR("Nicolas Frattaroli "); ++MODULE_DESCRIPTION("Rockchip MFPWM Driver"); ++MODULE_LICENSE("GPL"); +diff --git a/include/soc/rockchip/mfpwm.h b/include/soc/rockchip/mfpwm.h +new file mode 100644 +index 0000000000000000000000000000000000000000..345f13f438b57159a15cb2e0ae250800fb96ed43 +--- /dev/null ++++ b/include/soc/rockchip/mfpwm.h +@@ -0,0 +1,505 @@ ++/* SPDX-License-Identifier: GPL-2.0-or-later */ ++/* ++ * Copyright (c) 2025 Collabora Ltd. ++ * ++ * Common header file for all the Rockchip Multi-function PWM controller ++ * drivers that are spread across subsystems. ++ * ++ * Authors: ++ * Nicolas Frattaroli ++ */ ++ ++#ifndef __SOC_ROCKCHIP_MFPWM_H__ ++#define __SOC_ROCKCHIP_MFPWM_H__ ++ ++#include ++#include ++#include ++#include ++ ++struct rockchip_mfpwm; ++ ++/** ++ * struct rockchip_mfpwm_func - struct representing a single function driver ++ * ++ * @id: unique id for this function driver instance ++ * @base: pointer to start of MMIO registers ++ * @parent: a pointer to the parent mfpwm struct ++ * @irq: the shared IRQ gotten from the parent mfpwm device ++ */ ++struct rockchip_mfpwm_func { ++ int id; ++ void __iomem *base; ++ struct rockchip_mfpwm *parent; ++ int irq; ++}; ++ ++/* ++ * PWMV4 Register Definitions ++ * -------------------------- ++ * ++ * Attributes: ++ * RW - Read-Write ++ * RO - Read-Only ++ * WO - Write-Only ++ * W1T - Write high, Self-clearing ++ * W1C - Write high to clear interrupt ++ * ++ * Bit ranges to be understood with Verilog-like semantics, ++ * e.g. [03:00] is 4 bits: 0, 1, 2 and 3. ++ * ++ * All registers must be accessed with 32-bit width accesses only ++ */ ++ ++#define PWMV4_REG_VERSION 0x000 ++/* ++ * VERSION Register Description ++ * [31:24] RO | Hardware Major Version ++ * [23:16] RO | Hardware Minor Version docArrive ++ * [15:15] RO | Reserved ++ * [14:14] RO | Hardware supports biphasic counters ++ * [13:13] RO | Hardware supports filters ++ * [12:12] RO | Hardware supports waveform generation ++ * [11:11] RO | Hardware supports counter ++ * [10:10] RO | Hardware supports frequency metering ++ * [09:09] RO | Hardware supports power key functionality ++ * [08:08] RO | Hardware supports infrared transmissions ++ * [07:04] RO | Channel index of this instance ++ * [03:00] RO | Number of channels the base instance supports ++ */ ++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 ++/* ++ * ENABLE Register Description ++ * [31:16] WO | Write Enable Mask for the lower half of the register ++ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in ++ * the same write operation ++ * [15:06] RO | Reserved ++ * [05:05] RW | PWM Channel Counter Read Enable, 1 = enabled ++ */ ++#define PWMV4_CHN_CNT_RD_EN(v) REG_UPDATE_BIT_WE((v), 5) ++/* ++ * [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) ++/* ++ * [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) ++/* ++ * [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) ++/* ++ * [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) ++/* ++ * [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_EN_BOTH_MASK (PWMV4_EN_MASK | PWMV4_CLK_EN_MASK) ++static inline __pure bool pwmv4_is_enabled(unsigned int val) ++{ ++ return (val & PWMV4_EN_BOTH_MASK); ++} ++ ++#define PWMV4_REG_CLK_CTRL 0x008 ++/* ++ * CLK_CTRL Register Description ++ * [31:16] WO | Write Enable Mask for the lower half of the register ++ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in ++ * the same write operation ++ * [15:15] RW | Clock Global Selection ++ * 0 = current channel scale clock ++ * 1 = global channel scale clock ++ */ ++#define PWMV4_CLK_GLOBAL(v) REG_UPDATE_BIT_WE((v), 15) ++/* ++ * [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. ++ */ ++#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) ++/* ++ * [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) ++/* ++ * [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_REG_CTRL 0x00C ++/* ++ * CTRL Register Description ++ * [31:16] WO | Write Enable Mask for the lower half of the register ++ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in ++ * the same write operation ++ * [15:09] RO | Reserved ++ * [08:06] RW | PWM Input Channel Selection ++ * By default, the channel selects its own input, but writing v ++ * here selects PWM input from channel v instead. ++ */ ++#define PWMV4_CTRL_IN_SEL(v) REG_UPDATE_WE((v), 6, 8) ++/* [05:05] RW | Aligned Mode, 0 = Valid, 1 = Invalid */ ++#define PWMV4_CTRL_UNALIGNED(v) REG_UPDATE_BIT_WE((v), 5) ++/* [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) ++/* ++ * [03:03] RW | Inactive Polarity for when the channel is either disabled or ++ * has completed outputting the entire waveform in one-shot mode. ++ * 0 = Negative, 1 = Positive ++ */ ++#define PWMV4_POLARITY_N 0x0U ++#define PWMV4_POLARITY_P 0x1U ++#define PWMV4_INACTIVE_POL(v) REG_UPDATE_BIT_WE((v), 3) ++/* ++ * [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 ++/* ++ * [01:00] RW | PWM Mode ++ * 0 = One-shot mode, PWM generates waveform RPT times ++ * 1 = Continuous mode ++ * 2 = Capture mode, PWM measures cycles of input waveform ++ * 3 = Reserved ++ */ ++#define PWMV4_MODE_ONESHOT 0x0U ++#define PWMV4_MODE_CONT 0x1U ++#define PWMV4_MODE_CAPTURE 0x2U ++#define PWMV4_MODE(v) REG_UPDATE_WE((v), 0, 1) ++#define PWMV4_CTRL_COM_FLAGS (PWMV4_INACTIVE_POL(PWMV4_POLARITY_N) | \ ++ PWMV4_DUTY_POL(PWMV4_POLARITY_P) | \ ++ PWMV4_CTRL_OUT_MODE(PWMV4_LEFT_ALIGNED) | \ ++ PWMV4_CTRL_UNALIGNED(true)) ++#define PWMV4_CTRL_CONT_FLAGS (PWMV4_MODE(PWMV4_MODE_CONT) | \ ++ PWMV4_CTRL_COM_FLAGS) ++#define PWMV4_CTRL_CAP_FLAGS (PWMV4_MODE(PWMV4_MODE_CAPTURE) | \ ++ PWMV4_CTRL_COM_FLAGS) ++ ++#define PWMV4_REG_PERIOD 0x010 ++/* ++ * PERIOD Register Description ++ * [31:00] RW | Period of the output waveform ++ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED ++ */ ++ ++#define PWMV4_REG_DUTY 0x014 ++/* ++ * DUTY Register Description ++ * [31:00] RW | Duty cycle of the output waveform ++ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED ++ */ ++ ++#define PWMV4_REG_OFFSET 0x018 ++/* ++ * OFFSET Register Description ++ * [31:00] RW | Offset of the output waveform, based on the PWM clock ++ * Constraints: 0 <= v <= (PERIOD - DUTY) ++ */ ++ ++#define PWMV4_REG_RPT 0x01C ++/* ++ * RPT Register Description ++ * [31:16] RW | Second dimensional of the effective number of waveform ++ * repetitions. Increases by one every first dimensional times. ++ * Value `n` means `n + 1` repetitions. The final number of ++ * repetitions of the waveform in one-shot mode is: ++ * `(first_dimensional + 1) * (second_dimensional + 1)` ++ * [15:00] RW | First dimensional of the effective number of waveform ++ * repetitions. Value `n` means `n + 1` repetitions. ++ */ ++ ++#define PWMV4_REG_FILTER_CTRL 0x020 ++/* ++ * FILTER_CTRL Register Description ++ * [31:16] WO | Write Enable Mask for the lower half of the register ++ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in ++ * the same write operation ++ * [15:10] RO | Reserved ++ * [09:04] RW | Filter window number ++ * [03:01] RO | Reserved ++ * [00:00] RW | Filter Enable, 0 = disabled, 1 = enabled ++ */ ++ ++#define PWMV4_REG_CNT 0x024 ++/* ++ * CNT Register Description ++ * [31:00] RO | Current value of the PWM Channel 0 counter in pwm clock cycles, ++ * 0 <= v <= 2^32-1 ++ */ ++ ++#define PWMV4_REG_ENABLE_DELAY 0x028 ++/* ++ * ENABLE_DELAY Register Description ++ * [31:16] RO | Reserved ++ * [15:00] RW | PWM enable delay, in an unknown unit but probably cycles ++ */ ++ ++#define PWMV4_REG_HPC 0x02C ++/* ++ * HPC Register Description ++ * [31:00] RW | Number of effective high polarity cycles of the input waveform ++ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1 ++ */ ++ ++#define PWMV4_REG_LPC 0x030 ++/* ++ * LPC Register Description ++ * [31:00] RW | Number of effective low polarity cycles of the input waveform ++ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1 ++ */ ++ ++#define PWMV4_REG_BIPHASIC_CNT_CTRL0 0x040 ++/* ++ * BIPHASIC_CNT_CTRL0 Register Description ++ * [31:16] WO | Write Enable Mask for the lower half of the register ++ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in ++ * the same write operation ++ * [15:10] RO | Reserved ++ * [09:09] RW | Biphasic Counter Phase Edge Selection for mode 0, ++ * 0 = rising edge (posedge), 1 = falling edge (negedge) ++ * [08:08] RW | Biphasic Counter Clock force enable, 1 = force enable ++ * [07:07] W1T | Synchronous Enable ++ * [06:06] W1T | Mode Switch ++ * 0 = Normal Mode, 1 = Switch timer clock and measured clock ++ * Constraints: "Biphasic Counter Mode" must be 0 if this is 1 ++ * [05:03] RW | Biphasic Counter Mode ++ * 0x0 = Mode 0, 0x1 = Mode 1, 0x2 = Mode 2, 0x3 = Mode 3, ++ * 0x4 = Mode 4, 0x5 = Reserved ++ * [02:02] RW | Biphasic Counter Clock Selection ++ * 0 = clock is from PLL and frequency can be configured ++ * 1 = clock is from crystal oscillator and frequency is fixed ++ * [01:01] RW | Biphasic Counter Continuous Mode ++ * [00:00] W1T | Biphasic Counter Enable ++ */ ++ ++#define PWMV4_REG_BIPHASIC_CNT_CTRL1 0x044 ++/* ++ * BIPHASIC_CNT_CTRL1 Register Description ++ * [31:16] WO | Write Enable Mask for the lower half of the register ++ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in ++ * the same write operation ++ * [15:11] RO | Reserved ++ * [10:04] RW | Biphasic Counter Filter Window Number ++ * [03:01] RO | Reserved ++ * [00:00] RW | Biphasic Counter Filter Enable ++ */ ++ ++#define PWMV4_REG_BIPHASIC_CNT_TIMER 0x048 ++/* ++ * BIPHASIC_CNT_TIMER Register Description ++ * [31:00] RW | Biphasic Counter Timer Value, in number of biphasic counter ++ * timer clock cycles ++ */ ++ ++#define PWMV4_REG_BIPHASIC_CNT_RES 0x04C ++/* ++ * BIPHASIC_CNT_RES Register Description ++ * [31:00] RO | Biphasic Counter Result Value ++ * Constraints: Can only be read after INTSTS[9] is asserted ++ */ ++ ++#define PWMV4_REG_BIPHASIC_CNT_RES_S 0x050 ++/* ++ * BIPHASIC_CNT_RES_S Register Description ++ * [31:00] RO | Biphasic Counter Result Value with synchronised processing ++ * Can be read in real-time if BIPHASIC_CNT_CTRL0[7] was set to 1 ++ */ ++ ++#define PWMV4_REG_INTSTS 0x070 ++/* ++ * INTSTS Register Description ++ * [31:10] RO | Reserved ++ * [09:09] W1C | Biphasic Counter Interrupt Status, 1 = interrupt asserted ++ * [08:08] W1C | Waveform Middle Interrupt Status, 1 = interrupt asserted ++ * [07:07] W1C | Waveform Max Interrupt Status, 1 = interrupt asserted ++ * [06:06] W1C | IR Transmission End Interrupt Status, 1 = interrupt asserted ++ * [05:05] W1C | Power Key Match Interrupt Status, 1 = interrupt asserted ++ * [04:04] W1C | Frequency Meter Interrupt Status, 1 = interrupt asserted ++ * [03:03] W1C | Reload Interrupt Status, 1 = interrupt asserted ++ * [02:02] W1C | Oneshot End Interrupt Status, 1 = interrupt asserted ++ * [01:01] W1C | HPC Capture Interrupt Status, 1 = interrupt asserted ++ * [00:00] W1C | LPC Capture Interrupt Status, 1 = interrupt asserted ++ */ ++#define PWMV4_INT_LPC BIT(0) ++#define PWMV4_INT_HPC BIT(1) ++#define PWMV4_INT_LPC_W(v) REG_UPDATE_BIT_WE(((v) ? 1 : 0), 0) ++#define PWMV4_INT_HPC_W(v) REG_UPDATE_BIT_WE(((v) ? 1 : 0), 1) ++ ++#define PWMV4_REG_INT_EN 0x074 ++/* ++ * INT_EN Register Description ++ * [31:16] WO | Write Enable Mask for the lower half of the register ++ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in ++ * the same write operation ++ * [15:10] RO | Reserved ++ * [09:09] RW | Biphasic Counter Interrupt Enable, 1 = enabled ++ * [08:08] W1C | Waveform Middle Interrupt Enable, 1 = enabled ++ * [07:07] W1C | Waveform Max Interrupt Enable, 1 = enabled ++ * [06:06] W1C | IR Transmission End Interrupt Enable, 1 = enabled ++ * [05:05] W1C | Power Key Match Interrupt Enable, 1 = enabled ++ * [04:04] W1C | Frequency Meter Interrupt Enable, 1 = enabled ++ * [03:03] W1C | Reload Interrupt Enable, 1 = enabled ++ * [02:02] W1C | Oneshot End Interrupt Enable, 1 = enabled ++ * [01:01] W1C | HPC Capture Interrupt Enable, 1 = enabled ++ * [00:00] W1C | LPC Capture Interrupt Enable, 1 = enabled ++ */ ++ ++#define PWMV4_REG_INT_MASK 0x078 ++/* ++ * INT_MASK Register Description ++ * [31:16] WO | Write Enable Mask for the lower half of the register ++ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in ++ * the same write operation ++ * [15:10] RO | Reserved ++ * [09:09] RW | Biphasic Counter Interrupt Masked, 1 = masked ++ * [08:08] W1C | Waveform Middle Interrupt Masked, 1 = masked ++ * [07:07] W1C | Waveform Max Interrupt Masked, 1 = masked ++ * [06:06] W1C | IR Transmission End Interrupt Masked, 1 = masked ++ * [05:05] W1C | Power Key Match Interrupt Masked, 1 = masked ++ * [04:04] W1C | Frequency Meter Interrupt Masked, 1 = masked ++ * [03:03] W1C | Reload Interrupt Masked, 1 = masked ++ * [02:02] W1C | Oneshot End Interrupt Masked, 1 = masked ++ * [01:01] W1C | HPC Capture Interrupt Masked, 1 = masked ++ * [00:00] W1C | LPC Capture Interrupt Masked, 1 = masked ++ */ ++ ++static inline u32 mfpwm_reg_read(void __iomem *base, u32 reg) ++{ ++ return readl(base + reg); ++} ++ ++static inline void mfpwm_reg_write(void __iomem *base, u32 reg, u32 val) ++{ ++ writel(val, base + reg); ++} ++ ++/** ++ * mfpwm_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 ++ * ++ * mfpwm device "function" drivers must call this function before doing anything ++ * that either modifies or relies on the parent device's state, such as clocks, ++ * enabling/disabling outputs, modifying shared regs etc. ++ * ++ * The return statues should always be checked. ++ * ++ * All mfpwm_acquire() calls must be balanced with corresponding mfpwm_release() ++ * calls once the device is no longer making changes that affect other devices, ++ * or stops producing user-visible effects that depend on the current device ++ * state being kept as-is. (e.g. after the PWM output signal is stopped) ++ * ++ * The same device function may mfpwm_acquire() multiple times while it already ++ * is active, i.e. it is re-entrant, though it needs to balance this with the ++ * same number of mfpwm_release() calls. ++ * ++ * Context: This function does not sleep. ++ * ++ * Return: ++ * * %0 - success ++ * * %-EBUSY - a different device function is active ++ * * %-EOVERFLOW - the acquire counter is at its maximum ++ */ ++int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf); ++ ++/** ++ * mfpwm_release - drop usage of active mfpwm device function by 1 ++ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func ++ * ++ * This is the balancing call to mfpwm_acquire(). If no users of the device ++ * function remain, set the mfpwm device to have no active device function, ++ * allowing other device functions to claim it. ++ */ ++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); ++ ++#endif /* __SOC_ROCKCHIP_MFPWM_H__ */ + +-- +2.49.0 diff --git a/target/linux/rockchip/patches-6.12/341-05-pwm-add-rockchip-pwmv4-driver.patch b/target/linux/rockchip/patches-6.12/341-05-pwm-add-rockchip-pwmv4-driver.patch new file mode 100644 index 000000000..63c7e03c2 --- /dev/null +++ b/target/linux/rockchip/patches-6.12/341-05-pwm-add-rockchip-pwmv4-driver.patch @@ -0,0 +1,401 @@ +Signed-off-by: Nicolas Frattaroli +--- + 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 ++ */ ++ ++#include ++#include ++#include ++ ++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 "); ++MODULE_DESCRIPTION("Rockchip PWMv4 Driver"); ++MODULE_LICENSE("GPL"); ++MODULE_IMPORT_NS("ROCKCHIP_MFPWM"); + +-- +2.49.0 diff --git a/target/linux/rockchip/patches-6.12/341-06-counter-add-rockchip-pwm-capture-driver.patch b/target/linux/rockchip/patches-6.12/341-06-counter-add-rockchip-pwm-capture-driver.patch new file mode 100644 index 000000000..af8f0a065 --- /dev/null +++ b/target/linux/rockchip/patches-6.12/341-06-counter-add-rockchip-pwm-capture-driver.patch @@ -0,0 +1,403 @@ +Signed-off-by: Nicolas Frattaroli +--- + 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 ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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 "); ++MODULE_DESCRIPTION("Rockchip PWM Counter Capture Driver"); ++MODULE_LICENSE("GPL"); ++MODULE_IMPORT_NS("ROCKCHIP_MFPWM"); ++MODULE_IMPORT_NS("COUNTER"); + +-- +2.49.0 diff --git a/target/linux/rockchip/patches-6.12/341-07-pwm-pwmv4-add-device-set-of-node-from-parent.patch b/target/linux/rockchip/patches-6.12/341-07-pwm-pwmv4-add-device-set-of-node-from-parent.patch new file mode 100644 index 000000000..9d1c2db8d --- /dev/null +++ b/target/linux/rockchip/patches-6.12/341-07-pwm-pwmv4-add-device-set-of-node-from-parent.patch @@ -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; +