diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index 729776086879..d87d166e7836 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -748,6 +748,17 @@ config PWM_SUN4I
 	  To compile this driver as a module, choose M here: the module
 	  will be called pwm-sun4i.
 
+config PWM_SUN8I
+	tristate "Allwinner sun8i/sun50i PWM support"
+	depends on ARCH_SUNXI || COMPILE_TEST
+	depends on HAS_IOMEM && COMMON_CLK
+	help
+	  PWM framework driver for Allwinner controllers whose channels share
+	  paired source clocks.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called pwm-sun8i.
+
 config PWM_SUNPLUS
 	tristate "Sunplus PWM support"
 	depends on ARCH_SUNPLUS || COMPILE_TEST
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index 5630a521a7cf..9c922b1fd0b4 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -68,6 +68,7 @@ obj-$(CONFIG_PWM_STM32)		+= pwm-stm32.o
 obj-$(CONFIG_PWM_STM32_LP)	+= pwm-stm32-lp.o
 obj-$(CONFIG_PWM_STMPE)		+= pwm-stmpe.o
 obj-$(CONFIG_PWM_SUN4I)		+= pwm-sun4i.o
+obj-$(CONFIG_PWM_SUN8I)		+= pwm-sun8i.o
 obj-$(CONFIG_PWM_SUNPLUS)	+= pwm-sunplus.o
 obj-$(CONFIG_PWM_TEGRA)		+= pwm-tegra.o
 obj-$(CONFIG_PWM_TH1520)	+= pwm_th1520.o
