[PATCH v3 2/2] pwm: Add a driver for the Ingenic X1600 PWM controller

From: Olivier Louvignes

Date: Mon Nov 02 2026 - 17:05:15 EST


The X1600 has a PWM controller of its own, separate from the TCU timers
that pwm-jz4740 drives on earlier Ingenic SoCs (X1600 programming manual,
chapter 13): eight channels, each with a power-of-two clock divider and a
waveform word holding the counts at the high and at the low level, the
channel's initial level picking which comes first. So the duty offset is
0 or period - duty, and since neither count may be zero, a duty cycle of 0
or 100% switches the channel off with its idle level set accordingly.

The driver implements the waveform callbacks. A new word and initial
level go through the update register, which loads them at the end of the
running period, or when the channel starts; a load requested while one is
pending replaces it, so nothing waits and the chip is atomic. The manual
sets the divider before a channel is enabled, and on the R1 a divider
written to a running channel takes effect at once, so changing it changes
the length of the period then running.

Tested on a HiBy R1, whose backlight is channel 0.

Signed-off-by: Olivier Louvignes <olivier@xxxxxxxxx>
---
v3: the changes are listed in the cover letter.

MAINTAINERS | 1 +
drivers/pwm/Kconfig | 11 ++
drivers/pwm/Makefile | 1 +
drivers/pwm/pwm-ingenic-x1600.c | 289 ++++++++++++++++++++++++++++++++
4 files changed, 302 insertions(+)
create mode 100644 drivers/pwm/pwm-ingenic-x1600.c

diff --git a/MAINTAINERS b/MAINTAINERS
index c905aa13bcbc..c090512468a4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12882,6 +12882,7 @@ INGENIC X1600 PWM DRIVER
M: Olivier Louvignes <olivier@xxxxxxxxx>
S: Maintained
F: Documentation/devicetree/bindings/pwm/ingenic,x1600-pwm.yaml
+F: drivers/pwm/pwm-ingenic-x1600.c

INJOINIC IP5xxx POWER BANK IC DRIVER
M: Samuel Holland <samuel@xxxxxxxxxxxx>
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index 729776086879..d3bf11b94c43 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -336,6 +336,17 @@ config PWM_IMX_TPM
To compile this driver as a module, choose M here: the module
will be called pwm-imx-tpm.

