Re: [PATCH] pwm: meson: Convert to waveform API
Alexandre Mergnat <[email protected]>
| Newsgroups | org.kernel.vger.linux-pwm,org.infradead.lists.linux-amlogic,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <CAFGrd9qh8bHD5pJuiB-PLfAcYEyuw9ExHEWeAj_JFSp_sm52rQ@mail.gmail.com> |
Hi, a gentle ping on this patch. It has been around three weeks since the submission. Any feedback would be appreciated. Regards, Alexandre On Fri, Jul 17, 2026 at 9:18 PM Alexandre Mergnat <[email protected]> wrote: > > The PWM subsystem introduced a new waveform abstraction that > describes a PWM signal by period length, duty length and duty offset > instead of period, duty cycle and polarity. The meson driver still > uses the legacy .apply() and .get_state() callbacks and does not > implement the waveform callbacks, so consumers cannot use the > pwm_*_waveform() helper functions with it. > > Adopt the new API by implementing .round_waveform_tohw(), > .round_waveform_fromhw(), .read_waveform() and .write_waveform() > with a private meson_pwm_waveform struct holding the hardware > representation. The cached channel state fields (rate, hi, lo, > constant, inverted) are removed; counter values are now computed in > .round_waveform_tohw() via clk_round_rate() without register writes, > then committed in .write_waveform(). > > The hardware invert bit places the duty active phase at the end of the > period on revisions that have it. Input clock rates are kept at or > below 1 GHz so each counter cycle remains representable in nanoseconds > by .round_waveform_fromhw(). Requests for which clk_round_rate() cannot > provide such a rate are rejected. Otherwise, period counts above the > 16-bit range are clamped, while a zero count is rounded up to one and > signalled by .round_waveform_tohw() returning 1. > > Signed-off-by: Alexandre Mergnat <[email protected]> > --- > The PWM core gained a new waveform abstraction that lets drivers > describe the hardware more directly and gives consumers access to > rounding and read-back without touching registers. The meson driver > still uses the legacy .apply() and .get_state() callbacks, so > consumers of its PWMs cannot benefit from the new pwm_*_waveform() > helpers. > > This series implements the four waveform callbacks and removes the > cached channel state that the legacy .apply() path relied on. > Settings are now computed without side effects and committed in a > separate step, matching the split the waveform API requires. > > Behavioural changes visible to consumers: > > - On revisions without the invert bit, a nonzero duty offset is > rounded down to zero. An inverted polarity request through the > legacy path produces the same register settings as the previous > "duty = period - duty" emulation, so the generated signal is > unchanged for those consumers. > > - Disabling a PWM no longer emulates an inactive-high output on > revisions without the invert bit. The PWM API defines the output > of a disabled PWM as undefined (this was already the case for > .apply()), so affected consumers were relying on unspecified > behaviour. Consumers that need an inactive high output should keep > the PWM enabled with inverted polarity and zero duty cycle. > > - Input clock rates are kept at or below 1 GHz so counter cycles remain > representable in nanoseconds. Requests for which clk_round_rate() > cannot provide such a rate are rejected. Otherwise, period counts > exceeding the 16-bit range are clamped. A zero count is rounded up to > one, signalled by .round_waveform_tohw() returning 1. > > - Reported period and duty cycle values are rounded up instead of > down, so a returned setting reproduces the same counter values > when applied again. The constant output bit is now derived from > the converted counter values, which also covers cases where the > ns-to-counter conversion yields an empty high or low phase. > clk_set_rate() failures now propagate as write errors instead of > being logged and ignored, and .write_waveform() now also fails if > the input clock rate changed between rounding and writing. > > Testing > ======= > > Tested on a Libre Computer Le Potato (AML-S905X-CC, Amlogic S905X / > GXL SoC) with CONFIG_PWM_DEBUG=y, built in tree. Three PWM nodes were > enabled in the board device tree (pwm_ab, pwm_ef, pwm_AO_ab), exposing > 6 channels across the EE and AO clock domains. > > The libpwm tools drive the /dev/pwmchipN character device directly, so > the new waveform callbacks are exercised through their own ioctls. All > 6 channels went through pwmround for rounding queries (ROUNDWF: > standard, too-short and too-long periods, zero and full duty, duty > offsets, non-representable values), pwmset for rounded and exact > settings (SETROUNDEDWF / SETEXACTWF) with hardware read-back through > debugfs (.read_waveform()), and pwmtestperf duty sweeps. pwmtestperf > was run in all four direction/polarity combinations at 100 us, 1 ms and > 10 ms (72 runs), then with a 1 ns step at a non-tick-aligned period so > every duty and duty-offset rounding boundary is crossed (about 24000 > boundary checks). PWM_DEBUG's rounding and read-back checks stayed > silent throughout. > > ROUNDWF and SETEXACTWF agree as specified: SETEXACTWF applies exactly > representable waveforms and returns -EDOM otherwise. 1 ms is exact on > the EE chips and rounds to 999990 ns on the AO chip, reflecting > each PWM's clock granularity. A 1 ns period request is > rounded up to the shortest representable period with zero duty, > signalled by .round_waveform_tohw() returning 1. Periods too long for > the source clock to represent (349.52 ms on this board) are rejected > with -EINVAL when clk_round_rate() cannot provide a low enough rate. > GXL has no invert bit (has_polarity = false), so nonzero duty offsets > are rounded down to zero and the axg/g12/s4 invert paths are not > exercisable on this board. > > The sysfs interface (50%/25%/0%/100% duty, polarity inversion, > enable/disable on all 6 channels) was also exercised and reads back > consistently, confirming no regression for legacy consumers. > --- > drivers/pwm/pwm-meson.c | 372 ++++++++++++++++++++++++++++-------------------- > 1 file changed, 215 insertions(+), 157 deletions(-) > > diff --git a/drivers/pwm/pwm-meson.c b/drivers/pwm/pwm-meson.c > index 8c6bf3d49753..be17aae0941b 100644 > --- a/drivers/pwm/pwm-meson.c > +++ b/drivers/pwm/pwm-meson.c > @@ -6,11 +6,11 @@ > * PWM output is achieved by calculating a clock that permits calculating > * two periods (low and high). The counter then has to be set to switch after > * N cycles for the first half period. > - * Partly the hardware has no "polarity" setting. This driver reverses the period > - * cycles (the low length is inverted with the high length) for > - * PWM_POLARITY_INVERSED. This means that .get_state cannot read the polarity > - * from the hardware. > - * Setting the duty cycle will disable and re-enable the PWM output. > + * Some of the IP block revisions have an invert bit that swaps the high and > + * low parts of the output. This is used to implement waveforms with > + * duty_offset_ns + duty_length_ns == period_length_ns, i.e. what the legacy > + * API calls inverted polarity. On the other revisions a nonzero duty_offset_ns > + * is rounded down to zero. > * Disabling the PWM stops the output immediately (without waiting for the > * current period to complete first). > * > @@ -35,6 +35,7 @@ > #include <linux/io.h> > #include <linux/kernel.h> > #include <linux/math64.h> > +#include <linux/minmax.h> > #include <linux/module.h> > #include <linux/of.h> > #include <linux/platform_device.h> > @@ -96,12 +97,6 @@ static struct meson_pwm_channel_data { > }; > > struct meson_pwm_channel { > - unsigned long rate; > - unsigned int hi; > - unsigned int lo; > - bool constant; > - bool inverted; > - > struct clk_mux mux; > struct clk_divider div; > struct clk_gate gate; > @@ -156,30 +151,44 @@ static void meson_pwm_free(struct pwm_chip *chip, struct pwm_device *pwm) > clk_disable_unprepare(channel->clk); > } > > -static int meson_pwm_calc(struct pwm_chip *chip, struct pwm_device *pwm, > - const struct pwm_state *state) > +struct meson_pwm_waveform { > + unsigned long rate; > + u16 hi; > + u16 lo; > + bool enabled; > + bool inverted; > +}; > + > +static int meson_pwm_round_waveform_tohw(struct pwm_chip *chip, > + struct pwm_device *pwm, > + const struct pwm_waveform *wf, > + void *_wfhw) > { > + struct meson_pwm_waveform *wfhw = _wfhw; > struct meson_pwm *meson = to_meson_pwm(chip); > struct meson_pwm_channel *channel = &meson->channels[pwm->hwpwm]; > - unsigned int cnt, duty_cnt; > + u64 cnt, duty_cnt, freq; > long fin_freq; > - u64 duty, period, freq; > + int ret = 0; > > - duty = state->duty_cycle; > - period = state->period; > + if (wf->period_length_ns == 0) { > + *wfhw = (struct meson_pwm_waveform){ > + .enabled = false, > + }; > + > + return 0; > + } > > /* > - * Note this is wrong. The result is an output wave that isn't really > - * inverted and so is wrongly identified by .get_state as normal. > - * Fixing this needs some care however as some machines might rely on > - * this. > + * Get the highest input clock rate that makes the requested period > + * representable with the 16 bit wide counters, but at most 1 GHz so > + * one counter cycle stays at least a nanosecond long, which > + * .round_waveform_fromhw() relies on. clk_round_rate() may round up > + * past the requested rate, so reject a too-high result below. > */ > - if (state->polarity == PWM_POLARITY_INVERSED && !meson->data->has_polarity) > - duty = period - duty; > - > - freq = div64_u64(NSEC_PER_SEC * 0xffffULL, period); > - if (freq > ULONG_MAX) > - freq = ULONG_MAX; > + freq = div64_u64((u64)NSEC_PER_SEC * FIELD_MAX(PWM_LOW_MASK), > + wf->period_length_ns); > + freq = min_t(u64, freq, NSEC_PER_SEC); > > fin_freq = clk_round_rate(channel->clk, freq); > if (fin_freq <= 0) { > @@ -188,180 +197,226 @@ static int meson_pwm_calc(struct pwm_chip *chip, struct pwm_device *pwm, > return fin_freq ? fin_freq : -EINVAL; > } > > - dev_dbg(pwmchip_parent(chip), "fin_freq: %ld Hz\n", fin_freq); > - > - cnt = mul_u64_u64_div_u64(fin_freq, period, NSEC_PER_SEC); > - if (cnt > 0xffff) { > - dev_err(pwmchip_parent(chip), "unable to get period cnt\n"); > + if (fin_freq > NSEC_PER_SEC) { > + dev_err(pwmchip_parent(chip), > + "source clock frequency %ld too high\n", fin_freq); > return -EINVAL; > } > > - dev_dbg(pwmchip_parent(chip), "period=%llu cnt=%u\n", period, cnt); > - > - if (duty == period) { > - channel->hi = cnt; > - channel->lo = 0; > - channel->constant = true; > - } else if (duty == 0) { > - channel->hi = 0; > - channel->lo = cnt; > - channel->constant = true; > - } else { > - duty_cnt = mul_u64_u64_div_u64(fin_freq, duty, NSEC_PER_SEC); > - > - dev_dbg(pwmchip_parent(chip), "duty=%llu duty_cnt=%u\n", duty, duty_cnt); > + /* > + * If the requested period is too long to be counted even at the > + * lowest possible input clock rate, implement the longest possible > + * period instead. > + */ > + cnt = mul_u64_u64_div_u64(fin_freq, wf->period_length_ns, NSEC_PER_SEC); > + cnt = min_t(u64, cnt, FIELD_MAX(PWM_LOW_MASK)); > + > + if (cnt == 0) { > + /* > + * The requested period is shorter than one cycle of the > + * fastest available input clock, implement the minimal > + * period with zero duty cycle (the general calculation below > + * yields that as duty_length_ns and duty_offset_ns cannot > + * exceed the requested period) and signal the rounding up > + * in the return value. > + */ > + cnt = 1; > + ret = 1; > + } > > - channel->hi = duty_cnt; > - channel->lo = cnt - duty_cnt; > - channel->constant = false; > + duty_cnt = mul_u64_u64_div_u64(fin_freq, wf->duty_length_ns, NSEC_PER_SEC); > + duty_cnt = min(duty_cnt, cnt); > + > + *wfhw = (struct meson_pwm_waveform){ > + .rate = fin_freq, > + .hi = duty_cnt, > + .lo = cnt - duty_cnt, > + .enabled = true, > + }; > + > + if (meson->data->has_polarity && duty_cnt) { > + u64 offset_cnt = mul_u64_u64_div_u64(fin_freq, wf->duty_offset_ns, > + NSEC_PER_SEC); > + > + /* > + * Waveforms with duty_offset_ns + duty_length_ns == > + * period_length_ns are implemented by inverting the output: > + * The signal then is low for hi counts (the offset) followed > + * by high for lo counts (the duty cycle). Other nonzero > + * duty_offset_ns values are rounded down to 0. > + */ > + if (offset_cnt && duty_cnt + offset_cnt >= cnt) { > + swap(wfhw->hi, wfhw->lo); > + wfhw->inverted = true; > + } > } > > - channel->rate = fin_freq; > + dev_dbg(pwmchip_parent(chip), > + "pwm#%u: %llu/%llu [+%llu] @%lu -> hi: %u, lo: %u, inverted: %d\n", > + pwm->hwpwm, wf->duty_length_ns, wf->period_length_ns, > + wf->duty_offset_ns, wfhw->rate, wfhw->hi, wfhw->lo, > + wfhw->inverted); > > - return 0; > + return ret; > } > > -static void meson_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm) > +static u64 meson_pwm_cnt_to_ns(unsigned long fin_freq, u32 cnt) > { > - struct meson_pwm *meson = to_meson_pwm(chip); > - struct meson_pwm_channel *channel = &meson->channels[pwm->hwpwm]; > - struct meson_pwm_channel_data *channel_data; > - unsigned long flags; > - u32 value; > - int err; > - > - channel_data = &meson_pwm_per_channel_data[pwm->hwpwm]; > - > - err = clk_set_rate(channel->clk, channel->rate); > - if (err) > - dev_err(pwmchip_parent(chip), "setting clock rate failed\n"); > - > - spin_lock_irqsave(&meson->lock, flags); > + return DIV64_U64_ROUND_UP(NSEC_PER_SEC * (u64)cnt, fin_freq); > +} > > - value = FIELD_PREP(PWM_HIGH_MASK, channel->hi) | > - FIELD_PREP(PWM_LOW_MASK, channel->lo); > - writel(value, meson->base + channel_data->reg_offset); > +static int meson_pwm_round_waveform_fromhw(struct pwm_chip *chip, > + struct pwm_device *pwm, > + const void *_wfhw, > + struct pwm_waveform *wf) > +{ > + const struct meson_pwm_waveform *wfhw = _wfhw; > > - value = readl(meson->base + REG_MISC_AB); > - value |= channel_data->pwm_en_mask; > + if (!wfhw->enabled || !wfhw->rate) { > + *wf = (struct pwm_waveform){ > + .period_length_ns = 0, > + }; > > - if (meson->data->has_constant) { > - value &= ~channel_data->const_en_mask; > - if (channel->constant) > - value |= channel_data->const_en_mask; > + return 0; > } > > - if (meson->data->has_polarity) { > - value &= ~channel_data->inv_en_mask; > - if (channel->inverted) > - value |= channel_data->inv_en_mask; > + /* > + * wfhw->hi and wfhw->lo are u16, so their sum doesn't overflow and > + * NSEC_PER_SEC * (hi + lo) fits into the u64 dividend in > + * meson_pwm_cnt_to_ns(). > + */ > + wf->period_length_ns = meson_pwm_cnt_to_ns(wfhw->rate, wfhw->hi + wfhw->lo); > + > + if (wfhw->inverted && wfhw->lo == 0) { > + /* > + * The output is constant low. Report a normal constant-low > + * waveform instead of duty_offset_ns == period_length_ns > + * which wouldn't be a valid waveform description. This > + * setting is only reachable when the hardware was programmed > + * by the bootloader as .round_waveform_tohw() never produces > + * it. > + */ > + wf->duty_length_ns = 0; > + wf->duty_offset_ns = 0; > + } else if (wfhw->inverted) { > + wf->duty_length_ns = meson_pwm_cnt_to_ns(wfhw->rate, wfhw->lo); > + wf->duty_offset_ns = meson_pwm_cnt_to_ns(wfhw->rate, wfhw->hi); > + } else { > + wf->duty_length_ns = meson_pwm_cnt_to_ns(wfhw->rate, wfhw->hi); > + wf->duty_offset_ns = 0; > } > > - writel(value, meson->base + REG_MISC_AB); > - > - spin_unlock_irqrestore(&meson->lock, flags); > + return 0; > } > > -static void meson_pwm_disable(struct pwm_chip *chip, struct pwm_device *pwm) > +static int meson_pwm_read_waveform(struct pwm_chip *chip, > + struct pwm_device *pwm, > + void *_wfhw) > { > + struct meson_pwm_waveform *wfhw = _wfhw; > struct meson_pwm *meson = to_meson_pwm(chip); > struct meson_pwm_channel *channel = &meson->channels[pwm->hwpwm]; > struct meson_pwm_channel_data *channel_data; > - unsigned long flags; > - u32 value; > + u32 value, pwm_reg; > > channel_data = &meson_pwm_per_channel_data[pwm->hwpwm]; > > - spin_lock_irqsave(&meson->lock, flags); > - > value = readl(meson->base + REG_MISC_AB); > - value &= ~channel_data->pwm_en_mask; > + if (!(value & channel_data->pwm_en_mask)) { > + *wfhw = (struct meson_pwm_waveform){ > + .enabled = false, > + }; > > - if (meson->data->has_polarity) { > - value &= ~channel_data->inv_en_mask; > - if (channel->inverted) > - value |= channel_data->inv_en_mask; > + return 0; > } > > - writel(value, meson->base + REG_MISC_AB); > + pwm_reg = readl(meson->base + channel_data->reg_offset); > > - spin_unlock_irqrestore(&meson->lock, flags); > + *wfhw = (struct meson_pwm_waveform){ > + .rate = clk_get_rate(channel->clk), > + .hi = FIELD_GET(PWM_HIGH_MASK, pwm_reg), > + .lo = FIELD_GET(PWM_LOW_MASK, pwm_reg), > + .enabled = true, > + .inverted = meson->data->has_polarity && > + (value & channel_data->inv_en_mask), > + }; > + > + return 0; > } > > -static int meson_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm, > - const struct pwm_state *state) > +static int meson_pwm_write_waveform(struct pwm_chip *chip, > + struct pwm_device *pwm, > + const void *_wfhw) > { > + const struct meson_pwm_waveform *wfhw = _wfhw; > struct meson_pwm *meson = to_meson_pwm(chip); > struct meson_pwm_channel *channel = &meson->channels[pwm->hwpwm]; > - int err = 0; > - > - channel->inverted = (state->polarity == PWM_POLARITY_INVERSED); > - > - if (!state->enabled) { > - if (channel->inverted && !meson->data->has_polarity) { > - /* > - * Some of IP block revisions don't have an "always high" > - * setting which we can use for "inverted disabled". > - * Instead we achieve this by setting mux parent with > - * highest rate and minimum divider value, resulting > - * in the shortest possible duration for one "count" > - * and "period == duty_cycle". This results in a signal > - * which is LOW for one "count", while being HIGH for > - * the rest of the (so the signal is HIGH for slightly > - * less than 100% of the period, but this is the best > - * we can achieve). > - */ > - channel->rate = ULONG_MAX; > - channel->hi = ~0; > - channel->lo = 0; > - channel->constant = true; > - > - meson_pwm_enable(chip, pwm); > - } else { > - meson_pwm_disable(chip, pwm); > - } > - } else { > - err = meson_pwm_calc(chip, pwm, state); > - if (err < 0) > + struct meson_pwm_channel_data *channel_data; > + unsigned long flags; > + u32 value; > + int err; > + > + channel_data = &meson_pwm_per_channel_data[pwm->hwpwm]; > + > + /* > + * Channel clock operations also modify REG_MISC_AB under meson->lock, > + * including from .request()/.free() outside the pwmchip lock. Set the > + * rate before taking meson->lock to avoid recursive locking. Skip > + * clk_set_rate() when unchanged because even a no-op re-evaluates the > + * mux parent and reparses the device tree. > + */ > + if (wfhw->enabled && clk_get_rate(channel->clk) != wfhw->rate) { > + err = clk_set_rate(channel->clk, wfhw->rate); > + if (err) { > + dev_err(pwmchip_parent(chip), > + "setting clock rate failed: %d\n", err); > return err; > + } > > - meson_pwm_enable(chip, pwm); > + /* > + * The rate computed by .round_waveform_tohw() might not be > + * hit if an input clock changed its rate in between; the > + * counter values only implement the promised waveform at > + * exactly wfhw->rate. > + */ > + if (clk_get_rate(channel->clk) != wfhw->rate) { > + dev_err(pwmchip_parent(chip), > + "clock rate changed since rounding\n"); > + return -EINVAL; > + } > } > > - return 0; > -} > + spin_lock_irqsave(&meson->lock, flags); > > -static u64 meson_pwm_cnt_to_ns(unsigned long fin_freq, u32 cnt) > -{ > - return fin_freq ? div64_ul(NSEC_PER_SEC * (u64)cnt, fin_freq) : 0; > -} > + if (wfhw->enabled) { > + value = FIELD_PREP(PWM_HIGH_MASK, wfhw->hi) | > + FIELD_PREP(PWM_LOW_MASK, wfhw->lo); > + writel(value, meson->base + channel_data->reg_offset); > > -static int meson_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm, > - struct pwm_state *state) > -{ > - struct meson_pwm *meson = to_meson_pwm(chip); > - struct meson_pwm_channel_data *channel_data; > - unsigned long fin_freq; > - unsigned int hi, lo; > - u32 value; > - > - channel_data = &meson_pwm_per_channel_data[pwm->hwpwm]; > - fin_freq = clk_get_rate(meson->channels[pwm->hwpwm].clk); > + value = readl(meson->base + REG_MISC_AB); > + value |= channel_data->pwm_en_mask; > > - value = readl(meson->base + REG_MISC_AB); > - state->enabled = value & channel_data->pwm_en_mask; > + if (meson->data->has_constant) { > + value &= ~channel_data->const_en_mask; > + /* The signal is constant when one phase is empty */ > + if (!wfhw->hi || !wfhw->lo) > + value |= channel_data->const_en_mask; > + } > > - if (meson->data->has_polarity && (value & channel_data->inv_en_mask)) > - state->polarity = PWM_POLARITY_INVERSED; > - else > - state->polarity = PWM_POLARITY_NORMAL; > + if (meson->data->has_polarity) { > + value &= ~channel_data->inv_en_mask; > + if (wfhw->inverted) > + value |= channel_data->inv_en_mask; > + } > > - value = readl(meson->base + channel_data->reg_offset); > - lo = FIELD_GET(PWM_LOW_MASK, value); > - hi = FIELD_GET(PWM_HIGH_MASK, value); > + writel(value, meson->base + REG_MISC_AB); > + } else { > + value = readl(meson->base + REG_MISC_AB); > + value &= ~channel_data->pwm_en_mask; > + writel(value, meson->base + REG_MISC_AB); > + } > > - state->period = meson_pwm_cnt_to_ns(fin_freq, lo + hi); > - state->duty_cycle = meson_pwm_cnt_to_ns(fin_freq, hi); > + spin_unlock_irqrestore(&meson->lock, flags); > > return 0; > } > @@ -369,8 +424,11 @@ static int meson_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm, > static const struct pwm_ops meson_pwm_ops = { > .request = meson_pwm_request, > .free = meson_pwm_free, > - .apply = meson_pwm_apply, > - .get_state = meson_pwm_get_state, > + .sizeof_wfhw = sizeof(struct meson_pwm_waveform), > + .round_waveform_tohw = meson_pwm_round_waveform_tohw, > + .round_waveform_fromhw = meson_pwm_round_waveform_fromhw, > + .read_waveform = meson_pwm_read_waveform, > + .write_waveform = meson_pwm_write_waveform, > }; > > static int meson_pwm_init_clocks_meson8b(struct pwm_chip *chip, > > --- > base-commit: 2e0a43a9e6f2e846f370aa63ea274f5403628b79 > change-id: 20260710-pwm_meson_convert_to_waveform_api-8711b7f8c755 > > Best regards, > -- > Alexandre Mergnat <[email protected]> >