diff --git a/drivers/pwm/pwm-sun8i.c b/drivers/pwm/pwm-sun8i.c
new file mode 100644
index 000000000000..7e6c39815c18
--- /dev/null
+++ b/drivers/pwm/pwm-sun8i.c
@@ -0,0 +1,1080 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Driver for Allwinner sun8i Pulse Width Modulation Controller
+ *
+ * (C) Copyright 2025 Richard Genoud, Bootlin <richard.genoud@bootlin.com>
+ * (C) Copyright 2026 James Hilliard <james.hilliard1@gmail.com>
+ *
+ * Based on drivers/pwm/pwm-sun4i.c with Copyright:
+ *
+ * Copyright (C) 2014 Alexandre Belloni <alexandre.belloni@bootlin.com>
+ *
+ * Channels are paired (0/1, 2/3, 4/5). Each pair shares a clock source and
+ * first prescaler (div_m), while each channel has its own second prescaler
+ * (div_k) and bypass path.
+ *
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/limits.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/notifier.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+#include <linux/reset.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/time.h>
+
+#define SUN8I_PWM_NPWM			6
+#define SUN8I_PWM_NPAIRS			(SUN8I_PWM_NPWM / 2)
+
+/* PWMCC Pairs Clock Configuration Registers */
+#define SUN8I_PWM_PCCR(pair)		(0x20 + ((pair) * 0x4))
+#define SUN8I_PWM_PCCR_SRC_SHIFT	7
+#define SUN8I_PWM_PCCR_SRC_MASK		GENMASK(1, 0)
+#define SUN8I_PWM_PCCR_GATE_BIT		4
+#define SUN8I_PWM_PCCR_GATE		BIT(SUN8I_PWM_PCCR_GATE_BIT)
+#define SUN8I_PWM_PCCR_BYPASS_BIT(chan)	((chan) % 2 + 5)
+#define SUN8I_PWM_PCCR_DIV_M_SHIFT	0
+#define SUN8I_PWM_PCCR_DIV_M_WIDTH	4
+
+/* PWM Enable Register */
+#define SUN8I_PWM_PER			0x40
+#define SUN8I_PWM_ENABLE(chan)		BIT(chan)
+
+/* PWM Control Register */
+#define SUN8I_PWM_PCR(chan)		(0x60 + (chan) * 0x20)
+#define SUN8I_PWM_PCR_PRESCAL_K_MASK	GENMASK(7, 0)
+#define SUN8I_PWM_PCR_ACTIVE_STATE	BIT(8)
+#define SUN8I_PWM_PCR_MODE		BIT(9)
+#define SUN8I_PWM_PCR_PULSE_START	BIT(10)
+#define SUN8I_PWM_PCR_PERIOD_READY	BIT(11) /* 0: ready, 1: busy */
+#define SUN8I_PWM_PCR_WAVEFORM_MASK	(SUN8I_PWM_PCR_PRESCAL_K_MASK | \
+					 SUN8I_PWM_PCR_ACTIVE_STATE)
+
+/* PWM Period Register */
+#define SUN8I_PWM_PPR(chan)		(0x64 + (chan) * 0x20)
+#define SUN8I_PWM_PPR_PERIOD_MASK	GENMASK(31, 16)
+#define SUN8I_PWM_PPR_DUTY_MASK		GENMASK(15, 0)
+#define SUN8I_PWM_PPR_PERIOD_VALUE(reg)	(FIELD_GET(SUN8I_PWM_PPR_PERIOD_MASK, reg) + 1)
+#define SUN8I_PWM_PPR_DUTY_VALUE(reg)	FIELD_GET(SUN8I_PWM_PPR_DUTY_MASK, reg)
+#define SUN8I_PWM_PPR_PERIOD(prd)	FIELD_PREP(SUN8I_PWM_PPR_PERIOD_MASK, (prd) - 1)
+#define SUN8I_PWM_PPR_DUTY(dty)		FIELD_PREP(SUN8I_PWM_PPR_DUTY_MASK, dty)
+#define SUN8I_PWM_PPR_PERIOD_MAX	(FIELD_MAX(SUN8I_PWM_PPR_PERIOD_MASK) + 1)
+
+/* PWM pair dead-zone control registers. */
+#define SUN8I_PWM_PDZCR(pair)		(0x30 + ((pair) * 0x4))
+#define SUN8I_PWM_PDZCR_ENABLE		BIT(0)
+
+#define SUN8I_PWM_PERIOD_READY_MARGIN_US	50
+#define SUN8I_PWM_PERIOD_READY_MIN_POLL_US	10
+#define SUN8I_PWM_PERIOD_READY_MAX_POLL_US	10000
+#define SUN8I_PWM_PERIOD_READY_POLL_COUNT	1024
+
+#define SUN8I_PWM_PAIR_IDX(chan)	((chan) >> 1)
+
+/*
+ * Block diagram of the PWM clock controller:
+ *
+ *             _____      ______      ________
+ * OSC24M --->|     |    |      |    |        |
+ * APB1 ----->| Mux |--->| Gate |--->| /div_m |-----> SUN8I_PWM_clock_src_xy
+ *            |_____|    |______|    |________|
+ *                               ________
+ *                              |        |
+ *                           +->| /div_k |---> SUN8I_PWM_clock_x
+ *                           |  |________|
+ *                           |    ______
+ *                           |   |      |
+ *                           +-->| Gate |----> SUN8I_PWM_bypass_clock_x
+ *                           |   |______|
+ * SUN8I_PWM_clock_src_xy ---+   ________
+ *                           |  |        |
+ *                           +->| /div_k |---> SUN8I_PWM_clock_y
+ *                           |  |________|
+ *                           |    ______
+ *                           |   |      |
+ *                           +-->| Gate |----> SUN8I_PWM_bypass_clock_y
+ *                               |______|
+ *
+ * NB: when the bypass is set, all the PWM logic is bypassed.
+ * So, the duty cycle and polarity can't be modified (we just have a clock).
+ * The bypass in PWM mode is used to achieve a 1/2 relative duty cycle with the
+ * fastest clock.
+ *
+ */
+
+/* /div_m is a power-of-two divider limited to /256. */
+static const struct clk_div_table sun8i_pwm_div_m_table[] = {
+	{ .val = 0, .div = 1 },
+	{ .val = 1, .div = 2 },
+	{ .val = 2, .div = 4 },
+	{ .val = 3, .div = 8 },
+	{ .val = 4, .div = 16 },
+	{ .val = 5, .div = 32 },
+	{ .val = 6, .div = 64 },
+	{ .val = 7, .div = 128 },
+	{ .val = 8, .div = 256 },
+	{ /* sentinel */ }
+};
+
+struct sun8i_pwm_chip;
+
+struct sun8i_pwm_pair {
+	struct clk_mux mux;
+	struct clk_hw gate_hw;
+	struct clk_divider divider;
+	struct clk_hw *hw;
+	struct notifier_block rate_nb;
+	struct sun8i_pwm_chip *chip;
+	unsigned int index;
+};
+
+struct sun8i_pwm_channel {
+	/* Separate CCF consumer, held only from PWM request to free. */
+	struct clk *pair_clk;
+	u64 pending_period_ns;
+	bool rate_exclusive;
+};
+
+struct sun8i_pwm_chip {
+	struct sun8i_pwm_pair pairs[SUN8I_PWM_NPAIRS];
+	struct sun8i_pwm_channel channels[SUN8I_PWM_NPWM];
+	struct clk *bus_clk;
+	void __iomem *base;
+	/* Protects shared registers and rate_exclusive. */
+	spinlock_t lock;
+};
+
+struct sun8i_pwm_waveform {
+	u32 pcr;
+	u32 ppr;
+	u32 pair_rate;
+	bool enabled;
+	bool bypass_en;
+};
+
+static inline struct sun8i_pwm_chip *sun8i_pwm_from_chip(const struct pwm_chip *chip)
+{
+	return pwmchip_get_drvdata(chip);
+}
+
+static unsigned long sun8i_pwm_get_pair_rate(struct sun8i_pwm_chip *sun8i_chip,
+					     unsigned int idx)
+{
+	struct sun8i_pwm_pair *pair = &sun8i_chip->pairs[SUN8I_PWM_PAIR_IDX(idx)];
+
+	/* Readback also runs on channels without a PWM request. */
+	return clk_get_rate(pair->hw->clk);
+}
+
+static inline u32 sun8i_pwm_readl(struct sun8i_pwm_chip *sun8i_chip,
+				  unsigned long offset)
+{
+	return readl(sun8i_chip->base + offset);
+}
+
+static inline void sun8i_pwm_writel(struct sun8i_pwm_chip *sun8i_chip,
+				    u32 val, unsigned long offset)
+{
+	writel(val, sun8i_chip->base + offset);
+}
+
+static void sun8i_pwm_set_bypass_locked(struct sun8i_pwm_chip *sun8i_chip,
+					unsigned int idx, bool enable)
+{
+	unsigned long reg_offset;
+	u32 val;
+
+	lockdep_assert_held(&sun8i_chip->lock);
+
+	reg_offset = SUN8I_PWM_PCCR(SUN8I_PWM_PAIR_IDX(idx));
+	val = sun8i_pwm_readl(sun8i_chip, reg_offset);
+	if (enable)
+		val |= BIT(SUN8I_PWM_PCCR_BYPASS_BIT(idx));
+	else
+		val &= ~BIT(SUN8I_PWM_PCCR_BYPASS_BIT(idx));
+
+	sun8i_pwm_writel(sun8i_chip, val, reg_offset);
+}
+
+static void sun8i_pwm_set_enabled_locked(struct sun8i_pwm_chip *sun8i_chip,
+					 unsigned int idx, bool enable)
+{
+	u32 val;
+
+	lockdep_assert_held(&sun8i_chip->lock);
+
+	val = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PER);
+	if (enable)
+		val |= SUN8I_PWM_ENABLE(idx);
+	else
+		val &= ~SUN8I_PWM_ENABLE(idx);
+	sun8i_pwm_writel(sun8i_chip, val, SUN8I_PWM_PER);
+}
+
+static bool
+sun8i_pwm_channel_is_enabled_locked(struct sun8i_pwm_chip *sun8i_chip,
+				    unsigned int idx)
+{
+	unsigned int pair = SUN8I_PWM_PAIR_IDX(idx);
+	u32 pccr, per;
+
+	lockdep_assert_held(&sun8i_chip->lock);
+
+	pccr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PCCR(pair));
+	per = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PER);
+
+	return (pccr & SUN8I_PWM_PCCR_GATE) &&
+	       (per & SUN8I_PWM_ENABLE(idx));
+}
+
+static u32 sun8i_pwm_pair_enable_mask(unsigned int pair)
+{
+	return GENMASK(pair * 2 + 1, pair * 2);
+}
+
+static inline struct sun8i_pwm_pair *
+sun8i_pwm_pair_from_gate_hw(struct clk_hw *hw)
+{
+	return container_of(hw, struct sun8i_pwm_pair, gate_hw);
+}
+
+static int sun8i_pwm_pair_gate_enable(struct clk_hw *hw)
+{
+	struct sun8i_pwm_pair *pair = sun8i_pwm_pair_from_gate_hw(hw);
+	struct sun8i_pwm_chip *sun8i_chip = pair->chip;
+	u32 pccr;
+
+	guard(spinlock_irqsave)(&sun8i_chip->lock);
+	pccr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PCCR(pair->index));
+	pccr |= SUN8I_PWM_PCCR_GATE;
+	sun8i_pwm_writel(sun8i_chip, pccr, SUN8I_PWM_PCCR(pair->index));
+
+	return 0;
+}
+
+static void sun8i_pwm_pair_gate_disable(struct clk_hw *hw)
+{
+	struct sun8i_pwm_pair *pair = sun8i_pwm_pair_from_gate_hw(hw);
+	struct sun8i_pwm_chip *sun8i_chip = pair->chip;
+	unsigned long reg = SUN8I_PWM_PCCR(pair->index);
+	u32 pccr, per;
+
+	/* CCF counts do not include outputs awaiting firmware-state handoff. */
+	guard(spinlock_irqsave)(&sun8i_chip->lock);
+	per = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PER);
+	if (!(per & sun8i_pwm_pair_enable_mask(pair->index))) {
+		pccr = sun8i_pwm_readl(sun8i_chip, reg);
+		pccr &= ~SUN8I_PWM_PCCR_GATE;
+		sun8i_pwm_writel(sun8i_chip, pccr, reg);
+	}
+}
+
+static int sun8i_pwm_pair_gate_is_enabled(struct clk_hw *hw)
+{
+	struct sun8i_pwm_pair *pair = sun8i_pwm_pair_from_gate_hw(hw);
+	struct sun8i_pwm_chip *sun8i_chip = pair->chip;
+	unsigned long reg = SUN8I_PWM_PCCR(pair->index);
+
+	guard(spinlock_irqsave)(&sun8i_chip->lock);
+
+	return !!(sun8i_pwm_readl(sun8i_chip, reg) & SUN8I_PWM_PCCR_GATE);
+}
+
+static const struct clk_ops sun8i_pwm_pair_gate_ops = {
+	.enable = sun8i_pwm_pair_gate_enable,
+	.disable = sun8i_pwm_pair_gate_disable,
+	.is_enabled = sun8i_pwm_pair_gate_is_enabled,
+};
+
+static int sun8i_pwm_pair_rate_notifier(struct notifier_block *nb,
+					unsigned long event, void *data)
+{
+	struct sun8i_pwm_pair *pair =
+		container_of(nb, struct sun8i_pwm_pair, rate_nb);
+	struct sun8i_pwm_chip *sun8i_chip = pair->chip;
+	bool active;
+
+	if (event != PRE_RATE_CHANGE)
+		return NOTIFY_DONE;
+
+	guard(spinlock_irqsave)(&sun8i_chip->lock);
+	active = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PER) &
+		 sun8i_pwm_pair_enable_mask(pair->index);
+
+	return active ? NOTIFY_BAD : NOTIFY_OK;
+}
+
+static void sun8i_pwm_put_rate(struct sun8i_pwm_chip *sun8i_chip,
+			       unsigned int idx)
+{
+	struct sun8i_pwm_channel *chan = &sun8i_chip->channels[idx];
+	bool exclusive;
+
+	scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
+		exclusive = chan->rate_exclusive;
+		chan->rate_exclusive = false;
+	}
+	/* CCF takes the prepare mutex and may call back into this driver. */
+	if (exclusive)
+		clk_rate_exclusive_put(chan->pair_clk);
+}
+
+static int sun8i_pwm_request(struct pwm_chip *chip, struct pwm_device *pwm)
+{
+	struct sun8i_pwm_chip *sun8i_chip = sun8i_pwm_from_chip(chip);
+	struct sun8i_pwm_channel *chan = &sun8i_chip->channels[pwm->hwpwm];
+	unsigned int idx = pwm->hwpwm;
+	struct clk_hw *parent = sun8i_chip->pairs[SUN8I_PWM_PAIR_IDX(idx)].hw;
+	bool was_enabled;
+	int ret;
+
+	scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
+		was_enabled =
+			sun8i_pwm_channel_is_enabled_locked(sun8i_chip, idx);
+	}
+
+	chan->pair_clk = clk_hw_get_clk(parent, NULL);
+	if (IS_ERR(chan->pair_clk)) {
+		ret = PTR_ERR(chan->pair_clk);
+		goto err_clear_clock;
+	}
+
+	if (was_enabled) {
+		ret = clk_rate_exclusive_get(chan->pair_clk);
+		if (ret)
+			goto err_put_clock;
+		scoped_guard(spinlock_irqsave, &sun8i_chip->lock)
+			chan->rate_exclusive = true;
+	}
+
+	ret = clk_prepare_enable(chan->pair_clk);
+	if (ret) {
+		sun8i_pwm_put_rate(sun8i_chip, idx);
+		goto err_put_clock;
+	}
+
+	return 0;
+
+err_put_clock:
+	clk_put(chan->pair_clk);
+err_clear_clock:
+	chan->pair_clk = NULL;
+
+	return ret;
+}
+
+static void sun8i_pwm_free(struct pwm_chip *chip, struct pwm_device *pwm)
+{
+	struct sun8i_pwm_chip *sun8i_chip = sun8i_pwm_from_chip(chip);
+	struct sun8i_pwm_channel *chan = &sun8i_chip->channels[pwm->hwpwm];
+
+	scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
+		sun8i_pwm_set_enabled_locked(sun8i_chip, pwm->hwpwm, false);
+		sun8i_pwm_set_bypass_locked(sun8i_chip, pwm->hwpwm, false);
+	}
+
+	sun8i_pwm_put_rate(sun8i_chip, pwm->hwpwm);
+	clk_disable_unprepare(chan->pair_clk);
+	clk_put(chan->pair_clk);
+	chan->pair_clk = NULL;
+}
+
+static int sun8i_pwm_read_waveform(struct pwm_chip *chip,
+				   struct pwm_device *pwm,
+				   void *_wfhw)
+{
+	struct sun8i_pwm_waveform *wfhw = _wfhw;
+	struct sun8i_pwm_chip *sun8i_chip = sun8i_pwm_from_chip(chip);
+	unsigned int pair = SUN8I_PWM_PAIR_IDX(pwm->hwpwm);
+	unsigned long pair_rate;
+	u32 pccr, pcr, pdzcr, per, ppr;
+
+	pair_rate = sun8i_pwm_get_pair_rate(sun8i_chip, pwm->hwpwm);
+	if (pair_rate > U32_MAX)
+		return -ERANGE;
+
+	scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
+		pccr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PCCR(pair));
+		per = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PER);
+		pcr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PCR(pwm->hwpwm));
+		ppr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PPR(pwm->hwpwm));
+		pdzcr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PDZCR(pair));
+	}
+
+	*wfhw = (struct sun8i_pwm_waveform) {
+		.enabled = !!(pccr & SUN8I_PWM_PCCR_GATE) &&
+			   !!(per & SUN8I_PWM_ENABLE(pwm->hwpwm)),
+		.bypass_en = !!(pccr &
+				 BIT(SUN8I_PWM_PCCR_BYPASS_BIT(pwm->hwpwm))),
+		.pcr = pcr & SUN8I_PWM_PCR_WAVEFORM_MASK,
+		.ppr = ppr,
+		.pair_rate = pair_rate,
+	};
+
+	/* The waveform API cannot describe coupled dead-zone or pulse mode. */
+	if (wfhw->enabled && !wfhw->bypass_en &&
+	    ((pdzcr & SUN8I_PWM_PDZCR_ENABLE) || (pcr & SUN8I_PWM_PCR_MODE)))
+		return -EOPNOTSUPP;
+
+	return 0;
+}
+
+static u64 sun8i_pwm_ticks_to_ns(u32 ticks, u16 div_k, u32 pair_rate)
+{
+	return DIV_ROUND_UP_ULL(NSEC_PER_SEC * (u64)ticks * div_k,
+				pair_rate);
+}
+
+static u64 sun8i_pwm_period_ns(const struct sun8i_pwm_waveform *wfhw)
+{
+	if (wfhw->bypass_en)
+		return DIV_ROUND_UP_ULL(NSEC_PER_SEC, wfhw->pair_rate);
+
+	return sun8i_pwm_ticks_to_ns(SUN8I_PWM_PPR_PERIOD_VALUE(wfhw->ppr),
+		FIELD_GET(SUN8I_PWM_PCR_PRESCAL_K_MASK, wfhw->pcr) + 1,
+		wfhw->pair_rate);
+}
+
+static void
+__sun8i_pwm_round_waveform_fromhw(const struct sun8i_pwm_waveform *wfhw,
+				  struct pwm_waveform *wf)
+{
+	u32 pair_rate = wfhw->pair_rate;
+	u32 period_ticks = SUN8I_PWM_PPR_PERIOD_VALUE(wfhw->ppr);
+	u32 duty_ticks = SUN8I_PWM_PPR_DUTY_VALUE(wfhw->ppr);
+	u16 div_k = FIELD_GET(SUN8I_PWM_PCR_PRESCAL_K_MASK, wfhw->pcr) + 1;
+
+	wf->duty_offset_ns = 0;
+
+	if (!wfhw->enabled || !pair_rate) {
+		wf->period_length_ns = 0;
+		wf->duty_length_ns = 0;
+		return;
+	}
+
+	wf->period_length_ns = sun8i_pwm_period_ns(wfhw);
+	if (wfhw->bypass_en) {
+		wf->duty_length_ns =
+			DIV_ROUND_UP_ULL(NSEC_PER_SEC,
+					 2ULL * pair_rate);
+		return;
+	}
+
+	duty_ticks = min(duty_ticks, period_ticks);
+	if (!(wfhw->pcr & SUN8I_PWM_PCR_ACTIVE_STATE)) {
+		/* Constant outputs have no edge and therefore no offset. */
+		if (duty_ticks && duty_ticks < period_ticks)
+			wf->duty_offset_ns =
+				sun8i_pwm_ticks_to_ns(duty_ticks, div_k, pair_rate);
+		duty_ticks = period_ticks - duty_ticks;
+	}
+	wf->duty_length_ns = sun8i_pwm_ticks_to_ns(duty_ticks, div_k, pair_rate);
+}
+
+static int sun8i_pwm_round_waveform_fromhw(struct pwm_chip *chip,
+					   struct pwm_device *pwm,
+					   const void *_wfhw,
+					   struct pwm_waveform *wf)
+{
+	const struct sun8i_pwm_waveform *wfhw = _wfhw;
+
+	__sun8i_pwm_round_waveform_fromhw(wfhw, wf);
+
+	dev_dbg(pwmchip_parent(chip),
+		"pwm#%u: pair-rate=%u, pcr=%#x, ppr=%#x, bypass=%u -> %llu/%llu [+%llu]\n",
+		pwm->hwpwm, wfhw->pair_rate, wfhw->pcr, wfhw->ppr,
+		wfhw->bypass_en,
+		wf->duty_length_ns, wf->period_length_ns,
+		wf->duty_offset_ns);
+
+	return 0;
+}
+
+static bool
+sun8i_pwm_pair_rate_constrained(struct sun8i_pwm_chip *sun8i_chip,
+				unsigned int idx)
+{
+	unsigned int sibling = idx ^ 1;
+
+	guard(spinlock_irqsave)(&sun8i_chip->lock);
+	return sun8i_chip->channels[sibling].rate_exclusive ||
+	       sun8i_pwm_channel_is_enabled_locked(sun8i_chip, sibling);
+}
+
+static int sun8i_pwm_clear_idle_deadzone(struct sun8i_pwm_chip *sun8i_chip,
+					 unsigned int idx)
+{
+	unsigned int pair = SUN8I_PWM_PAIR_IDX(idx);
+	u32 pdzcr, per;
+
+	guard(spinlock_irqsave)(&sun8i_chip->lock);
+	pdzcr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PDZCR(pair));
+	if (!(pdzcr & SUN8I_PWM_PDZCR_ENABLE))
+		return 0;
+
+	per = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PER);
+	if (per & sun8i_pwm_pair_enable_mask(pair))
+		return -EOPNOTSUPP;
+
+	pdzcr &= ~SUN8I_PWM_PDZCR_ENABLE;
+	sun8i_pwm_writel(sun8i_chip, pdzcr, SUN8I_PWM_PDZCR(pair));
+
+	return 0;
+}
+
+static u64 sun8i_pwm_max_period_ns(struct sun8i_pwm_chip *sun8i_chip,
+				   unsigned int idx)
+{
+	unsigned long pair_rate;
+	u32 pcr;
+	u16 div_k;
+
+	pair_rate = sun8i_pwm_get_pair_rate(sun8i_chip, idx);
+	if (!pair_rate || pair_rate > U32_MAX)
+		return 0;
+
+	scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
+		pcr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PCR(idx));
+	}
+	div_k = FIELD_GET(SUN8I_PWM_PCR_PRESCAL_K_MASK, pcr) + 1;
+
+	return sun8i_pwm_ticks_to_ns(SUN8I_PWM_PPR_PERIOD_MAX, div_k,
+				     pair_rate);
+}
+
+static int sun8i_pwm_wait_period_ready(struct pwm_chip *chip, unsigned int idx)
+{
+	struct sun8i_pwm_chip *sun8i_chip = sun8i_pwm_from_chip(chip);
+	struct sun8i_pwm_channel *chan = &sun8i_chip->channels[idx];
+	unsigned long poll_us;
+	u64 period_ns, timeout_us;
+	u32 val;
+	int ret;
+
+	/* PPR contains the pending value, not necessarily the active period. */
+	period_ns = chan->pending_period_ns;
+	if (!period_ns)
+		period_ns = sun8i_pwm_max_period_ns(sun8i_chip, idx);
+	timeout_us = DIV_ROUND_UP_ULL(period_ns, NSEC_PER_USEC) +
+		     SUN8I_PWM_PERIOD_READY_MARGIN_US;
+	poll_us = clamp_t(u64,
+			  DIV_ROUND_UP_ULL(timeout_us,
+					   SUN8I_PWM_PERIOD_READY_POLL_COUNT),
+			  SUN8I_PWM_PERIOD_READY_MIN_POLL_US,
+			  SUN8I_PWM_PERIOD_READY_MAX_POLL_US);
+
+	ret = readl_poll_timeout(sun8i_chip->base + SUN8I_PWM_PCR(idx), val,
+				 !(val & SUN8I_PWM_PCR_PERIOD_READY), poll_us,
+				 timeout_us);
+	if (!ret)
+		chan->pending_period_ns = 0;
+	else
+		dev_err_ratelimited(pwmchip_parent(chip),
+				    "pwm#%u: update timeout after %llu us\n",
+				    idx, timeout_us);
+
+	return ret;
+}
+
+/*
+ * Protect the rate and quiesce the output if a live update is unsafe. On
+ * failure restore the old clock before restarting an output we stopped.
+ * CCF operations must stay outside the register lock.
+ */
+static int sun8i_pwm_prepare_update(struct pwm_chip *chip, unsigned int idx,
+				    const struct sun8i_pwm_waveform *wfhw,
+				  struct sun8i_pwm_waveform *old)
+{
+	struct sun8i_pwm_chip *sun8i_chip = sun8i_pwm_from_chip(chip);
+	struct sun8i_pwm_channel *chan = &sun8i_chip->channels[idx];
+	unsigned long rate, bus_rate;
+	bool had_exclusive, stop;
+	u32 pccr;
+	int ret, restore_ret;
+
+	scoped_guard(spinlock_irqsave, &sun8i_chip->lock)
+		had_exclusive = chan->rate_exclusive;
+
+	if (!had_exclusive) {
+		ret = clk_rate_exclusive_get(chan->pair_clk);
+		if (ret)
+			return ret;
+		scoped_guard(spinlock_irqsave, &sun8i_chip->lock)
+			chan->rate_exclusive = true;
+	}
+
+	/* Sample the old rate only after excluding other rate changes. */
+	rate = sun8i_pwm_get_pair_rate(sun8i_chip, idx);
+	if (!rate || rate > U32_MAX) {
+		ret = -ERANGE;
+		goto put_rate;
+	}
+	old->pair_rate = rate;
+	scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
+		old->enabled = sun8i_pwm_channel_is_enabled_locked(sun8i_chip, idx);
+		pccr = sun8i_pwm_readl(sun8i_chip,
+				       SUN8I_PWM_PCCR(SUN8I_PWM_PAIR_IDX(idx)));
+		old->bypass_en = !!(pccr & BIT(SUN8I_PWM_PCCR_BYPASS_BIT(idx)));
+		old->pcr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PCR(idx));
+		old->ppr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PPR(idx));
+	}
+
+	if (rate != wfhw->pair_rate &&
+	    sun8i_pwm_pair_rate_constrained(sun8i_chip, idx)) {
+		ret = -EBUSY;
+		goto put_rate;
+	}
+
+	/* Live PPR updates require unchanged configuration and a faster PCLK. */
+	bus_rate = clk_get_rate(sun8i_chip->bus_clk);
+	stop = old->enabled &&
+		(old->bypass_en != wfhw->bypass_en || rate != wfhw->pair_rate ||
+		 (!wfhw->bypass_en &&
+		  (((old->pcr ^ wfhw->pcr) &
+		    (SUN8I_PWM_PCR_WAVEFORM_MASK | SUN8I_PWM_PCR_MODE)) ||
+		   bus_rate <= DIV_ROUND_UP_ULL(wfhw->pair_rate,
+			FIELD_GET(SUN8I_PWM_PCR_PRESCAL_K_MASK, wfhw->pcr) + 1))));
+	if (stop) {
+		scoped_guard(spinlock_irqsave, &sun8i_chip->lock)
+			sun8i_pwm_set_enabled_locked(sun8i_chip, idx, false);
+	}
+
+	if (rate == wfhw->pair_rate)
+		return 0;
+
+	ret = clk_set_rate(chan->pair_clk, wfhw->pair_rate);
+	if (!ret && sun8i_pwm_get_pair_rate(sun8i_chip, idx) == wfhw->pair_rate)
+		return 0;
+	if (!ret)
+		ret = -EINVAL;
+
+	if (sun8i_pwm_get_pair_rate(sun8i_chip, idx) != rate) {
+		restore_ret = clk_set_rate(chan->pair_clk, rate);
+		if (restore_ret || sun8i_pwm_get_pair_rate(sun8i_chip, idx) != rate) {
+			dev_err(pwmchip_parent(chip),
+				"pwm#%u: failed to restore pair clock rate %lu\n",
+				idx, rate);
+			goto put_rate;
+		}
+	}
+	if (stop) {
+		scoped_guard(spinlock_irqsave, &sun8i_chip->lock)
+			sun8i_pwm_set_enabled_locked(sun8i_chip, idx, true);
+	}
+put_rate:
+	if (!had_exclusive)
+		sun8i_pwm_put_rate(sun8i_chip, idx);
+
+	return ret;
+}
+
+static int sun8i_pwm_write_waveform(struct pwm_chip *chip,
+				    struct pwm_device *pwm, const void *_wfhw)
+{
+	const struct sun8i_pwm_waveform *wfhw = _wfhw;
+	struct sun8i_pwm_chip *sun8i_chip = sun8i_pwm_from_chip(chip);
+	struct sun8i_pwm_channel *chan = &sun8i_chip->channels[pwm->hwpwm];
+	struct sun8i_pwm_waveform old;
+	u64 old_ns = 0, new_ns;
+	u32 pcr;
+	int ret;
+
+	if (!wfhw->enabled) {
+		scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
+			sun8i_pwm_set_enabled_locked(sun8i_chip, pwm->hwpwm, false);
+			sun8i_pwm_set_bypass_locked(sun8i_chip, pwm->hwpwm, false);
+		}
+		sun8i_pwm_put_rate(sun8i_chip, pwm->hwpwm);
+		return 0;
+	}
+
+	if (!wfhw->bypass_en) {
+		ret = sun8i_pwm_clear_idle_deadzone(sun8i_chip, pwm->hwpwm);
+		if (ret)
+			return ret;
+		ret = sun8i_pwm_wait_period_ready(chip, pwm->hwpwm);
+		if (ret)
+			return ret;
+	}
+
+	ret = sun8i_pwm_prepare_update(chip, pwm->hwpwm, wfhw, &old);
+	if (ret)
+		return ret;
+
+	if (!wfhw->bypass_en) {
+		/* Apply only waveform fields; never replay status or W1S bits. */
+		pcr = old.pcr & ~(SUN8I_PWM_PCR_WAVEFORM_MASK |
+				  SUN8I_PWM_PCR_MODE | SUN8I_PWM_PCR_PULSE_START |
+				  SUN8I_PWM_PCR_PERIOD_READY);
+		sun8i_pwm_writel(sun8i_chip, pcr | wfhw->pcr,
+				 SUN8I_PWM_PCR(pwm->hwpwm));
+
+		if (old.enabled && !old.bypass_en)
+			old_ns = sun8i_pwm_period_ns(&old);
+		new_ns = sun8i_pwm_period_ns(wfhw);
+		chan->pending_period_ns = max(old_ns, new_ns);
+		sun8i_pwm_writel(sun8i_chip, wfhw->ppr, SUN8I_PWM_PPR(pwm->hwpwm));
+	}
+
+	scoped_guard(spinlock_irqsave, &sun8i_chip->lock) {
+		sun8i_pwm_set_bypass_locked(sun8i_chip, pwm->hwpwm, wfhw->bypass_en);
+		sun8i_pwm_set_enabled_locked(sun8i_chip, pwm->hwpwm, true);
+	}
+
+	return 0;
+}
+
+struct sun8i_pwm_rounding {
+	const struct pwm_waveform *requested;
+	struct sun8i_pwm_waveform best;
+	struct pwm_waveform best_wf;
+	u32 current_pair_rate;
+	bool have_best;
+};
+
+static void
+sun8i_pwm_choose_waveform(struct sun8i_pwm_rounding *rounding,
+			  const struct sun8i_pwm_waveform *candidate)
+{
+	const struct pwm_waveform *requested = rounding->requested;
+	const struct pwm_waveform *best = &rounding->best_wf;
+	struct pwm_waveform wf;
+	bool better;
+
+	__sun8i_pwm_round_waveform_fromhw(candidate, &wf);
+	/* A zero-duty, zero-offset cycle-mode candidate always exists. */
+	if (wf.duty_length_ns > requested->duty_length_ns ||
+	    wf.duty_offset_ns > requested->duty_offset_ns)
+		return;
+
+	if (!rounding->have_best) {
+		better = true;
+	} else if (wf.period_length_ns != best->period_length_ns) {
+		/* Prefer the longest period below the request, else the shortest. */
+		if (wf.period_length_ns > requested->period_length_ns)
+			better = wf.period_length_ns < best->period_length_ns;
+		else
+			better = best->period_length_ns > requested->period_length_ns ||
+				 wf.period_length_ns > best->period_length_ns;
+	} else if (wf.duty_length_ns != best->duty_length_ns) {
+		better = wf.duty_length_ns > best->duty_length_ns;
+	} else if (wf.duty_offset_ns != best->duty_offset_ns) {
+		better = wf.duty_offset_ns > best->duty_offset_ns;
+	} else {
+		/* Prefer the current pair rate when waveforms are equivalent. */
+		better = candidate->pair_rate == rounding->current_pair_rate &&
+			 rounding->best.pair_rate != rounding->current_pair_rate;
+	}
+	if (!better)
+		return;
+
+	rounding->best = *candidate;
+	rounding->best_wf = wf;
+	rounding->have_best = true;
+}
+
+static struct sun8i_pwm_waveform
+sun8i_pwm_round_cycle(const struct pwm_waveform *requested, u32 pair_rate,
+		      u16 div_k)
+{
+	struct sun8i_pwm_waveform candidate = {
+		.enabled = true,
+		.pair_rate = pair_rate,
+		.pcr = FIELD_PREP(SUN8I_PWM_PCR_PRESCAL_K_MASK, div_k - 1),
+	};
+	u64 denominator = NSEC_PER_SEC * (u64)div_k;
+	u64 duty_ticks, period_ticks;
+	u32 inactive_ticks;
+
+	/* Floor division rounds down, except for the minimum one-tick period. */
+	period_ticks = mul_u64_u64_div_u64(requested->period_length_ns,
+					   pair_rate, denominator);
+	period_ticks = clamp_t(u64, period_ticks, 1, SUN8I_PWM_PPR_PERIOD_MAX);
+	duty_ticks = mul_u64_u64_div_u64(requested->duty_length_ns,
+					 pair_rate, denominator);
+	duty_ticks = min(duty_ticks, period_ticks);
+
+	/*
+	 * Zero active ticks encode either constant level. For a toggling
+	 * waveform, active-low mode places the rising edge after the inactive
+	 * part, which is the only non-zero offset supported by the hardware.
+	 */
+	if (!duty_ticks) {
+		candidate.pcr |= SUN8I_PWM_PCR_ACTIVE_STATE;
+	} else if (duty_ticks == period_ticks) {
+		duty_ticks = 0;
+	} else {
+		inactive_ticks = period_ticks - duty_ticks;
+		if (sun8i_pwm_ticks_to_ns(inactive_ticks, div_k, pair_rate) <=
+		    requested->duty_offset_ns)
+			duty_ticks = inactive_ticks;
+		else
+			candidate.pcr |= SUN8I_PWM_PCR_ACTIVE_STATE;
+	}
+
+	candidate.ppr = SUN8I_PWM_PPR_PERIOD(period_ticks) |
+			SUN8I_PWM_PPR_DUTY(duty_ticks);
+
+	return candidate;
+}
+
+static void
+sun8i_pwm_consider_pair_rate(struct sun8i_pwm_rounding *rounding, u32 pair_rate)
+{
+	struct sun8i_pwm_waveform candidate;
+
+	for (unsigned int div_k = 1; div_k <= 256; div_k++) {
+		candidate = sun8i_pwm_round_cycle(rounding->requested, pair_rate, div_k);
+		sun8i_pwm_choose_waveform(rounding, &candidate);
+	}
+
+	candidate = (struct sun8i_pwm_waveform) {
+		.pair_rate = pair_rate,
+		.enabled = true,
+		.bypass_en = true,
+	};
+	sun8i_pwm_choose_waveform(rounding, &candidate);
+}
+
+static int sun8i_pwm_round_waveform_tohw(struct pwm_chip *chip,
+					 struct pwm_device *pwm,
+					 const struct pwm_waveform *wf,
+					 void *_wfhw)
+{
+	struct sun8i_pwm_chip *sun8i_chip = sun8i_pwm_from_chip(chip);
+	struct sun8i_pwm_pair *pair =
+		&sun8i_chip->pairs[SUN8I_PWM_PAIR_IDX(pwm->hwpwm)];
+	struct sun8i_pwm_waveform *wfhw = _wfhw;
+	struct sun8i_pwm_rounding rounding = {
+		.requested = wf,
+	};
+	unsigned long current_rate;
+
+	if (!wf->period_length_ns) {
+		*wfhw = (struct sun8i_pwm_waveform) {
+			.enabled = false,
+		};
+		return 0;
+	}
+
+	current_rate = sun8i_pwm_get_pair_rate(sun8i_chip, pwm->hwpwm);
+	if (!current_rate || current_rate > U32_MAX)
+		return -ERANGE;
+	rounding.current_pair_rate = current_rate;
+
+	if (sun8i_pwm_pair_rate_constrained(sun8i_chip, pwm->hwpwm)) {
+		sun8i_pwm_consider_pair_rate(&rounding, current_rate);
+	} else {
+		for (unsigned int parent_idx = 0;
+		     parent_idx < clk_hw_get_num_parents(pair->hw);
+		     parent_idx++) {
+			struct clk_hw *parent;
+			unsigned long parent_rate;
+
+			parent = clk_hw_get_parent_by_index(pair->hw, parent_idx);
+			if (!parent)
+				continue;
+			parent_rate = clk_hw_get_rate(parent);
+			if (!parent_rate)
+				continue;
+
+			for (unsigned int i = 0; sun8i_pwm_div_m_table[i].div;
+			     i++) {
+				u16 div_m = sun8i_pwm_div_m_table[i].div;
+				u64 pair_rate;
+
+				pair_rate = DIV_ROUND_UP_ULL(parent_rate, div_m);
+				if (!pair_rate || pair_rate > U32_MAX)
+					continue;
+				sun8i_pwm_consider_pair_rate(&rounding, pair_rate);
+			}
+		}
+	}
+
+	if (!rounding.have_best)
+		return -EINVAL;
+	*wfhw = rounding.best;
+
+	dev_dbg(pwmchip_parent(chip),
+		"pwm#%u: %llu/%llu [+%llu] -> pair-rate=%u, pcr=%#x, ppr=%#x, bypass=%u\n",
+		pwm->hwpwm, wf->duty_length_ns, wf->period_length_ns,
+		wf->duty_offset_ns, wfhw->pair_rate, wfhw->pcr, wfhw->ppr,
+		wfhw->bypass_en);
+
+	return rounding.best_wf.period_length_ns > wf->period_length_ns;
+}
+
+static const struct pwm_ops sun8i_pwm_ops = {
+	.request = sun8i_pwm_request,
+	.free = sun8i_pwm_free,
+	.sizeof_wfhw = sizeof(struct sun8i_pwm_waveform),
+	.round_waveform_tohw = sun8i_pwm_round_waveform_tohw,
+	.round_waveform_fromhw = sun8i_pwm_round_waveform_fromhw,
+	.read_waveform = sun8i_pwm_read_waveform,
+	.write_waveform = sun8i_pwm_write_waveform,
+};
+
+/* Register the shared mux, gate and /div_m clock for each channel pair. */
+static int sun8i_pwm_register_pair_clocks(struct device *dev,
+					  struct sun8i_pwm_chip *sun8i_chip)
+{
+	static const struct clk_parent_data parent_data[] = {
+		{ .index = 0 },
+		{ .index = 1 },
+	};
+
+	for (unsigned int i = 0; i < SUN8I_PWM_NPAIRS; i++) {
+		struct sun8i_pwm_pair *pair = &sun8i_chip->pairs[i];
+		void __iomem *reg = sun8i_chip->base + SUN8I_PWM_PCCR(i);
+		const char *name;
+		int ret;
+
+		name = devm_kasprintf(dev, GFP_KERNEL,
+				      "%s#pwm-clk-src%u%u", dev_name(dev),
+				       i * 2, i * 2 + 1);
+		if (!name)
+			return -ENOMEM;
+
+		pair->mux.reg = reg;
+		pair->mux.shift = SUN8I_PWM_PCCR_SRC_SHIFT;
+		pair->mux.mask = SUN8I_PWM_PCCR_SRC_MASK;
+		pair->mux.flags = CLK_MUX_ROUND_CLOSEST;
+		pair->mux.lock = &sun8i_chip->lock;
+
+		pair->chip = sun8i_chip;
+		pair->index = i;
+
+		pair->divider.reg = reg;
+		pair->divider.shift = SUN8I_PWM_PCCR_DIV_M_SHIFT;
+		pair->divider.width = SUN8I_PWM_PCCR_DIV_M_WIDTH;
+		pair->divider.table = sun8i_pwm_div_m_table;
+		pair->divider.lock = &sun8i_chip->lock;
+
+		pair->hw = devm_clk_hw_register_composite_pdata(dev, name,
+								parent_data,
+								ARRAY_SIZE(parent_data),
+			&pair->mux.hw, &clk_mux_ops,
+			&pair->divider.hw, &clk_divider_ops,
+			&pair->gate_hw, &sun8i_pwm_pair_gate_ops, 0);
+		if (IS_ERR(pair->hw))
+			return dev_err_probe(dev, PTR_ERR(pair->hw),
+					     "Failed to register pair %u clock\n", i);
+
+		/* Borrow CCF's handle so registration cannot pin this module. */
+		pair->rate_nb.notifier_call = sun8i_pwm_pair_rate_notifier;
+		ret = devm_clk_notifier_register(dev, pair->hw->clk, &pair->rate_nb);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "Failed to protect pair %u clock rate\n", i);
+	}
+
+	return 0;
+}
+
+/*
+ * A disabled pair gate makes any set PER bits ineffective. Clear those stale
+ * enables before a clock consumer can turn the shared gate on and expose an
+ * unrequested output.
+ */
+static void
+sun8i_pwm_sanitize_disabled_pairs(struct sun8i_pwm_chip *sun8i_chip)
+{
+	u32 per;
+
+	guard(spinlock_irqsave)(&sun8i_chip->lock);
+	per = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PER);
+	for (unsigned int pair = 0; pair < SUN8I_PWM_NPAIRS; pair++) {
+		unsigned int first = pair * 2;
+		u32 pccr;
+
+		pccr = sun8i_pwm_readl(sun8i_chip, SUN8I_PWM_PCCR(pair));
+		if (pccr & SUN8I_PWM_PCCR_GATE)
+			continue;
+
+		per &= ~sun8i_pwm_pair_enable_mask(pair);
+		pccr &= ~BIT(SUN8I_PWM_PCCR_BYPASS_BIT(first));
+		pccr &= ~BIT(SUN8I_PWM_PCCR_BYPASS_BIT(first + 1));
+		sun8i_pwm_writel(sun8i_chip, pccr, SUN8I_PWM_PCCR(pair));
+	}
+	sun8i_pwm_writel(sun8i_chip, per, SUN8I_PWM_PER);
+}
+
+static int sun8i_pwm_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct sun8i_pwm_chip *sun8i_chip;
+	struct reset_control *rst;
+	struct pwm_chip *chip;
+	int ret;
+
+	chip = devm_pwmchip_alloc(dev, SUN8I_PWM_NPWM, sizeof(*sun8i_chip));
+	if (IS_ERR(chip))
+		return dev_err_probe(dev, PTR_ERR(chip),
+				     "Failed to allocate pwmchip\n");
+
+	sun8i_chip = sun8i_pwm_from_chip(chip);
+	spin_lock_init(&sun8i_chip->lock);
+	sun8i_chip->base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(sun8i_chip->base))
+		return dev_err_probe(dev, PTR_ERR(sun8i_chip->base),
+				     "Failed to get PWM base address\n");
+
+	sun8i_chip->bus_clk = devm_clk_get_enabled(dev, "bus");
+	if (IS_ERR(sun8i_chip->bus_clk))
+		return dev_err_probe(dev, PTR_ERR(sun8i_chip->bus_clk),
+				     "Failed to get bus clock\n");
+
+	rst = devm_reset_control_get_shared_deasserted(dev, NULL);
+	if (IS_ERR(rst))
+		return dev_err_probe(dev, PTR_ERR(rst),
+				     "Failed to get reset control\n");
+
+	chip->ops = &sun8i_pwm_ops;
+
+	sun8i_pwm_sanitize_disabled_pairs(sun8i_chip);
+
+	ret = sun8i_pwm_register_pair_clocks(dev, sun8i_chip);
+	if (ret)
+		return ret;
+
+	ret = devm_pwmchip_add(dev, chip);
+	if (ret < 0)
+		return dev_err_probe(dev, ret, "Failed to add PWM chip\n");
+
+	return 0;
+}
+
+static const struct of_device_id sun8i_pwm_dt_ids[] = {
+	{
+		.compatible = "allwinner,sun50i-h616-pwm",
+	}, {
+		/* sentinel */
+	}
+};
+MODULE_DEVICE_TABLE(of, sun8i_pwm_dt_ids);
+
+static struct platform_driver sun8i_pwm_driver = {
+	.driver = {
+		.name = "sun8i-pwm",
+		.of_match_table = sun8i_pwm_dt_ids,
+	},
+	.probe = sun8i_pwm_probe,
+};
+module_platform_driver(sun8i_pwm_driver);
+
+MODULE_AUTHOR("Richard Genoud <richard.genoud@bootlin.com>");
+MODULE_DESCRIPTION("Allwinner sun8i PWM driver");
+MODULE_LICENSE("GPL");
