All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/2] pwm: Add the Ingenic X1600 PWM controller
@ 2026-10-25  8:46 Olivier Louvignes
  2026-10-25  8:46 ` [PATCH 1/2] dt-bindings: " Olivier Louvignes
  2026-10-25  8:46 ` [PATCH 2/2] pwm: Add a driver for " Olivier Louvignes
  0 siblings, 2 replies; 4+ messages in thread
From: Olivier Louvignes @ 2026-10-25  8:46 UTC (permalink / raw)
  To: Uwe Kleine-König
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Paul Cercueil,
	H . Nikolaus Schaller, linux-pwm, devicetree, linux-mips,
	linux-kernel, Olivier Louvignes

The Ingenic X1600 has a PWM controller of its own, separate from the TCU
timers that pwm-jz4740 drives on the earlier Ingenic SoCs. This adds its
binding and a driver built on the waveform callbacks.

Tested on a HiBy R1 (X1600), whose backlight is channel 0, through
pwm-backlight: brightness 1, 25, 50, 60 and 100% give the expected high and
low counts, 0 and 100% switch the channel off at the right idle level,
and an inversed 250 us of 1 ms on channel 1 reads back as INL 0, LOW
9375 then HIGH 28125 with the clock divided by 8. PWM_DEBUG stayed
silent.

The X1600 has no device tree upstream yet; its support is being posted
one subsystem at a time.

Olivier Louvignes (2):
  dt-bindings: pwm: Add the Ingenic X1600 PWM controller
  pwm: Add a driver for the Ingenic X1600 PWM controller

 .../bindings/pwm/ingenic,x1600-pwm.yaml       |  47 ++++
 MAINTAINERS                                   |   6 +
 drivers/pwm/Kconfig                           |  11 +
 drivers/pwm/Makefile                          |   1 +
 drivers/pwm/pwm-ingenic-x1600.c               | 260 ++++++++++++++++++
 5 files changed, 325 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/pwm/ingenic,x1600-pwm.yaml
 create mode 100644 drivers/pwm/pwm-ingenic-x1600.c


base-commit: 00e917df050d79ac26ec700717680ad6421d2808
-- 
2.54.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH 1/2] dt-bindings: pwm: Add the Ingenic X1600 PWM controller
  2026-10-25  8:46 [PATCH 0/2] pwm: Add the Ingenic X1600 PWM controller Olivier Louvignes
@ 2026-10-25  8:46 ` Olivier Louvignes
  2026-10-25  8:46 ` [PATCH 2/2] pwm: Add a driver for " Olivier Louvignes
  1 sibling, 0 replies; 4+ messages in thread
From: Olivier Louvignes @ 2026-10-25  8:46 UTC (permalink / raw)
  To: Uwe Kleine-König
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Paul Cercueil,
	H . Nikolaus Schaller, linux-pwm, devicetree, linux-mips,
	linux-kernel, Olivier Louvignes

The X1600 has an eight-channel PWM controller of its own, separate from
the TCU timers that drive the PWMs of earlier Ingenic SoCs
(ingenic,tcu.yaml), so it gets a binding of its own.

Signed-off-by: Olivier Louvignes <olivier@mgcrea.io>
---
 .../bindings/pwm/ingenic,x1600-pwm.yaml       | 47 +++++++++++++++++++
 MAINTAINERS                                   |  5 ++
 2 files changed, 52 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/pwm/ingenic,x1600-pwm.yaml

