[PATCH v2 2/2] pwm: add Axiado AX3000 PWM driver
Petar Stepanovic <[email protected]> Thu, 23 Jul 2026 02:32:19 -0700
| Newsgroups | org.kernel.vger.linux-pwm,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
The Axiado AX3000 and AX3005 SoCs include PWM controllers that can be used to generate configurable PWM output signals. Add a PWM driver with support for configuring period, duty cycle, and enable state through the Linux PWM framework. Signed-off-by: Petar Stepanovic <[email protected]> --- MAINTAINERS | 1 + drivers/pwm/Kconfig | 11 ++ drivers/pwm/Makefile | 1 + drivers/pwm/pwm-axiado.c | 272 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 9999480376cc..14eea3bc4d4d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4319,6 +4319,7 @@ M: Prasad Bolisetty <[email protected]> L: [email protected] S: Supported F: Documentation/devicetree/bindings/pwm/axiado,ax3000-pwm.yaml +F: drivers/pwm/pwm-axiado.c AXIS ARTPEC ARM64 SoC SUPPORT M: Jesper Nilsson <[email protected]> diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 6f3147518376..76f6c04b0e23 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -129,6 +129,17 @@ config PWM_ATMEL_TCB To compile this driver as a module, choose M here: the module will be called pwm-atmel-tcb. +config PWM_AXIADO + tristate "Axiado PWM support" + depends on ARCH_AXIADO || COMPILE_TEST + depends on HAS_IOMEM + help + PWM framework driver for the PWM controller found on Axiado + AX3000 and AX3005 SoCs. + + To compile this driver as a module, choose M here: the module + will be called pwm-axiado. + config PWM_AXI_PWMGEN tristate "Analog Devices AXI PWM generator" depends on MICROBLAZE || NIOS2 || ARCH_ZYNQ || ARCH_ZYNQMP || ARCH_INTEL_SOCFPGA || COMPILE_TEST diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index 0dc0d2b69025..4466a29e780a 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -8,6 +8,7 @@ obj-$(CONFIG_PWM_ARGON_FAN_HAT) += pwm-argon-fan-hat.o obj-$(CONFIG_PWM_ATMEL) += pwm-atmel.o obj-$(CONFIG_PWM_ATMEL_HLCDC_PWM) += pwm-atmel-hlcdc.o obj-$(CONFIG_PWM_ATMEL_TCB) += pwm-atmel-tcb.o +obj-$(CONFIG_PWM_AXIADO) += pwm-axiado.o obj-$(CONFIG_PWM_AXI_PWMGEN) += pwm-axi-pwmgen.o obj-$(CONFIG_PWM_BCM2835) += pwm-bcm2835.o obj-$(CONFIG_PWM_BCM_IPROC) += pwm-bcm-iproc.o diff --git a/drivers/pwm/pwm-axiado.c b/drivers/pwm/pwm-axiado.c new file mode 100644 index 000000000000..f9932646ae04 --- /dev/null +++ b/drivers/pwm/pwm-axiado.c @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2021-2026 Axiado Corporation. + */ + +/* + * Limitations: + * - Only normal polarity is supported. + * - Configuration changes take effect immediately; the current period is + * not guaranteed to complete. + * - Disable operations take effect immediately; the current period is not + * guaranteed to complete. + * - When disabled, the output remains high. + * - The supported period range is 2 through 0xfffffffe PWM input clock + * cycles. Longer periods are rounded down to the maximum. + * - A 0% duty cycle is not supported. Programming a zero high time produces + * a constant high output, so the driver rejects 0% duty-cycle requests. + * - A 100% duty cycle is supported and produces a constant high output. + */ + +#include <linux/bits.h> +#include <linux/clk.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_DUTY_MIN 1 + +/* Control register bits */ +#define AXIADO_PWM_CTRL_ENABLE BIT(0) + +struct axiado_pwm_chip { + void __iomem *base; + unsigned long rate; +}; + +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) {}; + + return 0; + } + + /* The hardware cannot generate a 0% duty cycle. */ + if (!wf->duty_length_ns) + return -EINVAL; + + /* Only an edge-aligned waveform starting at offset zero is supported. */ + if (wf->duty_offset_ns) + return -EINVAL; + + if (wf->duty_length_ns > wf->period_length_ns) + return -EINVAL; + + 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; + } + + 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; + + /* + * A positive duty request can round down to zero clock cycles. + * Round it up to the minimum supported high time. + */ + if (duty < AXIADO_PWM_DUTY_MIN) { + duty = AXIADO_PWM_DUTY_MIN; + ret = 1; + } + + /* + * 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, + }; + + 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 ctrl; + + ctrl = readl(axpwm->base + AXIADO_PWM_CTRL_REG); + + *wfhw = (struct axiado_pwm_waveform) { + .period = readl(axpwm->base + AXIADO_PWM_PERIOD_REG), + .duty = readl(axpwm->base + AXIADO_PWM_HIGH_REG), + .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; + + if (!wfhw->enabled) { + writel(0, axpwm->base + AXIADO_PWM_CTRL_REG); + return 0; + } + + /* + * The hardware has no shadow registers. These writes may alter the + * active waveform before the current period has completed. + */ + writel(wfhw->period, axpwm->base + AXIADO_PWM_PERIOD_REG); + writel(wfhw->duty, axpwm->base + AXIADO_PWM_HIGH_REG); + writel(AXIADO_PWM_CTRL_ENABLE, axpwm->base + AXIADO_PWM_CTRL_REG); + + return 0; +} + +static const struct pwm_ops axiado_pwm_ops = { + .sizeof_wfhw = sizeof(struct axiado_pwm_waveform), + .round_waveform_tohw = axiado_pwm_round_waveform_tohw, + .round_waveform_fromhw = axiado_pwm_round_waveform_fromhw, + .read_waveform = axiado_pwm_read_waveform, + .write_waveform = axiado_pwm_write_waveform, +}; + +static int axiado_pwm_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct axiado_pwm_chip *axpwm; + struct pwm_chip *chip; + struct clk *clk; + int ret; + + chip = devm_pwmchip_alloc(dev, 1, sizeof(*axpwm)); + if (IS_ERR(chip)) + return PTR_ERR(chip); + + axpwm = pwmchip_get_drvdata(chip); + + axpwm->base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(axpwm->base)) + return dev_err_probe(dev, PTR_ERR(axpwm->base), + "Failed to map registers\n"); + + clk = devm_clk_get_enabled(dev, NULL); + if (IS_ERR(clk)) + return dev_err_probe(dev, PTR_ERR(clk), + "Failed to get/enable clock\n"); + + ret = devm_clk_rate_exclusive_get(dev, clk); + if (ret) + return dev_err_probe(dev, ret, + "Failed to lock clock rate\n"); + + axpwm->rate = clk_get_rate(clk); + if (!axpwm->rate) + return dev_err_probe(dev, -EINVAL, + "Failed to get clock rate\n"); + + chip->ops = &axiado_pwm_ops; + chip->atomic = true; + + ret = devm_pwmchip_add(dev, chip); + if (ret) + return dev_err_probe(dev, ret, "Failed to add PWM chip\n"); + + return 0; +} + +static const struct of_device_id axiado_pwm_match[] = { + { .compatible = "axiado,ax3000-pwm" }, + { .compatible = "axiado,ax3005-pwm" }, + { } +}; +MODULE_DEVICE_TABLE(of, axiado_pwm_match); + +static struct platform_driver axiado_pwm_driver = { + .driver = { + .name = "axiado-pwm", + .of_match_table = axiado_pwm_match, + }, + .probe = axiado_pwm_probe, +}; + +module_platform_driver(axiado_pwm_driver); +MODULE_AUTHOR("Axiado Corporation"); +MODULE_DESCRIPTION("Axiado PWM driver"); +MODULE_LICENSE("GPL"); -- 2.34.1