diff --git a/target/linux/qualcommbe/config-6.18 b/target/linux/qualcommbe/config-6.18 index 078d2f90c1..e1fea4b6c2 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 0000000000..d6c647dc78 --- /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 0000000000..d793b7d895 --- /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 0000000000..f3add218ae --- /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>,