From: sashiko-bot@kernel.org
To: "James Hilliard" <james.hilliard1@gmail.com>
Cc: linux-clk@vger.kernel.org, devicetree@vger.kernel.org,
linux-sunxi@lists.linux.dev, robh@kernel.org,
conor+dt@kernel.org
Subject: Re: [PATCH v11 3/5] pwm: sun8i: export the bypass outputs as clocks
Date: Tue, 03 Nov 2026 09:28:41 +0000 [thread overview]
Message-ID: <sashiko-outbox-194955@kernel.org> (raw)
In-Reply-To: <20261103-h616-pwm-v8-v11-3-07abb59575c3@gmail.com>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] pwm: sun8i: missed rate protection in sun8i_pwm_pair_rate_constrained()
- [Medium] pwm: sun8i: sun8i_pwm_pair_rate_notifier() blocks bypass clk_set_rate()
- [Medium] pwm: sun8i: sun8i_pwm_read_waveform() clamps concurrent clk_set_rate()
- [Medium] pwm: sun8i: torn rate and constraint snapshot in sun8i_pwm_round_waveform_tohw()
--
commit 77adf52feb5fc5043dc604aa0ff2272bf2185bb6
Author: Richard Genoud <richard.genoud@bootlin.com>
pwm: sun8i: export the bypass outputs as clocks
This commit exports each Allwinner H616 PWM channel's hardware bypass path
as a Common Clock Framework clock and arbitrates channel ownership and pair
rate protection between PWM consumers and clock consumers.
> diff --git a/drivers/pwm/pwm-sun8i.c b/drivers/pwm/pwm-sun8i.c
> index bcc2c57443780..56854e6218c9a 100644
> --- a/drivers/pwm/pwm-sun8i.c
> +++ b/drivers/pwm/pwm-sun8i.c
[ ... ]
> @@ -399,12 +519,20 @@ static int sun8i_pwm_read_waveform(struct pwm_chip *chip,
> struct sun8i_pwm_waveform *wfhw = _wfhw;
> struct sun8i_pwm_chip *sun8i_chip = sun8i_pwm_from_chip(chip);
> unsigned int pair = SUN8I_PWM_PAIR_IDX(pwm->hwpwm);
> + struct clk *pair_clk = sun8i_chip->pairs[pair].hw->clk;
> unsigned long pair_rate;
> u32 pccr, pcr, pdzcr, per, ppr;
> + int ret;
>
> - pair_rate = sun8i_pwm_get_pair_rate(sun8i_chip, pwm->hwpwm);
> - if (pair_rate > U32_MAX)
> - return -ERANGE;
> + /*
> + * An unrequested channel can be retuned and enabled by a clock
> + * consumer. Keep its rate stable until the register snapshot is taken.
> + * Use the provider handle, separate from the PWM rate-setting handles.
> + */
> + ret = clk_rate_exclusive_get(pair_clk);
> + if (ret)
> + return ret;
> + pair_rate = clk_get_rate(pair_clk);
>
> scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
> pccr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PCCR(pair));
> @@ -413,6 +541,10 @@ static int sun8i_pwm_read_waveform(struct pwm_chip *chip,
> ppr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PPR(pwm->hwpwm));
> pdzcr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PDZCR(pair));
> }
> + clk_rate_exclusive_put(pair_clk);
[Severity: Medium]
Could holding clk_rate_exclusive_get(pair_clk) across sun8i_pwm_read_waveform()
cause concurrent clk_set_rate(), clk_set_rate_exclusive(), or clk_round_rate()
calls on a child bypass clock to silently clamp to the existing rate?
In drivers/clk/clk.c, clk_rate_exclusive_get(pair_clk) increments
pair->hw->core->protect_count and releases prepare_lock before returning.
While sun8i_pwm_read_waveform() is between clk_rate_exclusive_get(pair_clk)
and clk_rate_exclusive_put(pair_clk), a clock consumer calling
clk_set_rate(bypass_clk, new_rate) or clk_set_rate_exclusive(bypass_clk,
new_rate) on chan->bypass_hw runs clk_core_set_rate_nolock() ->
clk_core_req_round_rate_nolock().
Because the temporary rate protection was taken directly on the parent
pair_clk handle rather than through bypass_clk, bypass_clk->core->protect_count
is 0, so clk_core_rate_nuke_protect() does not discount the hold on
pair->hw->core. When clk_core_round_rate_nolock() forwards the request to
pair->hw->core via CLK_SET_RATE_PARENT, clk_core_determine_round_nolock()
sees clk_core_rate_is_protected(pair->hw->core) is true and clamps the
request to the current pair rate, causing clk_core_set_rate_nolock() to
see no rate change and return 0 without updating the hardware clock rate.
[ ... ]
> @@ -510,7 +642,8 @@ sun8i_pwm_pair_rate_constrained(struct sun8i_pwm_chip *sun8i_chip,
> unsigned int sibling = idx ^ 1;
>
> guard(spinlock_irqsave)(&sun8i_chip->lock);
> - return sun8i_chip->channels[sibling].rate_exclusive ||
> + return sun8i_chip->channels[sibling].mode == SUN8I_PWM_MODE_CLK ||
> + sun8i_chip->channels[sibling].rate_exclusive ||
> sun8i_pwm_channel_is_enabled_locked(sun8i_chip, sibling);
> }
[Severity: Medium]
What happens if a consumer of the sibling bypass clock holds rate exclusivity
via clk_rate_exclusive_get() while the bypass clock is unprepared?
In drivers/net/phy/xpowers/ac300.c, ac300_ephy_ctl_create() locks the bypass
clock rate at probe time with devm_clk_rate_exclusive_get(dev, priv->clk),
while only calling clk_prepare_enable(priv->clk) in ac300_ephy_ctl_power_on()
and calling clk_disable_unprepare(priv->clk) in ac300_ephy_ctl_disable().
When the bypass clock is unprepared, sun8i_pwm_bypass_unprepare() resets
chan->mode to SUN8I_PWM_MODE_NONE:
drivers/pwm/pwm-sun8i.c:sun8i_pwm_bypass_unprepare() {
...
guard(spinlock_irqsave)(&sun8i_chip->lock);
if (chan->mode != SUN8I_PWM_MODE_CLK)
return;
chan->mode = SUN8I_PWM_MODE_NONE;
}
Even though pair->hw->core->protect_count remains non-zero in CCF from the
consumer's clk_rate_exclusive_get(), sun8i_pwm_pair_rate_constrained() sees
mode != SUN8I_PWM_MODE_CLK, rate_exclusive == false, and the sibling channel
disabled in hardware, so it returns false.
As a result, sun8i_pwm_round_waveform_tohw() treats the pair as unconstrained
and can pick a candidate waveform with a different pair_rate. Then in
sun8i_pwm_write_waveform() -> sun8i_pwm_prepare_update(),
sun8i_pwm_pair_rate_constrained() also returns false, so
sun8i_pwm_prepare_update() disables the active PWM channel in hardware when
stop is true and calls clk_set_rate(chan->pair_clk, wfhw->pair_rate), which
fails with -EBUSY in CCF because the pair clock is still rate-protected.
In addition, because chan->mode is SUN8I_PWM_MODE_NONE while unprepared,
sun8i_pwm_request() can claim the channel for PWM, causing the clock
consumer's next clk_prepare_enable() to fail with -EBUSY in
sun8i_pwm_bypass_prepare().
[Severity: Medium]
Can sun8i_pwm_round_waveform_tohw() also observe a mismatched pair_rate and
constraint state when racing with a sibling bypass clock consumer?
In sun8i_pwm_round_waveform_tohw(), current_rate is sampled before checking
sun8i_pwm_pair_rate_constrained():
drivers/pwm/pwm-sun8i.c:sun8i_pwm_round_waveform_tohw() {
...
current_rate = sun8i_pwm_get_pair_rate(sun8i_chip, pwm->hwpwm);
if (!current_rate || current_rate > U32_MAX)
return -ERANGE;
rounding.current_pair_rate = current_rate;
if (sun8i_pwm_pair_rate_constrained(sun8i_chip, pwm->hwpwm)) {
sun8i_pwm_consider_pair_rate(&rounding, current_rate);
...
}
When the PWM channel is currently disabled (so chan->rate_exclusive is false),
sun8i_pwm_round_waveform_tohw() does not hold rate exclusivity on the pair
clock. Because CCF operations on the sibling's bypass clock do not acquire
pwmchip_lock(chip), a concurrent sibling clock consumer can call
clk_set_rate() and clk_prepare_enable() after sun8i_pwm_get_pair_rate()
returns the old rate but before sun8i_pwm_pair_rate_constrained() checks
channels[sibling].mode == SUN8I_PWM_MODE_CLK.
Wouldn't sun8i_pwm_round_waveform_tohw() then round exclusively against the
stale pre-retune current_rate instead of the rate at which the sibling just
locked the pair clock?
> @@ -1033,6 +1166,42 @@ static int sun8i_pwm_register_pair_clocks(struct device *dev,
> return 0;
> }
>
> +/* Register the bypass clock for each channel. */
> +static int sun8i_pwm_register_bypass_clocks(struct device *dev,
> + struct sun8i_pwm_chip *sun8i_chip)
> +{
> + for (unsigned int i = 0; i < SUN8I_PWM_NPWM; i++) {
> + struct sun8i_pwm_channel *chan = &sun8i_chip->channels[i];
> + struct clk_hw *parent = sun8i_chip->pairs[SUN8I_PWM_PAIR_IDX(i)].hw;
> + const char *name;
> + int ret;
> +
> + name = devm_kasprintf(dev, GFP_KERNEL, "%s#pwm-bypass%u",
> + dev_name(dev), i);
> + if (!name)
> + return -ENOMEM;
> +
> + chan->chip = sun8i_chip;
> + chan->index = i;
> + /*
> + * Protect the shared rate while prepared. Preserve unclaimed
> + * firmware outputs through CCF's unused-clock cleanup, as for
> + * PWM waveforms; only a consumer may turn them off.
> + */
> + chan->bypass_hw.init =
> + CLK_HW_INIT_HW(name, parent, &sun8i_pwm_bypass_ops,
> + CLK_SET_RATE_PARENT | CLK_SET_RATE_GATE |
> + CLK_IGNORE_UNUSED);
[Severity: Medium]
If firmware leaves a channel enabled at boot, will clk_set_rate() on that
channel's bypass clock always be rejected with -EBUSY by
sun8i_pwm_pair_rate_notifier()?
When firmware leaves a channel enabled (with SUN8I_PWM_PCCR_GATE and
SUN8I_PWM_ENABLE(i) set), sun8i_pwm_sanitize_disabled_pairs() and
CLK_IGNORE_UNUSED preserve the enabled state in hardware. Because
chan->bypass_hw is registered with CLK_SET_RATE_GATE, clk_prepare() protects
the clock rate in CCF, so any clk_set_rate() call (including from DT
assigned-clock-rates or a consumer before clk_prepare_enable()) must happen
while chan->bypass_hw is still unprepared and its CCF enable_count is 0.
However, the hardware enable bit SUN8I_PWM_ENABLE(i) in SUN8I_PWM_PER is
only cleared later in sun8i_pwm_bypass_enable() or sun8i_pwm_bypass_disable().
When clk_set_rate() is called on the unprepared bypass clock, CCF propagates
PRE_RATE_CHANGE to pair->hw while SUN8I_PWM_ENABLE(i) is still set in
SUN8I_PWM_PER:
drivers/pwm/pwm-sun8i.c:sun8i_pwm_pair_rate_notifier() {
...
guard(spinlock_irqsave)(&sun8i_chip->lock);
active = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PER) &
sun8i_pwm_pair_enable_mask(pair->index);
return active ? NOTIFY_BAD : NOTIFY_OK;
}
Unlike sun8i_pwm_prepare_update() on the PWM path, which clears
SUN8I_PWM_ENABLE(idx) before calling clk_set_rate(), nothing clears
SUN8I_PWM_ENABLE(i) before a bypass clock rate change, so
sun8i_pwm_pair_rate_notifier() sees active is true and aborts the rate change
with NOTIFY_BAD (-EBUSY).
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103-h616-pwm-v8-v11-0-07abb59575c3@gmail.com?part=3
next prev parent reply other threads:[~2026-11-03 9:28 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-03 8:40 [PATCH v11 0/5] Introduce Allwinner H616 PWM controller James Hilliard
2026-11-03 8:40 ` [PATCH v11 1/5] dt-bindings: pwm: allwinner: add h616 pwm compatible James Hilliard
2026-11-03 8:47 ` sashiko-bot
2026-11-03 8:40 ` [PATCH v11 2/5] pwm: sun8i: Add H616 PWM support James Hilliard
2026-11-03 9:08 ` sashiko-bot
2026-11-03 8:40 ` [PATCH v11 3/5] pwm: sun8i: export the bypass outputs as clocks James Hilliard
2026-11-03 9:28 ` sashiko-bot [this message]
2026-11-03 8:40 ` [PATCH v11 4/5] arm64: dts: allwinner: h616: add PWM controller James Hilliard
2026-11-03 8:40 ` [PATCH v11 5/5] MAINTAINERS: Add entry on Allwinner sun8i/H616 PWM driver James Hilliard
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-194955@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=james.hilliard1@gmail.com \
--cc=linux-clk@vger.kernel.org \
--cc=linux-sunxi@lists.linux.dev \
--cc=robh@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox