[PATCH 4/5] pwm: cadence: Add TTC PWM driver
From: Meher Thanmaiee
Date: Wed Oct 07 2026 - 08:00:21 EST
Add a PWM driver for the Cadence Triple Timer Counter (TTC) IP found
on Xilinx Zynq, ZynqMP, and Versal SoCs.
The driver binds to auxiliary-bus devices created by the TTC timer
driver when #pwm-cells is present in the device tree, and exposes the
three TTC channels as PWM outputs. The PWM period is programmed
through the interval counter and the duty cycle through the match
counter.
Add the PWM-specific TTC register definitions, shared data structure,
and per-channel access helpers to the shared TTC header so the driver
can access the parent-provided MMIO base, clock, rate, and timer width.
Co-developed-by: Mubin Sayyed <mubin.sayyed@xxxxxxx>
Signed-off-by: Mubin Sayyed <mubin.sayyed@xxxxxxx>
Signed-off-by: Meher Thanmaiee <meherthanmaiee.vanukuri@xxxxxxx>
---
MAINTAINERS | 1 +
drivers/pwm/Kconfig | 13 +++
drivers/pwm/Makefile | 1 +
drivers/pwm/pwm-cadence.c | 231 ++++++++++++++++++++++++++++++++++++++
include/linux/timer-cadence-ttc.h | 40 +++++++
5 files changed, 286 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index b25ad0c18ffd..071290662523 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3922,6 +3922,7 @@ F: drivers/edac/synopsys_edac.c
F: drivers/i2c/busses/i2c-cadence.c
F: drivers/i2c/busses/i2c-xiic.c
F: drivers/mmc/host/sdhci-of-arasan.c
+F: drivers/pwm/pwm-cadence.c
F: include/linux/timer-cadence-ttc.h
N: zynq
N: xilinx
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index 729776086879..a267a15a9685 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -196,6 +196,19 @@ config PWM_BRCMSTB
To compile this driver as a module, choose M Here: the module
will be called pwm-brcmstb.c.
+config PWM_CADENCE
+ tristate "Cadence TTC PWM support"
+ depends on CADENCE_TTC_TIMER
+ help
+ PWM framework driver for the Cadence TTC IP found on Xilinx
+ Zynq/ZynqMP/Versal SoCs. Binds to auxiliary-bus devices
+ created by the TTC clocksource driver when a TTC node
+ contains the #pwm-cells property. Each TTC provides up to
+ 3 PWM channels.
+
+ To compile this driver as a module, choose M here: the
+ module will be called pwm-cadence.
+
config PWM_CLK
tristate "Clock based PWM support"
depends on HAVE_CLK || COMPILE_TEST
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index 5630a521a7cf..f116b9629eee 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -14,6 +14,7 @@ obj-$(CONFIG_PWM_BCM_IPROC) += pwm-bcm-iproc.o
obj-$(CONFIG_PWM_BCM_KONA) += pwm-bcm-kona.o
obj-$(CONFIG_PWM_BERLIN) += pwm-berlin.o
obj-$(CONFIG_PWM_BRCMSTB) += pwm-brcmstb.o
+obj-$(CONFIG_PWM_CADENCE) += pwm-cadence.o
obj-$(CONFIG_PWM_CLK) += pwm-clk.o
obj-$(CONFIG_PWM_CLPS711X) += pwm-clps711x.o
obj-$(CONFIG_PWM_CRC) += pwm-crc.o
diff --git a/drivers/pwm/pwm-cadence.c b/drivers/pwm/pwm-cadence.c
new file mode 100644
index 000000000000..f02f37ccb741
--- /dev/null
+++ b/drivers/pwm/pwm-cadence.c
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Cadence TTC PWM driver
+ *
+ * Configures Cadence Triple Timer Counter channels as PWM outputs.
+ * Binds to auxiliary-bus devices created by the TTC clocksource driver.
+ *
+ * Limitations:
+ * - When PWM is stopped, timer counter gets stopped immediately. This
+ * doesn't allow the current PWM period to complete and stops abruptly.
+ * - Disabled PWM emits inactive level.
+ * - When user requests a change in any parameter of PWM (period/duty cycle/polarity)
+ * while PWM is in enabled state:
+ * - PWM is stopped abruptly.
+ * - Requested parameter is changed.
+ * - Fresh PWM cycle is started.
+ *
+ * Copyright (C) 2026, Advanced Micro Devices, Inc.
+ */
+
+#include <linux/auxiliary_bus.h>
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/log2.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/pwm.h>
+#include <linux/timer-cadence-ttc.h>
+
+struct cdns_ttc_pwm {
+ const struct ttc_shared_data *sd;
+ u32 max_count;
+};
+
+static inline struct cdns_ttc_pwm *to_cdns_ttc_pwm(struct pwm_chip *chip)
+{
+ return pwmchip_get_drvdata(chip);
+}
+
+static void cdns_ttc_pwm_enable(struct cdns_ttc_pwm *priv, unsigned int hwpwm)
+{
+ u32 ctrl;
+
+ ctrl = ttc_ch_readl(priv->sd, hwpwm, TTC_CNT_CNTRL_OFFSET);
+ ctrl |= TTC_CNT_CNTRL_INT | TTC_CNT_CNTRL_MATCH | TTC_CNT_CNTRL_RST;
+ ctrl &= ~(TTC_CNT_CNTRL_DISABLE_MASK | TTC_CNT_CNTRL_WAVE_EN);
+ ttc_ch_writel(priv->sd, hwpwm, TTC_CNT_CNTRL_OFFSET, ctrl);
+}
+
+static void cdns_ttc_pwm_disable(struct cdns_ttc_pwm *priv, unsigned int hwpwm)
+{
+ u32 ctrl;
+
+ ctrl = ttc_ch_readl(priv->sd, hwpwm, TTC_CNT_CNTRL_OFFSET);
+ ctrl |= TTC_CNT_CNTRL_DISABLE_MASK;
+ ttc_ch_writel(priv->sd, hwpwm, TTC_CNT_CNTRL_OFFSET, ctrl);
+}
+
+static int cdns_ttc_pwm_apply(struct pwm_chip *chip,
+ struct pwm_device *pwm,
+ const struct pwm_state *state)
+{
+ struct cdns_ttc_pwm *priv = to_cdns_ttc_pwm(chip);
+ unsigned long rate = priv->sd->rate;
+ unsigned int hwpwm = pwm->hwpwm;
+ u64 period_cycles, duty_cycles;
+ bool use_prescaler = false;
+ u32 ctrl, psv;
+
+ if (!state->enabled) {
+ cdns_ttc_pwm_disable(priv, hwpwm);
+ return 0;
+ }
+
+ period_cycles = mul_u64_u64_div_u64(state->period, rate, NSEC_PER_SEC);
+
+ if (period_cycles > priv->max_count) {
+ unsigned int shift;
+
+ /*
+ * Prescaler divides input clock by 2^(psv+1).
+ * Calculate required shift and derive PSV register value.
+ */
+ shift = order_base_2(DIV_ROUND_UP_ULL(period_cycles,
+ priv->max_count));
+ if (shift > TTC_CLK_CNTRL_PSV_MAX + 1)
+ return -ERANGE;
+
+ use_prescaler = true;
+ psv = shift - 1;
+
+ period_cycles = mul_u64_u64_div_u64(state->period, rate,
+ (u64)NSEC_PER_SEC << shift);
+ }
+
+ if (!period_cycles)
+ return -EINVAL;
+
+ cdns_ttc_pwm_disable(priv, hwpwm);
+
+ ctrl = ttc_ch_readl(priv->sd, hwpwm, TTC_CNT_CNTRL_OFFSET);
+ if (state->polarity == PWM_POLARITY_NORMAL)
+ ctrl |= TTC_CNT_CNTRL_WAVE_POL;
+ else
+ ctrl &= ~TTC_CNT_CNTRL_WAVE_POL;
+ ttc_ch_writel(priv->sd, hwpwm, TTC_CNT_CNTRL_OFFSET, ctrl);
+
+ if (use_prescaler) {
+ duty_cycles = mul_u64_u64_div_u64(state->duty_cycle, rate,
+ (u64)NSEC_PER_SEC << (psv + 1));
+ } else {
+ duty_cycles = mul_u64_u64_div_u64(state->duty_cycle, rate,
+ NSEC_PER_SEC);
+ }
+
+ ttc_ch_writel(priv->sd, hwpwm, TTC_INTR_VAL_OFFSET, (u32)period_cycles);
+ ttc_ch_writel(priv->sd, hwpwm, TTC_MATCH_CNT_VAL_OFFSET, (u32)duty_cycles);
+
+ ctrl = ttc_ch_readl(priv->sd, hwpwm, TTC_CLK_CNTRL_OFFSET);
+ if (use_prescaler) {
+ ctrl &= ~TTC_CLK_CNTRL_PSV_MASK;
+ ctrl |= FIELD_PREP(TTC_CLK_CNTRL_PSV_MASK, psv);
+ ctrl |= TTC_CLK_CNTRL_PS_EN;
+ } else {
+ ctrl &= ~TTC_CLK_CNTRL_PS_EN;
+ }
+ ttc_ch_writel(priv->sd, hwpwm, TTC_CLK_CNTRL_OFFSET, ctrl);
+
+ cdns_ttc_pwm_enable(priv, hwpwm);
+
+ return 0;
+}
+
+static int cdns_ttc_pwm_get_state(struct pwm_chip *chip,
+ struct pwm_device *pwm,
+ struct pwm_state *state)
+{
+ struct cdns_ttc_pwm *priv = to_cdns_ttc_pwm(chip);
+ unsigned long rate = priv->sd->rate;
+ unsigned int hwpwm = pwm->hwpwm;
+ u32 ctrl, clk_ctrl, pres;
+ u64 tmp;
+
+ ctrl = ttc_ch_readl(priv->sd, hwpwm, TTC_CNT_CNTRL_OFFSET);
+
+ state->polarity = (ctrl & TTC_CNT_CNTRL_WAVE_POL) ?
+ PWM_POLARITY_NORMAL : PWM_POLARITY_INVERSED;
+
+ if (ctrl & TTC_CNT_CNTRL_DISABLE_MASK) {
+ state->enabled = false;
+ state->period = 0;
+ state->duty_cycle = 0;
+ return 0;
+ }
+
+ state->enabled = true;
+
+ clk_ctrl = ttc_ch_readl(priv->sd, hwpwm, TTC_CLK_CNTRL_OFFSET);
+ pres = 0;
+ if (clk_ctrl & TTC_CLK_CNTRL_PS_EN)
+ pres = FIELD_GET(TTC_CLK_CNTRL_PSV_MASK, clk_ctrl) + 1;
+
+ tmp = (u64)ttc_ch_readl(priv->sd, hwpwm, TTC_INTR_VAL_OFFSET) << pres;
+ state->period = mul_u64_u64_div_u64(tmp, NSEC_PER_SEC, rate);
+ if (mul_u64_u64_div_u64(state->period, rate, NSEC_PER_SEC) < tmp)
+ state->period++;
+
+ tmp = (u64)ttc_ch_readl(priv->sd, hwpwm, TTC_MATCH_CNT_VAL_OFFSET) << pres;
+ state->duty_cycle = mul_u64_u64_div_u64(tmp, NSEC_PER_SEC, rate);
+ if (mul_u64_u64_div_u64(state->duty_cycle, rate, NSEC_PER_SEC) < tmp)
+ state->duty_cycle++;
+
+ return 0;
+}
+
+static const struct pwm_ops cdns_ttc_pwm_ops = {
+ .apply = cdns_ttc_pwm_apply,
+ .get_state = cdns_ttc_pwm_get_state,
+};
+
+static int cdns_ttc_pwm_probe(struct auxiliary_device *auxdev,
+ const struct auxiliary_device_id *id)
+{
+ const struct ttc_shared_data *sd = dev_get_platdata(&auxdev->dev);
+ struct device *dev = &auxdev->dev;
+ struct cdns_ttc_pwm *priv;
+ struct pwm_chip *chip;
+ int ret;
+
+ if (!sd)
+ return -EINVAL;
+
+ chip = devm_pwmchip_alloc(dev, TTC_NUM_CHANNELS, sizeof(*priv));
+ if (IS_ERR(chip))
+ return PTR_ERR(chip);
+
+ priv = to_cdns_ttc_pwm(chip);
+ priv->sd = sd;
+ priv->max_count = BIT_ULL(sd->timer_width) - 1;
+
+ ret = devm_clk_rate_exclusive_get(dev, sd->clk);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to lock clock rate\n");
+
+ chip->ops = &cdns_ttc_pwm_ops;
+
+ ret = devm_pwmchip_add(dev, chip);
+ if (ret)
+ return dev_err_probe(dev, ret, "could not register PWM chip\n");
+
+ return 0;
+}
+
+static const struct auxiliary_device_id cdns_ttc_pwm_id_table[] = {
+ { .name = "timer_cadence_ttc.pwm" },
+ {},
+};
+MODULE_DEVICE_TABLE(auxiliary, cdns_ttc_pwm_id_table);
+
+static struct auxiliary_driver cdns_ttc_pwm_driver = {
+ .name = "cdns_ttc_pwm",
+ .probe = cdns_ttc_pwm_probe,
+ .id_table = cdns_ttc_pwm_id_table,
+};
+module_auxiliary_driver(cdns_ttc_pwm_driver);
+
+MODULE_AUTHOR("Mubin Sayyed <mubin.sayyed@xxxxxxx>");
+MODULE_AUTHOR("Meher Thanmaiee <meherthanmaiee.vanukuri@xxxxxxx>");
+MODULE_DESCRIPTION("Cadence TTC PWM driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/timer-cadence-ttc.h b/include/linux/timer-cadence-ttc.h
index 37ac775d6d50..05b7edd88626 100644
--- a/include/linux/timer-cadence-ttc.h
+++ b/include/linux/timer-cadence-ttc.h
@@ -10,24 +10,64 @@
#define _LINUX_TIMER_CADENCE_TTC_H
#include <linux/bits.h>
+#include <linux/io.h>
+#include <linux/types.h>
+
+struct clk;
#define TTC_DEFAULT_TIMER_WIDTH 16
/* Per-channel register offset stride */
#define TTC_CH_OFFSET 0x04
+#define TTC_NUM_CHANNELS 3
+
/* Timer Register Offset Definitions of Timer 1 */
#define TTC_CLK_CNTRL_OFFSET 0x00 /* Clock Control Reg, RW */
#define TTC_CNT_CNTRL_OFFSET 0x0C /* Counter Control Reg, RW */
#define TTC_COUNT_VAL_OFFSET 0x18 /* Counter Value Reg, RO */
#define TTC_INTR_VAL_OFFSET 0x24 /* Interval Count Reg, RW */
+#define TTC_MATCH_CNT_VAL_OFFSET 0x30 /* Match 1 Count Reg, RW */
#define TTC_ISR_OFFSET 0x54 /* Interrupt Status Reg, RO */
#define TTC_IER_OFFSET 0x60 /* Interrupt Enable Reg, RW */
#define TTC_CNT_CNTRL_DISABLE_MASK BIT(0)
+#define TTC_CNT_CNTRL_INT BIT(1)
+#define TTC_CNT_CNTRL_MATCH BIT(3)
+#define TTC_CNT_CNTRL_RST BIT(4)
+#define TTC_CNT_CNTRL_WAVE_EN BIT(5)
+#define TTC_CNT_CNTRL_WAVE_POL BIT(6)
+#define TTC_CLK_CNTRL_PS_EN BIT(0)
#define TTC_CLK_CNTRL_CSRC_MASK BIT(5) /* clock source */
#define TTC_CLK_CNTRL_PSV_MASK GENMASK(4, 1)
#define TTC_CLK_CNTRL_PSV_SHIFT 1
+#define TTC_CLK_CNTRL_PSV_MAX 15 /* 4-bit prescaler field */
+
+/**
+ * struct ttc_shared_data - Data passed from parent to auxiliary device
+ * @base: MMIO base address
+ * @clk: Input clock
+ * @rate: Input clock rate in Hz
+ * @timer_width: Counter width in bits (16 or 32)
+ */
+struct ttc_shared_data {
+ void __iomem *base;
+ struct clk *clk;
+ unsigned long rate;
+ u32 timer_width;
+};
+
+static inline u32 ttc_ch_readl(const struct ttc_shared_data *sd,
+ unsigned int ch, u32 reg)
+{
+ return readl_relaxed(sd->base + reg + TTC_CH_OFFSET * ch);
+}
+
+static inline void ttc_ch_writel(const struct ttc_shared_data *sd,
+ unsigned int ch, u32 reg, u32 val)
+{
+ writel_relaxed(val, sd->base + reg + TTC_CH_OFFSET * ch);
+}
#endif /* _LINUX_TIMER_CADENCE_TTC_H */
--
2.34.1