ARM Sunxi Platform Development
 help / color / mirror / Atom feed
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

  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