+config PWM_INGENIC_X1600
+ tristate "Ingenic X1600 PWM support"
+ depends on MACH_INGENIC || COMPILE_TEST
+ depends on COMMON_CLK && OF && HAS_IOMEM
+ help
+ Generic PWM framework driver for the PWM controller of the
+ Ingenic X1600.
+
+ To compile this driver as a module, choose M here: the module
+ will be called pwm-ingenic-x1600.
+
config PWM_INTEL_LGM
tristate "Intel LGM PWM support"
depends on HAS_IOMEM
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index 5630a521a7cf..4b1a1784692a 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -28,6 +28,7 @@ obj-$(CONFIG_PWM_IMG) += pwm-img.o
obj-$(CONFIG_PWM_IMX1) += pwm-imx1.o
obj-$(CONFIG_PWM_IMX27) += pwm-imx27.o
obj-$(CONFIG_PWM_IMX_TPM) += pwm-imx-tpm.o
+obj-$(CONFIG_PWM_INGENIC_X1600) += pwm-ingenic-x1600.o
obj-$(CONFIG_PWM_INTEL_LGM) += pwm-intel-lgm.o
obj-$(CONFIG_PWM_IPQ) += pwm-ipq.o
obj-$(CONFIG_PWM_IQS620A) += pwm-iqs620a.o
diff --git a/drivers/pwm/pwm-ingenic-x1600.c b/drivers/pwm/pwm-ingenic-x1600.c
new file mode 100644
index 000000000000..1855e0cb1a56
--- /dev/null
+++ b/drivers/pwm/pwm-ingenic-x1600.c
@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * PWM controller of the Ingenic X1600
+ *
+ * Copyright (c) 2026 Olivier Louvignes <olivier@xxxxxxxxx>
+ *
+ * Eight channels, each with a power-of-two clock divider and a waveform word
+ * holding the counts at the high and at the low level; the channel's initial
+ * level picks which comes first (X1600 programming manual, chapter 13). Unlike
+ * the JZ47xx and X1000, whose PWMs are TCU timer channels (pwm-jz4740), the
+ * X1600 has this separate controller.
+ *
+ * Reference manual:
+ * https://github.com/Ingenic-community/datasheets/blob/main/X/X1600_PM_20220117.pdf
+ *
+ * Limitations:
+ * - The duty offset is 0 or period - duty (the high level last).
+ * - Neither count may be zero, so for a duty cycle of 0 or 100% the channel
+ * is switched off, its output held low or high by its idle level.
+ * - A disabled channel finishes its period, then drives its idle level, low.
+ * - A new divider takes effect at once, so the period then running ends at
+ * the new rate; the other settings take effect when that period ends.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/err.h>
+#include <linux/io.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+#include <linux/time.h>
+#include <linux/types.h>
+
+#define X1600_PWM_ENS 0x00 /* write 1: switch a channel on */
+#define X1600_PWM_ENC 0x04 /* write 1: switch a channel off */
+#define X1600_PWM_EN 0x08 /* the channels switched on */
+#define X1600_PWM_UPT 0x10 /* write 1: load the word at period end */
+#define X1600_PWM_BUSY 0x14 /* a load is pending */
+#define X1600_PWM_MS 0x20 /* 0: waveform word, 1: DMA */
+#define X1600_PWM_INL 0x24 /* a period's first level, taken at load */
+#define X1600_PWM_IDL 0x28 /* the level while off */
+#define X1600_PWM_CCFG(n) (0x40 + 4 * (n))
+#define X1600_PWM_WCFG(n) (0x80 + 4 * (n))
+
+#define X1600_PWM_CCFG_PRESCALE GENMASK(3, 0) /* divide by 2^PRESCALE */
+#define X1600_PWM_WCFG_HIGH GENMASK(31, 16)
+#define X1600_PWM_WCFG_LOW GENMASK(15, 0)
+
+#define X1600_PWM_NUM 8
+/* The period's count, so that each of HIGH and LOW fits too. */
+#define X1600_PWM_MAX_COUNT FIELD_MAX(X1600_PWM_WCFG_LOW)
+/* PRESCALE is four bits wide, but its values from 8 up are reserved. */
+#define X1600_PWM_MAX_PRESCALE 7
+
+struct x1600_pwm {
+ void __iomem *base;
+ unsigned long rate;
+};
+
+struct x1600_pwm_wfhw {
+ u32 wcfg;
+ u8 prescale;
+ bool enabled;
+ bool inl;
+ bool idl;
+};
+
+/* Counts of the undivided clock, which runs at most at NSEC_PER_SEC. */
+static u64 x1600_pwm_counts(u64 ns, unsigned long rate)
+{
+ return mul_u64_u64_div_u64(ns, rate, NSEC_PER_SEC);
+}
+
+/*
+ * counts is HIGH + LOW, at most 2 * 0xffff (17 bits), and prescale at most 15
+ * (the field's width), so (counts * NSEC_PER_SEC) << prescale stays below 2^62.
+ */
+static u64 x1600_pwm_ns(u64 counts, unsigned long rate, unsigned int prescale)
+{
+ return DIV64_U64_ROUND_UP((counts * NSEC_PER_SEC) << prescale, rate);
+}
+
+static int x1600_pwm_round_waveform_tohw(struct pwm_chip *chip, struct pwm_device *pwm,
+ const struct pwm_waveform *wf, void *_wfhw)
+{
+ struct x1600_pwm *x = pwmchip_get_drvdata(chip);
+ struct x1600_pwm_wfhw *wfhw = _wfhw;
+ u64 period, high, offset;
+ unsigned int prescale;
+ int ret = 0;
+
+ *wfhw = (struct x1600_pwm_wfhw){ };
+ if (!wf->period_length_ns)
+ return 0;
+
+ /* The smallest divider that brings the period within the counter. */
+ period = x1600_pwm_counts(wf->period_length_ns, x->rate);
+ prescale = min(fls64(period / (X1600_PWM_MAX_COUNT + 1)),
+ X1600_PWM_MAX_PRESCALE);
+ period = min(period >> prescale, X1600_PWM_MAX_COUNT);
+ if (period < 2) {
+ period = 2;
+ ret = 1;
+ }
+ high = min(x1600_pwm_counts(wf->duty_length_ns, x->rate) >> prescale, period);
+ offset = x1600_pwm_counts(wf->duty_offset_ns, x->rate) >> prescale;
+
+ wfhw->prescale = prescale;
+ wfhw->wcfg = FIELD_PREP(X1600_PWM_WCFG_HIGH, high) |
+ FIELD_PREP(X1600_PWM_WCFG_LOW, period - high);
+ wfhw->enabled = high && high < period;
+ /* Low first only if the requested offset reaches period - duty. */
+ wfhw->inl = !wfhw->enabled || offset < period - high;
+ wfhw->idl = high == period;
+
+ return ret;
+}
+
+static int x1600_pwm_round_waveform_fromhw(struct pwm_chip *chip, struct pwm_device *pwm,
+ const void *_wfhw, struct pwm_waveform *wf)
+{
+ struct x1600_pwm *x = pwmchip_get_drvdata(chip);
+ const struct x1600_pwm_wfhw *wfhw = _wfhw;
+ u32 high = FIELD_GET(X1600_PWM_WCFG_HIGH, wfhw->wcfg);
+ u32 low = FIELD_GET(X1600_PWM_WCFG_LOW, wfhw->wcfg);
+
+ *wf = (struct pwm_waveform){ };
+
+ /* Cleared counts: the channel was disabled, or never set up. */
+ if (!high && !low)
+ return 0;
+
+ wf->period_length_ns = x1600_pwm_ns(high + low, x->rate, wfhw->prescale);
+
+ /*
+ * A channel that is off holds its idle level for as long as it is off:
+ * a duty cycle of 0 or 100% over the period its counts give. INL plays
+ * no part, it only sets the level a running period starts with.
+ */
+ if (!wfhw->enabled) {
+ wf->duty_length_ns = wfhw->idl ? wf->period_length_ns : 0;
+ return 0;
+ }
+
+ wf->duty_length_ns = x1600_pwm_ns(high, x->rate, wfhw->prescale);
+ wf->duty_offset_ns = wfhw->inl ? 0 : x1600_pwm_ns(low, x->rate, wfhw->prescale);
+
+ return 0;
+}
+
+static int x1600_pwm_read_waveform(struct pwm_chip *chip, struct pwm_device *pwm,
+ void *_wfhw)
+{
+ struct x1600_pwm *x = pwmchip_get_drvdata(chip);
+ struct x1600_pwm_wfhw *wfhw = _wfhw;
+ unsigned int ch = pwm->hwpwm;
+
+ wfhw->enabled = readl(x->base + X1600_PWM_EN) & BIT(ch);
+ wfhw->inl = readl(x->base + X1600_PWM_INL) & BIT(ch);
+ wfhw->idl = readl(x->base + X1600_PWM_IDL) & BIT(ch);
+ wfhw->prescale = FIELD_GET(X1600_PWM_CCFG_PRESCALE,
+ readl(x->base + X1600_PWM_CCFG(ch)));
+ wfhw->wcfg = readl(x->base + X1600_PWM_WCFG(ch));
+
+ return 0;
+}
+
+static void x1600_pwm_set_bit(struct x1600_pwm *x, unsigned int reg,
+ unsigned int ch, bool set)
+{
+ u32 val = readl(x->base + reg);
+
+ if (set)
+ val |= BIT(ch);
+ else
+ val &= ~BIT(ch);
+ writel(val, x->base + reg);
+}
+
+static int x1600_pwm_write_waveform(struct pwm_chip *chip, struct pwm_device *pwm,
+ const void *_wfhw)
+{
+ struct x1600_pwm *x = pwmchip_get_drvdata(chip);
+ const struct x1600_pwm_wfhw *wfhw = _wfhw;
+ unsigned int ch = pwm->hwpwm;
+ bool enabled = readl(x->base + X1600_PWM_EN) & BIT(ch);
+
+ /* The idle level only shows once the channel is off. */
+ x1600_pwm_set_bit(x, X1600_PWM_IDL, ch, wfhw->idl);
+
+ /*
+ * Switched off, the channel finishes its period, then holds its idle
+ * level. The rest is written all the same, to be read back.
+ */
+ if (enabled && !wfhw->enabled)
+ writel(BIT(ch), x->base + X1600_PWM_ENC);
+
+ /* The divider takes effect at once, even in the middle of a period. */
+ writel(FIELD_PREP(X1600_PWM_CCFG_PRESCALE, wfhw->prescale),
+ x->base + X1600_PWM_CCFG(ch));
+
+ /*
+ * The initial level and the word are loaded together, at the end of
+ * the period running or when the channel starts. A load requested
+ * while one is pending replaces it, so there is nothing to wait for.
+ */
+ x1600_pwm_set_bit(x, X1600_PWM_INL, ch, wfhw->inl);
+ writel(wfhw->wcfg, x->base + X1600_PWM_WCFG(ch));
+ if (!wfhw->enabled)
+ return 0;
+ writel(BIT(ch), x->base + X1600_PWM_UPT);
+
+ /*
+ * A channel just switched off still runs until its period ends, and
+ * reads as off: switched on again, it takes the word loaded above.
+ */
+ if (!enabled) {
+ x1600_pwm_set_bit(x, X1600_PWM_MS, ch, false);
+ writel(BIT(ch), x->base + X1600_PWM_ENS);
+ }
+
+ return 0;
+}
+
+static const struct pwm_ops x1600_pwm_ops = {
+ .sizeof_wfhw = sizeof(struct x1600_pwm_wfhw),
+ .round_waveform_tohw = x1600_pwm_round_waveform_tohw,
+ .round_waveform_fromhw = x1600_pwm_round_waveform_fromhw,
+ .read_waveform = x1600_pwm_read_waveform,
+ .write_waveform = x1600_pwm_write_waveform,
+};
+
+static int x1600_pwm_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct pwm_chip *chip;
+ struct x1600_pwm *x;
+ struct clk *clk;
+ int ret;
+
+ chip = devm_pwmchip_alloc(dev, X1600_PWM_NUM, sizeof(*x));
+ if (IS_ERR(chip))
+ return PTR_ERR(chip);
+ x = pwmchip_get_drvdata(chip);
+
+ x->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(x->base))
+ return PTR_ERR(x->base);
+
+ clk = devm_clk_get_enabled(dev, NULL);
+ if (IS_ERR(clk))
+ return dev_err_probe(dev, PTR_ERR(clk), "Unable to get clock\n");
+
+ ret = devm_clk_rate_exclusive_get(dev, clk);
+ if (ret)
+ return dev_err_probe(dev, ret, "Unable to get exclusive rate\n");
+
+ x->rate = clk_get_rate(clk);
+ if (!x->rate || x->rate > NSEC_PER_SEC)
+ return dev_err_probe(dev, -EINVAL, "Invalid clock rate: %lu\n", x->rate);
+
+ chip->ops = &x1600_pwm_ops;
+ chip->atomic = true;
+ return devm_pwmchip_add(dev, chip);
+}
+
+static const struct of_device_id x1600_pwm_of_match[] = {
+ { .compatible = "ingenic,x1600-pwm" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, x1600_pwm_of_match);
+
+static struct platform_driver x1600_pwm_driver = {
+ .driver = {
+ .name = "ingenic-x1600-pwm",
+ .of_match_table = x1600_pwm_of_match,
+ },
+ .probe = x1600_pwm_probe,
+};
+module_platform_driver(x1600_pwm_driver);
+
+MODULE_AUTHOR("Olivier Louvignes <olivier@xxxxxxxxx>");
+MODULE_DESCRIPTION("Ingenic X1600 PWM driver");
+MODULE_LICENSE("GPL");
--
2.54.0