* [PATCH] pwm: meson: Convert to waveform API
@ 2026-07-17 19:18 Alexandre Mergnat
2026-07-17 19:27 ` sashiko-bot
0 siblings, 1 reply; 2+ messages in thread
From: Alexandre Mergnat @ 2026-07-17 19:18 UTC (permalink / raw)
To: Uwe Kleine-König, Neil Armstrong, Kevin Hilman,
Jerome Brunet, Martin Blumenstingl
Cc: linux-pwm, linux-arm-kernel, linux-amlogic, linux-kernel,
Alexandre Mergnat
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 <amergnat@baylibre.com>
---
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 <amergnat@baylibre.com>
_______________________________________________
linux-amlogic mailing list
linux-amlogic@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-amlogic
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: [PATCH] pwm: meson: Convert to waveform API
2026-07-17 19:18 [PATCH] pwm: meson: Convert to waveform API Alexandre Mergnat
@ 2026-07-17 19:27 ` sashiko-bot
0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-07-17 19:27 UTC (permalink / raw)
To: Alexandre Mergnat; +Cc: neil.armstrong, linux-amlogic
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Truncation error in hardware waveform quantization breaks legacy polarity inversion.
- [Medium] Independent rounding in waveform readback produces an invalid state where duty + offset > period.
--
--- Patch [1]: [PATCH] pwm: meson: Convert to waveform API ---
commit 205d0aee1231456b923515acf95a461a9f0a217f
Author: Alexandre Mergnat <amergnat@baylibre.com>
pwm: meson: Convert to waveform API
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. 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.
> diff --git a/drivers/pwm/pwm-meson.c b/drivers/pwm/pwm-meson.c
> index 20e6cd07127c4..633b384358c08 100644
> --- a/drivers/pwm/pwm-meson.c
> +++ b/drivers/pwm/pwm-meson.c
[ ... ]
> +static int meson_pwm_round_waveform_tohw(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + const struct pwm_waveform *wf,
> + void *_wfhw)
> {
[ ... ]
> + 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) {
[Severity: High]
Could integer division truncation cause this check to fail for inverted
waveforms?
If duty_cnt and offset_cnt are calculated with independent truncation, their
sum might end up strictly less than cnt even when the original lengths sum to
the period.
Would this cause the driver to incorrectly fall back to normal polarity with a
zero offset?
> + swap(wfhw->hi, wfhw->lo);
> + wfhw->inverted = true;
> + }
> }
[ ... ]
> +static int meson_pwm_round_waveform_fromhw(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + const void *_wfhw,
> + struct pwm_waveform *wf)
> +{
[ ... ]
> + /*
> + * 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);
[Severity: Medium]
Since meson_pwm_cnt_to_ns() explicitly rounds up, can rounding duty_length_ns
and duty_offset_ns independently result in their sum exceeding
period_length_ns?
For example, if rate is 333333333 Hz and both hi and lo counts are 1,
period_length_ns rounds up to 7 ns, while duty_length_ns and duty_offset_ns
would each round up to 4 ns. Their sum (8 ns) would exceed the period length,
potentially leading to integer underflow in consumers attempting to calculate
the remaining period time.
> + } else {
> + wf->duty_length_ns = meson_pwm_cnt_to_ns(wfhw->rate, wfhw->hi);
> + wf->duty_offset_ns = 0;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717-pwm_meson_convert_to_waveform_api-v1-1-d7dbdae1318d@baylibre.com?part=1
_______________________________________________
linux-amlogic mailing list
linux-amlogic@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-amlogic
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-07-17 19:27 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-17 19:18 [PATCH] pwm: meson: Convert to waveform API Alexandre Mergnat
2026-07-17 19:27 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox