[PATCH v7 06/13] clk: zte: Add Clock registration infrastructure
From: Stefan Dösinger
Date: Thu Jul 16 2026 - 17:36:55 EST
The next patches will implement the regmap clocks and PLL driver. The
actual hardware specific clock listing will live in a separate module.
Signed-off-by: Stefan Dösinger <stefandoesinger@xxxxxxxxx>
---
Version 7:
*) Add fixed dividers to handle PLL subdivisions
*) Never register PLLs directly as exported clocks - everything on this
SoC goes through a gate before it leaves a controller.
Version 6:
*) Remove auxdev now that LSP clocks also use MFD
*) Error codepath fixes pointed out by Sashiko.
Version 5:
*) Pass the static clk data instead of calling get_match_data to prepare
for operating as an MFD child.
*) Don't use devm_kzalloc to allocate the auxiliary_device
structure. I guess Sashiko is right, and that's what "Because once the
device is placed on the bus the parent driver can not tell what other
code may have a reference to this data" is trying to dell me.
*) Fix error check for device_node_to_regmap.
---
MAINTAINERS | 1 +
drivers/clk/Kconfig | 1 +
drivers/clk/Makefile | 1 +
drivers/clk/zte/Kconfig | 16 +++++
drivers/clk/zte/Makefile | 5 ++
drivers/clk/zte/clk-regmap.c | 34 ++++++++++
drivers/clk/zte/clk-zx.c | 155 +++++++++++++++++++++++++++++++++++++++++++
drivers/clk/zte/clk-zx.h | 90 +++++++++++++++++++++++++
drivers/clk/zte/pll-zx.c | 16 +++++
9 files changed, 319 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 6ca3312aa746..f88a2eda737a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3884,6 +3884,7 @@ F: Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
F: Documentation/devicetree/bindings/phy/zte,zx297520v3-usb-phy.yaml
F: arch/arm/boot/dts/zte/
F: arch/arm/mach-zte/
+F: drivers/clk/zte/
F: drivers/soc/zte/
F: include/dt-bindings/clock/zte,zx297520v3-clk.h
F: include/dt-bindings/phy/phy-zte-zx297520v3-usb.h
diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig
index 1717ce75a907..6f0a863951ca 100644
--- a/drivers/clk/Kconfig
+++ b/drivers/clk/Kconfig
@@ -545,6 +545,7 @@ source "drivers/clk/uniphier/Kconfig"
source "drivers/clk/visconti/Kconfig"
source "drivers/clk/x86/Kconfig"
source "drivers/clk/xilinx/Kconfig"
+source "drivers/clk/zte/Kconfig"
source "drivers/clk/zynqmp/Kconfig"
# Kunit test cases
diff --git a/drivers/clk/Makefile b/drivers/clk/Makefile
index cc108a75a900..13a5478f1112 100644
--- a/drivers/clk/Makefile
+++ b/drivers/clk/Makefile
@@ -167,5 +167,6 @@ ifeq ($(CONFIG_COMMON_CLK), y)
obj-$(CONFIG_X86) += x86/
endif
obj-y += xilinx/
+obj-$(CONFIG_COMMON_CLK_ZTE) += zte/
obj-$(CONFIG_ARCH_ZYNQ) += zynq/
obj-$(CONFIG_COMMON_CLK_ZYNQMP) += zynqmp/
diff --git a/drivers/clk/zte/Kconfig b/drivers/clk/zte/Kconfig
new file mode 100644
index 000000000000..0222549dd211
--- /dev/null
+++ b/drivers/clk/zte/Kconfig
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# ZTE Clock Drivers
+#
+
+config COMMON_CLK_ZTE
+ tristate "Clock driver for ZTE SoCs"
+ depends on ARCH_ZTE || COMPILE_TEST
+ default ARCH_ZTE
+ select MFD_SYSCON
+ help
+ This option selects common clock infrastructure for ZTE based SoCs.
+ You will need to enable one or more SoC specific drivers to make use
+ of this.
+
+ Enable this if you are building a kernel for a ZTE designed board.
diff --git a/drivers/clk/zte/Makefile b/drivers/clk/zte/Makefile
new file mode 100644
index 000000000000..27db07293165
--- /dev/null
+++ b/drivers/clk/zte/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_COMMON_CLK_ZTE) += clk-zte.o
+
+clk-zte-y += clk-zx.o pll-zx.o clk-regmap.o
diff --git a/drivers/clk/zte/clk-regmap.c b/drivers/clk/zte/clk-regmap.c
new file mode 100644
index 000000000000..984abeb45ab2
--- /dev/null
+++ b/drivers/clk/zte/clk-regmap.c
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2014 MediaTek Inc.
+ * Copyright (c) 2018 BayLibre, SAS.
+ * Copyright (c) 2026 Stefan Dösinger.
+ * Author: Stefan Dösinger <stefandoesinger@xxxxxxxxx>
+ */
+
+#include <linux/clk-provider.h>
+#include <linux/device.h>
+#include <linux/regmap.h>
+#include <linux/errno.h>
+
+#include "clk-zx.h"
+
+int zx_clk_register_gates(struct device *dev, struct regmap *regmap,
+ const struct zx_gate_desc *desc, unsigned int num,
+ struct clk_hw_onecell_data *clocks)
+{
+ return -ENODEV;
+}
+
+int zx_clk_register_dividers(struct device *dev, struct regmap *regmap,
+ const struct zx_div_desc *desc, unsigned int num)
+{
+ return -ENODEV;
+}
+
+int zx_clk_register_muxes(struct device *dev, struct regmap *regmap,
+ const struct zx_mux_desc *desc, unsigned int num,
+ struct clk_hw_onecell_data *clocks)
+{
+ return -ENODEV;
+}
diff --git a/drivers/clk/zte/clk-zx.c b/drivers/clk/zte/clk-zx.c
new file mode 100644
index 000000000000..db4fad2b4b05
--- /dev/null
+++ b/drivers/clk/zte/clk-zx.c
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ */
+
+#include <linux/clk-provider.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/errno.h>
+#include <linux/clk.h>
+#include <linux/err.h>
+
+#include "clk-zx.h"
+
+static int zx_clk_register_fixed_dividers(struct device *dev, struct regmap *regmap,
+ const struct zx_fixed_divider_desc *desc,
+ unsigned int num)
+{
+ struct clk_hw *clk;
+ unsigned int i;
+
+ for (i = 0; i < num; ++i) {
+ clk = devm_clk_hw_register_fixed_factor(dev, desc[i].name, desc[i].parent,
+ CLK_SET_RATE_PARENT, 1, desc[i].div);
+ if (IS_ERR(clk)) {
+ return dev_err_probe(dev, PTR_ERR(clk), "Failed to register clk %s\n",
+ desc[i].name);
+ }
+ }
+
+ return 0;
+}
+
+static void zx_delete_clk_provider(void *data)
+{
+ of_clk_del_provider(data);
+}
+
+static void zx_clk_disable_unprepare_put(void *data)
+{
+ clk_disable_unprepare(data);
+ clk_put(data);
+}
+
+int zx_clk_common_probe(struct device *dev, struct device_node *of_node,
+ const struct zx_clk_data *data)
+{
+ unsigned int public_clk_count = 1, highest_id = 0;
+ struct clk_hw_onecell_data *clocks;
+ struct regmap *map;
+ struct clk *clk;
+ unsigned int i;
+ int res;
+
+ map = device_node_to_regmap(of_node);
+ if (IS_ERR(map))
+ return PTR_ERR(map);
+
+ for (i = 0; i < data->num_muxes; ++i) {
+ if (data->muxes[i].id) {
+ if (data->muxes[i].id > highest_id)
+ highest_id = data->muxes[i].id;
+ public_clk_count++;
+ }
+ }
+ for (i = 0; i < data->num_gates; ++i) {
+ if (data->gates[i].id) {
+ if (data->gates[i].id > highest_id)
+ highest_id = data->gates[i].id;
+ public_clk_count++;
+ }
+ }
+
+ if (WARN_ON(public_clk_count != highest_id + 1))
+ return -EINVAL;
+
+ clocks = devm_kzalloc(dev, struct_size(clocks, hws, public_clk_count), GFP_KERNEL);
+ if (!clocks)
+ return -ENOMEM;
+ clocks->num = public_clk_count;
+
+ for (i = 0; i < data->num_inputs_enable; ++i) {
+ clk = of_clk_get_by_name(of_node, data->inputs_enable[i]);
+ if (IS_ERR(clk)) {
+ return dev_err_probe(dev, PTR_ERR(clk), "Input clk %s failure\n",
+ data->inputs_enable[i]);
+ }
+
+ res = clk_prepare_enable(clk);
+ if (res) {
+ clk_put(clk);
+ return dev_err_probe(dev, res, "Input clk %s enable failure\n",
+ data->inputs_enable[i]);
+ }
+ res = devm_add_action_or_reset(dev, zx_clk_disable_unprepare_put, clk);
+ if (res)
+ return res;
+ }
+ for (i = 0; i < data->num_inputs; ++i) {
+ /* FIXME: devm_get_clk_from_child doesn't do any tree traversal, so it works here
+ * whether "of_node" belongs to "dev" or a parent of "dev". Is it supposed to be
+ * used that way though?
+ */
+ clk = devm_get_clk_from_child(dev, of_node, data->inputs[i]);
+ if (IS_ERR(clk)) {
+ return dev_err_probe(dev, PTR_ERR(clk), "Input clk %s failure\n",
+ data->inputs[i]);
+ }
+ }
+
+ if (data->init) {
+ res = data->init(map);
+ if (res)
+ return dev_err_probe(dev, PTR_ERR(clk), "Controller init failure\n");
+ }
+
+ res = zx_clk_register_plls(dev, map, data->plls, data->num_plls);
+ if (res)
+ return res;
+
+ res = zx_clk_register_fixed_dividers(dev, map, data->fixed_divs, data->num_fixed_divs);
+ if (res)
+ return res;
+
+ res = zx_clk_register_muxes(dev, map, data->muxes, data->num_muxes, clocks);
+ if (res)
+ return res;
+
+ res = zx_clk_register_dividers(dev, map, data->divs, data->num_divs);
+ if (res)
+ return res;
+
+ res = zx_clk_register_gates(dev, map, data->gates, data->num_gates, clocks);
+ if (res)
+ return res;
+
+ /* This is to catch holes in the tables rather than registration errors. The count vs
+ * highest ID should catch most static issues. This check here will trigger if an ID is
+ * reused by accident.
+ */
+ for (i = 1; i < public_clk_count; i++) {
+ if (WARN(!clocks->hws[i], "Clock %u not registered\n", i))
+ return -EINVAL;
+ }
+
+ res = of_clk_add_hw_provider(of_node, of_clk_hw_onecell_get, clocks);
+ if (res)
+ return res;
+ return devm_add_action_or_reset(dev, zx_delete_clk_provider, of_node);
+}
+EXPORT_SYMBOL_NS_GPL(zx_clk_common_probe, "ZTE_CLK");
+
+MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@xxxxxxxxx>");
+MODULE_DESCRIPTION("ZTE common clock driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/clk/zte/clk-zx.h b/drivers/clk/zte/clk-zx.h
new file mode 100644
index 000000000000..b30e4a29d951
--- /dev/null
+++ b/drivers/clk/zte/clk-zx.h
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ */
+
+#ifndef __DRV_CLK_ZX_H
+#define __DRV_CLK_ZX_H
+
+#include <linux/platform_device.h>
+#include <linux/clk-provider.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+
+#define ZX297520V3_PLL_PREPARE_IS_ENABLE 1
+
+/* PLLs, divs and fixed div/factor clocks are never exported directly, only
+ * through a gate.
+ */
+
+struct zx_pll_desc {
+ const char *name;
+ const char * const *parents;
+ unsigned int num_parents;
+ unsigned long rate;
+ u16 reg;
+ u16 flags;
+};
+
+struct zx_fixed_divider_desc {
+ const char *name;
+ const char *parent;
+ unsigned int div;
+};
+
+struct zx_mux_desc {
+ unsigned int id;
+ const char *name;
+ const char * const *parents;
+ unsigned int num_parents;
+ u16 reg;
+ u8 shift, size;
+};
+
+struct zx_div_desc {
+ const char *name, *parent;
+ u16 reg;
+ u8 shift, size;
+};
+
+struct zx_gate_desc {
+ unsigned int id;
+ const char *name, *parent;
+ unsigned long flags;
+ u16 reg;
+ u8 shift;
+};
+
+int zx_clk_register_plls(struct device *dev, struct regmap *regmap,
+ const struct zx_pll_desc *desc, unsigned int num);
+int zx_clk_register_muxes(struct device *dev, struct regmap *regmap,
+ const struct zx_mux_desc *desc, unsigned int num,
+ struct clk_hw_onecell_data *clocks);
+int zx_clk_register_dividers(struct device *dev, struct regmap *regmap,
+ const struct zx_div_desc *desc, unsigned int num);
+int zx_clk_register_gates(struct device *dev, struct regmap *regmap,
+ const struct zx_gate_desc *desc, unsigned int num,
+ struct clk_hw_onecell_data *clocks);
+
+struct zx_clk_data {
+ int (*init)(struct regmap *map);
+ const char * const *inputs_enable;
+ unsigned int num_inputs_enable;
+ const char * const *inputs;
+ unsigned int num_inputs;
+ const struct zx_pll_desc *plls;
+ unsigned int num_plls;
+ const struct zx_fixed_divider_desc *fixed_divs;
+ unsigned int num_fixed_divs;
+ const struct zx_mux_desc *muxes;
+ unsigned int num_muxes;
+ const struct zx_div_desc *divs;
+ unsigned int num_divs;
+ const struct zx_gate_desc *gates;
+ unsigned int num_gates;
+};
+
+int zx_clk_common_probe(struct device *dev, struct device_node *of_node,
+ const struct zx_clk_data *data);
+
+#endif /* __DRV_CLK_ZX_H */
diff --git a/drivers/clk/zte/pll-zx.c b/drivers/clk/zte/pll-zx.c
new file mode 100644
index 000000000000..f8ddb4c7dff6
--- /dev/null
+++ b/drivers/clk/zte/pll-zx.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan Dösinger
+ */
+#include <linux/clk-provider.h>
+#include <linux/device.h>
+#include <linux/regmap.h>
+#include <linux/errno.h>
+
+#include "clk-zx.h"
+
+int zx_clk_register_plls(struct device *dev, struct regmap *regmap,
+ const struct zx_pll_desc *desc, unsigned int num)
+{
+ return -ENODEV;
+}
--
2.54.0