[PATCH v2 03/12] clk: qcom: Add a driver for PDM GP_MN fractional clock divider

From: Taniya Das

Date: Mon Oct 26 2026 - 07:36:31 EST


The PDM (Pulse Density Modulation) hardware block on Qualcomm SoCs
contains a GP_MN clock divider that produces a fractional output
frequency from a fixed input clock (typically TCXO4):

Fout = Fin * (M / N)

The hardware encodes the period in the NDIV register as the 1's
complement of (N - M), and controls the duty cycle via a separate
DUTY register that counts the number of low-phase native clock
cycles over the period N.

Add a standalone platform driver for this block that uses
rational_best_approximation() to find the closest M/N pair within
the 9-bit M and 13-bit N hardware limits. Reject ratios which cannot
support a valid duty cycle, and program the MDIV, NDIV, and DUTY
registers via regmap. Implement determine_rate, set_rate,
recalc_rate, get_duty_cycle, and set_duty_cycle operations. The PDM
AHB bus clock is gated around every register access.

Signed-off-by: Taniya Das <taniya.das@xxxxxxxxxxxxxxxx>
---
drivers/clk/qcom/Kconfig | 15 ++
drivers/clk/qcom/Makefile | 1 +
drivers/clk/qcom/clk-gp-mnd.c | 333 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 349 insertions(+)

