From: Alexandru Gagniuc Date: Sun, 19 Apr 2026 00:37:10 +0000 (-0500) Subject: qualcommbe: enable pwm support for linux 6.18 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cc2d924f81b79fbcab7e1d7a13e697fe8f28532a;p=thirdparty%2Fopenwrt.git qualcommbe: enable pwm support for linux 6.18 The 6.18 kernel port and PWM patches were developed independently. the initial 6.18 port did not include the PWM patches, so add them now. Signed-off-by: Alexandru Gagniuc Link: https://github.com/openwrt/openwrt/pull/21506 Signed-off-by: Robert Marko --- diff --git a/target/linux/qualcommbe/config-6.18 b/target/linux/qualcommbe/config-6.18 index 078d2f90c15..e1fea4b6c2b 100644 --- a/target/linux/qualcommbe/config-6.18 +++ b/target/linux/qualcommbe/config-6.18 @@ -431,6 +431,8 @@ CONFIG_POWER_RESET=y CONFIG_POWER_SUPPLY=y CONFIG_PRINTK_TIME=y CONFIG_PTP_1588_CLOCK_OPTIONAL=y +CONFIG_PWM=y +CONFIG_PWM_IPQ=y CONFIG_QCA807X_PHY=y CONFIG_QCA808X_PHY=y # CONFIG_QCM_DISPCC_2290 is not set diff --git a/target/linux/qualcommbe/patches-6.18/0400-dt-bindings-pwm-add-IPQ6018-binding.patch b/target/linux/qualcommbe/patches-6.18/0400-dt-bindings-pwm-add-IPQ6018-binding.patch new file mode 100644 index 00000000000..d6c647dc780 --- /dev/null +++ b/target/linux/qualcommbe/patches-6.18/0400-dt-bindings-pwm-add-IPQ6018-binding.patch @@ -0,0 +1,75 @@ +From b07d37a8ee4b8e07551033fdb315bb729e5781fc Mon Sep 17 00:00:00 2001 +From: Devi Priya +Date: Fri, 28 Nov 2025 14:29:13 +0400 +Subject: [PATCH] dt-bindings: pwm: add IPQ6018 binding + +DT binding for the PWM block in Qualcomm IPQ6018 SoC. + +Reviewed-by: Bjorn Andersson +Reviewed-by: Krzysztof Kozlowski +Co-developed-by: Baruch Siach +Signed-off-by: Baruch Siach +Signed-off-by: Devi Priya +Signed-off-by: George Moussalem +--- + .../bindings/pwm/qcom,ipq6018-pwm.yaml | 51 +++++++++++++++++++ + 1 file changed, 51 insertions(+) + create mode 100644 Documentation/devicetree/bindings/pwm/qcom,ipq6018-pwm.yaml + +diff --git a/Documentation/devicetree/bindings/pwm/qcom,ipq6018-pwm.yaml b/Documentation/devicetree/bindings/pwm/qcom,ipq6018-pwm.yaml +new file mode 100644 +index 0000000000000..f9f1f652e7527 +--- /dev/null ++++ b/Documentation/devicetree/bindings/pwm/qcom,ipq6018-pwm.yaml +@@ -0,0 +1,51 @@ ++# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) ++%YAML 1.2 ++--- ++$id: http://devicetree.org/schemas/pwm/qcom,ipq6018-pwm.yaml# ++$schema: http://devicetree.org/meta-schemas/core.yaml# ++ ++title: Qualcomm IPQ6018 PWM controller ++ ++maintainers: ++ - George Moussalem ++ ++properties: ++ compatible: ++ oneOf: ++ - items: ++ - enum: ++ - qcom,ipq5018-pwm ++ - qcom,ipq5332-pwm ++ - qcom,ipq9574-pwm ++ - const: qcom,ipq6018-pwm ++ - const: qcom,ipq6018-pwm ++ ++ reg: ++ maxItems: 1 ++ ++ clocks: ++ maxItems: 1 ++ ++ "#pwm-cells": ++ const: 3 ++ ++required: ++ - compatible ++ - reg ++ - clocks ++ - "#pwm-cells" ++ ++additionalProperties: false ++ ++examples: ++ - | ++ #include ++ ++ pwm: pwm@1941010 { ++ compatible = "qcom,ipq6018-pwm"; ++ reg = <0x01941010 0x20>; ++ clocks = <&gcc GCC_ADSS_PWM_CLK>; ++ assigned-clocks = <&gcc GCC_ADSS_PWM_CLK>; ++ assigned-clock-rates = <100000000>; ++ #pwm-cells = <3>; ++ }; diff --git a/target/linux/qualcommbe/patches-6.18/0401-pwm-driver-for-qualcomm-ipq6018-pwm-block.patch b/target/linux/qualcommbe/patches-6.18/0401-pwm-driver-for-qualcomm-ipq6018-pwm-block.patch new file mode 100644 index 00000000000..d793b7d895d --- /dev/null +++ b/target/linux/qualcommbe/patches-6.18/0401-pwm-driver-for-qualcomm-ipq6018-pwm-block.patch @@ -0,0 +1,303 @@ +From 2728124b7d4e32c32ffdc3f9091ccf8c97179f93 Mon Sep 17 00:00:00 2001 +From: Devi Priya +Date: Wed, 4 Feb 2026 15:25:08 +0400 +Subject: [PATCH] pwm: driver for qualcomm ipq6018 pwm block + +Driver for the PWM block in Qualcomm IPQ6018 line of SoCs. Based on +driver from downstream Codeaurora kernel tree. Removed support for older +(V1) variants because I have no access to that hardware. + +Tested on IPQ5018 and IPQ6010 based hardware. + +Co-developed-by: Baruch Siach +Signed-off-by: Baruch Siach +Signed-off-by: Devi Priya +Reviewed-by: Bjorn Andersson +Signed-off-by: George Moussalem +--- + drivers/pwm/Kconfig | 12 +++ + drivers/pwm/Makefile | 1 + + drivers/pwm/pwm-ipq.c | 239 ++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 252 insertions(+) + create mode 100644 drivers/pwm/pwm-ipq.c + +diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig +index c2fd3f4b62d9e..33ac49251b3cc 100644 +--- a/drivers/pwm/Kconfig ++++ b/drivers/pwm/Kconfig +@@ -337,6 +337,18 @@ config PWM_INTEL_LGM + To compile this driver as a module, choose M here: the module + will be called pwm-intel-lgm. + ++config PWM_IPQ ++ tristate "IPQ PWM support" ++ depends on ARCH_QCOM || COMPILE_TEST ++ depends on HAVE_CLK && HAS_IOMEM ++ help ++ Generic PWM framework driver for IPQ PWM block which supports ++ 4 pwm channels. Each of the these channels can be configured ++ independent of each other. ++ ++ To compile this driver as a module, choose M here: the module ++ will be called pwm-ipq. ++ + config PWM_IQS620A + tristate "Azoteq IQS620A PWM support" + depends on MFD_IQS62X || COMPILE_TEST +diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile +index dfa8b4966ee19..74e07f654d43d 100644 +--- a/drivers/pwm/Makefile ++++ b/drivers/pwm/Makefile +@@ -28,6 +28,7 @@ obj-$(CONFIG_PWM_IMX1) += pwm-imx1.o + obj-$(CONFIG_PWM_IMX27) += pwm-imx27.o + obj-$(CONFIG_PWM_IMX_TPM) += pwm-imx-tpm.o + obj-$(CONFIG_PWM_INTEL_LGM) += pwm-intel-lgm.o ++obj-$(CONFIG_PWM_IPQ) += pwm-ipq.o + obj-$(CONFIG_PWM_IQS620A) += pwm-iqs620a.o + obj-$(CONFIG_PWM_JZ4740) += pwm-jz4740.o + obj-$(CONFIG_PWM_KEEMBAY) += pwm-keembay.o +diff --git a/drivers/pwm/pwm-ipq.c b/drivers/pwm/pwm-ipq.c +new file mode 100644 +index 0000000000000..b944ecb456d59 +--- /dev/null ++++ b/drivers/pwm/pwm-ipq.c +@@ -0,0 +1,239 @@ ++// SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0 ++/* ++ * Copyright (c) 2016-2017, 2020 The Linux Foundation. All rights reserved. ++ * ++ * Hardware notes / Limitations: ++ * - The PWM controller has no publicly available datasheet. ++ * - Each of the four channels is programmed via two 32-bit registers ++ * (REG0 and REG1 at 8-byte stride). ++ * - Period and duty-cycle reconfiguration is fully atomic: new divider, ++ * pre-divider, and high-duration values are latched by setting the ++ * UPDATE bit (bit 30 in REG1). The hardware applies the new settings ++ * at the beginning of the next period without disabling the output, ++ * so the currently running period is always completed. ++ * - On disable (clearing the ENABLE bit 31 in REG1), the hardware ++ * finishes the current period before stopping the output. The pin ++ * is then driven to the inactive (low) level. ++ * - Upon disabling, the hardware resets the pre-divider (PRE_DIV) and divider ++ * fields (PWM_DIV) in REG0 and REG1 to 0x0000 and 0x0001 respectively. ++ * - Only normal polarity is supported. ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++/* The frequency range supported is 1 Hz to clock rate */ ++#define IPQ_PWM_MAX_PERIOD_NS ((u64)NSEC_PER_SEC) ++ ++/* ++ * Two 32-bit registers for each PWM: REG0, and REG1. ++ * Base offset for PWM #i is at 8 * #i. ++ */ ++#define IPQ_PWM_REG0 0 ++#define IPQ_PWM_REG0_PWM_DIV GENMASK(15, 0) ++#define IPQ_PWM_REG0_HI_DURATION GENMASK(31, 16) ++ ++#define IPQ_PWM_REG1 4 ++#define IPQ_PWM_REG1_PRE_DIV GENMASK(15, 0) ++ ++/* ++ * The max value specified for each field is based on the number of bits ++ * in the pwm control register for that field (16-bit) ++ */ ++#define IPQ_PWM_MAX_DIV FIELD_MAX(IPQ_PWM_REG0_PWM_DIV) ++ ++/* ++ * Enable bit is set to enable output toggling in pwm device. ++ * Update bit is set to trigger the change and is unset automatically ++ * to reflect the changed divider and high duration values in register. ++ */ ++#define IPQ_PWM_REG1_UPDATE BIT(30) ++#define IPQ_PWM_REG1_ENABLE BIT(31) ++ ++struct ipq_pwm_chip { ++ void __iomem *mem; ++ unsigned long clk_rate; ++}; ++ ++static struct ipq_pwm_chip *ipq_pwm_from_chip(struct pwm_chip *chip) ++{ ++ return pwmchip_get_drvdata(chip); ++} ++ ++static unsigned int ipq_pwm_reg_read(struct pwm_device *pwm, unsigned int reg) ++{ ++ struct ipq_pwm_chip *ipq_chip = ipq_pwm_from_chip(pwm->chip); ++ unsigned int off = 8 * pwm->hwpwm + reg; ++ ++ return readl(ipq_chip->mem + off); ++} ++ ++static void ipq_pwm_reg_write(struct pwm_device *pwm, unsigned int reg, ++ unsigned int val) ++{ ++ struct ipq_pwm_chip *ipq_chip = ipq_pwm_from_chip(pwm->chip); ++ unsigned int off = 8 * pwm->hwpwm + reg; ++ ++ writel(val, ipq_chip->mem + off); ++} ++ ++static int ipq_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm, ++ const struct pwm_state *state) ++{ ++ struct ipq_pwm_chip *ipq_chip = ipq_pwm_from_chip(chip); ++ unsigned int pre_div, pwm_div; ++ u64 period_ns, duty_ns; ++ unsigned long val = 0; ++ unsigned long hi_dur; ++ ++ if (state->polarity != PWM_POLARITY_NORMAL) ++ return -EINVAL; ++ ++ if (!ipq_chip->clk_rate) ++ return -EINVAL; ++ ++ if (state->period < DIV64_U64_ROUND_UP(NSEC_PER_SEC, ++ ipq_chip->clk_rate)) ++ return -ERANGE; ++ ++ period_ns = min(state->period, IPQ_PWM_MAX_PERIOD_NS); ++ duty_ns = min(state->duty_cycle, period_ns); ++ ++ pwm_div = IPQ_PWM_MAX_DIV - 1; ++ pre_div = mul_u64_u64_div_u64(period_ns, ipq_chip->clk_rate, ++ (u64)NSEC_PER_SEC * (pwm_div + 1)); ++ pre_div = (pre_div > 0) ? pre_div - 1 : 0; ++ ++ if (pre_div > IPQ_PWM_MAX_DIV) ++ pre_div = IPQ_PWM_MAX_DIV; ++ ++ /* ++ * high duration = pwm duty * (pwm div + 1) ++ * pwm duty = duty_ns / period_ns ++ */ ++ hi_dur = mul_u64_u64_div_u64(duty_ns, ipq_chip->clk_rate, ++ (u64)(pre_div + 1) * NSEC_PER_SEC); ++ ++ val = FIELD_PREP(IPQ_PWM_REG0_HI_DURATION, hi_dur) | ++ FIELD_PREP(IPQ_PWM_REG0_PWM_DIV, pwm_div); ++ ipq_pwm_reg_write(pwm, IPQ_PWM_REG0, val); ++ ++ val = FIELD_PREP(IPQ_PWM_REG1_PRE_DIV, pre_div); ++ ipq_pwm_reg_write(pwm, IPQ_PWM_REG1, val); ++ ++ /* PWM enable toggle needs a separate write to REG1 */ ++ val |= IPQ_PWM_REG1_UPDATE; ++ if (state->enabled) ++ val |= IPQ_PWM_REG1_ENABLE; ++ ipq_pwm_reg_write(pwm, IPQ_PWM_REG1, val); ++ ++ return 0; ++} ++ ++static int ipq_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm, ++ struct pwm_state *state) ++{ ++ struct ipq_pwm_chip *ipq_chip = ipq_pwm_from_chip(chip); ++ unsigned int pre_div, pwm_div, hi_dur; ++ u64 effective_div, hi_div; ++ u32 reg0, reg1; ++ ++ reg1 = ipq_pwm_reg_read(pwm, IPQ_PWM_REG1); ++ state->enabled = reg1 & IPQ_PWM_REG1_ENABLE; ++ ++ if (!state->enabled) ++ return 0; ++ ++ reg0 = ipq_pwm_reg_read(pwm, IPQ_PWM_REG0); ++ ++ state->polarity = PWM_POLARITY_NORMAL; ++ ++ pwm_div = FIELD_GET(IPQ_PWM_REG0_PWM_DIV, reg0); ++ hi_dur = FIELD_GET(IPQ_PWM_REG0_HI_DURATION, reg0); ++ pre_div = FIELD_GET(IPQ_PWM_REG1_PRE_DIV, reg1); ++ ++ effective_div = (u64)(pre_div + 1) * (pwm_div + 1); ++ state->period = DIV64_U64_ROUND_UP(effective_div * NSEC_PER_SEC, ++ ipq_chip->clk_rate); ++ ++ hi_div = hi_dur * (pre_div + 1); ++ state->duty_cycle = DIV64_U64_ROUND_UP(hi_div * NSEC_PER_SEC, ++ ipq_chip->clk_rate); ++ ++ /* ++ * ensure a valid config is passed back to PWM core in case duty_cycle ++ * is > period (>100%) ++ */ ++ state->duty_cycle = min(state->duty_cycle, state->period); ++ ++ return 0; ++} ++ ++static const struct pwm_ops ipq_pwm_ops = { ++ .apply = ipq_pwm_apply, ++ .get_state = ipq_pwm_get_state, ++}; ++ ++static int ipq_pwm_probe(struct platform_device *pdev) ++{ ++ struct device *dev = &pdev->dev; ++ struct ipq_pwm_chip *pwm; ++ struct pwm_chip *chip; ++ struct clk *clk; ++ int ret; ++ ++ chip = devm_pwmchip_alloc(dev, 4, sizeof(*pwm)); ++ if (IS_ERR(chip)) ++ return PTR_ERR(chip); ++ pwm = ipq_pwm_from_chip(chip); ++ ++ pwm->mem = devm_platform_ioremap_resource(pdev, 0); ++ if (IS_ERR(pwm->mem)) ++ return dev_err_probe(dev, PTR_ERR(pwm->mem), ++ "Failed to acquire resource\n"); ++ ++ clk = devm_clk_get_enabled(dev, NULL); ++ if (IS_ERR(clk)) ++ return dev_err_probe(dev, PTR_ERR(clk), ++ "Failed to get clock\n"); ++ ++ ret = devm_clk_rate_exclusive_get(dev, clk); ++ if (ret) ++ return dev_err_probe(dev, ret, "Failed to lock clock rate\n"); ++ ++ pwm->clk_rate = clk_get_rate(clk); ++ ++ chip->ops = &ipq_pwm_ops; ++ ++ ret = devm_pwmchip_add(dev, chip); ++ if (ret < 0) ++ return dev_err_probe(dev, ret, "Failed to add pwm chip\n"); ++ ++ return 0; ++} ++ ++static const struct of_device_id pwm_ipq_dt_match[] = { ++ { .compatible = "qcom,ipq6018-pwm", }, ++ {} ++}; ++MODULE_DEVICE_TABLE(of, pwm_ipq_dt_match); ++ ++static struct platform_driver ipq_pwm_driver = { ++ .driver = { ++ .name = "ipq-pwm", ++ .of_match_table = pwm_ipq_dt_match, ++ }, ++ .probe = ipq_pwm_probe, ++}; ++ ++module_platform_driver(ipq_pwm_driver); ++ ++MODULE_LICENSE("GPL"); diff --git a/target/linux/qualcommbe/patches-6.18/0402-arm64-dts-qcom-ipq9574-add-pwm-node.patch b/target/linux/qualcommbe/patches-6.18/0402-arm64-dts-qcom-ipq9574-add-pwm-node.patch new file mode 100644 index 00000000000..f3add218ae4 --- /dev/null +++ b/target/linux/qualcommbe/patches-6.18/0402-arm64-dts-qcom-ipq9574-add-pwm-node.patch @@ -0,0 +1,39 @@ +From 26533a030121e69bede9757b86c30edf9f851b8c Mon Sep 17 00:00:00 2001 +From: George Moussalem +Date: Wed, 4 Feb 2026 15:25:12 +0400 +Subject: [PATCH] arm64: dts: qcom: ipq9574: add pwm node + +Describe the PWM block on IPQ9574. + +Although PWM is in the TCSR area, make pwm its own node as simple-mfd +has been removed from the bindings and as such hardware components +should have its own node. + +Reviewed-by: Dmitry Baryshkov +Signed-off-by: George Moussalem +Reviewed-by: Konrad Dybcio +--- + arch/arm64/boot/dts/qcom/ipq9574.dtsi | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/arch/arm64/boot/dts/qcom/ipq9574.dtsi b/arch/arm64/boot/dts/qcom/ipq9574.dtsi +index 83e0bd75086bc..2c4f1bc2a70ee 100644 +--- a/arch/arm64/boot/dts/qcom/ipq9574.dtsi ++++ b/arch/arm64/boot/dts/qcom/ipq9574.dtsi +@@ -469,6 +469,16 @@ tcsr: syscon@1937000 { + reg = <0x01937000 0x21000>; + }; + ++ pwm: pwm@1941010 { ++ compatible = "qcom,ipq9574-pwm", "qcom,ipq6018-pwm"; ++ reg = <0x01941010 0x20>; ++ clocks = <&gcc GCC_ADSS_PWM_CLK>; ++ assigned-clocks = <&gcc GCC_ADSS_PWM_CLK>; ++ assigned-clock-rates = <100000000>; ++ #pwm-cells = <3>; ++ status = "disabled"; ++ }; ++ + sdhc_1: mmc@7804000 { + compatible = "qcom,ipq9574-sdhci", "qcom,sdhci-msm-v5"; + reg = <0x07804000 0x1000>,