Re: [PATCH v4 2/2] pwm: add Axiado AX3000 PWM driver
From: Uwe Kleine-König
Date: Wed Sep 09 2026 - 06:37:57 EST
Hello,
On Tue, Sep 08, 2026 at 01:28:35AM -0700, Petar Stepanovic wrote:
> The Axiado AX3000 and AX3005 SoCs include a single-channel PWM
> controller.
>
> Add a driver supporting period, duty-cycle, and enable-state
> configuration through the Linux PWM framework.
>
> Disabling the hardware controller drives its output high. Implement a
> logically disabled PWM using the controller's constant-low encoding
> instead. The same encoding is used for an enabled PWM with a 0% duty
> cycle, so cache the logical state and requested period for readback.
The purpose of disabling the hardware is to save power. If the result is
that the output goes to the high state that's mildly strange, but not
unseen. Still disable the hardware in that case. (And any consumer
driver that relies on a low output on disable is wrong and needs
fixing.)
> diff --git a/drivers/pwm/pwm-axiado.c b/drivers/pwm/pwm-axiado.c
> new file mode 100644
> index 000000000000..e1d408809694
> --- /dev/null
> +++ b/drivers/pwm/pwm-axiado.c
> @@ -0,0 +1,332 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) 2021-2026 Axiado Corporation.
It's not universally done in the kernel, but I'd be happy to see this
formalized as:
SPDX-FileCopyrightText: 2021-2026 Axiado Corporation.
> + */
> +
> +/*
Please put "Limitations:" here such that
sed -rn '/Limitations:/,/\*\/?$/p' drivers/pwm/*.c
gives the details.
> + * - Supports normal polarity. Configuration changes take effect immediately
> + * without waiting for the current period to complete.
> + * - The hardware output remains high when the controller is disabled.
> + * Therefore, a logically disabled PWM is implemented using the hardware
> + * constant-low representation.
> + * - Supported period range: 2 through 0xfffffffe PWM input clock cycles;
> + * 0xffffffff is reserved by the hardware for a constant-low output.
> + * Longer periods are clamped to the maximum.
> + * - 0% duty cycle: Programmed as a constant-low period because the
> + * hardware interprets a zero high time as a constant-high output.
> + * Consequently, the requested period cannot be read back while the
> + * duty cycle is 0%.
> + * - 100% duty cycle: Fully supported and produces a constant-high output.
> + */
> +
> +#include <linux/bits.h>
> +#include <linux/clk.h>
> +#include <linux/err.h>
> +#include <linux/io.h>
> +#include <linux/math64.h>
> +#include <linux/module.h>
> +#include <linux/platform_device.h>
> +#include <linux/pwm.h>
> +
> +/* Register offsets */
> +#define AXIADO_PWM_CTRL_REG 0x0000
> +#define AXIADO_PWM_PERIOD_REG 0x0004
> +#define AXIADO_PWM_HIGH_REG 0x0008
> +
> +/* Period and duty cycle limits */
> +#define AXIADO_PWM_PERIOD_MIN 2
> +#define AXIADO_PWM_PERIOD_MAX 0xfffffffe
> +#define AXIADO_PWM_PERIOD_CONST_LOW 0xffffffff
> +#define AXIADO_PWM_DUTY_MIN 1
> +
> +/* Control register bits */
> +#define AXIADO_PWM_CTRL_ENABLE BIT(0)
> +
> +struct axiado_pwm_chip {
> + void __iomem *base;
> + unsigned long rate;
> + u32 cached_period;
> + bool logically_disabled;
> +};
> +
> +struct axiado_pwm_waveform {
> + u32 period;
> + u32 duty;
> + bool enabled;
> +};
> +
> +static int
> +axiado_pwm_round_waveform_tohw(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + const struct pwm_waveform *wf,
> + void *_wfhw)
> +{
> + struct axiado_pwm_chip *axpwm = pwmchip_get_drvdata(chip);
> + struct axiado_pwm_waveform *wfhw = _wfhw;
> + u64 period;
> + u64 duty;
> + int ret = 0;
> +
> + /* Encode a disabled request as a zeroed hardware waveform. */
> + if (!wf->period_length_ns) {
> + *wfhw = (struct axiado_pwm_waveform) {};
{ } please (i.e. a space in the empty initializer).
> + return 0;
> + }
> +
> + /* Only an edge-aligned waveform starting at offset zero is supported. */
> + if (wf->duty_offset_ns)
> + return -EINVAL;
That is wrong. You're supposed to implement the biggest duty_offset_ns
not bigger than the requested value. That's 0 and should happen
successfully (and silently).
> + if (wf->duty_length_ns > wf->period_length_ns)
> + return -EINVAL;
No need to check that. If you see this happen, that's a bug in the pwm
core.
> + period = mul_u64_u64_div_u64(wf->period_length_ns, axpwm->rate,
> + NSEC_PER_SEC);
> +
> + if (period < AXIADO_PWM_PERIOD_MIN) {
> + period = AXIADO_PWM_PERIOD_MIN;
> + ret = 1;
> + } else if (period > AXIADO_PWM_PERIOD_MAX) {
> + period = AXIADO_PWM_PERIOD_MAX;
> + }
> +
> + /*
> + * Keep the rounded period for a 0% duty cycle. .write_waveform()
> + * translates it to the hardware constant-low representation.
> + */
> + if (!wf->duty_length_ns) {
> + *wfhw = (struct axiado_pwm_waveform) {
> + .period = period,
> + .duty = 0,
> + .enabled = true,
> + };
This happens also without the special handling, right? So the if could
be dropped.
> + return ret;
> + }
> +
> + duty = mul_u64_u64_div_u64(wf->duty_length_ns, axpwm->rate,
> + NSEC_PER_SEC);
> +
> + /*
> + * Preserve an exact 100% duty request when the hardware period has
> + * been clamped.
> + */
> + if (wf->duty_length_ns == wf->period_length_ns)
> + duty = period;
With the check below for duty > period this isn't needed.
> + /*
> + * Period clamping can leave the converted duty greater than the
> + * final hardware period. In that case, clamp it to 100% duty.
> + */
> + if (duty > period)
> + duty = period;
> +
> + *wfhw = (struct axiado_pwm_waveform) {
> + .period = period,
> + .duty = duty,
> + .enabled = true,
> + };
> +
> + return ret;
> +}
> +
> +static int
> +axiado_pwm_round_waveform_fromhw(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + const void *_wfhw,
> + struct pwm_waveform *wf)
> +{
> + struct axiado_pwm_chip *axpwm = pwmchip_get_drvdata(chip);
> + const struct axiado_pwm_waveform *wfhw = _wfhw;
> +
> + if (!wfhw->enabled) {
> + *wf = (struct pwm_waveform) {
> + .period_length_ns = 0,
> + .duty_length_ns = 0,
> + .duty_offset_ns = 0,
> + };
nitpick: setting .period_length_ns = 0 is sufficient here and that's
what other drivers do.
> + return 0;
> + }
> +
> + *wf = (struct pwm_waveform) {
> + .period_length_ns =
> + mul_u64_u64_div_u64_roundup(wfhw->period, NSEC_PER_SEC,
> + axpwm->rate),
> + .duty_length_ns =
> + mul_u64_u64_div_u64_roundup(wfhw->duty, NSEC_PER_SEC,
> + axpwm->rate),
> + .duty_offset_ns = 0,
> + };
> +
> + return 0;
> +}
> +
> +static int axiado_pwm_read_waveform(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + void *_wfhw)
> +{
> + struct axiado_pwm_chip *axpwm = pwmchip_get_drvdata(chip);
> + struct axiado_pwm_waveform *wfhw = _wfhw;
> + u32 period;
> + u32 duty;
> + u32 ctrl;
> +
> + /*
> + * A logically disabled request is implemented as a constant-low
> + * hardware waveform because disabling the controller drives the
> + * output high.
> + */
> + if (axpwm->logically_disabled) {
> + *wfhw = (struct axiado_pwm_waveform) {};
> + return 0;
that looks wrong. At least the first readout before .write_waveform()
was called might be bogus then.
> + }
> +
> + ctrl = readl(axpwm->base + AXIADO_PWM_CTRL_REG);
> + period = readl(axpwm->base + AXIADO_PWM_PERIOD_REG);
> + duty = readl(axpwm->base + AXIADO_PWM_HIGH_REG);
> +
> + /* The constant-low encoding doesn't hold the period, so restore it. */
This isn't needed. If the output is constant low, just report period =
1 and drop .cached_period.
> + if (period == AXIADO_PWM_PERIOD_CONST_LOW) {
> + period = axpwm->cached_period;
> + duty = 0;
> + } else if (duty > period) {
> + duty = period;
> + }
> +
> + *wfhw = (struct axiado_pwm_waveform) {
> + .period = period,
> + .duty = duty,
> + .enabled = !!(ctrl & AXIADO_PWM_CTRL_ENABLE),
> + };
> +
> + return 0;
> +}
> +
> +static int axiado_pwm_write_waveform(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + const void *_wfhw)
> +{
> + struct axiado_pwm_chip *axpwm = pwmchip_get_drvdata(chip);
> + const struct axiado_pwm_waveform *wfhw = _wfhw;
> + u32 period = wfhw->period;
> + u32 duty = wfhw->duty;
> +
> + if (!wfhw->enabled) {
> + /*
> + * Disabling the controller drives its output high. Implement
> + * the logically disabled state using the constant-low hardware
> + * representation instead.
> + */
> + writel(AXIADO_PWM_PERIOD_CONST_LOW,
> + axpwm->base + AXIADO_PWM_PERIOD_REG);
> + writel(AXIADO_PWM_DUTY_MIN,
> + axpwm->base + AXIADO_PWM_HIGH_REG);
> + writel(AXIADO_PWM_CTRL_ENABLE,
> + axpwm->base + AXIADO_PWM_CTRL_REG);
> + axpwm->logically_disabled = true;
> +
> + return 0;
> + }
> +
> + /*
> + * A zero high time produces a constant high output, so use the
> + * constant-low period encoding for a 0% duty cycle. Keep the high time
> + * non-zero because a zero value takes precedence over that encoding.
> + * Cache the requested period for .read_waveform().
> + */
> + if (!duty) {
> + axpwm->cached_period = period;
> + period = AXIADO_PWM_PERIOD_CONST_LOW;
> + duty = AXIADO_PWM_DUTY_MIN;
> + }
> +
> + /*
> + * The hardware has no shadow registers. These writes may alter the
> + * active waveform before the current period has completed.
> + */
> + writel(period, axpwm->base + AXIADO_PWM_PERIOD_REG);
> + writel(duty, axpwm->base + AXIADO_PWM_HIGH_REG);
> + writel(AXIADO_PWM_CTRL_ENABLE, axpwm->base + AXIADO_PWM_CTRL_REG);
> + axpwm->logically_disabled = false;
Ideally all the special case handling happens in the
.round_waveform_tohw() callback such that .write_waveform() is as quick
as possible and just writes the content of axiado_pwm_waveform to the
registers.
> +
> + return 0;
> +}
I was about to write something else, but was interrupted and don't
remember. :-\ So I might still find something when I look at your next
revision.
Best regards
Uwe
Attachment:
signature.asc
Description: PGP signature