diff --git a/Documentation/devicetree/bindings/pwm/ingenic,x1600-pwm.yaml b/Documentation/devicetree/bindings/pwm/ingenic,x1600-pwm.yaml
new file mode 100644
index 000000000000..3ecaa92a5ca2
--- /dev/null
+++ b/Documentation/devicetree/bindings/pwm/ingenic,x1600-pwm.yaml
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pwm/ingenic,x1600-pwm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Ingenic X1600 PWM controller
+
+maintainers:
+  - Olivier Louvignes <olivier@mgcrea.io>
+
+description:
+  The eight-channel PWM controller of the Ingenic X1600 SoC, separate from
+  the TCU timers that drive the PWMs of earlier Ingenic SoCs.
+
+allOf:
+  - $ref: pwm.yaml#
+
+properties:
+  compatible:
+    const: ingenic,x1600-pwm
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    maxItems: 1
+
+  "#pwm-cells":
+    const: 3
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - "#pwm-cells"
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    pwm@134c0000 {
+        compatible = "ingenic,x1600-pwm";
+        reg = <0x134c0000 0x200>;
+        clocks = <&cgu 21>;
+        #pwm-cells = <3>;
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 3a19da74d00c..c905aa13bcbc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12878,6 +12878,11 @@ F:	include/linux/mfd/ingenic-tcu.h
 F:	sound/soc/codecs/jz47*
 F:	sound/soc/jz4740/
 
+INGENIC X1600 PWM DRIVER
+M:	Olivier Louvignes <olivier@mgcrea.io>
+S:	Maintained
+F:	Documentation/devicetree/bindings/pwm/ingenic,x1600-pwm.yaml
+
 INJOINIC IP5xxx POWER BANK IC DRIVER
 M:	Samuel Holland <samuel@sholland.org>
 S:	Maintained
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [PATCH 2/2] pwm: Add a driver for the Ingenic X1600 PWM controller
  2026-10-25  8:46 [PATCH 0/2] pwm: Add the Ingenic X1600 PWM controller Olivier Louvignes
  2026-10-25  8:46 ` [PATCH 1/2] dt-bindings: " Olivier Louvignes
@ 2026-10-25  8:46 ` Olivier Louvignes
  2026-10-25  9:03   ` sashiko-bot
  1 sibling, 1 reply; 4+ messages in thread
From: Olivier Louvignes @ 2026-10-25  8:46 UTC (permalink / raw)
  To: Uwe Kleine-König
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Paul Cercueil,
	H . Nikolaus Schaller, linux-pwm, devicetree, linux-mips,
	linux-kernel, Olivier Louvignes

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. It follows the manual's
order: the divider is only changed with the channel off, and a running
channel takes a new word through the update register.

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

Signed-off-by: Olivier Louvignes <olivier@mgcrea.io>
---
 MAINTAINERS                     |   1 +
 drivers/pwm/Kconfig             |  11 ++
 drivers/pwm/Makefile            |   1 +
 drivers/pwm/pwm-ingenic-x1600.c | 260 ++++++++++++++++++++++++++++++++
 4 files changed, 273 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@mgcrea.io>
 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@sholland.org>
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index 729776086879..244c8f4f140e 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -381,6 +381,17 @@ config PWM_JZ4740
 	  To compile this driver as a module, choose M here: the module
 	  will be called pwm-jz4740.
 
+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_KEEMBAY
 	tristate "Intel Keem Bay PWM driver"
 	depends on ARCH_KEEMBAY || COMPILE_TEST
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index 5630a521a7cf..5c59b54e0029 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_PWM_IMX_TPM)	+= pwm-imx-tpm.o
 obj-$(CONFIG_PWM_INTEL_LGM)	+= pwm-intel-lgm.o
 obj-$(CONFIG_PWM_IPQ)		+= pwm-ipq.o
 obj-$(CONFIG_PWM_IQS620A)	+= pwm-iqs620a.o
+obj-$(CONFIG_PWM_INGENIC_X1600)	+= pwm-ingenic-x1600.o
 obj-$(CONFIG_PWM_JZ4740)	+= pwm-jz4740.o
 obj-$(CONFIG_PWM_KEEMBAY)	+= pwm-keembay.o
 obj-$(CONFIG_PWM_LOONGSON)	+= pwm-loongson.o
diff --git a/drivers/pwm/pwm-ingenic-x1600.c b/drivers/pwm/pwm-ingenic-x1600.c
new file mode 100644
index 000000000000..83a8d5eec6bf
--- /dev/null
+++ b/drivers/pwm/pwm-ingenic-x1600.c
@@ -0,0 +1,260 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * PWM controller of the Ingenic X1600
+ *
+ * Copyright (c) 2026 Olivier Louvignes <olivier@mgcrea.io>
+ *
+ * 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.
+ *
+ * Limitations:
+ * - The duty offset is 0 or period - duty (the high level last).
+ * - Neither count may be zero, so a duty cycle of 0 or 100% switches the
+ *   channel off and holds its output at the idle level instead.
+ * - A disabled channel drives its idle level, low.
+ * - Changing the divider or the initial level stops the channel first, which
+ *   cuts the current period short.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/err.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/mod_devicetable.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 waveform word */
+#define X1600_PWM_BUSY			0x14	/* a load is pending */
+#define X1600_PWM_MS			0x20	/* 0: waveform word, 1: DMA */
+#define X1600_PWM_INL			0x24	/* the level a period starts with */
+#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
+#define X1600_PWM_MAX_COUNT		0xffff
+#define X1600_PWM_MAX_PRESCALE		7
+
+struct x1600_pwm {
+	void __iomem *base;
+	struct clk *clk;
+};
+
+struct x1600_pwm_wfhw {
+	u32 wcfg;
+	u8 prescale;
+	bool enabled;
+	bool inl;
+	bool idl;
+};
+
+static u64 x1600_pwm_counts(u64 ns, unsigned long rate, unsigned int prescale)
+{
+	return mul_u64_u64_div_u64(ns, rate, (u64)NSEC_PER_SEC << prescale);
+}
+
+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);
+	unsigned long rate = clk_get_rate(x->clk);
+	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;
+
+	for (prescale = 0; prescale < X1600_PWM_MAX_PRESCALE; prescale++)
+		if (x1600_pwm_counts(wf->period_length_ns, rate, prescale) <=
+		    X1600_PWM_MAX_COUNT)
+			break;
+
+	period = min(x1600_pwm_counts(wf->period_length_ns, rate, prescale),
+		     X1600_PWM_MAX_COUNT);
+	if (period < 2) {
+		period = 2;
+		ret = 1;
+	}
+	high = min(x1600_pwm_counts(wf->duty_length_ns, rate, prescale), period);
+	offset = x1600_pwm_counts(wf->duty_offset_ns, 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);
+	unsigned long rate = clk_get_rate(x->clk);
+	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);
+
+	if (!rate)
+		return -EINVAL;
+
+	wf->period_length_ns = x1600_pwm_ns(high + low, rate, wfhw->prescale);
+	wf->duty_length_ns = x1600_pwm_ns(high, rate, wfhw->prescale);
+	wf->duty_offset_ns = wfhw->enabled && !wfhw->inl ?
+			     x1600_pwm_ns(low, rate, wfhw->prescale) : 0;
+
+	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 long rate = clk_get_rate(x->clk);
+	struct x1600_pwm_wfhw cur;
+	unsigned int ch = pwm->hwpwm;
+	u32 busy;
+
+	x1600_pwm_read_waveform(chip, pwm, &cur);
+
+	/*
+	 * Running with the same divider and initial level: load the new word,
+	 * which the channel takes at the end of its period.
+	 */
+	if (cur.enabled && wfhw->enabled && cur.prescale == wfhw->prescale &&
+	    cur.inl == wfhw->inl) {
+		writel(wfhw->wcfg, x->base + X1600_PWM_WCFG(ch));
+		writel(BIT(ch), x->base + X1600_PWM_UPT);
+		return readl_poll_timeout(x->base + X1600_PWM_BUSY, busy,
+					  !(busy & BIT(ch)), 10,
+					  DIV_ROUND_UP_ULL(2ULL * USEC_PER_SEC *
+							   (X1600_PWM_MAX_COUNT << cur.prescale),
+							   rate));
+	}
+
+	/* The idle level first, so that stopping holds the right level. */
+	x1600_pwm_set_bit(x, X1600_PWM_IDL, ch, wfhw->idl);
+	if (cur.enabled)
+		writel(BIT(ch), x->base + X1600_PWM_ENC);
+
+	/* The prescaler only changes while the channel is off. */
+	writel(FIELD_PREP(X1600_PWM_CCFG_PRESCALE, wfhw->prescale),
+	       x->base + X1600_PWM_CCFG(ch));
+	x1600_pwm_set_bit(x, X1600_PWM_INL, ch, wfhw->inl);
+	writel(wfhw->wcfg, x->base + X1600_PWM_WCFG(ch));
+	x1600_pwm_set_bit(x, X1600_PWM_MS, ch, false);
+
+	if (wfhw->enabled)
+		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;
+
+	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);
+
+	x->clk = devm_clk_get_enabled(dev, NULL);
+	if (IS_ERR(x->clk))
+		return dev_err_probe(dev, PTR_ERR(x->clk), "Unable to get clock\n");
+
+	chip->ops = &x1600_pwm_ops;
+	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@mgcrea.io>");
+MODULE_DESCRIPTION("Ingenic X1600 PWM driver");
+MODULE_LICENSE("GPL");
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH 2/2] pwm: Add a driver for the Ingenic X1600 PWM controller
  2026-10-25  8:46 ` [PATCH 2/2] pwm: Add a driver for " Olivier Louvignes
@ 2026-10-25  9:03   ` sashiko-bot
  0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-10-25  9:03 UTC (permalink / raw)
  To: Olivier Louvignes; +Cc: devicetree, robh, conor+dt

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] pwm: ingenic-x1600: fix disabled state in x1600_pwm_round_waveform_fromhw()
- [Medium] pwm: ingenic-x1600: missing zero clock rate check in tohw and write callbacks
--

commit 74e98d5b2d04354ee35668eb97dc06066a966e45
Author: Olivier Louvignes <olivier@mgcrea.io>

pwm: Add a driver for the Ingenic X1600 PWM controller

This commit adds a PWM driver for the 8-channel PWM controller found on the
Ingenic X1600 SoC, implementing the PWM waveform callbacks.

> diff --git a/drivers/pwm/pwm-ingenic-x1600.c b/drivers/pwm/pwm-ingenic-x1600.c
> new file mode 100644
> index 0000000000000..83a8d5eec6bff
> --- /dev/null
> +++ b/drivers/pwm/pwm-ingenic-x1600.c
[ ... ]
> +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);
> +	unsigned long rate = clk_get_rate(x->clk);
> +	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;

[Severity: Medium]
Should x1600_pwm_round_waveform_tohw() check for !rate and return -EINVAL just
like x1600_pwm_round_waveform_fromhw() does?

When rate is 0 and wf->period_length_ns > 0, x1600_pwm_counts() returns 0,
which clamps period to 2 with high = 0 and wfhw->enabled = false, returning 1.
In pwm_set_waveform_might_sleep() -> __pwm_set_waveform() with exact == false
and CONFIG_PWM_DEBUG disabled, __pwm_round_waveform_fromhw() is not called, so
x1600_pwm_write_waveform() is invoked with that wfhw and disables the channel
while returning 0 to the caller.

> +
> +	for (prescale = 0; prescale < X1600_PWM_MAX_PRESCALE; prescale++)
> +		if (x1600_pwm_counts(wf->period_length_ns, rate, prescale) <=
> +		    X1600_PWM_MAX_COUNT)
> +			break;
> +
> +	period = min(x1600_pwm_counts(wf->period_length_ns, rate, prescale),
> +		     X1600_PWM_MAX_COUNT);
> +	if (period < 2) {
> +		period = 2;
> +		ret = 1;
> +	}
> +	high = min(x1600_pwm_counts(wf->duty_length_ns, rate, prescale), period);
> +	offset = x1600_pwm_counts(wf->duty_offset_ns, 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);
> +	unsigned long rate = clk_get_rate(x->clk);
> +	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);
> +
> +	if (!rate)
> +		return -EINVAL;
> +
> +	wf->period_length_ns = x1600_pwm_ns(high + low, rate, wfhw->prescale);
> +	wf->duty_length_ns = x1600_pwm_ns(high, rate, wfhw->prescale);
> +	wf->duty_offset_ns = wfhw->enabled && !wfhw->inl ?
> +			     x1600_pwm_ns(low, rate, wfhw->prescale) : 0;

[Severity: Medium]
Does x1600_pwm_round_waveform_fromhw() misreport the waveform when a channel
is disabled in hardware (wfhw->enabled == false)?

In x1600_pwm_read_waveform(), wfhw->enabled is read from X1600_PWM_EN and
wfhw->idl is read from X1600_PWM_IDL:

	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));

However, wfhw->idl is never used in x1600_pwm_round_waveform_fromhw(), and
wf->duty_length_ns is computed unconditionally from high in wfhw->wcfg even
when wfhw->enabled is false.

If a channel was stopped via X1600_PWM_ENC while X1600_PWM_WCFG(ch) still
holds non-zero high and low counts, hardware holds the static idle level from
X1600_PWM_IDL rather than toggling. Because x1600_pwm_round_waveform_fromhw()
still computes wf->period_length_ns and wf->duty_length_ns from wfhw->wcfg,
pwm_device_request() -> pwm_get_state_hw() -> pwm_wf2state() will populate
pwm->state with enabled = true and the stale WCFG period and duty cycle. A
subsequent call to pwm_apply_might_sleep() requesting that same period and
duty cycle will then short-circuit in __pwm_apply() without calling
x1600_pwm_write_waveform() to enable the channel via X1600_PWM_ENS.

> +
> +	return 0;
> +}
[ ... ]
> +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 long rate = clk_get_rate(x->clk);
> +	struct x1600_pwm_wfhw cur;
> +	unsigned int ch = pwm->hwpwm;
> +	u32 busy;
> +
> +	x1600_pwm_read_waveform(chip, pwm, &cur);
> +
> +	/*
> +	 * Running with the same divider and initial level: load the new word,
> +	 * which the channel takes at the end of its period.
> +	 */
> +	if (cur.enabled && wfhw->enabled && cur.prescale == wfhw->prescale &&
> +	    cur.inl == wfhw->inl) {
> +		writel(wfhw->wcfg, x->base + X1600_PWM_WCFG(ch));
> +		writel(BIT(ch), x->base + X1600_PWM_UPT);
> +		return readl_poll_timeout(x->base + X1600_PWM_BUSY, busy,
> +					  !(busy & BIT(ch)), 10,
> +					  DIV_ROUND_UP_ULL(2ULL * USEC_PER_SEC *
> +							   (X1600_PWM_MAX_COUNT << cur.prescale),
> +							   rate));

[Severity: Medium]
Can DIV_ROUND_UP_ULL() divide by zero here in x1600_pwm_write_waveform() if
clk_get_rate(x->clk) returns 0?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261025084643.93167-1-olivier@mgcrea.io?part=2

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-10-25  9:03 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-25  8:46 [PATCH 0/2] pwm: Add the Ingenic X1600 PWM controller Olivier Louvignes
2026-10-25  8:46 ` [PATCH 1/2] dt-bindings: " Olivier Louvignes
2026-10-25  8:46 ` [PATCH 2/2] pwm: Add a driver for " Olivier Louvignes
2026-10-25  9:03   ` sashiko-bot

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.