diff --git a/drivers/clk/qcom/Kconfig b/drivers/clk/qcom/Kconfig
index 27d0ab24d50a4ca5587ed8d6085c4d6fcd57eb8c..d9f1df77b715b08a32c164a6d25d88fc545f59cb 100644
--- a/drivers/clk/qcom/Kconfig
+++ b/drivers/clk/qcom/Kconfig
@@ -2148,4 +2148,19 @@ config SM_VIDEOCC_8450
SM8450 or SM8475 devices.
Say Y if you want to support video devices and functionality such as
video encode/decode.
+
+config QCOM_CLK_GP_MND
+ tristate "Qualcomm PDM GP_MN clock divider"
+ depends on ARM64 || COMPILE_TEST
+ help
+ Support for the Qualcomm PDM GP_MN clock divider found in PDM
+ (Pulse Density Modulation) hardware blocks.
+ Given an input clock of frequency Fin (TCXO4), the output
+ frequency is Fout = Fin * (M / N). For every N input cycles
+ the divider produces M output cycles. D controls the duty
+ cycle: it is the number of native clock cycles in which the
+ GP_MN output is low, counted over 8192 native clock cycles.
+
+ Say Y or M if you want to support GP_MN-based frequency and
+ duty-cycle configuration on Qualcomm SoCs.
endif
diff --git a/drivers/clk/qcom/Makefile b/drivers/clk/qcom/Makefile
index 6affb5073f269d18f1b78d6c7f92e14baeade1e5..4e8563be8e1b99aef48b227ff0c5dc04dc092d0a 100644
--- a/drivers/clk/qcom/Makefile
+++ b/drivers/clk/qcom/Makefile
@@ -227,6 +227,7 @@ obj-$(CONFIG_SM_VIDEOCC_8550) += videocc-sm8550.o
obj-$(CONFIG_SM_VIDEOCC_8750) += videocc-sm8750.o
obj-$(CONFIG_SM_VIDEOCC_MILOS) += videocc-milos.o
obj-$(CONFIG_SPMI_PMIC_CLKDIV) += clk-spmi-pmic-div.o
+obj-$(CONFIG_QCOM_CLK_GP_MND) += clk-gp-mnd.o
obj-$(CONFIG_KPSS_XCC) += kpss-xcc.o
obj-$(CONFIG_QCOM_HFPLL) += hfpll.o
obj-$(CONFIG_KRAITCC) += krait-cc.o
diff --git a/drivers/clk/qcom/clk-gp-mnd.c b/drivers/clk/qcom/clk-gp-mnd.c
new file mode 100644
index 0000000000000000000000000000000000000000..818726c02b615689928cf09b4b34f0889d4246d8
--- /dev/null
+++ b/drivers/clk/qcom/clk-gp-mnd.c
@@ -0,0 +1,333 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm_clock.h>
+#include <linux/pm_runtime.h>
+#include <linux/rational.h>
+#include <linux/regmap.h>
+
+/*
+ * PDM GP_MND clock divider register offsets.
+ *
+ * The hardware computes:
+ * Fout = Fin * (M / N)
+ *
+ * with duty cycle controlled by D, where M < D < (N - M).
+ *
+ * Register encoding:
+ * MDIV = M
+ * NDIV = ~(N - M) [1's complement of (N - M), masked to N_REG_WIDTH bits]
+ * DUTY = D
+ */
+#define GP_MND_MDIV_REG 0x0
+#define GP_MND_NDIV_REG 0x4
+#define GP_MND_DUTY_REG 0x8
+
+#define GP_MND_M_WIDTH 9
+#define GP_MND_N_WIDTH 13
+
+#define GP_MND_MAX_M GENMASK(GP_MND_M_WIDTH - 1, 0)
+#define GP_MND_MAX_N GENMASK(GP_MND_N_WIDTH - 1, 0)
+
+/**
+ * struct clk_gp_mnd - GP_MND fractional clock divider
+ * @dev: device for runtime PM
+ * @regmap: register map for the PDM block
+ * @hw: handle between common and hardware-specific interfaces
+ * @m_val: M value (numerator)
+ * @n_val: N value (period)
+ */
+struct clk_gp_mnd {
+ struct device *dev;
+ struct regmap *regmap;
+ struct clk_hw hw;
+ unsigned int m_val;
+ unsigned int n_val;
+};
+
+#define to_clk_gp_mnd(_hw) container_of(_hw, struct clk_gp_mnd, hw)
+
+static int gp_mnd_clk_runtime_resume(struct clk_gp_mnd *gp)
+{
+ return pm_runtime_resume_and_get(gp->dev);
+}
+
+static void gp_mnd_clk_runtime_put(struct clk_gp_mnd *gp)
+{
+ pm_runtime_put(gp->dev);
+}
+
+static int gp_mnd_clk_determine_rate(struct clk_hw *hw,
+ struct clk_rate_request *req)
+{
+ unsigned long m = 0, n = 0;
+
+ rational_best_approximation(req->rate, req->best_parent_rate,
+ (unsigned long)GP_MND_MAX_M,
+ (unsigned long)GP_MND_MAX_N,
+ &m, &n);
+ if (!m || !n)
+ return -EINVAL;
+
+ if (n <= 2 * m + 1)
+ return -EINVAL;
+
+ req->rate = DIV_ROUND_CLOSEST_ULL((u64)req->best_parent_rate * m, n);
+
+ return 0;
+}
+
+static int gp_mnd_clk_set_rate(struct clk_hw *hw, unsigned long rate,
+ unsigned long parent_rate)
+{
+ struct clk_gp_mnd *gp = to_clk_gp_mnd(hw);
+ unsigned long m = 0, n = 0;
+ unsigned int d_val, n_val;
+ int ret;
+
+ rational_best_approximation(rate, parent_rate,
+ (unsigned long)GP_MND_MAX_M,
+ (unsigned long)GP_MND_MAX_N,
+ &m, &n);
+ if (!m || !n)
+ return -EINVAL;
+
+ if (n <= 2 * m + 1)
+ return -EINVAL;
+
+ ret = gp_mnd_clk_runtime_resume(gp);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(gp->regmap, GP_MND_MDIV_REG, m);
+ if (ret)
+ goto err_runtime_put;
+
+ /* N divider holds the 1's complement of (N - M), N_WIDTH bits wide */
+ n_val = ~(n - m) & GP_MND_MAX_N;
+ ret = regmap_write(gp->regmap, GP_MND_NDIV_REG, n_val);
+ if (ret)
+ goto err_runtime_put;
+
+ /* Program the closest-to-50% duty cycle. */
+ d_val = n / 2;
+ ret = regmap_write(gp->regmap, GP_MND_DUTY_REG, d_val);
+ if (ret)
+ goto err_runtime_put;
+
+ gp->m_val = m;
+ gp->n_val = n;
+
+err_runtime_put:
+ gp_mnd_clk_runtime_put(gp);
+
+ return ret;
+}
+
+static unsigned long gp_mnd_clk_recalc_rate(struct clk_hw *hw,
+ unsigned long parent_rate)
+{
+ struct clk_gp_mnd *gp = to_clk_gp_mnd(hw);
+ unsigned int m_val, n_val;
+ int ret;
+
+ ret = gp_mnd_clk_runtime_resume(gp);
+ if (ret)
+ return 0;
+
+ ret = regmap_read(gp->regmap, GP_MND_MDIV_REG, &m_val);
+ if (ret)
+ goto out_runtime_put;
+
+ m_val &= GP_MND_MAX_M;
+
+ ret = regmap_read(gp->regmap, GP_MND_NDIV_REG, &n_val);
+ if (ret)
+ goto out_runtime_put;
+
+ /* Reverse the 1's complement encoding: N = ~NDIV_REG + M */
+ n_val = (~n_val & GP_MND_MAX_N) + m_val;
+
+out_runtime_put:
+ gp_mnd_clk_runtime_put(gp);
+
+ if (ret)
+ return 0;
+
+ if (!n_val)
+ return 0;
+
+ gp->m_val = m_val;
+ gp->n_val = n_val;
+
+ return DIV_ROUND_CLOSEST_ULL((u64)parent_rate * m_val, n_val);
+}
+
+static int gp_mnd_clk_get_duty_cycle(struct clk_hw *hw, struct clk_duty *duty)
+{
+ struct clk_gp_mnd *gp = to_clk_gp_mnd(hw);
+ unsigned int d_val;
+ int ret;
+
+ if (!gp->n_val) {
+ duty->num = 1;
+ duty->den = 2;
+ return 0;
+ }
+
+ ret = gp_mnd_clk_runtime_resume(gp);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(gp->regmap, GP_MND_DUTY_REG, &d_val);
+
+ gp_mnd_clk_runtime_put(gp);
+
+ if (ret)
+ return ret;
+
+ if (d_val >= gp->n_val)
+ return -EINVAL;
+
+ /* DUTY stores the low-phase count; report the high-phase duty cycle. */
+ duty->num = gp->n_val - d_val;
+ duty->den = gp->n_val;
+
+ return 0;
+}
+
+static int gp_mnd_clk_set_duty_cycle(struct clk_hw *hw, struct clk_duty *duty)
+{
+ struct clk_gp_mnd *gp = to_clk_gp_mnd(hw);
+ unsigned int d_val;
+ int ret;
+
+ if (!gp->n_val || !gp->m_val || !duty->den || duty->num > duty->den)
+ return -EINVAL;
+
+ /* D = (1 - duty) * N, giving the low-phase count */
+ d_val = DIV_ROUND_UP((u64)(duty->den - duty->num) * gp->n_val, duty->den);
+
+ /* Hardware constraint: M < D < (N - M) */
+ if (d_val <= gp->m_val || d_val >= (gp->n_val - gp->m_val))
+ return -EINVAL;
+
+ ret = gp_mnd_clk_runtime_resume(gp);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(gp->regmap, GP_MND_DUTY_REG, d_val);
+
+ gp_mnd_clk_runtime_put(gp);
+
+ return ret;
+}
+
+static const struct clk_ops clk_gp_mnd_ops = {
+ .determine_rate = gp_mnd_clk_determine_rate,
+ .set_rate = gp_mnd_clk_set_rate,
+ .recalc_rate = gp_mnd_clk_recalc_rate,
+ .get_duty_cycle = gp_mnd_clk_get_duty_cycle,
+ .set_duty_cycle = gp_mnd_clk_set_duty_cycle,
+};
+
+static const struct regmap_config gp_mnd_regmap_config = {
+ .reg_bits = 32,
+ .reg_stride = 4,
+ .val_bits = 32,
+ .fast_io = true,
+};
+
+static int clk_gp_mnd_probe(struct platform_device *pdev)
+{
+ struct clk_parent_data parent_data = { .index = 0 };
+ struct clk_init_data init = {
+ .name = "gp_mn_clk",
+ .ops = &clk_gp_mnd_ops,
+ .parent_data = &parent_data,
+ .num_parents = 1,
+ .flags = CLK_GET_RATE_NOCACHE,
+ };
+ struct device *dev = &pdev->dev;
+ struct clk_gp_mnd *gp;
+ struct clk *clk;
+ void __iomem *base;
+ int ret;
+
+ gp = devm_kzalloc(dev, sizeof(*gp), GFP_KERNEL);
+ if (!gp)
+ return -ENOMEM;
+
+ gp->dev = dev;
+
+ ret = devm_pm_runtime_enable(dev);
+ if (ret)
+ return ret;
+
+ ret = devm_pm_clk_create(dev);
+ if (ret)
+ return ret;
+
+ ret = pm_clk_add(dev, "ahb_clk");
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to get ahb_clk\n");
+
+ clk = devm_clk_get(dev, "pdm_clk");
+ if (IS_ERR(clk))
+ return PTR_ERR(clk);
+
+ /* Set default rate if not already configured */
+ if (!clk_get_rate(clk)) {
+ ret = clk_set_rate(clk, 19200000);
+ if (ret)
+ dev_warn(dev, "failed to set default pdm_clk rate\n");
+ }
+
+ base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(base))
+ return dev_err_probe(dev, PTR_ERR(base),
+ "failed to map PDM registers\n");
+
+ gp->regmap = devm_regmap_init_mmio(dev, base, &gp_mnd_regmap_config);
+ if (IS_ERR(gp->regmap))
+ return dev_err_probe(dev, PTR_ERR(gp->regmap),
+ "failed to init regmap\n");
+
+ gp->hw.init = &init;
+
+ ret = devm_clk_hw_register(dev, &gp->hw);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to register gp_mnd clock\n");
+
+ return devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get, &gp->hw);
+}
+
+static const struct of_device_id clk_gp_mnd_match_table[] = {
+ { .compatible = "qcom,sc7280-gp-mnd" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, clk_gp_mnd_match_table);
+
+static const struct dev_pm_ops clk_gp_mnd_pm_ops = {
+ SET_RUNTIME_PM_OPS(pm_clk_suspend, pm_clk_resume, NULL)
+};
+
+static struct platform_driver clk_gp_mnd_driver = {
+ .probe = clk_gp_mnd_probe,
+ .driver = {
+ .name = "qcom-clk-gp-mnd",
+ .of_match_table = clk_gp_mnd_match_table,
+ .pm = pm_ptr(&clk_gp_mnd_pm_ops),
+ },
+};
+module_platform_driver(clk_gp_mnd_driver);
+
+MODULE_DESCRIPTION("Qualcomm PDM GP_MND clock divider driver");
+MODULE_LICENSE("GPL");

--
2.34.1