* [PATCH v3 1/5] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm
2025-10-27 17:11 [PATCH v3 0/5] Add Rockchip RK3576 PWM Support Through MFPWM Nicolas Frattaroli
@ 2025-10-27 17:11 ` Nicolas Frattaroli
2025-10-28 3:06 ` Damon Ding
2025-10-27 17:11 ` [PATCH v3 2/5] mfd: Add Rockchip mfpwm driver Nicolas Frattaroli
` (3 subsequent siblings)
4 siblings, 1 reply; 17+ messages in thread
From: Nicolas Frattaroli @ 2025-10-27 17:11 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio,
Nicolas Frattaroli, Conor Dooley
The Rockchip RK3576 SoC has a newer PWM controller IP revision than
previous Rockchip SoCs. This IP, called "PWMv4" by Rockchip, introduces
several new features, and consequently differs in its bindings.
Instead of expanding the ever-growing rockchip-pwm binding that already
has an if-condition, add an entirely new binding to handle this.
There are two additional clocks, "osc" and "rc". These are available for
every PWM instance, and the PWM hardware can switch between the "pwm",
"osc" and "rc" clock at runtime.
The PWM controller also comes with an interrupt now. This interrupt is
used to signal various conditions.
Reviewed-by: Conor Dooley <conor.dooley@microchip.com>
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
.../bindings/pwm/rockchip,rk3576-pwm.yaml | 77 ++++++++++++++++++++++
MAINTAINERS | 7 ++
2 files changed, 84 insertions(+)
diff --git a/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
new file mode 100644
index 000000000000..48d5055c8b06
--- /dev/null
+++ b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
@@ -0,0 +1,77 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pwm/rockchip,rk3576-pwm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Rockchip PWMv4 controller
+
+maintainers:
+ - Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+
+description: |
+ The Rockchip PWMv4 controller is a PWM controller found on several Rockchip
+ SoCs, such as the RK3576.
+
+ It supports both generating and capturing PWM signals.
+
+allOf:
+ - $ref: pwm.yaml#
+
+properties:
+ compatible:
+ items:
+ - const: rockchip,rk3576-pwm
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: Used to derive the PWM signal.
+ - description: Used as the APB bus clock.
+ - description: Used as an alternative to derive the PWM signal.
+ - description: Used as another alternative to derive the PWM signal.
+
+ clock-names:
+ items:
+ - const: pwm
+ - const: pclk
+ - const: osc
+ - const: rc
+
+ interrupts:
+ maxItems: 1
+
+ "#pwm-cells":
+ const: 3
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - interrupts
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/rockchip,rk3576-cru.h>
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ soc {
+ #address-cells = <2>;
+ #size-cells = <2>;
+
+ pwm@2add0000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add0000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, <&cru CLK_OSC_PWM1>,
+ <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>;
+ #pwm-cells = <3>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 252b06d4240c..baecabab35a2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22366,6 +22366,13 @@ F: Documentation/userspace-api/media/v4l/metafmt-rkisp1.rst
F: drivers/media/platform/rockchip/rkisp1
F: include/uapi/linux/rkisp1-config.h
+ROCKCHIP MFPWM
+M: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+L: linux-rockchip@lists.infradead.org
+L: linux-pwm@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
M: Daniel Golle <daniel@makrotopia.org>
M: Aurelien Jarno <aurelien@aurel32.net>
--
2.51.1
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH v3 1/5] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm
2025-10-27 17:11 ` [PATCH v3 1/5] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm Nicolas Frattaroli
@ 2025-10-28 3:06 ` Damon Ding
2025-10-28 8:50 ` Conor Dooley
0 siblings, 1 reply; 17+ messages in thread
From: Damon Ding @ 2025-10-28 3:06 UTC (permalink / raw)
To: Nicolas Frattaroli, Uwe Kleine-König, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner, Lee Jones,
William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio,
Conor Dooley
Hi Nicolas,
On 10/28/2025 1:11 AM, Nicolas Frattaroli wrote:
> The Rockchip RK3576 SoC has a newer PWM controller IP revision than
> previous Rockchip SoCs. This IP, called "PWMv4" by Rockchip, introduces
> several new features, and consequently differs in its bindings.
>
> Instead of expanding the ever-growing rockchip-pwm binding that already
> has an if-condition, add an entirely new binding to handle this.
>
> There are two additional clocks, "osc" and "rc". These are available for
> every PWM instance, and the PWM hardware can switch between the "pwm",
> "osc" and "rc" clock at runtime.
>
> The PWM controller also comes with an interrupt now. This interrupt is
> used to signal various conditions.
>
> Reviewed-by: Conor Dooley <conor.dooley@microchip.com>
> Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
> Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> ---
> .../bindings/pwm/rockchip,rk3576-pwm.yaml | 77 ++++++++++++++++++++++
> MAINTAINERS | 7 ++
> 2 files changed, 84 insertions(+)
>
> diff --git a/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
> new file mode 100644
> index 000000000000..48d5055c8b06
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
> @@ -0,0 +1,77 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/pwm/rockchip,rk3576-pwm.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: Rockchip PWMv4 controller
> +
> +maintainers:
> + - Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> +
> +description: |
> + The Rockchip PWMv4 controller is a PWM controller found on several Rockchip
> + SoCs, such as the RK3576.
> +
> + It supports both generating and capturing PWM signals.
> +
> +allOf:
> + - $ref: pwm.yaml#
> +
> +properties:
> + compatible:
> + items:
> + - const: rockchip,rk3576-pwm
> +
> + reg:
> + maxItems: 1
> +
> + clocks:
> + items:
> + - description: Used to derive the PWM signal.
> + - description: Used as the APB bus clock.
> + - description: Used as an alternative to derive the PWM signal.
> + - description: Used as another alternative to derive the PWM signal.
> +
> + clock-names:
> + items:
> + - const: pwm
> + - const: pclk
> + - const: osc
> + - const: rc
> +
> + interrupts:
> + maxItems: 1
> +
> + "#pwm-cells":
> + const: 3
> +
> +required:
> + - compatible
> + - reg
> + - clocks
> + - clock-names
> + - interrupts
> +
> +additionalProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/clock/rockchip,rk3576-cru.h>
> + #include <dt-bindings/interrupt-controller/arm-gic.h>
> + #include <dt-bindings/interrupt-controller/irq.h>
> +
> + soc {
> + #address-cells = <2>;
> + #size-cells = <2>;
> +
> + pwm@2add0000 {
> + compatible = "rockchip,rk3576-pwm";
> + reg = <0x0 0x2add0000 0x0 0x1000>;
> + clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, <&cru CLK_OSC_PWM1>,
> + <&cru CLK_RC_PWM1>;
> + clock-names = "pwm", "pclk", "osc", "rc";
> + interrupts = <GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>;
> + #pwm-cells = <3>;
> + };
> + };
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 252b06d4240c..baecabab35a2 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -22366,6 +22366,13 @@ F: Documentation/userspace-api/media/v4l/metafmt-rkisp1.rst
> F: drivers/media/platform/rockchip/rkisp1
> F: include/uapi/linux/rkisp1-config.h
>
> +ROCKCHIP MFPWM
> +M: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> +L: linux-rockchip@lists.infradead.org
> +L: linux-pwm@vger.kernel.org
> +S: Maintained
> +F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
> +
> ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
> M: Daniel Golle <daniel@makrotopia.org>
> M: Aurelien Jarno <aurelien@aurel32.net>
>
The RK3506 and RV1126B platforms that are about to be upstream also use
this PWM IP. Would it be better to name the yaml file
"pwm-rockchip-v4.yaml"? Then subsequent platforms only need to expand
the compatible property.
Best regards,
Damon
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v3 1/5] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm
2025-10-28 3:06 ` Damon Ding
@ 2025-10-28 8:50 ` Conor Dooley
2025-10-28 10:42 ` Damon Ding
0 siblings, 1 reply; 17+ messages in thread
From: Conor Dooley @ 2025-10-28 8:50 UTC (permalink / raw)
To: Damon Ding
Cc: Nicolas Frattaroli, Uwe Kleine-König, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner, Lee Jones,
William Breathitt Gray, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio, Conor Dooley
[-- Attachment #1.1: Type: text/plain, Size: 455 bytes --]
On Tue, Oct 28, 2025 at 11:06:15AM +0800, Damon Ding wrote:
> On 10/28/2025 1:11 AM, Nicolas Frattaroli wrote:
> The RK3506 and RV1126B platforms that are about to be upstream also use this
> PWM IP. Would it be better to name the yaml file "pwm-rockchip-v4.yaml"?
No. Files should be named to match a compatibles.
> Then subsequent platforms only need to expand the compatible property.
That's all subsequent platforms need to do anyway!
[-- Attachment #1.2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
[-- Attachment #2: Type: text/plain, Size: 170 bytes --]
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v3 1/5] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm
2025-10-28 8:50 ` Conor Dooley
@ 2025-10-28 10:42 ` Damon Ding
0 siblings, 0 replies; 17+ messages in thread
From: Damon Ding @ 2025-10-28 10:42 UTC (permalink / raw)
To: Conor Dooley
Cc: Nicolas Frattaroli, Uwe Kleine-König, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner, Lee Jones,
William Breathitt Gray, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio, Conor Dooley
Hi Conor,
On 10/28/2025 4:50 PM, Conor Dooley wrote:
> On Tue, Oct 28, 2025 at 11:06:15AM +0800, Damon Ding wrote:
>> On 10/28/2025 1:11 AM, Nicolas Frattaroli wrote:
>
>> The RK3506 and RV1126B platforms that are about to be upstream also use this
>> PWM IP. Would it be better to name the yaml file "pwm-rockchip-v4.yaml"?
>
> No. Files should be named to match a compatibles.
>
>> Then subsequent platforms only need to expand the compatible property.
>
> That's all subsequent platforms need to do anyway!
Got it.
Best regards,
Damon
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v3 2/5] mfd: Add Rockchip mfpwm driver
2025-10-27 17:11 [PATCH v3 0/5] Add Rockchip RK3576 PWM Support Through MFPWM Nicolas Frattaroli
2025-10-27 17:11 ` [PATCH v3 1/5] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm Nicolas Frattaroli
@ 2025-10-27 17:11 ` Nicolas Frattaroli
2025-10-28 18:52 ` Johan Jonker
2025-10-27 17:11 ` [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver Nicolas Frattaroli
` (2 subsequent siblings)
4 siblings, 1 reply; 17+ messages in thread
From: Nicolas Frattaroli @ 2025-10-27 17:11 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio,
Nicolas Frattaroli
With the Rockchip RK3576, the PWM IP used by Rockchip has changed
substantially. Looking at both the downstream pwm-rockchip driver as
well as the mainline pwm-rockchip driver made it clear that with all its
additional features and its differences from previous IP revisions, it
is best supported in a new driver.
This brings us to the question as to what such a new driver should be.
To me, it soon became clear that it should actually be several new
drivers, most prominently when Uwe Kleine-König let me know that I
should not implement the pwm subsystem's capture callback, but instead
write a counter driver for this functionality.
Combined with the other as-of-yet unimplemented functionality of this
new IP, it became apparent that it needs to be spread across several
subsystems.
For this reason, we add a new MFD core driver, called mfpwm (short for
"Multi-function PWM"). This "parent" driver makes sure that only one
device function driver is using the device at a time, and is in charge
of registering the MFD cell devices for the individual device functions
offered by the device.
An acquire/release pattern is used to guarantee that device function
drivers don't step on each other's toes.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
MAINTAINERS | 2 +
drivers/mfd/Kconfig | 15 ++
drivers/mfd/Makefile | 1 +
drivers/mfd/rockchip-mfpwm.c | 340 +++++++++++++++++++++++++++
include/linux/mfd/rockchip-mfpwm.h | 454 +++++++++++++++++++++++++++++++++++++
5 files changed, 812 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index baecabab35a2..8f3235ba825e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22372,6 +22372,8 @@ L: linux-rockchip@lists.infradead.org
L: linux-pwm@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+F: drivers/soc/rockchip/mfpwm.c
+F: include/soc/rockchip/mfpwm.h
ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
M: Daniel Golle <daniel@makrotopia.org>
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index dbeac6825a10..8b3a3160fbdf 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -1367,6 +1367,21 @@ config MFD_RC5T583
Additional drivers must be enabled in order to use the
different functionality of the device.
+config MFD_ROCKCHIP_MFPWM
+ tristate "Rockchip multi-function PWM controller"
+ depends on OF
+ depends on HAS_IOMEM
+ depends on COMMON_CLK
+ select MFD_CORE
+ help
+ Some Rockchip SoCs, such as the RK3576, use a PWM controller that has
+ several different functions, such as generating PWM waveforms but also
+ counting waveforms.
+
+ This driver manages the overall device, and selects between different
+ functionalities at runtime as needed. Drivers for them are implemented
+ in their respective subsystems.
+
config MFD_RK8XX
tristate
select MFD_CORE
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index e75e8045c28a..ebadbaea9e4a 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -231,6 +231,7 @@ obj-$(CONFIG_MFD_PALMAS) += palmas.o
obj-$(CONFIG_MFD_VIPERBOARD) += viperboard.o
obj-$(CONFIG_MFD_NTXEC) += ntxec.o
obj-$(CONFIG_MFD_RC5T583) += rc5t583.o rc5t583-irq.o
+obj-$(CONFIG_MFD_ROCKCHIP_MFPWM) += rockchip-mfpwm.o
obj-$(CONFIG_MFD_RK8XX) += rk8xx-core.o
obj-$(CONFIG_MFD_RK8XX_I2C) += rk8xx-i2c.o
obj-$(CONFIG_MFD_RK8XX_SPI) += rk8xx-spi.o
diff --git a/drivers/mfd/rockchip-mfpwm.c b/drivers/mfd/rockchip-mfpwm.c
new file mode 100644
index 000000000000..08c2d8da41b7
--- /dev/null
+++ b/drivers/mfd/rockchip-mfpwm.c
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * A driver to manage all the different functionalities exposed by Rockchip's
+ * PWMv4 hardware.
+ *
+ * This driver is chiefly focused on guaranteeing non-concurrent operation
+ * between the different device functions, as well as setting the clocks.
+ * It registers the device function platform devices, e.g. PWM output or
+ * PWM capture.
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#include <linux/array_size.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/rockchip-mfpwm.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/overflow.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+
+/**
+ * struct rockchip_mfpwm - private mfpwm driver instance state struct
+ * @pdev: pointer to this instance's &struct platform_device
+ * @base: pointer to the memory mapped registers of this device
+ * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from
+ * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from
+ * @rc_clk: pointer to the RC oscillator the PWM signal may be derived from
+ * @chosen_clk: a clk-mux of pwm_clk, osc_clk and rc_clk
+ * @pclk: pointer to the APB bus clock needed for mmio register access
+ * @active_func: pointer to the currently active device function, or %NULL if no
+ * device function is currently actively using any of the shared
+ * resources. May only be checked/modified with @state_lock held.
+ * @acquire_cnt: number of times @active_func has currently mfpwm_acquire()'d
+ * it. Must only be checked or modified while holding @state_lock.
+ * @state_lock: this lock is held while either the active device function, the
+ * enable register, or the chosen clock is being changed.
+ * @irq: the IRQ number of this device
+ */
+struct rockchip_mfpwm {
+ struct platform_device *pdev;
+ void __iomem *base;
+ struct clk *pwm_clk;
+ struct clk *osc_clk;
+ struct clk *rc_clk;
+ struct clk *chosen_clk;
+ struct clk *pclk;
+ struct rockchip_mfpwm_func *active_func;
+ unsigned int acquire_cnt;
+ spinlock_t state_lock;
+ int irq;
+};
+
+static atomic_t subdev_id = ATOMIC_INIT(0);
+
+static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev)
+{
+ return platform_get_drvdata(pdev);
+}
+
+static int mfpwm_check_pwmf(const struct rockchip_mfpwm_func *pwmf,
+ const char *fname)
+{
+ struct device *dev = &pwmf->parent->pdev->dev;
+
+ if (IS_ERR_OR_NULL(pwmf)) {
+ dev_warn(dev, "called %s with an erroneous handle, no effect\n",
+ fname);
+ return -EINVAL;
+ }
+
+ if (IS_ERR_OR_NULL(pwmf->parent)) {
+ dev_warn(dev, "called %s with an erroneous mfpwm_func parent, no effect\n",
+ fname);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+__attribute__((nonnull))
+static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
+ unsigned int cnt;
+
+ if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
+ return -EBUSY;
+
+ if (!mfpwm->active_func)
+ mfpwm->active_func = pwmf;
+
+ if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
+ mfpwm->acquire_cnt = cnt;
+ } else {
+ dev_warn(&mfpwm->pdev->dev, "prevented acquire counter overflow in %s\n",
+ __func__);
+ return -EOVERFLOW;
+ }
+
+ dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
+ pwmf->id, mfpwm->acquire_cnt);
+
+ return clk_enable(mfpwm->pclk);
+}
+
+int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm;
+ unsigned long flags;
+ int ret = 0;
+
+ ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
+ if (ret)
+ return ret;
+
+ mfpwm = pwmf->parent;
+ dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id);
+
+ if (!spin_trylock_irqsave(&mfpwm->state_lock, flags))
+ return -EBUSY;
+
+ ret = mfpwm_do_acquire(pwmf);
+
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, "ROCKCHIP_MFPWM");
+
+__attribute__((nonnull))
+static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm = pwmf->parent;
+
+ if (!mfpwm->active_func)
+ return;
+
+ if (mfpwm->active_func->id != pwmf->id)
+ return;
+
+ /*
+ * No need to check_sub_overflow here, !mfpwm->active_func above catches
+ * this type of problem already.
+ */
+ mfpwm->acquire_cnt--;
+
+ if (!mfpwm->acquire_cnt)
+ mfpwm->active_func = NULL;
+
+ clk_disable(mfpwm->pclk);
+}
+
+void mfpwm_release(const struct rockchip_mfpwm_func *pwmf)
+{
+ struct rockchip_mfpwm *mfpwm;
+ unsigned long flags;
+
+ if (mfpwm_check_pwmf(pwmf, "mfpwm_release"))
+ return;
+
+ mfpwm = pwmf->parent;
+
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
+ mfpwm_do_release(pwmf);
+ dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n",
+ pwmf->id, mfpwm->acquire_cnt);
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_release, "ROCKCHIP_MFPWM");
+
+/**
+ * mfpwm_register_subdev - register a single mfpwm_func
+ * @mfpwm: pointer to the parent &struct rockchip_mfpwm
+ * @name: sub-device name string
+ *
+ * Allocate a single &struct mfpwm_func, fill its members with appropriate data,
+ * and register a new mfd cell.
+ *
+ * Returns: 0 on success, negative errno on error
+ */
+static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm,
+ const char *name)
+{
+ struct rockchip_mfpwm_func *func;
+ struct mfd_cell cell = {};
+
+ func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL);
+ if (IS_ERR(func))
+ return PTR_ERR(func);
+ func->irq = mfpwm->irq;
+ func->parent = mfpwm;
+ func->id = atomic_inc_return(&subdev_id);
+ func->base = mfpwm->base;
+ func->core = mfpwm->chosen_clk;
+ cell.name = name;
+ cell.platform_data = func;
+ cell.pdata_size = sizeof(*func);
+ // cell.ignore_resource_conflicts = true;
+ // cell.resources = mfpwm->pdev->resource;
+ // cell.num_resources = mfpwm->pdev->num_resources;
+
+ return devm_mfd_add_devices(&mfpwm->pdev->dev, func->id, &cell, 1, NULL,
+ 0, NULL);
+}
+
+static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm)
+{
+ int ret;
+
+ ret = mfpwm_register_subdev(mfpwm, "pwm-rockchip-v4");
+ if (ret)
+ return ret;
+
+ ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-capture");
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int rockchip_mfpwm_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct rockchip_mfpwm *mfpwm;
+ char *clk_mux_name;
+ const char *mux_p_names[3];
+ int ret = 0;
+
+ mfpwm = devm_kzalloc(&pdev->dev, sizeof(*mfpwm), GFP_KERNEL);
+ if (IS_ERR(mfpwm))
+ return PTR_ERR(mfpwm);
+
+ mfpwm->pdev = pdev;
+
+ spin_lock_init(&mfpwm->state_lock);
+
+ mfpwm->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(mfpwm->base))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->base),
+ "failed to ioremap address\n");
+
+ mfpwm->pclk = devm_clk_get_prepared(dev, "pclk");
+ if (IS_ERR(mfpwm->pclk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pclk),
+ "couldn't get and prepare 'pclk' clock\n");
+
+ mfpwm->irq = platform_get_irq(pdev, 0);
+ if (mfpwm->irq < 0)
+ return dev_err_probe(dev, mfpwm->irq, "couldn't get irq 0\n");
+
+ mfpwm->pwm_clk = devm_clk_get_prepared(dev, "pwm");
+ if (IS_ERR(mfpwm->pwm_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk),
+ "couldn't get and prepare 'pwm' clock\n");
+
+ mfpwm->osc_clk = devm_clk_get_prepared(dev, "osc");
+ if (IS_ERR(mfpwm->osc_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->osc_clk),
+ "couldn't get and prepare 'osc' clock\n");
+
+ mfpwm->rc_clk = devm_clk_get_prepared(dev, "rc");
+ if (IS_ERR(mfpwm->rc_clk))
+ return dev_err_probe(dev, PTR_ERR(mfpwm->rc_clk),
+ "couldn't get and prepare 'rc' clock\n");
+
+ clk_mux_name = devm_kasprintf(dev, GFP_KERNEL, "%s_chosen", dev_name(dev));
+ if (!clk_mux_name)
+ return -ENOMEM;
+
+ mux_p_names[0] = __clk_get_name(mfpwm->pwm_clk);
+ mux_p_names[1] = __clk_get_name(mfpwm->osc_clk);
+ mux_p_names[2] = __clk_get_name(mfpwm->rc_clk);
+ mfpwm->chosen_clk = clk_register_mux(dev, clk_mux_name, mux_p_names,
+ ARRAY_SIZE(mux_p_names),
+ CLK_SET_RATE_PARENT,
+ mfpwm->base + PWMV4_REG_CLK_CTRL,
+ PWMV4_CLK_SRC_SHIFT, PWMV4_CLK_SRC_WIDTH,
+ CLK_MUX_HIWORD_MASK, NULL);
+ ret = clk_prepare(mfpwm->chosen_clk);
+ if (ret) {
+ dev_err(dev, "failed to prepare PWM clock mux: %pe\n",
+ ERR_PTR(ret));
+ return ret;
+ }
+
+ platform_set_drvdata(pdev, mfpwm);
+
+ ret = mfpwm_register_subdevs(mfpwm);
+ if (ret) {
+ dev_err(dev, "failed to register sub-devices: %pe\n",
+ ERR_PTR(ret));
+ return ret;
+ }
+
+ return ret;
+}
+
+static void rockchip_mfpwm_remove(struct platform_device *pdev)
+{
+ struct rockchip_mfpwm *mfpwm = to_rockchip_mfpwm(pdev);
+ unsigned long flags;
+
+ spin_lock_irqsave(&mfpwm->state_lock, flags);
+
+ if (mfpwm->chosen_clk) {
+ clk_unprepare(mfpwm->chosen_clk);
+ clk_unregister_mux(mfpwm->chosen_clk);
+ }
+
+ spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+
+static const struct of_device_id rockchip_mfpwm_of_match[] = {
+ {
+ .compatible = "rockchip,rk3576-pwm",
+ },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rockchip_mfpwm_of_match);
+
+static struct platform_driver rockchip_mfpwm_driver = {
+ .driver = {
+ .name = KBUILD_MODNAME,
+ .of_match_table = rockchip_mfpwm_of_match,
+ },
+ .probe = rockchip_mfpwm_probe,
+ .remove = rockchip_mfpwm_remove,
+};
+module_platform_driver(rockchip_mfpwm_driver);
+
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
+MODULE_DESCRIPTION("Rockchip MFPWM Driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/rockchip-mfpwm.h b/include/linux/mfd/rockchip-mfpwm.h
new file mode 100644
index 000000000000..78d9c3396f7e
--- /dev/null
+++ b/include/linux/mfd/rockchip-mfpwm.h
@@ -0,0 +1,454 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * Common header file for all the Rockchip Multi-function PWM controller
+ * drivers that are spread across subsystems.
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#ifndef __SOC_ROCKCHIP_MFPWM_H__
+#define __SOC_ROCKCHIP_MFPWM_H__
+
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/hw_bitfield.h>
+#include <linux/io.h>
+#include <linux/spinlock.h>
+
+struct rockchip_mfpwm;
+
+/**
+ * struct rockchip_mfpwm_func - struct representing a single function driver
+ *
+ * @id: unique id for this function driver instance
+ * @base: pointer to start of MMIO registers
+ * @parent: a pointer to the parent mfpwm struct
+ * @irq: the shared IRQ gotten from the parent mfpwm device
+ * @core: a pointer to the clk mux that drives this channel's PWM
+ */
+struct rockchip_mfpwm_func {
+ int id;
+ void __iomem *base;
+ struct rockchip_mfpwm *parent;
+ int irq;
+ struct clk *core;
+};
+
+/*
+ * PWMV4 Register Definitions
+ * --------------------------
+ *
+ * Attributes:
+ * RW - Read-Write
+ * RO - Read-Only
+ * WO - Write-Only
+ * W1T - Write high, Self-clearing
+ * W1C - Write high to clear interrupt
+ *
+ * Bit ranges to be understood with Verilog-like semantics,
+ * e.g. [03:00] is 4 bits: 0, 1, 2 and 3.
+ *
+ * All registers must be accessed with 32-bit width accesses only
+ */
+
+#define PWMV4_REG_VERSION 0x000
+/*
+ * VERSION Register Description
+ * [31:24] RO | Hardware Major Version
+ * [23:16] RO | Hardware Minor Version
+ * [15:15] RO | Reserved
+ * [14:14] RO | Hardware supports biphasic counters
+ * [13:13] RO | Hardware supports filters
+ * [12:12] RO | Hardware supports waveform generation
+ * [11:11] RO | Hardware supports counter
+ * [10:10] RO | Hardware supports frequency metering
+ * [09:09] RO | Hardware supports power key functionality
+ * [08:08] RO | Hardware supports infrared transmissions
+ * [07:04] RO | Channel index of this instance
+ * [03:00] RO | Number of channels the base instance supports
+ */
+
+#define PWMV4_REG_ENABLE 0x004
+/*
+ * ENABLE Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:06] RO | Reserved
+ * [05:05] RW | PWM Channel Counter Read Enable, 1 = enabled
+ */
+#define PWMV4_CHN_CNT_RD_EN(v) FIELD_PREP_WM16(BIT(5), (v))
+/*
+ * [04:04] W1T | PWM Globally Joined Control Enable
+ * 1 = this PWM channel will be enabled by a global pwm enable
+ * bit instead of the PWM Enable bit.
+ */
+#define PWMV4_GLOBAL_CTRL_EN(v) FIELD_PREP_WM16(BIT(4), (v))
+/*
+ * [03:03] RW | Force Clock Enable
+ * 0 = disabled, if the PWM channel is inactive then so is the
+ * clock prescale module
+ */
+#define PWMV4_FORCE_CLK_EN(v) FIELD_PREP_WM16(BIT(3), (v))
+/*
+ * [02:02] W1T | PWM Control Update Enable
+ * 1 = enabled, commits modifications of _CTRL, _PERIOD, _DUTY and
+ * _OFFSET registers once 1 is written to it
+ */
+#define PWMV4_CTRL_UPDATE_EN FIELD_PREP_WM16_CONST(BIT(2), 1)
+/*
+ * [01:01] RW | PWM Enable, 1 = enabled
+ * If in one-shot mode, clears after end of operation
+ */
+#define PWMV4_EN_MASK BIT(1)
+#define PWMV4_EN(v) FIELD_PREP_WM16(PWMV4_EN_MASK, \
+ ((v) ? 1 : 0))
+/*
+ * [00:00] RW | PWM Clock Enable, 1 = enabled
+ * If in one-shot mode, clears after end of operation
+ */
+#define PWMV4_CLK_EN_MASK BIT(0)
+#define PWMV4_CLK_EN(v) FIELD_PREP_WM16(PWMV4_CLK_EN_MASK, \
+ ((v) ? 1 : 0))
+#define PWMV4_EN_BOTH_MASK (PWMV4_EN_MASK | PWMV4_CLK_EN_MASK)
+static inline __pure bool rockchip_pwm_v4_is_enabled(unsigned int val)
+{
+ return (val & PWMV4_EN_BOTH_MASK);
+}
+
+#define PWMV4_REG_CLK_CTRL 0x008
+/*
+ * CLK_CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:15] RW | Clock Global Selection
+ * 0 = current channel scale clock
+ * 1 = global channel scale clock
+ */
+#define PWMV4_CLK_GLOBAL(v) FIELD_PREP_WM16(BIT(15), (v))
+/*
+ * [14:13] RW | Clock Source Selection
+ * 0 = Clock from PLL, frequency can be configured
+ * 1 = Clock from crystal oscillator, frequency is fixed
+ * 2 = Clock from RC oscillator, frequency is fixed
+ * 3 = Reserved
+ * NOTE: The purpose for this clock-mux-outside-CRU construct is
+ * to let the SoC go into a sleep state with the PWM
+ * hardware still having a clock signal for IR input, which
+ * can then wake up the SoC.
+ */
+#define PWMV4_CLK_SRC_PLL 0x0U
+#define PWMV4_CLK_SRC_CRYSTAL 0x1U
+#define PWMV4_CLK_SRC_RC 0x2U
+#define PWMV4_CLK_SRC_SHIFT 13
+#define PWMV4_CLK_SRC_WIDTH 2
+/*
+ * [12:04] RW | Scale Factor to apply to pre-scaled clock
+ * 1 <= v <= 256, v means clock divided by 2*v
+ */
+#define PWMV4_CLK_SCALE_F(v) FIELD_PREP_WM16(GENMASK(12, 4), (v))
+/*
+ * [03:03] RO | Reserved
+ * [02:00] RW | Prescale Factor
+ * v here means the input clock is divided by pow(2, v)
+ */
+#define PWMV4_CLK_PRESCALE_F(v) FIELD_PREP_WM16(GENMASK(2, 0), (v))
+
+#define PWMV4_REG_CTRL 0x00C
+/*
+ * CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:09] RO | Reserved
+ * [08:06] RW | PWM Input Channel Selection
+ * By default, the channel selects its own input, but writing v
+ * here selects PWM input from channel v instead.
+ */
+#define PWMV4_CTRL_IN_SEL(v) FIELD_PREP_WM16(GENMASK(8, 6), (v))
+/* [05:05] RW | Aligned Mode, 0 = Valid, 1 = Invalid */
+#define PWMV4_CTRL_UNALIGNED(v) FIELD_PREP_WM16(BIT(5), (v))
+/* [04:04] RW | Output Mode, 0 = Left Aligned, 1 = Centre Aligned */
+#define PWMV4_LEFT_ALIGNED 0x0U
+#define PWMV4_CENTRE_ALIGNED 0x1U
+#define PWMV4_CTRL_OUT_MODE(v) FIELD_PREP_WM16(BIT(4), (v))
+/*
+ * [03:03] RW | Inactive Polarity for when the channel is either disabled or
+ * has completed outputting the entire waveform in one-shot mode.
+ * 0 = Negative, 1 = Positive
+ */
+#define PWMV4_POLARITY_N 0x0U
+#define PWMV4_POLARITY_P 0x1U
+#define PWMV4_INACTIVE_POL(v) FIELD_PREP_WM16(BIT(3), (v))
+/*
+ * [02:02] RW | Duty Cycle Polarity to use at the start of the waveform.
+ * 0 = Negative, 1 = Positive
+ */
+#define PWMV4_DUTY_POL_SHIFT 2
+#define PWMV4_DUTY_POL_MASK BIT(PWMV4_DUTY_POL_SHIFT)
+#define PWMV4_DUTY_POL(v) FIELD_PREP_WM16(PWMV4_DUTY_POL_MASK, \
+ (v))
+/*
+ * [01:00] RW | PWM Mode
+ * 0 = One-shot mode, PWM generates waveform RPT times
+ * 1 = Continuous mode
+ * 2 = Capture mode, PWM measures cycles of input waveform
+ * 3 = Reserved
+ */
+#define PWMV4_MODE_ONESHOT 0x0U
+#define PWMV4_MODE_CONT 0x1U
+#define PWMV4_MODE_CAPTURE 0x2U
+#define PWMV4_MODE_MASK GENMASK(1, 0)
+#define PWMV4_MODE(v) FIELD_PREP_WM16(PWMV4_MODE_MASK, (v))
+#define PWMV4_CTRL_COM_FLAGS (PWMV4_INACTIVE_POL(PWMV4_POLARITY_N) | \
+ PWMV4_DUTY_POL(PWMV4_POLARITY_P) | \
+ PWMV4_CTRL_OUT_MODE(PWMV4_LEFT_ALIGNED) | \
+ PWMV4_CTRL_UNALIGNED(true))
+#define PWMV4_CTRL_CONT_FLAGS (PWMV4_MODE(PWMV4_MODE_CONT) | \
+ PWMV4_CTRL_COM_FLAGS)
+#define PWMV4_CTRL_CAP_FLAGS (PWMV4_MODE(PWMV4_MODE_CAPTURE) | \
+ PWMV4_CTRL_COM_FLAGS)
+
+#define PWMV4_REG_PERIOD 0x010
+/*
+ * PERIOD Register Description
+ * [31:00] RW | Period of the output waveform
+ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
+ */
+
+#define PWMV4_REG_DUTY 0x014
+/*
+ * DUTY Register Description
+ * [31:00] RW | Duty cycle of the output waveform
+ * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
+ */
+
+#define PWMV4_REG_OFFSET 0x018
+/*
+ * OFFSET Register Description
+ * [31:00] RW | Offset of the output waveform, based on the PWM clock
+ * Constraints: 0 <= v <= (PERIOD - DUTY)
+ */
+
+#define PWMV4_REG_RPT 0x01C
+/*
+ * RPT Register Description
+ * [31:16] RW | Second dimensional of the effective number of waveform
+ * repetitions. Increases by one every first dimensional times.
+ * Value `n` means `n + 1` repetitions. The final number of
+ * repetitions of the waveform in one-shot mode is:
+ * `(first_dimensional + 1) * (second_dimensional + 1)`
+ * [15:00] RW | First dimensional of the effective number of waveform
+ * repetitions. Value `n` means `n + 1` repetitions.
+ */
+
+#define PWMV4_REG_FILTER_CTRL 0x020
+/*
+ * FILTER_CTRL Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:04] RW | Filter window number
+ * [03:01] RO | Reserved
+ * [00:00] RW | Filter Enable, 0 = disabled, 1 = enabled
+ */
+
+#define PWMV4_REG_CNT 0x024
+/*
+ * CNT Register Description
+ * [31:00] RO | Current value of the PWM Channel 0 counter in pwm clock cycles,
+ * 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_ENABLE_DELAY 0x028
+/*
+ * ENABLE_DELAY Register Description
+ * [31:16] RO | Reserved
+ * [15:00] RW | PWM enable delay, in an unknown unit but probably cycles
+ */
+
+#define PWMV4_REG_HPC 0x02C
+/*
+ * HPC Register Description
+ * [31:00] RW | Number of effective high polarity cycles of the input waveform
+ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_LPC 0x030
+/*
+ * LPC Register Description
+ * [31:00] RW | Number of effective low polarity cycles of the input waveform
+ * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_CTRL0 0x040
+/*
+ * BIPHASIC_CNT_CTRL0 Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Phase Edge Selection for mode 0,
+ * 0 = rising edge (posedge), 1 = falling edge (negedge)
+ * [08:08] RW | Biphasic Counter Clock force enable, 1 = force enable
+ * [07:07] W1T | Synchronous Enable
+ * [06:06] W1T | Mode Switch
+ * 0 = Normal Mode, 1 = Switch timer clock and measured clock
+ * Constraints: "Biphasic Counter Mode" must be 0 if this is 1
+ * [05:03] RW | Biphasic Counter Mode
+ * 0x0 = Mode 0, 0x1 = Mode 1, 0x2 = Mode 2, 0x3 = Mode 3,
+ * 0x4 = Mode 4, 0x5 = Reserved
+ * [02:02] RW | Biphasic Counter Clock Selection
+ * 0 = clock is from PLL and frequency can be configured
+ * 1 = clock is from crystal oscillator and frequency is fixed
+ * [01:01] RW | Biphasic Counter Continuous Mode
+ * [00:00] W1T | Biphasic Counter Enable
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_CTRL1 0x044
+/*
+ * BIPHASIC_CNT_CTRL1 Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:11] RO | Reserved
+ * [10:04] RW | Biphasic Counter Filter Window Number
+ * [03:01] RO | Reserved
+ * [00:00] RW | Biphasic Counter Filter Enable
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_TIMER 0x048
+/*
+ * BIPHASIC_CNT_TIMER Register Description
+ * [31:00] RW | Biphasic Counter Timer Value, in number of biphasic counter
+ * timer clock cycles
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_RES 0x04C
+/*
+ * BIPHASIC_CNT_RES Register Description
+ * [31:00] RO | Biphasic Counter Result Value
+ * Constraints: Can only be read after INTSTS[9] is asserted
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_RES_S 0x050
+/*
+ * BIPHASIC_CNT_RES_S Register Description
+ * [31:00] RO | Biphasic Counter Result Value with synchronised processing
+ * Can be read in real-time if BIPHASIC_CNT_CTRL0[7] was set to 1
+ */
+
+#define PWMV4_REG_INTSTS 0x070
+/*
+ * INTSTS Register Description
+ * [31:10] RO | Reserved
+ * [09:09] W1C | Biphasic Counter Interrupt Status, 1 = interrupt asserted
+ * [08:08] W1C | Waveform Middle Interrupt Status, 1 = interrupt asserted
+ * [07:07] W1C | Waveform Max Interrupt Status, 1 = interrupt asserted
+ * [06:06] W1C | IR Transmission End Interrupt Status, 1 = interrupt asserted
+ * [05:05] W1C | Power Key Match Interrupt Status, 1 = interrupt asserted
+ * [04:04] W1C | Frequency Meter Interrupt Status, 1 = interrupt asserted
+ * [03:03] W1C | Reload Interrupt Status, 1 = interrupt asserted
+ * [02:02] W1C | Oneshot End Interrupt Status, 1 = interrupt asserted
+ * [01:01] W1C | HPC Capture Interrupt Status, 1 = interrupt asserted
+ * [00:00] W1C | LPC Capture Interrupt Status, 1 = interrupt asserted
+ */
+#define PWMV4_INT_LPC BIT(0)
+#define PWMV4_INT_HPC BIT(1)
+#define PWMV4_INT_LPC_W(v) FIELD_PREP_WM16(PWMV4_INT_LPC, \
+ ((v) ? 1 : 0))
+#define PWMV4_INT_HPC_W(v) FIELD_PREP_WM16(PWMV4_INT_HPC, \
+ ((v) ? 1 : 0))
+
+#define PWMV4_REG_INT_EN 0x074
+/*
+ * INT_EN Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Interrupt Enable, 1 = enabled
+ * [08:08] W1C | Waveform Middle Interrupt Enable, 1 = enabled
+ * [07:07] W1C | Waveform Max Interrupt Enable, 1 = enabled
+ * [06:06] W1C | IR Transmission End Interrupt Enable, 1 = enabled
+ * [05:05] W1C | Power Key Match Interrupt Enable, 1 = enabled
+ * [04:04] W1C | Frequency Meter Interrupt Enable, 1 = enabled
+ * [03:03] W1C | Reload Interrupt Enable, 1 = enabled
+ * [02:02] W1C | Oneshot End Interrupt Enable, 1 = enabled
+ * [01:01] W1C | HPC Capture Interrupt Enable, 1 = enabled
+ * [00:00] W1C | LPC Capture Interrupt Enable, 1 = enabled
+ */
+
+#define PWMV4_REG_INT_MASK 0x078
+/*
+ * INT_MASK Register Description
+ * [31:16] WO | Write Enable Mask for the lower half of the register
+ * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ * the same write operation
+ * [15:10] RO | Reserved
+ * [09:09] RW | Biphasic Counter Interrupt Masked, 1 = masked
+ * [08:08] W1C | Waveform Middle Interrupt Masked, 1 = masked
+ * [07:07] W1C | Waveform Max Interrupt Masked, 1 = masked
+ * [06:06] W1C | IR Transmission End Interrupt Masked, 1 = masked
+ * [05:05] W1C | Power Key Match Interrupt Masked, 1 = masked
+ * [04:04] W1C | Frequency Meter Interrupt Masked, 1 = masked
+ * [03:03] W1C | Reload Interrupt Masked, 1 = masked
+ * [02:02] W1C | Oneshot End Interrupt Masked, 1 = masked
+ * [01:01] W1C | HPC Capture Interrupt Masked, 1 = masked
+ * [00:00] W1C | LPC Capture Interrupt Masked, 1 = masked
+ */
+
+static inline u32 mfpwm_reg_read(void __iomem *base, u32 reg)
+{
+ return readl(base + reg);
+}
+
+static inline void mfpwm_reg_write(void __iomem *base, u32 reg, u32 val)
+{
+ writel(val, base + reg);
+}
+
+/**
+ * mfpwm_acquire - try becoming the active mfpwm function device
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * mfpwm device "function" drivers must call this function before doing anything
+ * that either modifies or relies on the parent device's state, such as clocks,
+ * enabling/disabling outputs, modifying shared regs etc.
+ *
+ * The return statues should always be checked.
+ *
+ * All mfpwm_acquire() calls must be balanced with corresponding mfpwm_release()
+ * calls once the device is no longer making changes that affect other devices,
+ * or stops producing user-visible effects that depend on the current device
+ * state being kept as-is. (e.g. after the PWM output signal is stopped)
+ *
+ * The same device function may mfpwm_acquire() multiple times while it already
+ * is active, i.e. it is re-entrant, though it needs to balance this with the
+ * same number of mfpwm_release() calls.
+ *
+ * Context: This function does not sleep.
+ *
+ * Return:
+ * * %0 - success
+ * * %-EBUSY - a different device function is active
+ * * %-EOVERFLOW - the acquire counter is at its maximum
+ */
+extern int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_release - drop usage of active mfpwm device function by 1
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * This is the balancing call to mfpwm_acquire(). If no users of the device
+ * function remain, set the mfpwm device to have no active device function,
+ * allowing other device functions to claim it.
+ */
+extern void mfpwm_release(const struct rockchip_mfpwm_func *pwmf);
+
+#endif /* __SOC_ROCKCHIP_MFPWM_H__ */
--
2.51.1
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH v3 2/5] mfd: Add Rockchip mfpwm driver
2025-10-27 17:11 ` [PATCH v3 2/5] mfd: Add Rockchip mfpwm driver Nicolas Frattaroli
@ 2025-10-28 18:52 ` Johan Jonker
2025-10-31 12:20 ` Nicolas Frattaroli
2025-11-03 15:25 ` Lee Jones
0 siblings, 2 replies; 17+ messages in thread
From: Johan Jonker @ 2025-10-28 18:52 UTC (permalink / raw)
To: Nicolas Frattaroli, Uwe Kleine-König, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner, Lee Jones,
William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio
On 10/27/25 18:11, Nicolas Frattaroli wrote:
> With the Rockchip RK3576, the PWM IP used by Rockchip has changed
> substantially. Looking at both the downstream pwm-rockchip driver as
> well as the mainline pwm-rockchip driver made it clear that with all its
> additional features and its differences from previous IP revisions, it
> is best supported in a new driver.
>
> This brings us to the question as to what such a new driver should be.
> To me, it soon became clear that it should actually be several new
> drivers, most prominently when Uwe Kleine-König let me know that I
> should not implement the pwm subsystem's capture callback, but instead
> write a counter driver for this functionality.
>
> Combined with the other as-of-yet unimplemented functionality of this
> new IP, it became apparent that it needs to be spread across several
> subsystems.
>
> For this reason, we add a new MFD core driver, called mfpwm (short for
> "Multi-function PWM"). This "parent" driver makes sure that only one
> device function driver is using the device at a time, and is in charge
> of registering the MFD cell devices for the individual device functions
> offered by the device.
>
> An acquire/release pattern is used to guarantee that device function
> drivers don't step on each other's toes.
>
> Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> ---
> MAINTAINERS | 2 +
> drivers/mfd/Kconfig | 15 ++
> drivers/mfd/Makefile | 1 +
> drivers/mfd/rockchip-mfpwm.c | 340 +++++++++++++++++++++++++++
> include/linux/mfd/rockchip-mfpwm.h | 454 +++++++++++++++++++++++++++++++++++++
> 5 files changed, 812 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index baecabab35a2..8f3235ba825e 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -22372,6 +22372,8 @@ L: linux-rockchip@lists.infradead.org
> L: linux-pwm@vger.kernel.org
> S: Maintained
> F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
A question not so much for Nicolas specific:
The yaml documents already have a 'maintainers' entry.
However MAINTAINERS is full yaml entries.
Could someone explain why we still need dual registration?
maintainers:
- Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> +F: drivers/soc/rockchip/mfpwm.c
> +F: include/soc/rockchip/mfpwm.h
different file name and location?
drivers/mfd/rockchip-mfpwm.c | 340 +++++++++++++++++++++++++++
include/linux/mfd/rockchip-mfpwm.h | 454 +++++++++++++++++++++++++++++++++++++
>
> ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
> M: Daniel Golle <daniel@makrotopia.org>
> diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
> index dbeac6825a10..8b3a3160fbdf 100644
> --- a/drivers/mfd/Kconfig
> +++ b/drivers/mfd/Kconfig
> @@ -1367,6 +1367,21 @@ config MFD_RC5T583
> Additional drivers must be enabled in order to use the
> different functionality of the device.
>
> +config MFD_ROCKCHIP_MFPWM
> + tristate "Rockchip multi-function PWM controller"
> + depends on OF
> + depends on HAS_IOMEM
> + depends on COMMON_CLK
> + select MFD_CORE
> + help
> + Some Rockchip SoCs, such as the RK3576, use a PWM controller that has
> + several different functions, such as generating PWM waveforms but also
> + counting waveforms.
> +
> + This driver manages the overall device, and selects between different
> + functionalities at runtime as needed. Drivers for them are implemented
> + in their respective subsystems.
> +
> config MFD_RK8XX
> tristate
> select MFD_CORE
> diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
> index e75e8045c28a..ebadbaea9e4a 100644
> --- a/drivers/mfd/Makefile
> +++ b/drivers/mfd/Makefile
> @@ -231,6 +231,7 @@ obj-$(CONFIG_MFD_PALMAS) += palmas.o
> obj-$(CONFIG_MFD_VIPERBOARD) += viperboard.o
> obj-$(CONFIG_MFD_NTXEC) += ntxec.o
> obj-$(CONFIG_MFD_RC5T583) += rc5t583.o rc5t583-irq.o
> +obj-$(CONFIG_MFD_ROCKCHIP_MFPWM) += rockchip-mfpwm.o
> obj-$(CONFIG_MFD_RK8XX) += rk8xx-core.o
> obj-$(CONFIG_MFD_RK8XX_I2C) += rk8xx-i2c.o
> obj-$(CONFIG_MFD_RK8XX_SPI) += rk8xx-spi.o
> diff --git a/drivers/mfd/rockchip-mfpwm.c b/drivers/mfd/rockchip-mfpwm.c
> new file mode 100644
> index 000000000000..08c2d8da41b7
> --- /dev/null
> +++ b/drivers/mfd/rockchip-mfpwm.c
> @@ -0,0 +1,340 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) 2025 Collabora Ltd.
> + *
> + * A driver to manage all the different functionalities exposed by Rockchip's
> + * PWMv4 hardware.
> + *
> + * This driver is chiefly focused on guaranteeing non-concurrent operation
> + * between the different device functions, as well as setting the clocks.
> + * It registers the device function platform devices, e.g. PWM output or
> + * PWM capture.
> + *
> + * Authors:
> + * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> + */
> +
> +#include <linux/array_size.h>
> +#include <linux/clk.h>
> +#include <linux/clk-provider.h>
> +#include <linux/mfd/core.h>
> +#include <linux/mfd/rockchip-mfpwm.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/overflow.h>
> +#include <linux/platform_device.h>
> +#include <linux/spinlock.h>
> +
> +/**
> + * struct rockchip_mfpwm - private mfpwm driver instance state struct
> + * @pdev: pointer to this instance's &struct platform_device
> + * @base: pointer to the memory mapped registers of this device
> + * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from
> + * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from
> + * @rc_clk: pointer to the RC oscillator the PWM signal may be derived from
> + * @chosen_clk: a clk-mux of pwm_clk, osc_clk and rc_clk
> + * @pclk: pointer to the APB bus clock needed for mmio register access
> + * @active_func: pointer to the currently active device function, or %NULL if no
> + * device function is currently actively using any of the shared
> + * resources. May only be checked/modified with @state_lock held.
> + * @acquire_cnt: number of times @active_func has currently mfpwm_acquire()'d
> + * it. Must only be checked or modified while holding @state_lock.
> + * @state_lock: this lock is held while either the active device function, the
> + * enable register, or the chosen clock is being changed.
> + * @irq: the IRQ number of this device
> + */
> +struct rockchip_mfpwm {
> + struct platform_device *pdev;
> + void __iomem *base;
> + struct clk *pwm_clk;
> + struct clk *osc_clk;
> + struct clk *rc_clk;
> + struct clk *chosen_clk;
> + struct clk *pclk;
> + struct rockchip_mfpwm_func *active_func;
> + unsigned int acquire_cnt;
> + spinlock_t state_lock;
> + int irq;
> +};
> +
> +static atomic_t subdev_id = ATOMIC_INIT(0);
> +
> +static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev)
> +{
> + return platform_get_drvdata(pdev);
> +}
> +
> +static int mfpwm_check_pwmf(const struct rockchip_mfpwm_func *pwmf,
> + const char *fname)
> +{
> + struct device *dev = &pwmf->parent->pdev->dev;
> +
> + if (IS_ERR_OR_NULL(pwmf)) {
> + dev_warn(dev, "called %s with an erroneous handle, no effect\n",
> + fname);
> + return -EINVAL;
> + }
> +
> + if (IS_ERR_OR_NULL(pwmf->parent)) {
> + dev_warn(dev, "called %s with an erroneous mfpwm_func parent, no effect\n",
> + fname);
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +__attribute__((nonnull))
> +static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
> +{
> + struct rockchip_mfpwm *mfpwm = pwmf->parent;
> + unsigned int cnt;
> +
> + if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
> + return -EBUSY;
> +
> + if (!mfpwm->active_func)
> + mfpwm->active_func = pwmf;
> +
> + if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
> + mfpwm->acquire_cnt = cnt;
> + } else {
> + dev_warn(&mfpwm->pdev->dev, "prevented acquire counter overflow in %s\n",
> + __func__);
> + return -EOVERFLOW;
> + }
> +
> + dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
> + pwmf->id, mfpwm->acquire_cnt);
> +
> + return clk_enable(mfpwm->pclk);
> +}
> +
> +int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf)
> +{
> + struct rockchip_mfpwm *mfpwm;
> + unsigned long flags;
> + int ret = 0;
> +
> + ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
> + if (ret)
> + return ret;
> +
> + mfpwm = pwmf->parent;
> + dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id);
> +
> + if (!spin_trylock_irqsave(&mfpwm->state_lock, flags))
> + return -EBUSY;
> +
> + ret = mfpwm_do_acquire(pwmf);
> +
> + spin_unlock_irqrestore(&mfpwm->state_lock, flags);
> +
> + return ret;
> +}
> +EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, "ROCKCHIP_MFPWM");
> +
> +__attribute__((nonnull))
> +static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf)
> +{
> + struct rockchip_mfpwm *mfpwm = pwmf->parent;
> +
> + if (!mfpwm->active_func)
> + return;
> +
> + if (mfpwm->active_func->id != pwmf->id)
> + return;
> +
> + /*
> + * No need to check_sub_overflow here, !mfpwm->active_func above catches
> + * this type of problem already.
> + */
> + mfpwm->acquire_cnt--;
> +
> + if (!mfpwm->acquire_cnt)
> + mfpwm->active_func = NULL;
> +
> + clk_disable(mfpwm->pclk);
> +}
> +
> +void mfpwm_release(const struct rockchip_mfpwm_func *pwmf)
> +{
> + struct rockchip_mfpwm *mfpwm;
> + unsigned long flags;
> +
> + if (mfpwm_check_pwmf(pwmf, "mfpwm_release"))
> + return;
> +
> + mfpwm = pwmf->parent;
> +
> + spin_lock_irqsave(&mfpwm->state_lock, flags);
> + mfpwm_do_release(pwmf);
> + dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n",
> + pwmf->id, mfpwm->acquire_cnt);
> + spin_unlock_irqrestore(&mfpwm->state_lock, flags);
> +}
> +EXPORT_SYMBOL_NS_GPL(mfpwm_release, "ROCKCHIP_MFPWM");
> +
> +/**
> + * mfpwm_register_subdev - register a single mfpwm_func
> + * @mfpwm: pointer to the parent &struct rockchip_mfpwm
> + * @name: sub-device name string
> + *
> + * Allocate a single &struct mfpwm_func, fill its members with appropriate data,
> + * and register a new mfd cell.
> + *
> + * Returns: 0 on success, negative errno on error
> + */
> +static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm,
> + const char *name)
> +{
> + struct rockchip_mfpwm_func *func;
> + struct mfd_cell cell = {};
> +
> + func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL);
> + if (IS_ERR(func))
> + return PTR_ERR(func);
> + func->irq = mfpwm->irq;
> + func->parent = mfpwm;
> + func->id = atomic_inc_return(&subdev_id);
> + func->base = mfpwm->base;
> + func->core = mfpwm->chosen_clk;
> + cell.name = name;
> + cell.platform_data = func;
> + cell.pdata_size = sizeof(*func);
> + // cell.ignore_resource_conflicts = true;
> + // cell.resources = mfpwm->pdev->resource;
> + // cell.num_resources = mfpwm->pdev->num_resources;
> +
> + return devm_mfd_add_devices(&mfpwm->pdev->dev, func->id, &cell, 1, NULL,
> + 0, NULL);
> +}
> +
> +static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm)
> +{
> + int ret;
> +
> + ret = mfpwm_register_subdev(mfpwm, "pwm-rockchip-v4");
Not sure who came up with this name?
In case we need to filter wouldn't be easier to order it just like the bindings: manufacture '-' function
> + if (ret)
> + return ret;
> +
> + ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-capture");
> + if (ret)
> + return ret;
> +
> + return 0;
> +}
> +
> +static int rockchip_mfpwm_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + struct rockchip_mfpwm *mfpwm;
> + char *clk_mux_name;
> + const char *mux_p_names[3];
> + int ret = 0;
> +
> + mfpwm = devm_kzalloc(&pdev->dev, sizeof(*mfpwm), GFP_KERNEL);
> + if (IS_ERR(mfpwm))
> + return PTR_ERR(mfpwm);
> +
> + mfpwm->pdev = pdev;
> +
> + spin_lock_init(&mfpwm->state_lock);
> +
> + mfpwm->base = devm_platform_ioremap_resource(pdev, 0);
> + if (IS_ERR(mfpwm->base))
> + return dev_err_probe(dev, PTR_ERR(mfpwm->base),
> + "failed to ioremap address\n");
> +
> + mfpwm->pclk = devm_clk_get_prepared(dev, "pclk");
> + if (IS_ERR(mfpwm->pclk))
> + return dev_err_probe(dev, PTR_ERR(mfpwm->pclk),
> + "couldn't get and prepare 'pclk' clock\n");
> +
> + mfpwm->irq = platform_get_irq(pdev, 0);
> + if (mfpwm->irq < 0)
> + return dev_err_probe(dev, mfpwm->irq, "couldn't get irq 0\n");
> +
> + mfpwm->pwm_clk = devm_clk_get_prepared(dev, "pwm");
> + if (IS_ERR(mfpwm->pwm_clk))
> + return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk),
> + "couldn't get and prepare 'pwm' clock\n");
> +
> + mfpwm->osc_clk = devm_clk_get_prepared(dev, "osc");
> + if (IS_ERR(mfpwm->osc_clk))
> + return dev_err_probe(dev, PTR_ERR(mfpwm->osc_clk),
> + "couldn't get and prepare 'osc' clock\n");
> +
> + mfpwm->rc_clk = devm_clk_get_prepared(dev, "rc");
> + if (IS_ERR(mfpwm->rc_clk))
> + return dev_err_probe(dev, PTR_ERR(mfpwm->rc_clk),
> + "couldn't get and prepare 'rc' clock\n");
> +
> + clk_mux_name = devm_kasprintf(dev, GFP_KERNEL, "%s_chosen", dev_name(dev));
> + if (!clk_mux_name)
> + return -ENOMEM;
> +
> + mux_p_names[0] = __clk_get_name(mfpwm->pwm_clk);
> + mux_p_names[1] = __clk_get_name(mfpwm->osc_clk);
> + mux_p_names[2] = __clk_get_name(mfpwm->rc_clk);
> + mfpwm->chosen_clk = clk_register_mux(dev, clk_mux_name, mux_p_names,
> + ARRAY_SIZE(mux_p_names),
> + CLK_SET_RATE_PARENT,
> + mfpwm->base + PWMV4_REG_CLK_CTRL,
> + PWMV4_CLK_SRC_SHIFT, PWMV4_CLK_SRC_WIDTH,
> + CLK_MUX_HIWORD_MASK, NULL);
> + ret = clk_prepare(mfpwm->chosen_clk);
> + if (ret) {
> + dev_err(dev, "failed to prepare PWM clock mux: %pe\n",
> + ERR_PTR(ret));
> + return ret;
> + }
> +
> + platform_set_drvdata(pdev, mfpwm);
> +
> + ret = mfpwm_register_subdevs(mfpwm);
> + if (ret) {
> + dev_err(dev, "failed to register sub-devices: %pe\n",
> + ERR_PTR(ret));
> + return ret;
> + }
> +
> + return ret;
> +}
> +
> +static void rockchip_mfpwm_remove(struct platform_device *pdev)
> +{
> + struct rockchip_mfpwm *mfpwm = to_rockchip_mfpwm(pdev);
> + unsigned long flags;
> +
> + spin_lock_irqsave(&mfpwm->state_lock, flags);
> +
> + if (mfpwm->chosen_clk) {
> + clk_unprepare(mfpwm->chosen_clk);
> + clk_unregister_mux(mfpwm->chosen_clk);
> + }
> +
> + spin_unlock_irqrestore(&mfpwm->state_lock, flags);
> +}
> +
> +static const struct of_device_id rockchip_mfpwm_of_match[] = {
> + {
> + .compatible = "rockchip,rk3576-pwm",
> + },
> + { /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(of, rockchip_mfpwm_of_match);
> +
> +static struct platform_driver rockchip_mfpwm_driver = {
> + .driver = {
> + .name = KBUILD_MODNAME,
> + .of_match_table = rockchip_mfpwm_of_match,
> + },
> + .probe = rockchip_mfpwm_probe,
> + .remove = rockchip_mfpwm_remove,
> +};
> +module_platform_driver(rockchip_mfpwm_driver);
> +
> +MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
> +MODULE_DESCRIPTION("Rockchip MFPWM Driver");
> +MODULE_LICENSE("GPL");
> diff --git a/include/linux/mfd/rockchip-mfpwm.h b/include/linux/mfd/rockchip-mfpwm.h
> new file mode 100644
> index 000000000000..78d9c3396f7e
> --- /dev/null
> +++ b/include/linux/mfd/rockchip-mfpwm.h
> @@ -0,0 +1,454 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (c) 2025 Collabora Ltd.
> + *
> + * Common header file for all the Rockchip Multi-function PWM controller
> + * drivers that are spread across subsystems.
> + *
> + * Authors:
> + * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> + */
> +
> +#ifndef __SOC_ROCKCHIP_MFPWM_H__
> +#define __SOC_ROCKCHIP_MFPWM_H__
> +
> +#include <linux/bits.h>
> +#include <linux/clk.h>
> +#include <linux/hw_bitfield.h>
> +#include <linux/io.h>
> +#include <linux/spinlock.h>
> +
> +struct rockchip_mfpwm;
> +
> +/**
> + * struct rockchip_mfpwm_func - struct representing a single function driver
> + *
> + * @id: unique id for this function driver instance
> + * @base: pointer to start of MMIO registers
> + * @parent: a pointer to the parent mfpwm struct
> + * @irq: the shared IRQ gotten from the parent mfpwm device
> + * @core: a pointer to the clk mux that drives this channel's PWM
> + */
> +struct rockchip_mfpwm_func {
> + int id;
> + void __iomem *base;
> + struct rockchip_mfpwm *parent;
> + int irq;
> + struct clk *core;
> +};
> +
> +/*
> + * PWMV4 Register Definitions
> + * --------------------------
> + *
> + * Attributes:
> + * RW - Read-Write
> + * RO - Read-Only
> + * WO - Write-Only
> + * W1T - Write high, Self-clearing
> + * W1C - Write high to clear interrupt
> + *
> + * Bit ranges to be understood with Verilog-like semantics,
> + * e.g. [03:00] is 4 bits: 0, 1, 2 and 3.
> + *
> + * All registers must be accessed with 32-bit width accesses only
> + */
> +
> +#define PWMV4_REG_VERSION 0x000
> +/*
> + * VERSION Register Description
> + * [31:24] RO | Hardware Major Version
> + * [23:16] RO | Hardware Minor Version
> + * [15:15] RO | Reserved
> + * [14:14] RO | Hardware supports biphasic counters
> + * [13:13] RO | Hardware supports filters
> + * [12:12] RO | Hardware supports waveform generation
> + * [11:11] RO | Hardware supports counter
> + * [10:10] RO | Hardware supports frequency metering
> + * [09:09] RO | Hardware supports power key functionality
> + * [08:08] RO | Hardware supports infrared transmissions
> + * [07:04] RO | Channel index of this instance
> + * [03:00] RO | Number of channels the base instance supports
> + */
> +
> +#define PWMV4_REG_ENABLE 0x004
> +/*
> + * ENABLE Register Description
> + * [31:16] WO | Write Enable Mask for the lower half of the register
> + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
> + * the same write operation
> + * [15:06] RO | Reserved
> + * [05:05] RW | PWM Channel Counter Read Enable, 1 = enabled
> + */
> +#define PWMV4_CHN_CNT_RD_EN(v) FIELD_PREP_WM16(BIT(5), (v))
> +/*
> + * [04:04] W1T | PWM Globally Joined Control Enable
> + * 1 = this PWM channel will be enabled by a global pwm enable
> + * bit instead of the PWM Enable bit.
> + */
> +#define PWMV4_GLOBAL_CTRL_EN(v) FIELD_PREP_WM16(BIT(4), (v))
> +/*
> + * [03:03] RW | Force Clock Enable
> + * 0 = disabled, if the PWM channel is inactive then so is the
> + * clock prescale module
> + */
> +#define PWMV4_FORCE_CLK_EN(v) FIELD_PREP_WM16(BIT(3), (v))
> +/*
> + * [02:02] W1T | PWM Control Update Enable
> + * 1 = enabled, commits modifications of _CTRL, _PERIOD, _DUTY and
> + * _OFFSET registers once 1 is written to it
> + */
> +#define PWMV4_CTRL_UPDATE_EN FIELD_PREP_WM16_CONST(BIT(2), 1)
> +/*
> + * [01:01] RW | PWM Enable, 1 = enabled
> + * If in one-shot mode, clears after end of operation
> + */
> +#define PWMV4_EN_MASK BIT(1)
> +#define PWMV4_EN(v) FIELD_PREP_WM16(PWMV4_EN_MASK, \
> + ((v) ? 1 : 0))
> +/*
> + * [00:00] RW | PWM Clock Enable, 1 = enabled
> + * If in one-shot mode, clears after end of operation
> + */
> +#define PWMV4_CLK_EN_MASK BIT(0)
> +#define PWMV4_CLK_EN(v) FIELD_PREP_WM16(PWMV4_CLK_EN_MASK, \
> + ((v) ? 1 : 0))
> +#define PWMV4_EN_BOTH_MASK (PWMV4_EN_MASK | PWMV4_CLK_EN_MASK)
> +static inline __pure bool rockchip_pwm_v4_is_enabled(unsigned int val)
> +{
> + return (val & PWMV4_EN_BOTH_MASK);
> +}
> +
> +#define PWMV4_REG_CLK_CTRL 0x008
> +/*
> + * CLK_CTRL Register Description
> + * [31:16] WO | Write Enable Mask for the lower half of the register
> + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
> + * the same write operation
> + * [15:15] RW | Clock Global Selection
> + * 0 = current channel scale clock
> + * 1 = global channel scale clock
> + */
> +#define PWMV4_CLK_GLOBAL(v) FIELD_PREP_WM16(BIT(15), (v))
> +/*
> + * [14:13] RW | Clock Source Selection
> + * 0 = Clock from PLL, frequency can be configured
> + * 1 = Clock from crystal oscillator, frequency is fixed
> + * 2 = Clock from RC oscillator, frequency is fixed
> + * 3 = Reserved
> + * NOTE: The purpose for this clock-mux-outside-CRU construct is
> + * to let the SoC go into a sleep state with the PWM
> + * hardware still having a clock signal for IR input, which
> + * can then wake up the SoC.
> + */
> +#define PWMV4_CLK_SRC_PLL 0x0U
> +#define PWMV4_CLK_SRC_CRYSTAL 0x1U
> +#define PWMV4_CLK_SRC_RC 0x2U
> +#define PWMV4_CLK_SRC_SHIFT 13
> +#define PWMV4_CLK_SRC_WIDTH 2
> +/*
> + * [12:04] RW | Scale Factor to apply to pre-scaled clock
> + * 1 <= v <= 256, v means clock divided by 2*v
> + */
> +#define PWMV4_CLK_SCALE_F(v) FIELD_PREP_WM16(GENMASK(12, 4), (v))
> +/*
> + * [03:03] RO | Reserved
> + * [02:00] RW | Prescale Factor
> + * v here means the input clock is divided by pow(2, v)
> + */
> +#define PWMV4_CLK_PRESCALE_F(v) FIELD_PREP_WM16(GENMASK(2, 0), (v))
> +
> +#define PWMV4_REG_CTRL 0x00C
> +/*
> + * CTRL Register Description
> + * [31:16] WO | Write Enable Mask for the lower half of the register
> + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
> + * the same write operation
> + * [15:09] RO | Reserved
> + * [08:06] RW | PWM Input Channel Selection
> + * By default, the channel selects its own input, but writing v
> + * here selects PWM input from channel v instead.
> + */
> +#define PWMV4_CTRL_IN_SEL(v) FIELD_PREP_WM16(GENMASK(8, 6), (v))
> +/* [05:05] RW | Aligned Mode, 0 = Valid, 1 = Invalid */
> +#define PWMV4_CTRL_UNALIGNED(v) FIELD_PREP_WM16(BIT(5), (v))
> +/* [04:04] RW | Output Mode, 0 = Left Aligned, 1 = Centre Aligned */
> +#define PWMV4_LEFT_ALIGNED 0x0U
> +#define PWMV4_CENTRE_ALIGNED 0x1U
> +#define PWMV4_CTRL_OUT_MODE(v) FIELD_PREP_WM16(BIT(4), (v))
> +/*
> + * [03:03] RW | Inactive Polarity for when the channel is either disabled or
> + * has completed outputting the entire waveform in one-shot mode.
> + * 0 = Negative, 1 = Positive
> + */
> +#define PWMV4_POLARITY_N 0x0U
> +#define PWMV4_POLARITY_P 0x1U
> +#define PWMV4_INACTIVE_POL(v) FIELD_PREP_WM16(BIT(3), (v))
> +/*
> + * [02:02] RW | Duty Cycle Polarity to use at the start of the waveform.
> + * 0 = Negative, 1 = Positive
> + */
> +#define PWMV4_DUTY_POL_SHIFT 2
> +#define PWMV4_DUTY_POL_MASK BIT(PWMV4_DUTY_POL_SHIFT)
> +#define PWMV4_DUTY_POL(v) FIELD_PREP_WM16(PWMV4_DUTY_POL_MASK, \
> + (v))
> +/*
> + * [01:00] RW | PWM Mode
> + * 0 = One-shot mode, PWM generates waveform RPT times
> + * 1 = Continuous mode
> + * 2 = Capture mode, PWM measures cycles of input waveform
> + * 3 = Reserved
> + */
> +#define PWMV4_MODE_ONESHOT 0x0U
> +#define PWMV4_MODE_CONT 0x1U
> +#define PWMV4_MODE_CAPTURE 0x2U
> +#define PWMV4_MODE_MASK GENMASK(1, 0)
> +#define PWMV4_MODE(v) FIELD_PREP_WM16(PWMV4_MODE_MASK, (v))
> +#define PWMV4_CTRL_COM_FLAGS (PWMV4_INACTIVE_POL(PWMV4_POLARITY_N) | \
> + PWMV4_DUTY_POL(PWMV4_POLARITY_P) | \
> + PWMV4_CTRL_OUT_MODE(PWMV4_LEFT_ALIGNED) | \
> + PWMV4_CTRL_UNALIGNED(true))
> +#define PWMV4_CTRL_CONT_FLAGS (PWMV4_MODE(PWMV4_MODE_CONT) | \
> + PWMV4_CTRL_COM_FLAGS)
> +#define PWMV4_CTRL_CAP_FLAGS (PWMV4_MODE(PWMV4_MODE_CAPTURE) | \
> + PWMV4_CTRL_COM_FLAGS)
> +
> +#define PWMV4_REG_PERIOD 0x010
> +/*
> + * PERIOD Register Description
> + * [31:00] RW | Period of the output waveform
> + * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
> + */
> +
> +#define PWMV4_REG_DUTY 0x014
> +/*
> + * DUTY Register Description
> + * [31:00] RW | Duty cycle of the output waveform
> + * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
> + */
> +
> +#define PWMV4_REG_OFFSET 0x018
> +/*
> + * OFFSET Register Description
> + * [31:00] RW | Offset of the output waveform, based on the PWM clock
> + * Constraints: 0 <= v <= (PERIOD - DUTY)
> + */
> +
> +#define PWMV4_REG_RPT 0x01C
> +/*
> + * RPT Register Description
> + * [31:16] RW | Second dimensional of the effective number of waveform
> + * repetitions. Increases by one every first dimensional times.
> + * Value `n` means `n + 1` repetitions. The final number of
> + * repetitions of the waveform in one-shot mode is:
> + * `(first_dimensional + 1) * (second_dimensional + 1)`
> + * [15:00] RW | First dimensional of the effective number of waveform
> + * repetitions. Value `n` means `n + 1` repetitions.
> + */
> +
> +#define PWMV4_REG_FILTER_CTRL 0x020
> +/*
> + * FILTER_CTRL Register Description
> + * [31:16] WO | Write Enable Mask for the lower half of the register
> + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
> + * the same write operation
> + * [15:10] RO | Reserved
> + * [09:04] RW | Filter window number
> + * [03:01] RO | Reserved
> + * [00:00] RW | Filter Enable, 0 = disabled, 1 = enabled
> + */
> +
> +#define PWMV4_REG_CNT 0x024
> +/*
> + * CNT Register Description
> + * [31:00] RO | Current value of the PWM Channel 0 counter in pwm clock cycles,
> + * 0 <= v <= 2^32-1
> + */
> +
> +#define PWMV4_REG_ENABLE_DELAY 0x028
> +/*
> + * ENABLE_DELAY Register Description
> + * [31:16] RO | Reserved
> + * [15:00] RW | PWM enable delay, in an unknown unit but probably cycles
> + */
> +
> +#define PWMV4_REG_HPC 0x02C
> +/*
> + * HPC Register Description
> + * [31:00] RW | Number of effective high polarity cycles of the input waveform
> + * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
> + */
> +
> +#define PWMV4_REG_LPC 0x030
> +/*
> + * LPC Register Description
> + * [31:00] RW | Number of effective low polarity cycles of the input waveform
> + * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
> + */
> +
> +#define PWMV4_REG_BIPHASIC_CNT_CTRL0 0x040
> +/*
> + * BIPHASIC_CNT_CTRL0 Register Description
> + * [31:16] WO | Write Enable Mask for the lower half of the register
> + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
> + * the same write operation
> + * [15:10] RO | Reserved
> + * [09:09] RW | Biphasic Counter Phase Edge Selection for mode 0,
> + * 0 = rising edge (posedge), 1 = falling edge (negedge)
> + * [08:08] RW | Biphasic Counter Clock force enable, 1 = force enable
> + * [07:07] W1T | Synchronous Enable
> + * [06:06] W1T | Mode Switch
> + * 0 = Normal Mode, 1 = Switch timer clock and measured clock
> + * Constraints: "Biphasic Counter Mode" must be 0 if this is 1
> + * [05:03] RW | Biphasic Counter Mode
> + * 0x0 = Mode 0, 0x1 = Mode 1, 0x2 = Mode 2, 0x3 = Mode 3,
> + * 0x4 = Mode 4, 0x5 = Reserved
> + * [02:02] RW | Biphasic Counter Clock Selection
> + * 0 = clock is from PLL and frequency can be configured
> + * 1 = clock is from crystal oscillator and frequency is fixed
> + * [01:01] RW | Biphasic Counter Continuous Mode
> + * [00:00] W1T | Biphasic Counter Enable
> + */
> +
> +#define PWMV4_REG_BIPHASIC_CNT_CTRL1 0x044
> +/*
> + * BIPHASIC_CNT_CTRL1 Register Description
> + * [31:16] WO | Write Enable Mask for the lower half of the register
> + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
> + * the same write operation
> + * [15:11] RO | Reserved
> + * [10:04] RW | Biphasic Counter Filter Window Number
> + * [03:01] RO | Reserved
> + * [00:00] RW | Biphasic Counter Filter Enable
> + */
> +
> +#define PWMV4_REG_BIPHASIC_CNT_TIMER 0x048
> +/*
> + * BIPHASIC_CNT_TIMER Register Description
> + * [31:00] RW | Biphasic Counter Timer Value, in number of biphasic counter
> + * timer clock cycles
> + */
> +
> +#define PWMV4_REG_BIPHASIC_CNT_RES 0x04C
> +/*
> + * BIPHASIC_CNT_RES Register Description
> + * [31:00] RO | Biphasic Counter Result Value
> + * Constraints: Can only be read after INTSTS[9] is asserted
> + */
> +
> +#define PWMV4_REG_BIPHASIC_CNT_RES_S 0x050
> +/*
> + * BIPHASIC_CNT_RES_S Register Description
> + * [31:00] RO | Biphasic Counter Result Value with synchronised processing
> + * Can be read in real-time if BIPHASIC_CNT_CTRL0[7] was set to 1
> + */
> +
> +#define PWMV4_REG_INTSTS 0x070
> +/*
> + * INTSTS Register Description
> + * [31:10] RO | Reserved
> + * [09:09] W1C | Biphasic Counter Interrupt Status, 1 = interrupt asserted
> + * [08:08] W1C | Waveform Middle Interrupt Status, 1 = interrupt asserted
> + * [07:07] W1C | Waveform Max Interrupt Status, 1 = interrupt asserted
> + * [06:06] W1C | IR Transmission End Interrupt Status, 1 = interrupt asserted
> + * [05:05] W1C | Power Key Match Interrupt Status, 1 = interrupt asserted
> + * [04:04] W1C | Frequency Meter Interrupt Status, 1 = interrupt asserted
> + * [03:03] W1C | Reload Interrupt Status, 1 = interrupt asserted
> + * [02:02] W1C | Oneshot End Interrupt Status, 1 = interrupt asserted
> + * [01:01] W1C | HPC Capture Interrupt Status, 1 = interrupt asserted
> + * [00:00] W1C | LPC Capture Interrupt Status, 1 = interrupt asserted
> + */
> +#define PWMV4_INT_LPC BIT(0)
> +#define PWMV4_INT_HPC BIT(1)
> +#define PWMV4_INT_LPC_W(v) FIELD_PREP_WM16(PWMV4_INT_LPC, \
> + ((v) ? 1 : 0))
> +#define PWMV4_INT_HPC_W(v) FIELD_PREP_WM16(PWMV4_INT_HPC, \
> + ((v) ? 1 : 0))
> +
> +#define PWMV4_REG_INT_EN 0x074
> +/*
> + * INT_EN Register Description
> + * [31:16] WO | Write Enable Mask for the lower half of the register
> + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
> + * the same write operation
> + * [15:10] RO | Reserved
> + * [09:09] RW | Biphasic Counter Interrupt Enable, 1 = enabled
> + * [08:08] W1C | Waveform Middle Interrupt Enable, 1 = enabled
> + * [07:07] W1C | Waveform Max Interrupt Enable, 1 = enabled
> + * [06:06] W1C | IR Transmission End Interrupt Enable, 1 = enabled
> + * [05:05] W1C | Power Key Match Interrupt Enable, 1 = enabled
> + * [04:04] W1C | Frequency Meter Interrupt Enable, 1 = enabled
> + * [03:03] W1C | Reload Interrupt Enable, 1 = enabled
> + * [02:02] W1C | Oneshot End Interrupt Enable, 1 = enabled
> + * [01:01] W1C | HPC Capture Interrupt Enable, 1 = enabled
> + * [00:00] W1C | LPC Capture Interrupt Enable, 1 = enabled
> + */
> +
> +#define PWMV4_REG_INT_MASK 0x078
> +/*
> + * INT_MASK Register Description
> + * [31:16] WO | Write Enable Mask for the lower half of the register
> + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
> + * the same write operation
> + * [15:10] RO | Reserved
> + * [09:09] RW | Biphasic Counter Interrupt Masked, 1 = masked
> + * [08:08] W1C | Waveform Middle Interrupt Masked, 1 = masked
> + * [07:07] W1C | Waveform Max Interrupt Masked, 1 = masked
> + * [06:06] W1C | IR Transmission End Interrupt Masked, 1 = masked
> + * [05:05] W1C | Power Key Match Interrupt Masked, 1 = masked
> + * [04:04] W1C | Frequency Meter Interrupt Masked, 1 = masked
> + * [03:03] W1C | Reload Interrupt Masked, 1 = masked
> + * [02:02] W1C | Oneshot End Interrupt Masked, 1 = masked
> + * [01:01] W1C | HPC Capture Interrupt Masked, 1 = masked
> + * [00:00] W1C | LPC Capture Interrupt Masked, 1 = masked
> + */
> +
> +static inline u32 mfpwm_reg_read(void __iomem *base, u32 reg)
> +{
> + return readl(base + reg);
> +}
> +
> +static inline void mfpwm_reg_write(void __iomem *base, u32 reg, u32 val)
> +{
> + writel(val, base + reg);
> +}
> +
> +/**
> + * mfpwm_acquire - try becoming the active mfpwm function device
> + * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
> + *
> + * mfpwm device "function" drivers must call this function before doing anything
> + * that either modifies or relies on the parent device's state, such as clocks,
> + * enabling/disabling outputs, modifying shared regs etc.
> + *
> + * The return statues should always be checked.
> + *
> + * All mfpwm_acquire() calls must be balanced with corresponding mfpwm_release()
> + * calls once the device is no longer making changes that affect other devices,
> + * or stops producing user-visible effects that depend on the current device
> + * state being kept as-is. (e.g. after the PWM output signal is stopped)
> + *
> + * The same device function may mfpwm_acquire() multiple times while it already
> + * is active, i.e. it is re-entrant, though it needs to balance this with the
> + * same number of mfpwm_release() calls.
> + *
> + * Context: This function does not sleep.
> + *
> + * Return:
> + * * %0 - success
> + * * %-EBUSY - a different device function is active
> + * * %-EOVERFLOW - the acquire counter is at its maximum
> + */
> +extern int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf);
> +
> +/**
> + * mfpwm_release - drop usage of active mfpwm device function by 1
> + * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
> + *
> + * This is the balancing call to mfpwm_acquire(). If no users of the device
> + * function remain, set the mfpwm device to have no active device function,
> + * allowing other device functions to claim it.
> + */
> +extern void mfpwm_release(const struct rockchip_mfpwm_func *pwmf);
> +
> +#endif /* __SOC_ROCKCHIP_MFPWM_H__ */
>
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v3 2/5] mfd: Add Rockchip mfpwm driver
2025-10-28 18:52 ` Johan Jonker
@ 2025-10-31 12:20 ` Nicolas Frattaroli
2025-11-03 15:25 ` Lee Jones
1 sibling, 0 replies; 17+ messages in thread
From: Nicolas Frattaroli @ 2025-10-31 12:20 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
Johan Jonker
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio
On Tuesday, 28 October 2025 19:52:53 Central European Standard Time Johan Jonker wrote:
>
> On 10/27/25 18:11, Nicolas Frattaroli wrote:
> > With the Rockchip RK3576, the PWM IP used by Rockchip has changed
> > substantially. Looking at both the downstream pwm-rockchip driver as
> > well as the mainline pwm-rockchip driver made it clear that with all its
> > additional features and its differences from previous IP revisions, it
> > is best supported in a new driver.
> >
> > This brings us to the question as to what such a new driver should be.
> > To me, it soon became clear that it should actually be several new
> > drivers, most prominently when Uwe Kleine-König let me know that I
> > should not implement the pwm subsystem's capture callback, but instead
> > write a counter driver for this functionality.
> >
> > Combined with the other as-of-yet unimplemented functionality of this
> > new IP, it became apparent that it needs to be spread across several
> > subsystems.
> >
> > For this reason, we add a new MFD core driver, called mfpwm (short for
> > "Multi-function PWM"). This "parent" driver makes sure that only one
> > device function driver is using the device at a time, and is in charge
> > of registering the MFD cell devices for the individual device functions
> > offered by the device.
> >
> > An acquire/release pattern is used to guarantee that device function
> > drivers don't step on each other's toes.
> >
> > Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> > ---
> > MAINTAINERS | 2 +
> > drivers/mfd/Kconfig | 15 ++
> > drivers/mfd/Makefile | 1 +
> > drivers/mfd/rockchip-mfpwm.c | 340 +++++++++++++++++++++++++++
> > include/linux/mfd/rockchip-mfpwm.h | 454 +++++++++++++++++++++++++++++++++++++
> > 5 files changed, 812 insertions(+)
> >
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index baecabab35a2..8f3235ba825e 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -22372,6 +22372,8 @@ L: linux-rockchip@lists.infradead.org
> > L: linux-pwm@vger.kernel.org
> > S: Maintained
>
> > F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
>
> A question not so much for Nicolas specific:
> The yaml documents already have a 'maintainers' entry.
> However MAINTAINERS is full yaml entries.
> Could someone explain why we still need dual registration?
>
> maintainers:
> - Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
>
> > +F: drivers/soc/rockchip/mfpwm.c
> > +F: include/soc/rockchip/mfpwm.h
>
> different file name and location?
>
> drivers/mfd/rockchip-mfpwm.c | 340 +++++++++++++++++++++++++++
> include/linux/mfd/rockchip-mfpwm.h | 454 +++++++++++++++++++++++++++++++++++++
>
>
Yeah, I forgot to adjust this when moving this to being an MFD.
I'll fix it in v4.
> > [... snip ...]
> > diff --git a/drivers/mfd/rockchip-mfpwm.c b/drivers/mfd/rockchip-mfpwm.c
> > new file mode 100644
> > index 000000000000..08c2d8da41b7
> > --- /dev/null
> > +++ b/drivers/mfd/rockchip-mfpwm.c
> > [... snip ...]
> > +
> > +static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm)
> > +{
> > + int ret;
> > +
>
> > + ret = mfpwm_register_subdev(mfpwm, "pwm-rockchip-v4");
>
> Not sure who came up with this name?
I did.
> In case we need to filter wouldn't be easier to order it just like the bindings: manufacture '-' function
It's based on the filename of the pwm output driver. pwm-rockchip.c
is already taken by v1 to v3 hardware. Apparently however, pwm
subsystem drivers then reverse the order in the driver name, so
`pwm-rockchip.c` registers a driver with the name `rockchip-pwm`.
So I'll rename my PWM output driver to `rockchip-pwm-v4`. The v4
stays, it refers to the hardware IP revision.
> > + if (ret)
> > + return ret;
> > +
> > + ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-capture");
> > + if (ret)
> > + return ret;
> > +
> > + return 0;
> > +}
> > +
> > [... snip ...]
Kind regards,
Nicolas Frattaroli
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v3 2/5] mfd: Add Rockchip mfpwm driver
2025-10-28 18:52 ` Johan Jonker
2025-10-31 12:20 ` Nicolas Frattaroli
@ 2025-11-03 15:25 ` Lee Jones
1 sibling, 0 replies; 17+ messages in thread
From: Lee Jones @ 2025-11-03 15:25 UTC (permalink / raw)
To: Johan Jonker
Cc: Nicolas Frattaroli, Uwe Kleine-König, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
William Breathitt Gray, kernel, Jonas Karlman, Alexey Charkov,
linux-rockchip, linux-pwm, devicetree, linux-arm-kernel,
linux-kernel, linux-iio
On Tue, 28 Oct 2025, Johan Jonker wrote:
>
>
> On 10/27/25 18:11, Nicolas Frattaroli wrote:
> > With the Rockchip RK3576, the PWM IP used by Rockchip has changed
> > substantially. Looking at both the downstream pwm-rockchip driver as
> > well as the mainline pwm-rockchip driver made it clear that with all its
> > additional features and its differences from previous IP revisions, it
> > is best supported in a new driver.
> >
> > This brings us to the question as to what such a new driver should be.
> > To me, it soon became clear that it should actually be several new
> > drivers, most prominently when Uwe Kleine-König let me know that I
> > should not implement the pwm subsystem's capture callback, but instead
> > write a counter driver for this functionality.
> >
> > Combined with the other as-of-yet unimplemented functionality of this
> > new IP, it became apparent that it needs to be spread across several
> > subsystems.
> >
> > For this reason, we add a new MFD core driver, called mfpwm (short for
> > "Multi-function PWM"). This "parent" driver makes sure that only one
> > device function driver is using the device at a time, and is in charge
> > of registering the MFD cell devices for the individual device functions
> > offered by the device.
> >
> > An acquire/release pattern is used to guarantee that device function
> > drivers don't step on each other's toes.
> >
> > Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> > ---
> > MAINTAINERS | 2 +
> > drivers/mfd/Kconfig | 15 ++
> > drivers/mfd/Makefile | 1 +
> > drivers/mfd/rockchip-mfpwm.c | 340 +++++++++++++++++++++++++++
> > include/linux/mfd/rockchip-mfpwm.h | 454 +++++++++++++++++++++++++++++++++++++
> > 5 files changed, 812 insertions(+)
> >
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index baecabab35a2..8f3235ba825e 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -22372,6 +22372,8 @@ L: linux-rockchip@lists.infradead.org
> > L: linux-pwm@vger.kernel.org
> > S: Maintained
>
> > F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
>
> A question not so much for Nicolas specific:
> The yaml documents already have a 'maintainers' entry.
> However MAINTAINERS is full yaml entries.
> Could someone explain why we still need dual registration?
>
> maintainers:
> - Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
>
> > +F: drivers/soc/rockchip/mfpwm.c
> > +F: include/soc/rockchip/mfpwm.h
>
> different file name and location?
>
> drivers/mfd/rockchip-mfpwm.c | 340 +++++++++++++++++++++++++++
> include/linux/mfd/rockchip-mfpwm.h | 454 +++++++++++++++++++++++++++++++++++++
>
>
> >
> > ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
> > M: Daniel Golle <daniel@makrotopia.org>
> > diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
> > index dbeac6825a10..8b3a3160fbdf 100644
> > --- a/drivers/mfd/Kconfig
> > +++ b/drivers/mfd/Kconfig
> > @@ -1367,6 +1367,21 @@ config MFD_RC5T583
> > Additional drivers must be enabled in order to use the
> > different functionality of the device.
> >
> > +config MFD_ROCKCHIP_MFPWM
> > + tristate "Rockchip multi-function PWM controller"
> > + depends on OF
> > + depends on HAS_IOMEM
> > + depends on COMMON_CLK
> > + select MFD_CORE
> > + help
> > + Some Rockchip SoCs, such as the RK3576, use a PWM controller that has
> > + several different functions, such as generating PWM waveforms but also
> > + counting waveforms.
> > +
> > + This driver manages the overall device, and selects between different
> > + functionalities at runtime as needed. Drivers for them are implemented
> > + in their respective subsystems.
> > +
> > config MFD_RK8XX
> > tristate
> > select MFD_CORE
> > diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
> > index e75e8045c28a..ebadbaea9e4a 100644
> > --- a/drivers/mfd/Makefile
> > +++ b/drivers/mfd/Makefile
> > @@ -231,6 +231,7 @@ obj-$(CONFIG_MFD_PALMAS) += palmas.o
> > obj-$(CONFIG_MFD_VIPERBOARD) += viperboard.o
> > obj-$(CONFIG_MFD_NTXEC) += ntxec.o
> > obj-$(CONFIG_MFD_RC5T583) += rc5t583.o rc5t583-irq.o
> > +obj-$(CONFIG_MFD_ROCKCHIP_MFPWM) += rockchip-mfpwm.o
> > obj-$(CONFIG_MFD_RK8XX) += rk8xx-core.o
> > obj-$(CONFIG_MFD_RK8XX_I2C) += rk8xx-i2c.o
> > obj-$(CONFIG_MFD_RK8XX_SPI) += rk8xx-spi.o
> > diff --git a/drivers/mfd/rockchip-mfpwm.c b/drivers/mfd/rockchip-mfpwm.c
> > new file mode 100644
> > index 000000000000..08c2d8da41b7
> > --- /dev/null
> > +++ b/drivers/mfd/rockchip-mfpwm.c
> > @@ -0,0 +1,340 @@
> > +// SPDX-License-Identifier: GPL-2.0-or-later
> > +/*
> > + * Copyright (c) 2025 Collabora Ltd.
> > + *
> > + * A driver to manage all the different functionalities exposed by Rockchip's
> > + * PWMv4 hardware.
> > + *
> > + * This driver is chiefly focused on guaranteeing non-concurrent operation
> > + * between the different device functions, as well as setting the clocks.
> > + * It registers the device function platform devices, e.g. PWM output or
> > + * PWM capture.
> > + *
> > + * Authors:
> > + * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> > + */
> > +
> > +#include <linux/array_size.h>
> > +#include <linux/clk.h>
> > +#include <linux/clk-provider.h>
> > +#include <linux/mfd/core.h>
> > +#include <linux/mfd/rockchip-mfpwm.h>
> > +#include <linux/module.h>
> > +#include <linux/of.h>
> > +#include <linux/overflow.h>
> > +#include <linux/platform_device.h>
> > +#include <linux/spinlock.h>
> > +
> > +/**
> > + * struct rockchip_mfpwm - private mfpwm driver instance state struct
> > + * @pdev: pointer to this instance's &struct platform_device
> > + * @base: pointer to the memory mapped registers of this device
> > + * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from
> > + * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from
> > + * @rc_clk: pointer to the RC oscillator the PWM signal may be derived from
> > + * @chosen_clk: a clk-mux of pwm_clk, osc_clk and rc_clk
> > + * @pclk: pointer to the APB bus clock needed for mmio register access
> > + * @active_func: pointer to the currently active device function, or %NULL if no
> > + * device function is currently actively using any of the shared
> > + * resources. May only be checked/modified with @state_lock held.
> > + * @acquire_cnt: number of times @active_func has currently mfpwm_acquire()'d
> > + * it. Must only be checked or modified while holding @state_lock.
> > + * @state_lock: this lock is held while either the active device function, the
> > + * enable register, or the chosen clock is being changed.
> > + * @irq: the IRQ number of this device
> > + */
> > +struct rockchip_mfpwm {
> > + struct platform_device *pdev;
> > + void __iomem *base;
> > + struct clk *pwm_clk;
> > + struct clk *osc_clk;
> > + struct clk *rc_clk;
> > + struct clk *chosen_clk;
> > + struct clk *pclk;
> > + struct rockchip_mfpwm_func *active_func;
> > + unsigned int acquire_cnt;
> > + spinlock_t state_lock;
> > + int irq;
> > +};
> > +
> > +static atomic_t subdev_id = ATOMIC_INIT(0);
> > +
> > +static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev)
> > +{
> > + return platform_get_drvdata(pdev);
> > +}
> > +
> > +static int mfpwm_check_pwmf(const struct rockchip_mfpwm_func *pwmf,
> > + const char *fname)
> > +{
> > + struct device *dev = &pwmf->parent->pdev->dev;
> > +
> > + if (IS_ERR_OR_NULL(pwmf)) {
> > + dev_warn(dev, "called %s with an erroneous handle, no effect\n",
> > + fname);
> > + return -EINVAL;
> > + }
> > +
> > + if (IS_ERR_OR_NULL(pwmf->parent)) {
> > + dev_warn(dev, "called %s with an erroneous mfpwm_func parent, no effect\n",
> > + fname);
> > + return -EINVAL;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +__attribute__((nonnull))
> > +static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
> > +{
> > + struct rockchip_mfpwm *mfpwm = pwmf->parent;
> > + unsigned int cnt;
> > +
> > + if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
> > + return -EBUSY;
> > +
> > + if (!mfpwm->active_func)
> > + mfpwm->active_func = pwmf;
> > +
> > + if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
> > + mfpwm->acquire_cnt = cnt;
> > + } else {
> > + dev_warn(&mfpwm->pdev->dev, "prevented acquire counter overflow in %s\n",
> > + __func__);
> > + return -EOVERFLOW;
> > + }
> > +
> > + dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
> > + pwmf->id, mfpwm->acquire_cnt);
> > +
> > + return clk_enable(mfpwm->pclk);
> > +}
> > +
> > +int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf)
> > +{
> > + struct rockchip_mfpwm *mfpwm;
> > + unsigned long flags;
> > + int ret = 0;
> > +
> > + ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
> > + if (ret)
> > + return ret;
> > +
> > + mfpwm = pwmf->parent;
> > + dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id);
> > +
> > + if (!spin_trylock_irqsave(&mfpwm->state_lock, flags))
> > + return -EBUSY;
> > +
> > + ret = mfpwm_do_acquire(pwmf);
> > +
> > + spin_unlock_irqrestore(&mfpwm->state_lock, flags);
> > +
> > + return ret;
> > +}
> > +EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, "ROCKCHIP_MFPWM");
> > +
> > +__attribute__((nonnull))
> > +static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf)
> > +{
> > + struct rockchip_mfpwm *mfpwm = pwmf->parent;
> > +
> > + if (!mfpwm->active_func)
> > + return;
> > +
> > + if (mfpwm->active_func->id != pwmf->id)
> > + return;
> > +
> > + /*
> > + * No need to check_sub_overflow here, !mfpwm->active_func above catches
> > + * this type of problem already.
> > + */
> > + mfpwm->acquire_cnt--;
> > +
> > + if (!mfpwm->acquire_cnt)
> > + mfpwm->active_func = NULL;
> > +
> > + clk_disable(mfpwm->pclk);
> > +}
> > +
> > +void mfpwm_release(const struct rockchip_mfpwm_func *pwmf)
> > +{
> > + struct rockchip_mfpwm *mfpwm;
> > + unsigned long flags;
> > +
> > + if (mfpwm_check_pwmf(pwmf, "mfpwm_release"))
> > + return;
> > +
> > + mfpwm = pwmf->parent;
> > +
> > + spin_lock_irqsave(&mfpwm->state_lock, flags);
> > + mfpwm_do_release(pwmf);
> > + dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n",
> > + pwmf->id, mfpwm->acquire_cnt);
> > + spin_unlock_irqrestore(&mfpwm->state_lock, flags);
> > +}
> > +EXPORT_SYMBOL_NS_GPL(mfpwm_release, "ROCKCHIP_MFPWM");
> > +
> > +/**
> > + * mfpwm_register_subdev - register a single mfpwm_func
> > + * @mfpwm: pointer to the parent &struct rockchip_mfpwm
> > + * @name: sub-device name string
> > + *
> > + * Allocate a single &struct mfpwm_func, fill its members with appropriate data,
> > + * and register a new mfd cell.
> > + *
> > + * Returns: 0 on success, negative errno on error
> > + */
> > +static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm,
> > + const char *name)
> > +{
> > + struct rockchip_mfpwm_func *func;
> > + struct mfd_cell cell = {};
> > +
> > + func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL);
> > + if (IS_ERR(func))
> > + return PTR_ERR(func);
> > + func->irq = mfpwm->irq;
> > + func->parent = mfpwm;
> > + func->id = atomic_inc_return(&subdev_id);
> > + func->base = mfpwm->base;
> > + func->core = mfpwm->chosen_clk;
> > + cell.name = name;
> > + cell.platform_data = func;
> > + cell.pdata_size = sizeof(*func);
> > + // cell.ignore_resource_conflicts = true;
> > + // cell.resources = mfpwm->pdev->resource;
> > + // cell.num_resources = mfpwm->pdev->num_resources;
> > +
> > + return devm_mfd_add_devices(&mfpwm->pdev->dev, func->id, &cell, 1, NULL,
> > + 0, NULL);
> > +}
> > +
> > +static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm)
> > +{
> > + int ret;
> > +
>
> > + ret = mfpwm_register_subdev(mfpwm, "pwm-rockchip-v4");
>
> Not sure who came up with this name?
> In case we need to filter wouldn't be easier to order it just like the bindings: manufacture '-' function
Please snip your replies in the future.
--
Lee Jones [李琼斯]
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver
2025-10-27 17:11 [PATCH v3 0/5] Add Rockchip RK3576 PWM Support Through MFPWM Nicolas Frattaroli
2025-10-27 17:11 ` [PATCH v3 1/5] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm Nicolas Frattaroli
2025-10-27 17:11 ` [PATCH v3 2/5] mfd: Add Rockchip mfpwm driver Nicolas Frattaroli
@ 2025-10-27 17:11 ` Nicolas Frattaroli
2025-10-28 8:16 ` Damon Ding
2025-11-14 10:41 ` Uwe Kleine-König
2025-10-27 17:11 ` [PATCH v3 4/5] counter: Add rockchip-pwm-capture driver Nicolas Frattaroli
2025-10-27 17:12 ` [PATCH v3 5/5] arm64: dts: rockchip: add PWM nodes to RK3576 SoC dtsi Nicolas Frattaroli
4 siblings, 2 replies; 17+ messages in thread
From: Nicolas Frattaroli @ 2025-10-27 17:11 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio,
Nicolas Frattaroli
The Rockchip RK3576 brings with it a new PWM IP, in downstream code
referred to as "v4". This new IP is different enough from the previous
Rockchip IP that I felt it necessary to add a new driver for it, instead
of shoehorning it in the old one.
Add this new driver, based on the PWM core's waveform APIs. Its platform
device is registered by the parent mfpwm driver, from which it also
receives a little platform data struct, so that mfpwm can guarantee that
all the platform device drivers spread across different subsystems for
this specific hardware IP do not interfere with each other.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
MAINTAINERS | 1 +
drivers/pwm/Kconfig | 13 ++
drivers/pwm/Makefile | 1 +
drivers/pwm/pwm-rockchip-v4.c | 353 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 368 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 8f3235ba825e..2079c2d51d5c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22372,6 +22372,7 @@ L: linux-rockchip@lists.infradead.org
L: linux-pwm@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+F: drivers/pwm/pwm-rockchip-v4.c
F: drivers/soc/rockchip/mfpwm.c
F: include/soc/rockchip/mfpwm.h
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index c2fd3f4b62d9..b852a7b2a29d 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -615,6 +615,19 @@ config PWM_ROCKCHIP
Generic PWM framework driver for the PWM controller found on
Rockchip SoCs.
+config PWM_ROCKCHIP_V4
+ tristate "Rockchip PWM v4 support"
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
+ depends on HAS_IOMEM
+ depends on MFD_ROCKCHIP_MFPWM
+ help
+ Generic PWM framework driver for the PWM controller found on
+ later Rockchip SoCs such as the RK3576.
+
+ Uses the Rockchip Multi-function PWM controller driver infrastructure
+ to guarantee fearlessly concurrent operation with other functions of
+ the same device implemented by drivers in other subsystems.
+
config PWM_SAMSUNG
tristate "Samsung PWM support"
depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index dfa8b4966ee1..fe0d16558d99 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -55,6 +55,7 @@ obj-$(CONFIG_PWM_RENESAS_RZG2L_GPT) += pwm-rzg2l-gpt.o
obj-$(CONFIG_PWM_RENESAS_RZ_MTU3) += pwm-rz-mtu3.o
obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
+obj-$(CONFIG_PWM_ROCKCHIP_V4) += pwm-rockchip-v4.o
obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o
diff --git a/drivers/pwm/pwm-rockchip-v4.c b/drivers/pwm/pwm-rockchip-v4.c
new file mode 100644
index 000000000000..7afa83f12b6a
--- /dev/null
+++ b/drivers/pwm/pwm-rockchip-v4.c
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * A Pulse-Width-Modulation (PWM) generator driver for the generators found in
+ * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". Uses
+ * the MFPWM infrastructure to guarantee exclusive use over the device without
+ * other functions of the device from different drivers interfering with its
+ * operation while it's active.
+ *
+ * Technical Reference Manual: Chapter 31 of the RK3506 TRM Part 1, a SoC which
+ * uses the same PWM hardware and has a publicly available TRM.
+ * https://opensource.rock-chips.com/images/3/36/Rockchip_RK3506_TRM_Part_1_V1.2-20250811.pdf
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ *
+ * Limitations:
+ * - When the output is disabled, it will end abruptly without letting the
+ * current period complete.
+ * TODO: This can be fixed in the driver in the future by having the enable-
+ * to-disable transition switch to oneshot mode with one repetition,
+ * and then disable the pwmclk and release mfpwm when the oneshot
+ * complete interrupt fires.
+ * - When the output is disabled, the pin will remain driven to whatever state
+ * it last had.
+ * - Adjustments to the duty cycle will only take effect during the next period.
+ * - Adjustments to the period length will only take effect during the next
+ * period.
+ * - offset should be between 0 and (period - duty)
+ */
+
+#include <linux/math64.h>
+#include <linux/mfd/rockchip-mfpwm.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+
+struct rockchip_pwm_v4 {
+ struct rockchip_mfpwm_func *pwmf;
+ struct pwm_chip chip;
+};
+
+struct rockchip_pwm_v4_wf {
+ u32 period;
+ u32 duty;
+ u32 offset;
+ u8 enable;
+};
+
+static inline struct rockchip_pwm_v4 *to_rockchip_pwm_v4(struct pwm_chip *chip)
+{
+ return pwmchip_get_drvdata(chip);
+}
+
+/**
+ * rockchip_pwm_v4_round_single - convert a PWM parameter to hardware
+ * @rate: clock rate of the PWM clock, as per clk_get_rate
+ * @in_val: parameter in nanoseconds to convert
+ *
+ * Returns the rounded value, saturating at U32_MAX if too large
+ */
+static u32 rockchip_pwm_v4_round_single(unsigned long rate, u64 in_val)
+{
+ u64 tmp;
+
+ tmp = mul_u64_u64_div_u64(rate, in_val, NSEC_PER_SEC);
+ if (tmp > U32_MAX)
+ tmp = U32_MAX;
+
+ return (u32)tmp;
+}
+
+/**
+ * rockchip_pwm_v4_round_params - convert PWM parameters to hardware
+ * @rate: PWM clock rate to do the calculations at
+ * @duty: PWM duty cycle in nanoseconds
+ * @period: PWM period in nanoseconds
+ * @offset: PWM offset in nanoseconds
+ * @out_duty: pointer to where the rounded duty value should be stored
+ * @out_period: pointer to where the rounded period value should be stored
+ * @out_offset: pointer to where the rounded offset value should be stored
+ *
+ * Convert nanosecond-based duty/period/offset parameters to the PWM hardware's
+ * native rounded representation in number of cycles at clock rate @rate. Should
+ * any of the input parameters be out of range for the hardware, the
+ * corresponding output parameter is the maximum permissible value for said
+ * parameter with considerations to the others.
+ */
+static void rockchip_pwm_v4_round_params(unsigned long rate, u64 duty,
+ u64 period, u64 offset, u32 *out_duty,
+ u32 *out_period, u32 *out_offset)
+{
+ *out_period = rockchip_pwm_v4_round_single(rate, period);
+
+ *out_duty = rockchip_pwm_v4_round_single(rate, duty);
+
+ /* As per TRM, PWM_OFFSET: "The value ranges from 0 to (period-duty)" */
+ *out_offset = rockchip_pwm_v4_round_single(rate, offset);
+ if (*out_offset > (*out_period - *out_duty))
+ *out_offset = *out_period - *out_duty;
+}
+
+static int rockchip_pwm_v4_round_wf_tohw(struct pwm_chip *chip,
+ struct pwm_device *pwm,
+ const struct pwm_waveform *wf,
+ void *_wfhw)
+{
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
+ unsigned long rate;
+
+ rate = clk_get_rate(pc->pwmf->core);
+
+ rockchip_pwm_v4_round_params(rate, wf->duty_length_ns,
+ wf->period_length_ns, wf->duty_offset_ns,
+ &wfhw->duty, &wfhw->period, &wfhw->offset);
+
+ if (wf->period_length_ns > 0)
+ wfhw->enable = PWMV4_EN_BOTH_MASK;
+ else
+ wfhw->enable = 0;
+
+ dev_dbg(&chip->dev,
+ "tohw: duty %llu -> %u, period %llu -> %u, offset %llu -> %u, rate %lu\n",
+ wf->duty_length_ns, wfhw->duty, wf->period_length_ns,
+ wfhw->period, wf->duty_offset_ns, wfhw->offset, rate);
+
+ return 0;
+}
+
+static int rockchip_pwm_v4_round_wf_fromhw(struct pwm_chip *chip,
+ struct pwm_device *pwm,
+ const void *_wfhw,
+ struct pwm_waveform *wf)
+{
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
+ unsigned long rate;
+
+ rate = clk_get_rate(pc->pwmf->core);
+
+ if (rockchip_pwm_v4_is_enabled(wfhw->enable)) {
+ if (!rate)
+ return -EINVAL;
+
+ wf->period_length_ns = (u64)wfhw->period * NSEC_PER_SEC / rate;
+ wf->duty_length_ns = (u64)wfhw->duty * NSEC_PER_SEC / rate;
+ wf->duty_offset_ns = (u64)wfhw->offset * NSEC_PER_SEC / rate;
+ } else {
+ wf->period_length_ns = 0;
+ wf->duty_length_ns = 0;
+ wf->duty_offset_ns = 0;
+ }
+
+ dev_dbg(&chip->dev,
+ "fromhw: duty %u -> %llu, period %u -> %llu, offset %u -> %llu, rate %lu\n",
+ wfhw->duty, wf->duty_length_ns, wfhw->period,
+ wf->period_length_ns, wfhw->offset, wf->duty_offset_ns, rate);
+
+ return 0;
+}
+
+static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm,
+ void *_wfhw)
+{
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
+ struct rockchip_pwm_v4_wf *wfhw = _wfhw;
+ int ret = 0;
+
+
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ return ret;
+
+ wfhw->period = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_PERIOD);
+ wfhw->duty = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_DUTY);
+ wfhw->offset = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_OFFSET);
+ wfhw->enable = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_ENABLE) & PWMV4_EN_BOTH_MASK;
+
+ mfpwm_release(pc->pwmf);
+
+ return 0;
+}
+
+static int rockchip_pwm_v4_write_wf(struct pwm_chip *chip, struct pwm_device *pwm,
+ const void *_wfhw)
+{
+ struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
+ const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
+ bool was_enabled = false;
+ int ret = 0;
+
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ return ret;
+
+ was_enabled = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
+ PWMV4_REG_ENABLE));
+
+ /*
+ * "But Nicolas", you ask with valid concerns, "why would you enable the
+ * PWM before setting all the parameter registers?"
+ *
+ * Excellent question, Mr. Reader M. Strawman! The RK3576 TRM Part 1
+ * Section 34.6.3 specifies that this is the intended order of writes.
+ * Doing the PWM_EN and PWM_CLK_EN writes after the params but before
+ * the CTRL_UPDATE_EN, or even after the CTRL_UPDATE_EN, results in
+ * erratic behaviour where repeated turning on and off of the PWM may
+ * not turn it off under all circumstances. This is also why we don't
+ * use relaxed writes; it's not worth the footgun.
+ */
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ FIELD_PREP_WM16(PWMV4_EN_BOTH_MASK, wfhw->enable));
+
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_PERIOD, wfhw->period);
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_DUTY, wfhw->duty);
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_OFFSET, wfhw->offset);
+
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL, PWMV4_CTRL_CONT_FLAGS);
+
+ /* Commit new configuration to hardware output. */
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ PWMV4_CTRL_UPDATE_EN);
+
+ if (rockchip_pwm_v4_is_enabled(wfhw->enable)) {
+ if (!was_enabled) {
+ dev_dbg(&chip->dev, "Enabling PWM output\n");
+ ret = clk_enable(pc->pwmf->core);
+ if (ret)
+ goto err_mfpwm_release;
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
+ if (ret) {
+ clk_disable(pc->pwmf->core);
+ goto err_mfpwm_release;
+ }
+
+ /*
+ * Output should be on now, acquire device to guarantee
+ * exclusion with other device functions while it's on.
+ */
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ goto err_mfpwm_release;
+ }
+ } else if (was_enabled) {
+ dev_dbg(&chip->dev, "Disabling PWM output\n");
+ clk_rate_exclusive_put(pc->pwmf->core);
+ clk_disable(pc->pwmf->core);
+ /* Output is off now, extra release to balance extra acquire */
+ mfpwm_release(pc->pwmf);
+ }
+
+err_mfpwm_release:
+ mfpwm_release(pc->pwmf);
+
+ return ret;
+}
+
+static const struct pwm_ops rockchip_pwm_v4_ops = {
+ .sizeof_wfhw = sizeof(struct rockchip_pwm_v4_wf),
+ .round_waveform_tohw = rockchip_pwm_v4_round_wf_tohw,
+ .round_waveform_fromhw = rockchip_pwm_v4_round_wf_fromhw,
+ .read_waveform = rockchip_pwm_v4_read_wf,
+ .write_waveform = rockchip_pwm_v4_write_wf,
+};
+
+static bool rockchip_pwm_v4_on_and_continuous(struct rockchip_pwm_v4 *pc)
+{
+ bool en;
+ u32 val;
+
+ en = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
+ PWMV4_REG_ENABLE));
+ val = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_CTRL);
+
+ return en && ((val & PWMV4_MODE_MASK) == PWMV4_MODE_CONT);
+}
+
+static int rockchip_pwm_v4_probe(struct platform_device *pdev)
+{
+ struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
+ struct rockchip_pwm_v4 *pc;
+ struct pwm_chip *chip;
+ struct device *dev = &pdev->dev;
+ int ret;
+
+ /*
+ * For referencing the PWM in the DT to work, we need the parent MFD
+ * device's OF node. Code shamelessly adapted from `drivers/pci/of.c`'s
+ * pci_set_of_node(), which does this for bus reasons.
+ */
+ dev->parent->of_node_reused = true;
+ device_set_node(dev,
+ of_fwnode_handle(no_free_ptr(dev->parent->of_node)));
+
+ chip = devm_pwmchip_alloc(dev, 1, sizeof(*pc));
+ if (IS_ERR(chip))
+ return PTR_ERR(chip);
+
+ pc = to_rockchip_pwm_v4(chip);
+ pc->pwmf = pwmf;
+
+ ret = mfpwm_acquire(pwmf);
+ if (ret == -EBUSY)
+ dev_warn(dev, "PWM hardware already in use, can't check initial state\n");
+ else if (ret < 0)
+ return dev_err_probe(dev, ret, "Couldn't acquire mfpwm in probe\n");
+
+ if (!rockchip_pwm_v4_on_and_continuous(pc))
+ mfpwm_release(pwmf);
+ else {
+ dev_dbg(dev, "PWM was already on at probe time\n");
+ ret = clk_enable(pwmf->core);
+ if (ret)
+ return dev_err_probe(dev, ret, "Enabling pwm clock failed\n");
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
+ if (ret) {
+ clk_disable(pwmf->core);
+ return dev_err_probe(dev, ret, "Protecting pwm clock failed\n");
+ }
+ }
+
+ platform_set_drvdata(pdev, chip);
+
+ chip->ops = &rockchip_pwm_v4_ops;
+
+ 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 platform_device_id rockchip_pwm_v4_ids[] = {
+ { .name = "pwm-rockchip-v4", },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(platform, rockchip_pwm_v4_ids);
+
+static struct platform_driver rockchip_pwm_v4_driver = {
+ .probe = rockchip_pwm_v4_probe,
+ .driver = {
+ .name = "pwm-rockchip-v4",
+ },
+ .id_table = rockchip_pwm_v4_ids,
+};
+module_platform_driver(rockchip_pwm_v4_driver);
+
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
+MODULE_DESCRIPTION("Rockchip PWMv4 Driver");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
+MODULE_ALIAS("platform:pwm-rockchip-v4");
--
2.51.1
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver
2025-10-27 17:11 ` [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver Nicolas Frattaroli
@ 2025-10-28 8:16 ` Damon Ding
2025-11-14 9:51 ` Uwe Kleine-König
2025-11-14 10:41 ` Uwe Kleine-König
1 sibling, 1 reply; 17+ messages in thread
From: Damon Ding @ 2025-10-28 8:16 UTC (permalink / raw)
To: Nicolas Frattaroli, Uwe Kleine-König, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner, Lee Jones,
William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio
[-- Attachment #1: Type: text/plain, Size: 6383 bytes --]
Hi Nicolas,
On 10/28/2025 1:11 AM, Nicolas Frattaroli wrote:
> The Rockchip RK3576 brings with it a new PWM IP, in downstream code
> referred to as "v4". This new IP is different enough from the previous
> Rockchip IP that I felt it necessary to add a new driver for it, instead
> of shoehorning it in the old one.
>
> Add this new driver, based on the PWM core's waveform APIs. Its platform
> device is registered by the parent mfpwm driver, from which it also
> receives a little platform data struct, so that mfpwm can guarantee that
> all the platform device drivers spread across different subsystems for
> this specific hardware IP do not interfere with each other.
>
> Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> ---
> MAINTAINERS | 1 +
> drivers/pwm/Kconfig | 13 ++
> drivers/pwm/Makefile | 1 +
> drivers/pwm/pwm-rockchip-v4.c | 353 ++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 368 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 8f3235ba825e..2079c2d51d5c 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -22372,6 +22372,7 @@ L: linux-rockchip@lists.infradead.org
> L: linux-pwm@vger.kernel.org
> S: Maintained
> F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
> +F: drivers/pwm/pwm-rockchip-v4.c
> F: drivers/soc/rockchip/mfpwm.c
> F: include/soc/rockchip/mfpwm.h
>
> diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
> index c2fd3f4b62d9..b852a7b2a29d 100644
> --- a/drivers/pwm/Kconfig
> +++ b/drivers/pwm/Kconfig
> @@ -615,6 +615,19 @@ config PWM_ROCKCHIP
> Generic PWM framework driver for the PWM controller found on
> Rockchip SoCs.
>
> +config PWM_ROCKCHIP_V4
> + tristate "Rockchip PWM v4 support"
> + depends on ARCH_ROCKCHIP || COMPILE_TEST
> + depends on HAS_IOMEM
> + depends on MFD_ROCKCHIP_MFPWM
> + help
> + Generic PWM framework driver for the PWM controller found on
> + later Rockchip SoCs such as the RK3576.
> +
> + Uses the Rockchip Multi-function PWM controller driver infrastructure
> + to guarantee fearlessly concurrent operation with other functions of
> + the same device implemented by drivers in other subsystems.
> +
> config PWM_SAMSUNG
> tristate "Samsung PWM support"
> depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST
> diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
> index dfa8b4966ee1..fe0d16558d99 100644
> --- a/drivers/pwm/Makefile
> +++ b/drivers/pwm/Makefile
> @@ -55,6 +55,7 @@ obj-$(CONFIG_PWM_RENESAS_RZG2L_GPT) += pwm-rzg2l-gpt.o
> obj-$(CONFIG_PWM_RENESAS_RZ_MTU3) += pwm-rz-mtu3.o
> obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
> obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
> +obj-$(CONFIG_PWM_ROCKCHIP_V4) += pwm-rockchip-v4.o
> obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
> obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
> obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o
> diff --git a/drivers/pwm/pwm-rockchip-v4.c b/drivers/pwm/pwm-rockchip-v4.c
> new file mode 100644
> index 000000000000..7afa83f12b6a
> --- /dev/null
> +++ b/drivers/pwm/pwm-rockchip-v4.c
> @@ -0,0 +1,353 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) 2025 Collabora Ltd.
> + *
> + * A Pulse-Width-Modulation (PWM) generator driver for the generators found in
> + * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". Uses
> + * the MFPWM infrastructure to guarantee exclusive use over the device without
> + * other functions of the device from different drivers interfering with its
> + * operation while it's active.
> + *
> + * Technical Reference Manual: Chapter 31 of the RK3506 TRM Part 1, a SoC which
> + * uses the same PWM hardware and has a publicly available TRM.
> + * https://opensource.rock-chips.com/images/3/36/Rockchip_RK3506_TRM_Part_1_V1.2-20250811.pdf
> + *
> + * Authors:
> + * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> + *
> + * Limitations:
> + * - When the output is disabled, it will end abruptly without letting the
> + * current period complete.
> + * TODO: This can be fixed in the driver in the future by having the enable-
> + * to-disable transition switch to oneshot mode with one repetition,
> + * and then disable the pwmclk and release mfpwm when the oneshot
> + * complete interrupt fires.
> + * - When the output is disabled, the pin will remain driven to whatever state
> + * it last had.
> + * - Adjustments to the duty cycle will only take effect during the next period.
> + * - Adjustments to the period length will only take effect during the next
> + * period.
> + * - offset should be between 0 and (period - duty)
> + */
> +
> +#include <linux/math64.h>
> +#include <linux/mfd/rockchip-mfpwm.h>
> +#include <linux/platform_device.h>
> +#include <linux/pwm.h>
> +
> +struct rockchip_pwm_v4 {
> + struct rockchip_mfpwm_func *pwmf;
> + struct pwm_chip chip;
> +};
> +
> +struct rockchip_pwm_v4_wf {
> + u32 period;
> + u32 duty;
> + u32 offset;
> + u8 enable;
> +};
> +
...
> +
> +static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm,
> + void *_wfhw)
> +{
> + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
> + struct rockchip_pwm_v4_wf *wfhw = _wfhw;
> + int ret = 0;
> +
Redundant blank lin. ;-)
> +
> + ret = mfpwm_acquire(pc->pwmf);
> + if (ret)
> + return ret;
> +
> + wfhw->period = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_PERIOD);
> + wfhw->duty = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_DUTY);
> + wfhw->offset = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_OFFSET);
> + wfhw->enable = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_ENABLE) & PWMV4_EN_BOTH_MASK;
> +
> + mfpwm_release(pc->pwmf);
> +
> + return 0;
> +}
> +
...
> +MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
> +MODULE_DESCRIPTION("Rockchip PWMv4 Driver");
> +MODULE_LICENSE("GPL");
> +MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
> +MODULE_ALIAS("platform:pwm-rockchip-v4");
>
Tested-by: Damon Ding <damon.ding@rock-chips.com>
I have tested all the PWM channels in continuous mode on my
RK3576-IOTEST board.
Test commands are like:
cd /sys/class/pwm/pwmchip0/
echo 0 > export
cd pwm0
echo 10000 > period
echo 5000 > duty_cycle
echo normal > polarity
echo 1 > enable
In addition, the patch related to DTS are attached.
Best regards,
Damon
[-- Attachment #2: rk3576_evb1_enable_all_pwm_channels.patch --]
[-- Type: text/plain, Size: 1653 bytes --]
diff --git a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts
index db8fef7a4f1b..b8db6e4c1246 100644
--- a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts
@@ -24,6 +24,7 @@ aliases {
chosen: chosen {
stdout-path = "serial0:1500000n8";
+ bootargs = "root=PARTUUID=614e0000-0000 rootwait";
};
adc_keys: adc-keys {
@@ -941,3 +942,83 @@ vp0_out_hdmi: endpoint@ROCKCHIP_VOP2_EP_HDMI0 {
remote-endpoint = <&hdmi_in_vp0>;
};
};
+
+&pwm0_2ch_0 {
+ status = "okay";
+ pinctrl-0 = <&pwm0m1_ch0>;
+};
+
+&pwm0_2ch_1 {
+ status = "okay";
+ pinctrl-0 = <&pwm0m2_ch1>;
+};
+
+&pwm1_6ch_0 {
+ status = "okay";
+ pinctrl-0 = <&pwm1m1_ch0>;
+};
+
+&pwm1_6ch_1 {
+ status = "okay";
+ pinctrl-0 = <&pwm1m1_ch1>;
+};
+
+&pwm1_6ch_2 {
+ status = "okay";
+ pinctrl-0 = <&pwm1m2_ch2>;
+};
+
+&pwm1_6ch_3 {
+ status = "okay";
+ pinctrl-0 = <&pwm1m1_ch3>;
+};
+
+&pwm1_6ch_4 {
+ status = "okay";
+ pinctrl-0 = <&pwm1m1_ch4>;
+};
+
+&pwm1_6ch_5 {
+ status = "okay";
+ pinctrl-0 = <&pwm1m1_ch5>;
+};
+
+&pwm2_8ch_0 {
+ status = "okay";
+ pinctrl-0 = <&pwm2m1_ch0>;
+};
+
+&pwm2_8ch_1 {
+ status = "okay";
+ pinctrl-0 = <&pwm2m1_ch1>;
+};
+
+&pwm2_8ch_2 {
+ status = "okay";
+ pinctrl-0 = <&pwm2m1_ch2>;
+};
+
+&pwm2_8ch_3 {
+ status = "okay";
+ pinctrl-0 = <&pwm2m1_ch3>;
+};
+
+&pwm2_8ch_4 {
+ status = "okay";
+ pinctrl-0 = <&pwm2m1_ch4>;
+};
+
+&pwm2_8ch_5 {
+ status = "okay";
+ pinctrl-0 = <&pwm2m1_ch5>;
+};
+
+&pwm2_8ch_6 {
+ status = "okay";
+ pinctrl-0 = <&pwm2m2_ch6>;
+};
+
+&pwm2_8ch_7 {
+ status = "okay";
+ pinctrl-0 = <&pwm2m2_ch7>;
+};
[-- Attachment #3: Type: text/plain, Size: 170 bytes --]
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver
2025-10-28 8:16 ` Damon Ding
@ 2025-11-14 9:51 ` Uwe Kleine-König
2025-11-14 10:13 ` Nicolas Frattaroli
0 siblings, 1 reply; 17+ messages in thread
From: Uwe Kleine-König @ 2025-11-14 9:51 UTC (permalink / raw)
To: Damon Ding
Cc: Nicolas Frattaroli, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray,
kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio
[-- Attachment #1.1: Type: text/plain, Size: 7127 bytes --]
On Tue, Oct 28, 2025 at 04:16:26PM +0800, Damon Ding wrote:
> Hi Nicolas,
>
> On 10/28/2025 1:11 AM, Nicolas Frattaroli wrote:
> > The Rockchip RK3576 brings with it a new PWM IP, in downstream code
> > referred to as "v4". This new IP is different enough from the previous
> > Rockchip IP that I felt it necessary to add a new driver for it, instead
> > of shoehorning it in the old one.
> >
> > Add this new driver, based on the PWM core's waveform APIs. Its platform
> > device is registered by the parent mfpwm driver, from which it also
> > receives a little platform data struct, so that mfpwm can guarantee that
> > all the platform device drivers spread across different subsystems for
> > this specific hardware IP do not interfere with each other.
> >
> > Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> > ---
> > MAINTAINERS | 1 +
> > drivers/pwm/Kconfig | 13 ++
> > drivers/pwm/Makefile | 1 +
> > drivers/pwm/pwm-rockchip-v4.c | 353 ++++++++++++++++++++++++++++++++++++++++++
> > 4 files changed, 368 insertions(+)
> >
>
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index 8f3235ba825e..2079c2d51d5c 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -22372,6 +22372,7 @@ L: linux-rockchip@lists.infradead.org
> > L: linux-pwm@vger.kernel.org
> > S: Maintained
> > F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
> > +F: drivers/pwm/pwm-rockchip-v4.c
> > F: drivers/soc/rockchip/mfpwm.c
> > F: include/soc/rockchip/mfpwm.h
> > diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
> > index c2fd3f4b62d9..b852a7b2a29d 100644
> > --- a/drivers/pwm/Kconfig
> > +++ b/drivers/pwm/Kconfig
> > @@ -615,6 +615,19 @@ config PWM_ROCKCHIP
> > Generic PWM framework driver for the PWM controller found on
> > Rockchip SoCs.
> > +config PWM_ROCKCHIP_V4
> > + tristate "Rockchip PWM v4 support"
> > + depends on ARCH_ROCKCHIP || COMPILE_TEST
> > + depends on HAS_IOMEM
> > + depends on MFD_ROCKCHIP_MFPWM
> > + help
> > + Generic PWM framework driver for the PWM controller found on
> > + later Rockchip SoCs such as the RK3576.
> > +
> > + Uses the Rockchip Multi-function PWM controller driver infrastructure
> > + to guarantee fearlessly concurrent operation with other functions of
> > + the same device implemented by drivers in other subsystems.
> > +
> > config PWM_SAMSUNG
> > tristate "Samsung PWM support"
> > depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST
> > diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
> > index dfa8b4966ee1..fe0d16558d99 100644
> > --- a/drivers/pwm/Makefile
> > +++ b/drivers/pwm/Makefile
> > @@ -55,6 +55,7 @@ obj-$(CONFIG_PWM_RENESAS_RZG2L_GPT) += pwm-rzg2l-gpt.o
> > obj-$(CONFIG_PWM_RENESAS_RZ_MTU3) += pwm-rz-mtu3.o
> > obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
> > obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
> > +obj-$(CONFIG_PWM_ROCKCHIP_V4) += pwm-rockchip-v4.o
> > obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
> > obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
> > obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o
> > diff --git a/drivers/pwm/pwm-rockchip-v4.c b/drivers/pwm/pwm-rockchip-v4.c
> > new file mode 100644
> > index 000000000000..7afa83f12b6a
> > --- /dev/null
> > +++ b/drivers/pwm/pwm-rockchip-v4.c
> > @@ -0,0 +1,353 @@
> > +// SPDX-License-Identifier: GPL-2.0-or-later
> > +/*
> > + * Copyright (c) 2025 Collabora Ltd.
> > + *
> > + * A Pulse-Width-Modulation (PWM) generator driver for the generators found in
> > + * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". Uses
> > + * the MFPWM infrastructure to guarantee exclusive use over the device without
> > + * other functions of the device from different drivers interfering with its
> > + * operation while it's active.
> > + *
> > + * Technical Reference Manual: Chapter 31 of the RK3506 TRM Part 1, a SoC which
> > + * uses the same PWM hardware and has a publicly available TRM.
> > + * https://opensource.rock-chips.com/images/3/36/Rockchip_RK3506_TRM_Part_1_V1.2-20250811.pdf
> > + *
> > + * Authors:
> > + * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> > + *
> > + * Limitations:
> > + * - When the output is disabled, it will end abruptly without letting the
> > + * current period complete.
> > + * TODO: This can be fixed in the driver in the future by having the enable-
> > + * to-disable transition switch to oneshot mode with one repetition,
> > + * and then disable the pwmclk and release mfpwm when the oneshot
> > + * complete interrupt fires.
> > + * - When the output is disabled, the pin will remain driven to whatever state
> > + * it last had.
> > + * - Adjustments to the duty cycle will only take effect during the next period.
> > + * - Adjustments to the period length will only take effect during the next
> > + * period.
> > + * - offset should be between 0 and (period - duty)
> > + */
> > +
> > +#include <linux/math64.h>
> > +#include <linux/mfd/rockchip-mfpwm.h>
> > +#include <linux/platform_device.h>
> > +#include <linux/pwm.h>
> > +
> > +struct rockchip_pwm_v4 {
> > + struct rockchip_mfpwm_func *pwmf;
> > + struct pwm_chip chip;
> > +};
> > +
> > +struct rockchip_pwm_v4_wf {
> > + u32 period;
> > + u32 duty;
> > + u32 offset;
> > + u8 enable;
> > +};
> > +
>
> ...
>
> > +
> > +static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm,
> > + void *_wfhw)
> > +{
> > + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
> > + struct rockchip_pwm_v4_wf *wfhw = _wfhw;
> > + int ret = 0;
> > +
>
> Redundant blank lin. ;-)
>
> > +
> > + ret = mfpwm_acquire(pc->pwmf);
> > + if (ret)
> > + return ret;
> > +
> > + wfhw->period = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_PERIOD);
> > + wfhw->duty = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_DUTY);
> > + wfhw->offset = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_OFFSET);
> > + wfhw->enable = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_ENABLE) & PWMV4_EN_BOTH_MASK;
> > +
> > + mfpwm_release(pc->pwmf);
> > +
> > + return 0;
> > +}
> > +
>
> ...
>
> > +MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
> > +MODULE_DESCRIPTION("Rockchip PWMv4 Driver");
> > +MODULE_LICENSE("GPL");
> > +MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
> > +MODULE_ALIAS("platform:pwm-rockchip-v4");
> >
>
> Tested-by: Damon Ding <damon.ding@rock-chips.com>
>
> I have tested all the PWM channels in continuous mode on my RK3576-IOTEST
> board.
>
> Test commands are like:
>
> cd /sys/class/pwm/pwmchip0/
> echo 0 > export
> cd pwm0
> echo 10000 > period
> echo 5000 > duty_cycle
> echo normal > polarity
> echo 1 > enable
Thanks for the test, very appreciated.
I wonder what made you test using sysfs instead of
https://git.kernel.org/pub/scm/linux/kernel/git/ukleinek/libpwm.git/.
Is it unknown? Too complicated? Other problems?
Best regards
Uwe
[-- Attachment #1.2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
[-- Attachment #2: Type: text/plain, Size: 170 bytes --]
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver
2025-11-14 9:51 ` Uwe Kleine-König
@ 2025-11-14 10:13 ` Nicolas Frattaroli
0 siblings, 0 replies; 17+ messages in thread
From: Nicolas Frattaroli @ 2025-11-14 10:13 UTC (permalink / raw)
To: Damon Ding, Uwe Kleine-König
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
Lee Jones, William Breathitt Gray, kernel, Jonas Karlman,
Alexey Charkov, linux-rockchip, linux-pwm, devicetree,
linux-arm-kernel, linux-kernel, linux-iio
On Friday, 14 November 2025 10:51:27 Central European Standard Time you wrote:
> On Tue, Oct 28, 2025 at 04:16:26PM +0800, Damon Ding wrote:
> > Hi Nicolas,
> >
> > On 10/28/2025 1:11 AM, Nicolas Frattaroli wrote:
> > > [...]
> > >
> >
> > Tested-by: Damon Ding <damon.ding@rock-chips.com>
> >
> > I have tested all the PWM channels in continuous mode on my RK3576-IOTEST
> > board.
> >
> > Test commands are like:
> >
> > cd /sys/class/pwm/pwmchip0/
> > echo 0 > export
> > cd pwm0
> > echo 10000 > period
> > echo 5000 > duty_cycle
> > echo normal > polarity
> > echo 1 > enable
>
> Thanks for the test, very appreciated.
>
> I wonder what made you test using sysfs instead of
> https://git.kernel.org/pub/scm/linux/kernel/git/ukleinek/libpwm.git/.
> Is it unknown? Too complicated? Other problems?
Can't speak for Damon Ding but this is the first time I've heard of
libpwm, so I think you do need to market it better. :) Perhaps by
mentioning it in the PWM subsystem docs as a way to interface with
the kernel, if that's permitted by the docs people. (It should be,
since libiio is mentioned for IIO.)
Kind regards,
Nicolas Frattaroli
>
> Best regards
> Uwe
>
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver
2025-10-27 17:11 ` [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver Nicolas Frattaroli
2025-10-28 8:16 ` Damon Ding
@ 2025-11-14 10:41 ` Uwe Kleine-König
1 sibling, 0 replies; 17+ messages in thread
From: Uwe Kleine-König @ 2025-11-14 10:41 UTC (permalink / raw)
To: Nicolas Frattaroli
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
Lee Jones, William Breathitt Gray, kernel, Jonas Karlman,
Alexey Charkov, linux-rockchip, linux-pwm, devicetree,
linux-arm-kernel, linux-kernel, linux-iio
[-- Attachment #1.1: Type: text/plain, Size: 18473 bytes --]
Hello Nicolas,
On Mon, Oct 27, 2025 at 06:11:58PM +0100, Nicolas Frattaroli wrote:
> The Rockchip RK3576 brings with it a new PWM IP, in downstream code
> referred to as "v4". This new IP is different enough from the previous
> Rockchip IP that I felt it necessary to add a new driver for it, instead
> of shoehorning it in the old one.
>
> Add this new driver, based on the PWM core's waveform APIs. Its platform
> device is registered by the parent mfpwm driver, from which it also
> receives a little platform data struct, so that mfpwm can guarantee that
> all the platform device drivers spread across different subsystems for
> this specific hardware IP do not interfere with each other.
>
> Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> ---
> MAINTAINERS | 1 +
> drivers/pwm/Kconfig | 13 ++
> drivers/pwm/Makefile | 1 +
> drivers/pwm/pwm-rockchip-v4.c | 353 ++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 368 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 8f3235ba825e..2079c2d51d5c 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -22372,6 +22372,7 @@ L: linux-rockchip@lists.infradead.org
> L: linux-pwm@vger.kernel.org
> S: Maintained
> F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
> +F: drivers/pwm/pwm-rockchip-v4.c
> F: drivers/soc/rockchip/mfpwm.c
> F: include/soc/rockchip/mfpwm.h
>
> diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
> index c2fd3f4b62d9..b852a7b2a29d 100644
> --- a/drivers/pwm/Kconfig
> +++ b/drivers/pwm/Kconfig
> @@ -615,6 +615,19 @@ config PWM_ROCKCHIP
> Generic PWM framework driver for the PWM controller found on
> Rockchip SoCs.
>
> +config PWM_ROCKCHIP_V4
> + tristate "Rockchip PWM v4 support"
> + depends on ARCH_ROCKCHIP || COMPILE_TEST
> + depends on HAS_IOMEM
> + depends on MFD_ROCKCHIP_MFPWM
IMHO it's a bit strange that PWM_ROCKCHIP_V4 depends on ARCH_ROCKCHIP
while MFD_ROCKCHIP_MFPWM doesn't. So you can enable the MFD on all
platforms but the PWM component driver only on rockchip.
> + help
> + Generic PWM framework driver for the PWM controller found on
> + later Rockchip SoCs such as the RK3576.
> +
> + Uses the Rockchip Multi-function PWM controller driver infrastructure
> + to guarantee fearlessly concurrent operation with other functions of
> + the same device implemented by drivers in other subsystems.
> +
> config PWM_SAMSUNG
> tristate "Samsung PWM support"
> depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST
> diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
> index dfa8b4966ee1..fe0d16558d99 100644
> --- a/drivers/pwm/Makefile
> +++ b/drivers/pwm/Makefile
> @@ -55,6 +55,7 @@ obj-$(CONFIG_PWM_RENESAS_RZG2L_GPT) += pwm-rzg2l-gpt.o
> obj-$(CONFIG_PWM_RENESAS_RZ_MTU3) += pwm-rz-mtu3.o
> obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
> obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
> +obj-$(CONFIG_PWM_ROCKCHIP_V4) += pwm-rockchip-v4.o
> obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
> obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
> obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o
> diff --git a/drivers/pwm/pwm-rockchip-v4.c b/drivers/pwm/pwm-rockchip-v4.c
> new file mode 100644
> index 000000000000..7afa83f12b6a
> --- /dev/null
> +++ b/drivers/pwm/pwm-rockchip-v4.c
> @@ -0,0 +1,353 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) 2025 Collabora Ltd.
> + *
> + * A Pulse-Width-Modulation (PWM) generator driver for the generators found in
> + * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". Uses
> + * the MFPWM infrastructure to guarantee exclusive use over the device without
> + * other functions of the device from different drivers interfering with its
> + * operation while it's active.
> + *
> + * Technical Reference Manual: Chapter 31 of the RK3506 TRM Part 1, a SoC which
> + * uses the same PWM hardware and has a publicly available TRM.
> + * https://opensource.rock-chips.com/images/3/36/Rockchip_RK3506_TRM_Part_1_V1.2-20250811.pdf
> + *
> + * Authors:
> + * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> + *
> + * Limitations:
> + * - When the output is disabled, it will end abruptly without letting the
> + * current period complete.
> + * TODO: This can be fixed in the driver in the future by having the enable-
> + * to-disable transition switch to oneshot mode with one repetition,
> + * and then disable the pwmclk and release mfpwm when the oneshot
> + * complete interrupt fires.
I don't consider this behaviour a bug. So maybe:
- The hardware supports both completing the currently running period
on disable (by switching to oneshot mode with a single repetition
and only disable when the complete irq fires) and abrupt disable.
Only the latter is implemented in the driver.
> + * - When the output is disabled, the pin will remain driven to whatever state
> + * it last had.
> + * - Adjustments to the duty cycle will only take effect during the next period.
> + * - Adjustments to the period length will only take effect during the next
> + * period.
Is it possible that a race condition occurs here and between updating
duty and period a new cycle starts and thus a period with the new duty
and old period (or vice versa) is externally visible?
> + * - offset should be between 0 and (period - duty)
This sounds wrong here. Assuming the driver enforces that (it should,
didn't check yet), this should be:
- The hardware only supports offsets in [0, period-duty_cycle]
> + */
> +
> +#include <linux/math64.h>
> +#include <linux/mfd/rockchip-mfpwm.h>
> +#include <linux/platform_device.h>
> +#include <linux/pwm.h>
> +
> +struct rockchip_pwm_v4 {
> + struct rockchip_mfpwm_func *pwmf;
> + struct pwm_chip chip;
> +};
> +
> +struct rockchip_pwm_v4_wf {
> + u32 period;
> + u32 duty;
> + u32 offset;
> + u8 enable;
> +};
> +
> +static inline struct rockchip_pwm_v4 *to_rockchip_pwm_v4(struct pwm_chip *chip)
> +{
> + return pwmchip_get_drvdata(chip);
> +}
> +
> +/**
> + * rockchip_pwm_v4_round_single - convert a PWM parameter to hardware
> + * @rate: clock rate of the PWM clock, as per clk_get_rate
> + * @in_val: parameter in nanoseconds to convert
> + *
> + * Returns the rounded value, saturating at U32_MAX if too large
> + */
> +static u32 rockchip_pwm_v4_round_single(unsigned long rate, u64 in_val)
> +{
> + u64 tmp;
> +
> + tmp = mul_u64_u64_div_u64(rate, in_val, NSEC_PER_SEC);
This might overflow for rate > NSEC_PER_SEC and big values of in_val.
I suggest to call devm_clk_rate_exclusive_get() in .probe() and fail to
probe for rate > NSEC_PER_SEC.
> + if (tmp > U32_MAX)
> + tmp = U32_MAX;
> +
> + return (u32)tmp;
unneeded cast.
> +}
> +
> +/**
> + * rockchip_pwm_v4_round_params - convert PWM parameters to hardware
> + * @rate: PWM clock rate to do the calculations at
> + * @duty: PWM duty cycle in nanoseconds
> + * @period: PWM period in nanoseconds
> + * @offset: PWM offset in nanoseconds
> + * @out_duty: pointer to where the rounded duty value should be stored
> + * @out_period: pointer to where the rounded period value should be stored
> + * @out_offset: pointer to where the rounded offset value should be stored
> + *
> + * Convert nanosecond-based duty/period/offset parameters to the PWM hardware's
> + * native rounded representation in number of cycles at clock rate @rate. Should
> + * any of the input parameters be out of range for the hardware, the
> + * corresponding output parameter is the maximum permissible value for said
> + * parameter with considerations to the others.
> + */
> +static void rockchip_pwm_v4_round_params(unsigned long rate, u64 duty,
> + u64 period, u64 offset, u32 *out_duty,
> + u32 *out_period, u32 *out_offset)
Hmm, it would be more natural to pass rate, wf and wfhw here, wouldn't
it?
> +{
> + *out_period = rockchip_pwm_v4_round_single(rate, period);
> +
> + *out_duty = rockchip_pwm_v4_round_single(rate, duty);
> +
> + /* As per TRM, PWM_OFFSET: "The value ranges from 0 to (period-duty)" */
> + *out_offset = rockchip_pwm_v4_round_single(rate, offset);
> + if (*out_offset > (*out_period - *out_duty))
needless parenthesis
You rely on out_period >= out_duty. You can assume that
rockchip_pwm_v4_round_wf_tohw() is only called with wf->period_length_ns
>= wf->duty_offset_ns, but still worth a comment IMHO.
> + *out_offset = *out_period - *out_duty;
> +}
> +
> +static int rockchip_pwm_v4_round_wf_tohw(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + const struct pwm_waveform *wf,
> + void *_wfhw)
> +{
> + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
> + struct rockchip_pwm_v4_wf *wfhw = _wfhw;
> + unsigned long rate;
> +
> + rate = clk_get_rate(pc->pwmf->core);
> +
> + rockchip_pwm_v4_round_params(rate, wf->duty_length_ns,
> + wf->period_length_ns, wf->duty_offset_ns,
> + &wfhw->duty, &wfhw->period, &wfhw->offset);
> +
> + if (wf->period_length_ns > 0)
> + wfhw->enable = PWMV4_EN_BOTH_MASK;
> + else
> + wfhw->enable = 0;
> +
> + dev_dbg(&chip->dev,
> + "tohw: duty %llu -> %u, period %llu -> %u, offset %llu -> %u, rate %lu\n",
> + wf->duty_length_ns, wfhw->duty, wf->period_length_ns,
> + wfhw->period, wf->duty_offset_ns, wfhw->offset, rate);
Can you make that:
dev_dbg(&chip->dev,
"tohw: pwm#%u: %lld/%lld [+%lld] @%lu -> DUTY: %08x, PERIOD: %08x, OFFSET: %08x\n",
pwm->hwpwm, wf->duty_length_ns, wf->period_length_ns,
wf->duty_offset_ns, rate, wfhw->duty, wfhw->period,
wfhw->offset);
to stick to the common way to format a struct pwm_waveform and have
input and output separated. (This is not objectively better than pairing
input and matching output parameter, but also it's not worse and matches
what other drivers are doing.)
> +
> + return 0;
> +}
> +
> +static int rockchip_pwm_v4_round_wf_fromhw(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + const void *_wfhw,
> + struct pwm_waveform *wf)
> +{
> + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
> + const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
> + unsigned long rate;
> +
> + rate = clk_get_rate(pc->pwmf->core);
> +
> + if (rockchip_pwm_v4_is_enabled(wfhw->enable)) {
> + if (!rate)
> + return -EINVAL;
Can rate be 0? Either consider it fixed, or store rate in wfhw.
> +
> + wf->period_length_ns = (u64)wfhw->period * NSEC_PER_SEC / rate;
> + wf->duty_length_ns = (u64)wfhw->duty * NSEC_PER_SEC / rate;
> + wf->duty_offset_ns = (u64)wfhw->offset * NSEC_PER_SEC / rate;
You need to round up here.
> + } else {
> + wf->period_length_ns = 0;
> + wf->duty_length_ns = 0;
> + wf->duty_offset_ns = 0;
> + }
> +
> + dev_dbg(&chip->dev,
> + "fromhw: duty %u -> %llu, period %u -> %llu, offset %u -> %llu, rate %lu\n",
> + wfhw->duty, wf->duty_length_ns, wfhw->period,
> + wf->period_length_ns, wfhw->offset, wf->duty_offset_ns, rate);
> +
> + return 0;
> +}
> +
> +static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm,
> + void *_wfhw)
> +{
> + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
> + struct rockchip_pwm_v4_wf *wfhw = _wfhw;
> + int ret = 0;
> +
> +
> + ret = mfpwm_acquire(pc->pwmf);
> + if (ret)
> + return ret;
> +
> + wfhw->period = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_PERIOD);
> + wfhw->duty = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_DUTY);
> + wfhw->offset = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_OFFSET);
> + wfhw->enable = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_ENABLE) & PWMV4_EN_BOTH_MASK;
Can it happen that only a part of the bits in PWMV4_EN_BOTH_MASK are
set? rockchip_pwm_v4_is_enabled() then returns true.
> +
> + mfpwm_release(pc->pwmf);
> +
> + return 0;
> +}
> +
> +static int rockchip_pwm_v4_write_wf(struct pwm_chip *chip, struct pwm_device *pwm,
> + const void *_wfhw)
> +{
> + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip);
> + const struct rockchip_pwm_v4_wf *wfhw = _wfhw;
> + bool was_enabled = false;
> + int ret = 0;
> +
> + ret = mfpwm_acquire(pc->pwmf);
> + if (ret)
> + return ret;
> +
> + was_enabled = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
> + PWMV4_REG_ENABLE));
> +
> + /*
> + * "But Nicolas", you ask with valid concerns, "why would you enable the
> + * PWM before setting all the parameter registers?"
> + *
> + * Excellent question, Mr. Reader M. Strawman! The RK3576 TRM Part 1
> + * Section 34.6.3 specifies that this is the intended order of writes.
> + * Doing the PWM_EN and PWM_CLK_EN writes after the params but before
> + * the CTRL_UPDATE_EN, or even after the CTRL_UPDATE_EN, results in
> + * erratic behaviour where repeated turning on and off of the PWM may
> + * not turn it off under all circumstances. This is also why we don't
> + * use relaxed writes; it's not worth the footgun.
> + */
> + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
> + FIELD_PREP_WM16(PWMV4_EN_BOTH_MASK, wfhw->enable));
> +
> + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_PERIOD, wfhw->period);
> + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_DUTY, wfhw->duty);
> + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_OFFSET, wfhw->offset);
> +
> + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL, PWMV4_CTRL_CONT_FLAGS);
> +
> + /* Commit new configuration to hardware output. */
> + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
> + PWMV4_CTRL_UPDATE_EN);
> +
> + if (rockchip_pwm_v4_is_enabled(wfhw->enable)) {
> + if (!was_enabled) {
> + dev_dbg(&chip->dev, "Enabling PWM output\n");
> + ret = clk_enable(pc->pwmf->core);
> + if (ret)
> + goto err_mfpwm_release;
> + ret = clk_rate_exclusive_get(pc->pwmf->core);
> + if (ret) {
> + clk_disable(pc->pwmf->core);
> + goto err_mfpwm_release;
> + }
> +
> + /*
> + * Output should be on now, acquire device to guarantee
> + * exclusion with other device functions while it's on.
> + */
> + ret = mfpwm_acquire(pc->pwmf);
> + if (ret)
> + goto err_mfpwm_release;
Missing clk_disable() + clk_rate_exclusive_put() in the error path?
(Though that would violate that PWMV4_REG_ENABLE and clk state are in
sync.)
> + }
> + } else if (was_enabled) {
> + dev_dbg(&chip->dev, "Disabling PWM output\n");
> + clk_rate_exclusive_put(pc->pwmf->core);
> + clk_disable(pc->pwmf->core);
> + /* Output is off now, extra release to balance extra acquire */
> + mfpwm_release(pc->pwmf);
> + }
> +
> +err_mfpwm_release:
> + mfpwm_release(pc->pwmf);
> +
> + return ret;
> +}
> +
> +static const struct pwm_ops rockchip_pwm_v4_ops = {
> + .sizeof_wfhw = sizeof(struct rockchip_pwm_v4_wf),
> + .round_waveform_tohw = rockchip_pwm_v4_round_wf_tohw,
> + .round_waveform_fromhw = rockchip_pwm_v4_round_wf_fromhw,
> + .read_waveform = rockchip_pwm_v4_read_wf,
> + .write_waveform = rockchip_pwm_v4_write_wf,
> +};
> +
> +static bool rockchip_pwm_v4_on_and_continuous(struct rockchip_pwm_v4 *pc)
> +{
> + bool en;
> + u32 val;
> +
> + en = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base,
> + PWMV4_REG_ENABLE));
> + val = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_CTRL);
> +
> + return en && ((val & PWMV4_MODE_MASK) == PWMV4_MODE_CONT);
> +}
> +
> +static int rockchip_pwm_v4_probe(struct platform_device *pdev)
> +{
> + struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
> + struct rockchip_pwm_v4 *pc;
> + struct pwm_chip *chip;
> + struct device *dev = &pdev->dev;
> + int ret;
> +
> + /*
> + * For referencing the PWM in the DT to work, we need the parent MFD
> + * device's OF node. Code shamelessly adapted from `drivers/pci/of.c`'s
> + * pci_set_of_node(), which does this for bus reasons.
> + */
> + dev->parent->of_node_reused = true;
The help text for of_node_reused says: "Set if the device-tree node is
shared with an ancestor device". So I wonder if it's dev->of_node_reused
that should be set?
> + device_set_node(dev,
> + of_fwnode_handle(no_free_ptr(dev->parent->of_node)));
Huh, so you're setting dev->parent->of_node to NULL. Is that really what
should happen here?
> + chip = devm_pwmchip_alloc(dev, 1, sizeof(*pc));
> + if (IS_ERR(chip))
> + return PTR_ERR(chip);
> +
> + pc = to_rockchip_pwm_v4(chip);
> + pc->pwmf = pwmf;
> +
> + ret = mfpwm_acquire(pwmf);
> + if (ret == -EBUSY)
> + dev_warn(dev, "PWM hardware already in use, can't check initial state\n");
I think it's sensible to let .probe() fail here.
> + else if (ret < 0)
> + return dev_err_probe(dev, ret, "Couldn't acquire mfpwm in probe\n");
> +
> + if (!rockchip_pwm_v4_on_and_continuous(pc))
> + mfpwm_release(pwmf);
> + else {
> + dev_dbg(dev, "PWM was already on at probe time\n");
> + ret = clk_enable(pwmf->core);
> + if (ret)
> + return dev_err_probe(dev, ret, "Enabling pwm clock failed\n");
> + ret = clk_rate_exclusive_get(pc->pwmf->core);
> + if (ret) {
> + clk_disable(pwmf->core);
> + return dev_err_probe(dev, ret, "Protecting pwm clock failed\n");
> + }
> + }
> +
> + platform_set_drvdata(pdev, chip);
> +
> + chip->ops = &rockchip_pwm_v4_ops;
> +
> + ret = devm_pwmchip_add(dev, chip);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to add PWM chip\n");
Missing mfpwm_release() if rockchip_pwm_v4_on_and_continuous() returned
true above.
> +
> + return 0;
> +}
> +
> +static const struct platform_device_id rockchip_pwm_v4_ids[] = {
> + { .name = "pwm-rockchip-v4", },
> + { /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(platform, rockchip_pwm_v4_ids);
> +
> +static struct platform_driver rockchip_pwm_v4_driver = {
> + .probe = rockchip_pwm_v4_probe,
> + .driver = {
> + .name = "pwm-rockchip-v4",
> + },
> + .id_table = rockchip_pwm_v4_ids,
> +};
> +module_platform_driver(rockchip_pwm_v4_driver);
> +
> +MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
> +MODULE_DESCRIPTION("Rockchip PWMv4 Driver");
> +MODULE_LICENSE("GPL");
> +MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
> +MODULE_ALIAS("platform:pwm-rockchip-v4");
Best regards
Uwe
[-- Attachment #1.2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
[-- Attachment #2: Type: text/plain, Size: 170 bytes --]
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v3 4/5] counter: Add rockchip-pwm-capture driver
2025-10-27 17:11 [PATCH v3 0/5] Add Rockchip RK3576 PWM Support Through MFPWM Nicolas Frattaroli
` (2 preceding siblings ...)
2025-10-27 17:11 ` [PATCH v3 3/5] pwm: Add rockchip PWMv4 driver Nicolas Frattaroli
@ 2025-10-27 17:11 ` Nicolas Frattaroli
2025-10-28 11:05 ` Damon Ding
2025-10-27 17:12 ` [PATCH v3 5/5] arm64: dts: rockchip: add PWM nodes to RK3576 SoC dtsi Nicolas Frattaroli
4 siblings, 1 reply; 17+ messages in thread
From: Nicolas Frattaroli @ 2025-10-27 17:11 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio,
Nicolas Frattaroli
Among many other things, Rockchip's new PWMv4 IP in the RK3576 supports
PWM capture functionality.
Add a basic driver for this that works to expose HPC/LPC counts and
state change events to userspace through the counter framework. It's
quite basic, but works well enough to demonstrate the device function
exclusion stuff that mfpwm does, in order to eventually support all the
functions of this device in drivers within their appropriate subsystems,
without them interfering with each other.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
MAINTAINERS | 1 +
drivers/counter/Kconfig | 13 ++
drivers/counter/Makefile | 1 +
drivers/counter/rockchip-pwm-capture.c | 306 +++++++++++++++++++++++++++++++++
4 files changed, 321 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 2079c2d51d5c..17d625ef76d1 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22372,6 +22372,7 @@ L: linux-rockchip@lists.infradead.org
L: linux-pwm@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+F: drivers/counter/rockchip-pwm-capture.c
F: drivers/pwm/pwm-rockchip-v4.c
F: drivers/soc/rockchip/mfpwm.c
F: include/soc/rockchip/mfpwm.h
diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig
index d30d22dfe577..12e823dd0ec7 100644
--- a/drivers/counter/Kconfig
+++ b/drivers/counter/Kconfig
@@ -90,6 +90,19 @@ config MICROCHIP_TCB_CAPTURE
To compile this driver as a module, choose M here: the
module will be called microchip-tcb-capture.
+config ROCKCHIP_PWM_CAPTURE
+ tristate "Rockchip PWM Counter Capture driver"
+ depends on ARCH_ROCKCHIP || COMPILE_TEST
+ depends on MFD_ROCKCHIP_MFPWM
+ depends on HAS_IOMEM
+ help
+ Generic counter framework driver for the multi-function PWM on
+ Rockchip SoCs such as the RK3576.
+
+ Uses the Rockchip Multi-function PWM controller driver infrastructure
+ to guarantee exclusive operation with other functions of the same
+ device implemented by drivers in other subsystems.
+
config RZ_MTU3_CNT
tristate "Renesas RZ/G2L MTU3a counter driver"
depends on RZ_MTU3
diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile
index fa3c1d08f706..2bfcfc2c584b 100644
--- a/drivers/counter/Makefile
+++ b/drivers/counter/Makefile
@@ -17,3 +17,4 @@ obj-$(CONFIG_FTM_QUADDEC) += ftm-quaddec.o
obj-$(CONFIG_MICROCHIP_TCB_CAPTURE) += microchip-tcb-capture.o
obj-$(CONFIG_INTEL_QEP) += intel-qep.o
obj-$(CONFIG_TI_ECAP_CAPTURE) += ti-ecap-capture.o
+obj-$(CONFIG_ROCKCHIP_PWM_CAPTURE) += rockchip-pwm-capture.o
diff --git a/drivers/counter/rockchip-pwm-capture.c b/drivers/counter/rockchip-pwm-capture.c
new file mode 100644
index 000000000000..63e1286303f9
--- /dev/null
+++ b/drivers/counter/rockchip-pwm-capture.c
@@ -0,0 +1,306 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * A counter driver for the Pulse-Width-Modulation (PWM) hardware found on
+ * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". It
+ * allows for measuring the high cycles and low cycles of a PWM signal through
+ * the generic counter framework, while guaranteeing exclusive use over the
+ * MFPWM device while the counter is enabled.
+ *
+ * Authors:
+ * Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#include <linux/cleanup.h>
+#include <linux/counter.h>
+#include <linux/devm-helpers.h>
+#include <linux/interrupt.h>
+#include <linux/mfd/rockchip-mfpwm.h>
+#include <linux/mod_devicetable.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+
+#define RKPWMC_INT_MASK (PWMV4_INT_LPC | PWMV4_INT_HPC)
+
+struct rockchip_pwm_capture {
+ struct rockchip_mfpwm_func *pwmf;
+ struct counter_device *counter;
+ bool is_enabled;
+ spinlock_t enable_lock;
+};
+
+/*
+ * Channel 0 receives a state changed notification whenever the LPC interrupt
+ * fires.
+ *
+ * Channel 1 receives a state changed notification whenever the HPC interrupt
+ * fires.
+ */
+static struct counter_signal rkpwmc_signals[] = {
+ {
+ .id = 0,
+ .name = "Channel 0"
+ },
+ {
+ .id = 1,
+ .name = "Channel 1"
+ },
+};
+
+static const enum counter_synapse_action rkpwmc_hpc_lpc_actions[] = {
+ COUNTER_SYNAPSE_ACTION_BOTH_EDGES,
+
+};
+
+static struct counter_synapse rkpwmc_pwm_synapses[] = {
+ {
+ .actions_list = rkpwmc_hpc_lpc_actions,
+ .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions),
+ .signal = &rkpwmc_signals[0]
+ },
+ {
+ .actions_list = rkpwmc_hpc_lpc_actions,
+ .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions),
+ .signal = &rkpwmc_signals[1]
+ },
+};
+
+static const enum counter_function rkpwmc_functions[] = {
+ COUNTER_FUNCTION_INCREASE,
+};
+
+static int rkpwmc_enable_read(struct counter_device *counter,
+ struct counter_count *count,
+ u8 *enable)
+{
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
+
+ guard(spinlock)(&pc->enable_lock);
+
+ *enable = pc->is_enabled;
+
+ return 0;
+}
+
+static int rkpwmc_enable_write(struct counter_device *counter,
+ struct counter_count *count,
+ u8 enable)
+{
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
+ int ret;
+
+ guard(spinlock)(&pc->enable_lock);
+
+ if (!!enable != pc->is_enabled) {
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ return ret;
+
+ if (enable) {
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ PWMV4_EN(false));
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL,
+ PWMV4_CTRL_CAP_FLAGS);
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN,
+ PWMV4_INT_LPC_W(true) |
+ PWMV4_INT_HPC_W(true));
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ PWMV4_EN(true) | PWMV4_CLK_EN(true));
+
+ ret = clk_enable(pc->pwmf->core);
+ if (ret)
+ goto err_release;
+
+ ret = clk_rate_exclusive_get(pc->pwmf->core);
+ if (ret)
+ goto err_disable_pwm_clk;
+
+ ret = mfpwm_acquire(pc->pwmf);
+ if (ret)
+ goto err_unprotect_pwm_clk;
+
+ pc->is_enabled = true;
+ } else {
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN,
+ PWMV4_INT_LPC_W(false) |
+ PWMV4_INT_HPC_W(false));
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE,
+ PWMV4_EN(false) | PWMV4_CLK_EN(false));
+ clk_rate_exclusive_put(pc->pwmf->core);
+ clk_disable(pc->pwmf->core);
+ pc->is_enabled = false;
+ mfpwm_release(pc->pwmf);
+ }
+
+ mfpwm_release(pc->pwmf);
+ }
+
+ return 0;
+
+err_unprotect_pwm_clk:
+ clk_rate_exclusive_put(pc->pwmf->core);
+err_disable_pwm_clk:
+ clk_disable(pc->pwmf->core);
+err_release:
+ mfpwm_release(pc->pwmf);
+
+ return ret;
+}
+
+static struct counter_comp rkpwmc_ext[] = {
+ COUNTER_COMP_ENABLE(rkpwmc_enable_read, rkpwmc_enable_write),
+};
+
+enum rkpwmc_count_id {
+ COUNT_LPC = 0,
+ COUNT_HPC = 1,
+};
+
+static struct counter_count rkpwmc_counts[] = {
+ {
+ .id = COUNT_LPC,
+ .name = "PWM core clock cycles during which the signal was low",
+ .functions_list = rkpwmc_functions,
+ .num_functions = ARRAY_SIZE(rkpwmc_functions),
+ .synapses = rkpwmc_pwm_synapses,
+ .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
+ .ext = rkpwmc_ext,
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
+ },
+ {
+ .id = COUNT_HPC,
+ .name = "PWM core clock cycles during which the signal was high",
+ .functions_list = rkpwmc_functions,
+ .num_functions = ARRAY_SIZE(rkpwmc_functions),
+ .synapses = rkpwmc_pwm_synapses,
+ .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
+ .ext = rkpwmc_ext,
+ .num_ext = ARRAY_SIZE(rkpwmc_ext),
+ },
+};
+
+static int rkpwmc_count_read(struct counter_device *counter,
+ struct counter_count *count, u64 *value)
+{
+ struct rockchip_pwm_capture *pc = counter_priv(counter);
+
+ guard(spinlock)(&pc->enable_lock);
+
+ if (!pc->is_enabled) {
+ *value = 0;
+ return 0;
+ }
+
+ switch (count->id) {
+ case COUNT_LPC:
+ *value = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_LPC);
+ return 0;
+ case COUNT_HPC:
+ *value = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_HPC);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct counter_ops rkpwmc_ops = {
+ .count_read = rkpwmc_count_read,
+};
+
+static irqreturn_t rkpwmc_irq_handler(int irq, void *data)
+{
+ struct rockchip_pwm_capture *pc = data;
+ u32 intsts;
+ u32 clr = 0;
+
+ intsts = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_INTSTS);
+
+ if (!(intsts & RKPWMC_INT_MASK))
+ return IRQ_NONE;
+
+ if (intsts & PWMV4_INT_LPC) {
+ clr |= PWMV4_INT_LPC;
+ counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 0);
+ }
+
+ if (intsts & PWMV4_INT_HPC) {
+ clr |= PWMV4_INT_HPC;
+ counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 1);
+ }
+
+ if (clr)
+ mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INTSTS, clr);
+
+ /* If other interrupt status bits are set, they're not for this driver */
+ if (intsts != clr)
+ return IRQ_NONE;
+
+ return IRQ_HANDLED;
+}
+
+static int rockchip_pwm_capture_probe(struct platform_device *pdev)
+{
+ struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev);
+ struct rockchip_pwm_capture *pc;
+ struct counter_device *counter;
+ int ret;
+
+ /* Set our (still unset) OF node to the parent MFD device's OF node */
+ pdev->dev.parent->of_node_reused = true;
+ device_set_node(&pdev->dev,
+ of_fwnode_handle(no_free_ptr(pdev->dev.parent->of_node)));
+
+ counter = devm_counter_alloc(&pdev->dev, sizeof(*pc));
+ if (IS_ERR(counter))
+ return PTR_ERR(counter);
+
+ pc = counter_priv(counter);
+ pc->pwmf = pwmf;
+ spin_lock_init(&pc->enable_lock);
+
+ platform_set_drvdata(pdev, pc);
+
+ ret = devm_request_irq(&pdev->dev, pwmf->irq, rkpwmc_irq_handler,
+ IRQF_SHARED, pdev->name, pc);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret, "Failed requesting IRQ\n");
+
+ counter->name = pdev->name;
+ counter->signals = rkpwmc_signals;
+ counter->num_signals = ARRAY_SIZE(rkpwmc_signals);
+ counter->ops = &rkpwmc_ops;
+ counter->counts = rkpwmc_counts;
+ counter->num_counts = ARRAY_SIZE(rkpwmc_counts);
+
+ pc->counter = counter;
+
+ ret = devm_counter_add(&pdev->dev, counter);
+ if (ret < 0)
+ return dev_err_probe(&pdev->dev, ret, "Failed to add counter\n");
+
+ return 0;
+}
+
+static const struct platform_device_id rockchip_pwm_capture_id_table[] = {
+ { .name = "rockchip-pwm-capture", },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(platform, rockchip_pwm_capture_id_table);
+
+static struct platform_driver rockchip_pwm_capture_driver = {
+ .probe = rockchip_pwm_capture_probe,
+ .id_table = rockchip_pwm_capture_id_table,
+ .driver = {
+ .name = "rockchip-pwm-capture",
+ },
+};
+module_platform_driver(rockchip_pwm_capture_driver);
+
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
+MODULE_DESCRIPTION("Rockchip PWM Counter Capture Driver");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("ROCKCHIP_MFPWM");
+MODULE_IMPORT_NS("COUNTER");
+MODULE_ALIAS("platform:rockchip-pwm-capture");
--
2.51.1
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH v3 4/5] counter: Add rockchip-pwm-capture driver
2025-10-27 17:11 ` [PATCH v3 4/5] counter: Add rockchip-pwm-capture driver Nicolas Frattaroli
@ 2025-10-28 11:05 ` Damon Ding
0 siblings, 0 replies; 17+ messages in thread
From: Damon Ding @ 2025-10-28 11:05 UTC (permalink / raw)
To: Nicolas Frattaroli, Uwe Kleine-König, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner, Lee Jones,
William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio
Hi Nicolas,
On 10/28/2025 1:11 AM, Nicolas Frattaroli wrote:
> Among many other things, Rockchip's new PWMv4 IP in the RK3576 supports
> PWM capture functionality.
>
> Add a basic driver for this that works to expose HPC/LPC counts and
> state change events to userspace through the counter framework. It's
> quite basic, but works well enough to demonstrate the device function
> exclusion stuff that mfpwm does, in order to eventually support all the
> functions of this device in drivers within their appropriate subsystems,
> without them interfering with each other.
>
> Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
> ---
......
> +
> +/*
> + * Channel 0 receives a state changed notification whenever the LPC interrupt
> + * fires.
> + *
> + * Channel 1 receives a state changed notification whenever the HPC interrupt
> + * fires.
> + */
> +static struct counter_signal rkpwmc_signals[] = {
> + {
> + .id = 0,
> + .name = "Channel 0"
> + },
> + {
> + .id = 1,
> + .name = "Channel 1"
> + },
> +};
> +
> +static const enum counter_synapse_action rkpwmc_hpc_lpc_actions[] = {
> + COUNTER_SYNAPSE_ACTION_BOTH_EDGES,
> +
Redundant blank line. ;-)
If the dclk cycle is regarded as the 'input signal', then the HPC/LPC
counters should be COUNTER_SYNAPSE_ACTION_BOTH_EDGES.
If to deal with the interrupt for HPC or LPC, the LPC interrupt occurs
in several pclk cycles after the rising edge while the HPC interrupt is
generated similarly after the falling edge. Shall we distinguish the
synapses between LPC and HPC? LPC is COUNTER_SYNAPSE_ACTION_RISING_EDGE
and HPC is COUNTER_SYNAPSE_ACTION_FALLING_EDGE.
> +};
> +
> +static struct counter_synapse rkpwmc_pwm_synapses[] = {
> + {
> + .actions_list = rkpwmc_hpc_lpc_actions,
> + .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions),
> + .signal = &rkpwmc_signals[0]
> + },
> + {
> + .actions_list = rkpwmc_hpc_lpc_actions,
> + .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions),
> + .signal = &rkpwmc_signals[1]
> + },
> +};
> +
> +static const enum counter_function rkpwmc_functions[] = {
> + COUNTER_FUNCTION_INCREASE,
> +};
> +
......
> +
> +static struct counter_comp rkpwmc_ext[] = {
> + COUNTER_COMP_ENABLE(rkpwmc_enable_read, rkpwmc_enable_write),
> +};
> +
> +enum rkpwmc_count_id {
> + COUNT_LPC = 0,
> + COUNT_HPC = 1,
> +};
> +
> +static struct counter_count rkpwmc_counts[] = {
> + {
> + .id = COUNT_LPC,
> + .name = "PWM core clock cycles during which the signal was low",
> + .functions_list = rkpwmc_functions,
> + .num_functions = ARRAY_SIZE(rkpwmc_functions),
> + .synapses = rkpwmc_pwm_synapses,
> + .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
> + .ext = rkpwmc_ext,
> + .num_ext = ARRAY_SIZE(rkpwmc_ext),
> + },
> + {
> + .id = COUNT_HPC,
> + .name = "PWM core clock cycles during which the signal was high",
> + .functions_list = rkpwmc_functions,
> + .num_functions = ARRAY_SIZE(rkpwmc_functions),
> + .synapses = rkpwmc_pwm_synapses,
> + .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses),
> + .ext = rkpwmc_ext,
> + .num_ext = ARRAY_SIZE(rkpwmc_ext),
> + },
> +};
> +
>
Additionally, I test the capture by connecting pwm0_ch0 and pwm2_ch0,
and the capture results are correct.
I think the counter/frequency meter/biphasic counter functions can also
be adapted to the counter framework. And they will be available soon in
the future. :-)
Best regards,
Damon
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v3 5/5] arm64: dts: rockchip: add PWM nodes to RK3576 SoC dtsi
2025-10-27 17:11 [PATCH v3 0/5] Add Rockchip RK3576 PWM Support Through MFPWM Nicolas Frattaroli
` (3 preceding siblings ...)
2025-10-27 17:11 ` [PATCH v3 4/5] counter: Add rockchip-pwm-capture driver Nicolas Frattaroli
@ 2025-10-27 17:12 ` Nicolas Frattaroli
4 siblings, 0 replies; 17+ messages in thread
From: Nicolas Frattaroli @ 2025-10-27 17:12 UTC (permalink / raw)
To: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Heiko Stuebner, Lee Jones, William Breathitt Gray
Cc: kernel, Jonas Karlman, Alexey Charkov, linux-rockchip, linux-pwm,
devicetree, linux-arm-kernel, linux-kernel, linux-iio,
Nicolas Frattaroli
The RK3576 SoC features three distinct PWM controllers, with variable
numbers of channels. Add each channel as a separate node to the SoC's
device tree, as they don't really overlap in register ranges.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
arch/arm64/boot/dts/rockchip/rk3576.dtsi | 208 +++++++++++++++++++++++++++++++
1 file changed, 208 insertions(+)
diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi
index f0c3ab00a7f3..1e135cf09efc 100644
--- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi
+++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi
@@ -1030,6 +1030,32 @@ uart1: serial@27310000 {
status = "disabled";
};
+ pwm0_2ch_0: pwm@27330000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x27330000 0x0 0x1000>;
+ clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>,
+ <&cru CLK_PMU1PWM_OSC>, <&cru CLK_PMU1PWM_RC>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm0m0_ch0>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm0_2ch_1: pwm@27331000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x27331000 0x0 0x1000>;
+ clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>,
+ <&cru CLK_PMU1PWM_OSC>, <&cru CLK_PMU1PWM_RC>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm0m0_ch1>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
pmu: power-management@27380000 {
compatible = "rockchip,rk3576-pmu", "syscon", "simple-mfd";
reg = <0x0 0x27380000 0x0 0x800>;
@@ -2480,6 +2506,188 @@ uart9: serial@2adc0000 {
status = "disabled";
};
+ pwm1_6ch_0: pwm@2add0000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add0000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch0>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_1: pwm@2add1000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add1000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch1>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_2: pwm@2add2000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add2000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 104 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch2>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_3: pwm@2add3000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add3000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 105 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch3>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_4: pwm@2add4000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add4000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 106 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch4>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm1_6ch_5: pwm@2add5000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2add5000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>,
+ <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 107 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm1m0_ch5>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_0: pwm@2ade0000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade0000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 108 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch0>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_1: pwm@2ade1000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade1000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 109 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch1>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_2: pwm@2ade2000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade2000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 110 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch2>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_3: pwm@2ade3000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade3000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 111 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch3>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_4: pwm@2ade4000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade4000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 112 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch4>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_5: pwm@2ade5000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade5000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 113 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch5>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_6: pwm@2ade6000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade6000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 114 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch6>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
+ pwm2_8ch_7: pwm@2ade7000 {
+ compatible = "rockchip,rk3576-pwm";
+ reg = <0x0 0x2ade7000 0x0 0x1000>;
+ clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>,
+ <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>;
+ clock-names = "pwm", "pclk", "osc", "rc";
+ interrupts = <GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm2m0_ch7>;
+ #pwm-cells = <3>;
+ status = "disabled";
+ };
+
saradc: adc@2ae00000 {
compatible = "rockchip,rk3576-saradc", "rockchip,rk3588-saradc";
reg = <0x0 0x2ae00000 0x0 0x10000>;
--
2.51.1
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 17+ messages in thread